diff --git a/.claude/context/gui/CLAUDE.md b/.claude/context/gui/CLAUDE.md new file mode 100644 index 0000000000000..3aa2064cb6484 --- /dev/null +++ b/.claude/context/gui/CLAUDE.md @@ -0,0 +1,44 @@ +# Kontext-Karte: src/gui + +Diese Struktur unter `.claude/context/gui/` spiegelt `src/gui/` und liefert für jeden Bereich eine kompakte, KI-lesbare Beschreibung: Zweck, Kernklassen, wie die Teile zusammenspielen, und was fork-spezifisch (BRICKMAKERS/IONOS/HiDrive Next/Strato-Whitelabel) statt generischer Nextcloud-Upstream-Code ist. Bewusst **getrennt vom Quellcode** gehalten, damit sie nicht mit echten Source-Dateien verwechselt oder versehentlich mitgebaut wird. + +`nc-desktop` ist ein Whitelabel-Fork des Nextcloud-Desktop-Clients für IONOS/HiDrive Next/STRATO. `src/gui/` enthält ~450 Dateien: 229 davon liegen flach direkt im Wurzelverzeichnis, der Rest verteilt sich auf 11 Unterordner. + +## Navigation + +**Unterordner** (jeweils eigenes `CLAUDE.md`, Struktur spiegelt `src/gui//`): + +| Ordner | Inhalt | +|---|---| +| [tray/](tray/CLAUDE.md) | Systray-Popup: Header, Account-Menü, Activity-Feed, Sync-Status, Unified Search *(größter Unterordner, bekannter Merge-Hotspot)* | +| [wizard/](wizard/CLAUDE.md) | Account-Setup-Wizard (Server-URL, Login, Datenschutz-Consent, Ordner-Konfiguration) | +| [macOS/](macOS/CLAUDE.md) | macOS-spezifische Integration (FileProvider/VFS, XPC, Single-Instance) | +| [filedetails/](filedetails/CLAUDE.md) | Datei-Detailansicht mit Sharing-Tab | +| [creds/](creds/CLAUDE.md) | Authentifizierung & Credential-Speicherung (Keychain, Login Flow v2) | +| [updater/](updater/CLAUDE.md) | Selbst-Update-Mechanismus (NSIS/Sparkle/Passive) | +| [SesComponents/](SesComponents/CLAUDE.md) | Fork-eigene, wiederverwendete UI-/Logik-Bausteine ("Ses"-Präfix) | +| [cloudproviders/](cloudproviders/CLAUDE.md) | Linux `libcloudproviders`/GNOME-Integration | +| [ga4/](ga4/CLAUDE.md) | Google-Analytics-4-Telemetrie (fork-eigen) | +| [integration/](integration/CLAUDE.md) | Server-definierte Datei-Aktionen (Kontextmenü-Popup) | +| [socketapi/](socketapi/CLAUDE.md) | IPC-Server für Explorer/Finder-Shell-Erweiterungen | + +**Wurzelverzeichnis** (229 Dateien ohne Unterordner, keine 1:1-Ordnerstruktur möglich): + +- [COMPONENTS.md](COMPONENTS.md) — Tabelle aller Dateien, gruppiert in acht fachliche Cluster: Account & Connection, Folder & Sync Management, **Settings Dialog & App Settings (⚠️ bekannter Merge-Hotspot)**, Sharing & OCS API Jobs, User Status & Emoji, App Shell/Theming & Window Chrome, File Activity/Tags & External Editing, Platform Glue (macOS/Cocoa) & Misc. + +## Fork-Erkennungsmerkmale + +Wiederkehrende Marker, an denen sich fork-eigener Code von generischem Nextcloud-Upstream-Code unterscheiden lässt: +- Namenspräfix **"ses"** (BRICKMAKERS-Ticketpräfix, z. B. `sesStyle`, `SesComponents`, `sesFileIconProvider`) — praktisch immer fork-eigen. +- Compile-Guards **`IONOS_BUILD`, `IONOS_WL_BUILD`, `STRATO_WL_BUILD`** — schalten Code zwischen IONOS- und Strato/HiDrive-Next-Branding um; `StratoTheme` ist der `#else`-Default-Zweig. +- QML-Modul-Import **`com.strato.hidrivenext.desktopclient`** statt eines generischen `com.nextcloud.desktopclient` — praktisch jede `.qml`-Datei im Fork nutzt das. +- Klassen **`BaseTheme`/`IonosTheme`/`StratoTheme`** als zentrale Theming-Schicht über dem generischen `Theme`; **`WLTheme`** ist keine eigene Klasse, sondern die global genutzte `static`-Singleton-Instanz davon (je nach Build-Guard `IONOS_WL_BUILD` vom Typ `IonosTheme` oder `StratoTheme`, siehe `whitelabeltheme.h`). +- Referenzen auf **"hidrivenext"** in Log-Kategorien/Dateinamen (z. B. `hidrivenext.log`) als Branding-Leck in sonst generischem Code. + +## Bekannter Merge-Hotspot + +Die **Settings-Dialog-Dateien im Wurzelverzeichnis** (`settingsdialog.*`, `generalsettings.*`, `networksettings.*`) sowie Teile von `tray/` (`MainWindow.qml`, `TrayWindowAccountMenu.qml`, `UserLine.qml`) sind historisch die häufigsten Stellen für Merge-Konflikte mit dem upstream `stable-x.y`-Branch. Bei Arbeiten dort den [`stable-merge-check`-Skill](../../skills/stable-merge-check/SKILL.md) nutzen (Referenz-Branch siehe dort in `reference-branch.txt`). + +## Pflegehinweis + +Diese Dokumente wurden am 2026-08-17 automatisiert per Code-Scan erstellt (nicht Zeile-für-Zeile von Boris verifiziert) und sollten vor blindem Vertrauen gegengelesen werden — insbesondere Aussagen zu "dead code"/unbenutzten Pfaden. Sie beschreiben Struktur/Zweck, nicht Implementierungsdetails, und veralten daher langsamer als der Code selbst — trotzdem bei größeren Umbauten in einem Bereich das jeweilige `CLAUDE.md` mit aktualisieren. diff --git a/.claude/context/gui/COMPONENTS.md b/.claude/context/gui/COMPONENTS.md new file mode 100644 index 0000000000000..d27fe86bc7ad7 --- /dev/null +++ b/.claude/context/gui/COMPONENTS.md @@ -0,0 +1,167 @@ +# src/gui — Komponenten-Tabelle (Wurzelverzeichnis) + +Diese Tabelle erfasst die ~200 Implementierungsdateien, die **direkt** unter `src/gui/` liegen (kein Unterordner), gruppiert in fachliche Cluster. Für die Unterordner (`tray/`, `wizard/`, `macOS/`, `filedetails/`, `creds/`, `updater/`, `SesComponents/`, `cloudproviders/`, `ga4/`, `integration/`, `socketapi/`) siehe die jeweiligen `CLAUDE.md` in den gleichnamigen Verzeichnissen hier unter `.claude/context/gui/`. + +*Automatisch per Scan erstellt, Stand 2026-08-20 — bitte gegenlesen, bevor darauf vertraut wird.* + +## Account & Connection + +Deckt den Account-Lebenszyklus (Erstellung, Speicherung, Verbindungsstatus), den Setup-Wizard-Einstieg und die CLI-Provisionierung sowie Credential-/SSL-Trust-Dialoge ab, die authentifizierte Verbindungen zum Server absichern. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| accountmanager.cpp/.h | `AccountManager` | Singleton that loads, saves, and tracks all `AccountState` instances from settings. | accountstate.h, account.h | | +| accountsettings.cpp/.h/.ui | `AccountSettings` | Main per-account settings page widget (folders, quota, encryption, connection status UI). | userinfo.h, folderstatusmodel.h | Fork-specific: uses `whitelabeltheme.h`/`WLTheme` for styling, `#ifndef IONOS_BUILD` guard on error styling, `#ifdef STRATO_WL_BUILD` branch for a branded "ExpandMemory" storage-upsell link. | +| accountstate.cpp/.h | `AccountState`, `AccountApp` | Tracks live connectivity/auth state of an account and drives reconnection logic; `AccountApp` models a server-advertised app entry. | connectionvalidator.h, remotewipe.h, accountmanager.h | Also defines `TermsOfServiceChecker` usage via connectionvalidator.h | +| accountsetupcommandlinemanager.cpp/.h | `AccountSetupCommandLineManager` | Singleton parsing CLI options (`--user`, `--password`, etc.) to script account creation. | accountsetupfromcommandlinejob.h | | +| accountsetupfromcommandlinejob.cpp/.h | `AccountSetupFromCommandLineJob` | Executes non-interactive account creation/login and local folder setup from CLI-supplied credentials. | accountsetupcommandlinemanager.h, accountstate.h | | +| connectionvalidator.cpp/.h | `ConnectionValidator`, `TermsOfServiceChecker` | State-machine job that checks server status, auth, capabilities, and ToS to determine connection health. | accountstate.h, userinfo.h | | +| authenticationdialog.cpp/.h | `AuthenticationDialog` | Simple modal prompting for username/password for a given HTTP auth realm/domain. | — | Not instantiated anywhere else in the codebase and has no include relationship with `proxyauthdialog.h`; currently unreferenced/dead code. | +| legacyaccountselectiondialog.cpp/.h | `LegacyAccountSelectionDialog` | Checklist dialog letting the user pick which legacy-client accounts to import. | accountmanager.h (legacy import flow) | Fork-specific: styled via `whitelabeltheme.h`/`WLTheme` and `buttonstyle.h`. | +| userinfo.cpp/.h | `UserInfo` | Periodically fetches user quota/avatar info for display, owned by AccountSettings. | accountsettings.h, connectionvalidator.h | | +| owncloudsetupwizard.cpp/.h | `OwncloudSetupWizard` | Orchestrates the multi-step "add account" wizard (server discovery, auth, folder creation). | wizard/wizardproxysettingsdialog.h, accountstate.h | | +| profilepagewidget.cpp/.h | `ProfilePageMenu` | Popup menu showing a user's profile hovercard/actions (share-with-user profile). | ocsprofileconnector.h, account.h | | +| remotewipe.cpp/.h | `RemoteWipe` | Handles server-triggered remote wipe check/response flow on auth failure (401/403). | accountmanager.h, accountstate.h | | +| proxyauthdialog.cpp/.h/.ui | `ProxyAuthDialog` | Modal collecting username/password for an authenticating proxy. | proxyauthhandler.h | | +| proxyauthhandler.cpp/.h | `ProxyAuthHandler` | Singleton handling `QNetworkAccessManager::proxyAuthenticationRequired`, coordinating dialog + keychain lookups synchronously. | proxyauthdialog.h, owncloudgui.h | | +| addcertificatedialog.cpp/.h/.ui | `AddCertificateDialog` | Lets the user browse for and supply a passphrase-protected client certificate. | sslerrordialog.h (SSL/cert flow) | | +| sslbutton.cpp/.h | `SslButton` | Toolbar button showing SSL/cert status and a menu of trusted certificates for an account. | accountstate.h | | +| sslerrordialog.cpp/.h/.ui | `SslErrorDialog`, `SslDialogErrorHandler` | Prompts the user to trust/reject unknown SSL certificates encountered during a connection. | account.h, addcertificatedialog.h | | +| passwordinputdialog.cpp/.h/.ui | `PasswordInputDialog` | Generic modal for entering a password with an optional error message (e.g. E2EE mnemonic passphrase). | — | Currently unused: not referenced anywhere outside its own definition files; the E2EE mnemonic flow in `accountsettings.cpp::displayMnemonic()` uses `ui_mnemonicdialog.h` instead. Likely dead code. | + +## Folder & Sync Management + +Deckt alles ab, was den Zustand lokaler Sync-Ordner modelliert, überwacht und abgleicht — von Ordner-Definition/-Konfiguration über wizard-gesteuerte Erstellung und Selective-Sync-Konfiguration bis zu Konflikt-/Fehler-Dialogen während der Synchronisation. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| folder.cpp/.h | `FolderDefinition`, `Folder` | Represents a single configured sync folder (paths, VFS mode, settings) and drives its SyncEngine, journal, watcher, and lifecycle. | folderman, folderwatcher, syncrunfilelog | Test-notification file uses `.hidrivenextsync.log`/`.hidrivenextpermissions.log` (fork-specific naming); macOS sandbox bookmark handling is Nextcloud upstream. | +| foldercreationdialog.cpp/.h/.ui | `FolderCreationDialog` | Dialog to name/create a new local subfolder under a destination path. | folderwizard | Includes `whitelabeltheme.h` for `customizeStyle()` (fork-specific styling). | +| folderman.cpp/.h | `FolderMan` | Singleton managing all loaded folders: setup from settings, scheduling, path validity checks, socket API registration, lock/etag watching. | folder, lockwatcher, socketapi | Generic Nextcloud core; central orchestrator. | +| folderstatusdelegate.cpp/.h | `FolderStatusDelegate` | Custom `QStyledItemDelegate` that paints folder rows (status icon, progress bar, add/options buttons) in the folder list. | folderstatusmodel, folderstatusview | Uses `whitelabeltheme.h` for custom styling. | +| folderstatusmodel.cpp/.h | `FolderStatusModel` | Tree model exposing folders and their (lazily fetched) subfolders/selective-sync state for the UI. | folderstatusview, selectivesyncdialog | Uses `whitelabeltheme.h`; `IONOS_BUILD` ifdef swaps in whitelabel status icons via `WLTheme`. | +| folderstatusview.cpp/.h | `FolderStatusView` | `QTreeView` subclass with custom hit-testing/visual-rect logic for the folder status tree. | folderstatusmodel, folderstatusdelegate | Includes `whitelabeltheme.h`. | +| folderwatcher.cpp/.h | `FolderWatcher` | Cross-platform façade that recursively monitors a folder for local filesystem changes and file lock events. | folderwatcher_linux/_mac/_win, folder | Generic upstream abstraction; delegates to platform-specific `FolderWatcherPrivate`. | +| folderwatcher_linux.cpp/.h | `FolderWatcherPrivate` (Linux) | inotify-based recursive directory watch implementation. | folderwatcher | Platform backend, generic upstream. | +| folderwatcher_mac.cpp/.h | `FolderWatcherPrivate` (macOS) | FSEvents-based directory watch implementation. | folderwatcher | Platform backend, generic upstream. | +| folderwatcher_win.cpp/.h | `WatcherThread`, `FolderWatcherPrivate` (Windows) | Thread-based `ReadDirectoryChangesW` watch implementation. | folderwatcher | Platform backend, generic upstream. | +| folderwizard.cpp/.h (+ folderwizardsourcepage.ui, folderwizardtargetpage.ui, folderwizardselectivesync.ui) | `FolderWizardLocalPath`, `FolderWizardRemotePath`, `FolderWizardSelectiveSync`, `FolderWizard` | Multi-page `QWizard` for creating a new sync folder: choose local path, choose/create remote path, configure selective sync/VFS. | selectivesyncdialog, folderman | `IONOS_BUILD` ifdef switches to a whitelabel VFS checkbox setup (`setupVirtualFilesCheckbox()`) instead of the generic checkbox code. | +| selectivesyncdialog.cpp/.h | `SelectiveSyncWidget`, `SelectiveSyncDialog` | Tree UI + dialog for picking which remote subfolders are excluded ("selective sync") for a folder or whole account. | folderwizard, folderstatusmodel | Includes `whitelabeltheme.h` for `customizeStyle()`. | +| ignorelisteditor.cpp/.h/.ui | `IgnoreListEditor` | Dialog for editing the global/user ignore-pattern list and hidden-file setting. | ignorelisttablewidget | Generic Nextcloud upstream. | +| ignorelisttablewidget.cpp/.h/.ui | `IgnoreListTableWidget` | Reusable table widget listing ignore patterns (add/remove/read-only defaults), used inside the ignore list editor. | ignorelisteditor | Generic Nextcloud upstream. | +| syncconflictsmodel.cpp/.h | `SyncConflictsModel` | QML-facing list model presenting sync conflict activities with per-item existing/conflicting selection state, feeding the resolve-conflicts dialog. | conflictsolver, ResolveConflictsDialog.qml | Generic; QML `Q_PROPERTY` model for the tray UI. | +| conflictdialog.cpp/.h/.ui | `ConflictDialog` | Widget-based dialog to resolve a single file conflict (keep local/remote/both). | conflictsolver | Generic Nextcloud upstream. | +| conflictsolver.cpp/.h | `ConflictSolver` | Executes a chosen conflict resolution (delete/rename local, overwrite remote) for one conflicting file. | conflictdialog, syncconflictsmodel | Includes `whitelabeltheme.h`. | +| ConflictDelegate.qml | QML delegate `ConflictDelegate` | Renders one conflict entry (existing vs. conflicting file previews and info) inside the resolve-conflicts list. | ConflictItemFileInfo.qml, syncconflictsmodel | Imports fork-specific QML module `com.strato.hidrivenext.desktopclient`. | +| ConflictItemFileInfo.qml | QML component `ConflictItemFileInfo` | Reusable file-info block (checkbox, preview thumbnail, version/date/size labels) used by both sides of a conflict delegate. | ConflictDelegate.qml | Generic within fork's QML style module. | +| ResolveConflictsDialog.qml | QML `ApplicationWindow` | Tray-integrated dialog listing all current sync conflicts and letting the user bulk-resolve them. | syncconflictsmodel, ConflictDelegate.qml | Imports fork-specific `com.strato.hidrivenext.desktopclient` module and `Style.ses*` theme colors (whitelabel-specific). | +| caseclashfilenamedialog.cpp/.h/.ui | `CaseClashFilenameDialog` | Dialog to rename a file that clashes with another only by case, using `CaseClashConflictSolver`. | invalidfilenamedialog | Generic Nextcloud upstream (newer case-insensitivity handling). | +| invalidfilenamedialog.cpp/.h/.ui | `InvalidFilenameDialog` | Dialog to rename a file with a system/server-invalid filename before it can sync. | caseclashfilenamedialog | Includes `whitelabeltheme.h`. | +| vfsdownloaderrordialog.cpp/.h/.ui | `VfsDownloadErrorDialog` | Simple dialog reporting an error that occurred while hydrating (downloading) a virtual file. | folder | Generic Nextcloud upstream. | +| syncrunfilelog.cpp/.h | `SyncRunFileLog` | Writes a per-sync-run plaintext log file listing item actions and timing/lap durations. | folder | Generic Nextcloud upstream. | +| lockwatcher.cpp/.h | `LockWatcher` | Periodically polls a set of watched files (e.g. Office documents) to detect when they become unlocked, then signals to trigger a sync. | folderman, folderwatcher | Generic Nextcloud upstream (Windows-only relevance per header comment). | + +## Settings Dialog & App Settings — ⚠️ bekannter Merge-Hotspot + +Der Settings-Shell (SettingsDialog + Toolbar/Navigation), die General-/Network-Settings-Seiten mit ihren `.ui`-Formularen, ein kleiner E2E-Verschlüsselungs-Mnemonic-Dialog, der Legal-Notice-Dialog und der Debug-Log-Viewer. Fast jede Datei enthält starke `#ifdef IONOS`/`IONOS_BUILD`/`STRATO_WL_BUILD`-Verzweigungen und eigene GA4-Tracking-Hooks — **das ist historisch der konfliktträchtigste Bereich des Forks**. Bei Änderungen hier den [stable-merge-check-Skill](../../skills/stable-merge-check/SKILL.md) nutzen. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| settingsdialog.h / .cpp | `SettingsDialog` (+ private `WindowDragHandle`, `ToolButtonAction`, `CurrentPageSizeStackedWidget`) | Main settings window: vertical toolbar navigation + stacked pages for General/Network/per-account settings, avatar/display-name updates | generalsettings.h/.cpp, networksettings.h/.cpp, accountsettings.h/.cpp, owncloudgui.h, whitelabeltheme.h | Heavily fork-specific: `#ifdef IONOS`/`IONOS_BUILD` alternate toolbar CSS, custom `WLTheme`-driven color-aware icons/avatars, macOS-only window-drag-handle hint, spacer removed for IONOS build — structurally diverges a lot from upstream's simpler settings dialog | +| generalsettings.h / .cpp / generalsettings.ui | `GeneralSettings` | "General" settings page: autostart, notifications, ignore list, debug archive, update channel, legal/privacy links, data-protection checkboxes | legalnotice.h/.cpp, ignorelisteditor.h, ocupdater.h, ga4/datacollectionwrapper.h | Strongly fork-modified: `DataCollectionWrapper`/GA4 click-tracking wired to nearly every control (`connectToTracking`), custom "Datenschutz"/data-protection group box with mandatory `sendNecessaryData_checkbox` + optional `sendAnonymousData_checkbox`, `#ifdef STRATO_WL_BUILD` alternate help/legal/privacy/open-source links, `#ifdef IONOS_BUILD` hides mono-icons/call-notifications/advanced group/updates frame ("SES-4 removed"), `WLTheme`-driven `customizeStyle()` overriding fonts/colors — this whole file/ui is the prime merge-conflict hotspot | +| networksettings.h / .cpp / networksettings.ui | `NetworkSettings` | Per-account proxy configuration (system/manual/none) and upload/download bandwidth limit settings | account.h, folderman.h, settingsdialog.cpp | Mostly generic upstream logic; only minor whitelabel touch is `Theme::instance()->doNotUseProxy()`/`forceSystemNetworkProxy()` gating proxy UI — not deeply fork-restructured | +| mnemonicdialog.ui | (no dedicated class; loaded via `Ui_Dialog`/`ui_mnemonicdialog.h`) | Small dialog showing the E2E-encryption recovery mnemonic phrase to the user | accountsettings.cpp (`AccountSettings::displayMnemonic`) | Generic upstream Nextcloud E2E-encryption feature; not branded/whitelabel-specific | +| legalnotice.h / .cpp / legalnotice.ui | `LegalNotice` | Modal dialog showing copyright/license text and theme's "about details" | generalsettings.cpp (`slotShowLegalNotice`), theme.h | Largely generic; copyright string hardcodes "Nextcloud GmbH"/"ownCloud GmbH" untouched by whitelabel branding, content is theme-injected via `Theme::instance()->aboutDetails()` | +| logbrowser.h / .cpp | `LogBrowser` | Non-modal dialog for enabling/inspecting temporary debug logging and opening the log folder | logger.h, configfile.h | Mostly generic upstream utility dialog; only fork touch is hardcoded log filename `"hidrivenext.log"` in `togglePermanentLogging` (HiDrive Next branding leak) | + +## Sharing & OCS API Jobs + +OCS-basiertes Sharing-Subsystem: Datenmodelle für Sharees/Shares (`Sharee`, `Share`/`LinkShare`/`UserGroupShare`, `ShareManager`, `SharePermissions`) plus die OCS-Job-Hierarchie (`OcsJob`-Basis und ihre Subklassen) und ein kleiner OCS-naher Job zum Bestätigen von Benachrichtigungsaktionen — fast vollständig generischer Upstream-Code, mit einer IONOS-spezifischen Anpassung. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| ocsjob.h / ocsjob.cpp | `OcsJob` | Abstract base class for all jobs talking to OCS endpoints; handles verb, params, headers, status-code parsing/validation and emits `jobFinished`/`ocsError`. | Base for `OcsShareJob`, `OcsShareeJob`, `OcsNavigationAppsJob`; extends `AbstractNetworkJob` | Generic upstream. | +| sharemanager.h / sharemanager.cpp | `Share`, `LinkShare`, `UserGroupShare`, `SharePtr` | QObject data models representing a server share (base), a public link share, and a user/group share, with slots to delete/set permissions/password/note/expiry via OCS jobs. | Uses `OcsShareJob` to perform network operations; uses `Sharee`/`SharePermissions` | Generic upstream. | +| sharemanager.h / sharemanager.cpp | `ShareManager` | Facade for creating, fetching and deleting shares; parses OCS JSON replies into `Share`/`LinkShare`/`UserGroupShare` objects so callers don't use `OcsShareJob` directly. | Wraps `OcsShareJob`; produces `SharePtr`/`LinkShare` objects defined in same file | Generic upstream. | +| sharepermissions.h | `SharePermission` / `SharePermissions` (header-only) | Defines the bitflag enum of share permissions (read/update/create/delete/share) mirroring server constants. | Used as `Share::Permissions` in sharemanager.h | Generic upstream, header-only. | +| ocsshareejob.h / ocsshareejob.cpp | `OcsShareeJob` | OCS job to fetch sharee (user/group/email/etc.) search results for autocomplete. | Extends `OcsJob`; results consumed to build `Sharee` objects | Generic upstream. | +| ocssharejob.h / ocssharejob.cpp | `OcsShareJob` | OCS job implementing full share CRUD (get/create/delete shares, set password/expiry/note/permissions/label/hideDownload/public upload). | Extends `OcsJob`; used by `ShareManager` and `Share`/`LinkShare`/`UserGroupShare` slots | Generic upstream. | +| ocsnavigationappsjob.h / ocsnavigationappsjob.cpp | `OcsNavigationAppsJob` | OCS job to fetch the list of enabled server apps/external sites for the navigation menu. | Extends `OcsJob` | Generic upstream. | +| sharee.h / sharee.cpp | `Sharee`, `ShareePtr` | Q_GADGET value object representing a shareable recipient (user/group/email/federated/team/room) with display name and colored icon URL. | Used by `Share`/`ShareManager`; populated from `OcsShareeJob` results | Fork-specific: `#ifdef IONOS_BUILD` forces icon color to black instead of theme-dependent black/white in `updateIconUrl()`. | +| notificationconfirmjob.h / notificationconfirmjob.cpp | `NotificationConfirmJob` | Generic network job that calls a server notification's action link (GET/PUT/POST/DELETE) and reports the reply. | Extends `AbstractNetworkJob` directly (not `OcsJob`); conceptually adjacent to OCS jobs but for notifications API | Generic upstream. | + +## User Status & Emoji + +Der Online-Status-/Statusnachricht-Selector (Modell + QML-Views) und der Emoji-Picker (Modell + QML) — zusammen lassen sie Präsenz, eine Statusnachricht und ein Emoji dazu setzen. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| userstatusselectormodel.h/.cpp | `OCC::UserStatusSelectorModel` | QObject exposed to QML managing online status, status message/emoji, predefined statuses, and clear-at timing via a `UserStatusConnector`. | UserStatusSetStatusView.qml, UserStatusMessageView.qml, UserStatusSelectorPage.qml | Generic upstream Nextcloud logic. | +| emojimodel.h/.cpp | `OCC::EmojiModel`, `OCC::EmojiCategoriesModel`, `OCC::Emoji` | Provides categorized emoji lists (people/nature/food/etc.), emoji category navigation model, and a persisted "recently used" emoji history via `QSettings`. | EmojiPicker.qml | Generic upstream Nextcloud logic (static Unicode emoji tables). | +| UserStatusSelectorPage.qml | `Page` | Top-level page hosting a `UserStatusSelectorModel` instance and switching (via `StackLayout`) between the online-status view and the status-message view. | UserStatusSetStatusView.qml, UserStatusMessageView.qml | Imports `com.strato.hidrivenext.desktopclient as NC` — fork-specific (Strato/HiDrive Next) QML module namespace instead of upstream `com.nextcloud.desktopclient`. | +| UserStatusSetStatusView.qml | `ColumnLayout` | Lists selectable online-status options (Online/Away/Busy/DND/Invisible) as buttons bound to the model, with a link to the status-message view. | UserStatusSelectorButton.qml, UserStatusSelectorPage.qml | Uses fork-specific `com.strato.hidrivenext.desktopclient` import. | +| UserStatusMessageView.qml | `ColumnLayout` | Lets the user edit the status message/emoji, pick a predefined status, and set a clear-after duration; hosts the emoji picker popup. | EmojiPicker.qml, PredefinedStatusButton.qml, userstatusselectormodel | Uses fork-specific `com.strato.hidrivenext.desktopclient` import. | +| UserStatusSelectorButton.qml | `AbstractButton` | Reusable styled button (icon + primary/secondary text, highlight/border states) used for online-status choices. | UserStatusSetStatusView.qml | Generic styling component. | +| PredefinedStatusButton.qml | `AbstractButton` | Row-style button rendering an emoji, status text, and "clears at" text for a predefined status entry. | UserStatusMessageView.qml | Generic. | +| EmojiPicker.qml | `ColumnLayout` | Emoji picker widget with a category header row and a scrollable grid of emojis, backed by `EmojiModel`; emits `chosen(emoji)`. | emojimodel, UserStatusMessageView.qml | Uses fork-specific `com.strato.hidrivenext.desktopclient` import. | + +## App Shell, Theming & Window Chrome + +Prozess-Bootstrap, globale QApplication/Tray/Menu-Shell und die umfangreiche Theming-Schicht (BRICKMAKERS "ses"-Widget-Style-Engine plus IONOS-/Strato-Theme-Klassen), die Qt's natives Look für Buttons, Snackbars, Icons und Fensterrahmen überschreibt. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| main.cpp | `main()` | Entry point: sets up Qt surface format, QML/widget style (Fusion/Universal/FluentWinUI3), loads `ionos`/`ionos_theme` Qt resources, constructs `Application`, handles systray-availability fallback. | application.h, whitelabeltheme.h | Fork-specific: references `IONOS_BUILD`/`IONOS_WL_BUILD` macros, `Q_INIT_RESOURCE(ionos)`, applies `WLTheme.settingsFontDefault()` on Windows. | +| application.h / application.cpp | `OCC::Application` (extends `QApplication`) | Owns app-wide lifecycle: single-instance handling, account/folder setup, logging, command-line parsing, installs global `sesStyle` widget style. | owncloudgui.h, sesstyle.h, main.cpp | `setStyle(new sesStyle(...))` in application.cpp is BRICKMAKERS/"ses"-specific; rest is largely upstream Nextcloud `Application`. Since SES-578, on Windows an `eventFilter()` override calls `applyImmersiveDarkMode()` (DWM `DWMWA_USE_IMMERSIVE_DARK_MODE`) on every top-level widget's `Show` event and again on `Theme::darkModeChanged`, giving native window title bars a dark mode Qt itself doesn't provide. | +| owncloudgui.h / owncloudgui.cpp | `OCC::ownCloudGui` | Central GUI controller: builds tray, settings dialog, share/activity dialogs, wires tray icon states to sync status. | systray.h, application.h, whitelabeltheme.h | Registers `WLTheme` as QML singleton under `com.strato.hidrivenext.desktopclient` URI and uses `WLTheme.syncOfflineIcon("tray")`/`syncSyncingIcon("tray")`/`syncPausedIcon("tray")`/`syncSuccessIcon("tray")`/`syncWarningIcon("tray")`/`syncErrorIcon("tray")` for tray icon states — fork-specific theming hook; otherwise upstream ownCloudGui logic. | +| systray.h / systray.cpp | `OCC::Systray`, `OCC::AccessManagerFactory` | QSystemTrayIcon subclass driving the QML tray window: dialogs (share, file actions, conflicts), window positioning, notification triggers. | owncloudgui.cpp, tray/svgimageprovider, tray/trayimageprovider | Includes `whitelabeltheme.h`; mostly generic Nextcloud upstream systray implementation with whitelabel image providers wired in. | +| systray_mac_common.mm | Free functions: `menuBarThickness()`, `setTrayWindowLevelAndVisibleOnAllSpaces()`, `osXInDarkMode()` | macOS-native helpers for tray window placement and dark-mode/menu-bar geometry detection. | systray.h | Logging category renamed to `hidrivenext.gui.macsystraycommon` — fork-branded logging of an otherwise-generic Nextcloud macOS helper. | +| systray_mac_usernotifications.mm | `NotificationCenterDelegate` (Obj-C), `sendTalkReply()` | Implements macOS `UNUserNotificationCenter` delegate for Talk reply notifications and interactive notification handling. | systray.h, tray/talkreply.h | Logging category `hidrivenext.gui.macsystrayusernotifications` — fork-branded logging, otherwise upstream Nextcloud Talk-notification logic. | +| basetheme.h | `OCC::BaseTheme` (abstract, extends `QObject`) | Base class defining 39 QProperty-exposed colors/icons/fonts/sizes shared by all whitelabel themes; provides default ("ses") values and icon paths. Since SES-578, most color properties are `NOTIFY themeColorsChanged` (fired via `Theme::darkModeChanged`) instead of `CONSTANT`, and their getters return `themedColor(light, dark)` — i.e. the whole theme now reacts live to OS dark-mode switches instead of being fixed at process start. | ionostheme.h, stratotheme.h, theme.h | Fork-specific: entirely BRICKMAKERS/"ses" theming abstraction (icon paths under `ses/` resource folder, `sesHeaderLogoIcon()` pure-virtual) layered on top of upstream `Theme`. | +| ionostheme.h | `OCC::IonosTheme` (extends `BaseTheme`) | Minimal theme override supplying the IONOS logo/icon prefix; otherwise inherits all BaseTheme defaults. | basetheme.h, whitelabeltheme.h | Fork-specific (IONOS whitelabel variant). | +| stratotheme.h | `OCC::StratoTheme` (extends `BaseTheme`) | Full color/style override for the Strato/HiDrive Next brand (buttons, tray, menu, error colors, logo); as of SES-578 each override supplies both a light and dark value via `themedColor(light, dark)` rather than a single fixed hex. | basetheme.h, whitelabeltheme.h | Fork-specific (Strato/HiDrive Next whitelabel variant); overrides far more properties than IonosTheme, suggesting Strato is the more actively maintained branding. | +| whitelabeltheme.h | Global `OCC::WLTheme` static instance selector | Chooses which theme (`IonosTheme` vs `StratoTheme`) becomes the process-wide `WLTheme` singleton, based on `IONOS_WL_BUILD` compile define. | basetheme.h, ionostheme.h, stratotheme.h, IONOS.cmake | Fork-specific glue; per `IONOS.cmake`, `STRATO_WL_BUILD`/`IONOS_WL_BUILD` are set from `APPLICATION_NAME` ("STRATO HiDrive Next" vs "IONOS HiDrive Next") — **`StratoTheme` is the default/`#else` branch**, i.e. active unless `IONOS_WL_BUILD` is defined. | +| buttonstyle.h | `OCC::ButtonStyle` (abstract) + `PrimaryButtonStyle`/`SecondaryButtonStyle`/`MoreOptionsButtonStyle` (singletons) | Maps semantic button styles (Primary/Secondary/MoreOptions) to concrete colors pulled from `WLTheme`, for use by the custom `sesStyle`. | whitelabeltheme.h, buttonstylestrategy.h | Fork-specific ("ses"/BRICKMAKERS custom button theming layer), also declares `ButtonStyleName` Qt meta-enum. | +| buttonstylestrategy.h | `ButtonStyleStrategy` (static helper) | Determines which `ButtonStyle` instance applies to a given `QWidget`/`QStyleOptionButton`, via a `buttonStyle` dynamic property or object-name lookup (e.g. wizard finish button = Primary). | buttonstyle.h, pushbuttonstylehelper.cpp, moreoptionsbuttonstylehelper.cpp | Fork-specific dispatch logic for the custom button styling system. | +| moreoptionsbuttonstylehelper.h / .cpp | `MoreOptionsButtonStyleHelper` | Paints the pill-shaped "more options" tool button (state-based fill/border) and recolors its icon via pixmap tinting. | buttonstylestrategy.h, sesstyle.cpp | Fork-specific ("ses" custom Qt style helper); unlike `pushbuttonstylehelper.h`, carries no KDAB/Oxygen2 license header. | +| pushbuttonstylehelper.h / .cpp | `PushButtonStyleHelper` | Paints standard push-button shape/border per state, computes content size, sets font/text-color palette per `WLTheme`. | buttonstylestrategy.h, sesstyle.cpp | Fork-specific; header retains original KDAB "Oxygen2 project" MIT-licensed boilerplate that was adapted for this whitelabel styling. | +| sesstyle.h / sesstyle.cpp | `sesStyle` (extends `QProxyStyle`) | Custom global `QStyle` implementation (installed in `application.cpp`) driving all button/tree-view/control painting via the push-button and more-options helpers. | application.cpp, pushbuttonstylehelper.h, moreoptionsbuttonstylehelper.h, whitelabeltheme.h | Fork-specific ("ses" prefix = BRICKMAKERS); also KDAB Oxygen2-derived; this is the style installed app-wide, making it central to the whitelabel look. | +| sessnackbar.h / sessnackbar.cpp | `OCC::sesSnackBar` (extends `QFrame`) | Reusable snackbar/notification widget with caption/message and error/warning/success visual states. | basetheme.h (colors) | Fork-specific ("ses"-prefixed custom widget, not present upstream). | +| guiutility.h / guiutility.cpp | `OCC::Utility::{openBrowser, openEmailComposer, vfsCurrentAvailabilityText, ...}` | Free-function helpers for opening URLs/email clients and generating VFS availability strings. | systray.cpp | Generic upstream Nextcloud utility, no fork-specific branding. | +| iconutils.h / iconutils.cpp | `OCC::Ui::IconUtils::{pixmapForBackground, createSvgImageWithCustomColor, ...}` | SVG-to-pixmap/image rendering helpers supporting custom-colored icons (with caching). | tray/svgimageprovider, moreoptionsbuttonstylehelper.cpp | Generic upstream Nextcloud helper (used heavily by whitelabel icon recoloring, but itself brand-agnostic). | +| elidedlabel.h / elidedlabel.cpp | `OCC::ElidedLabel` (extends `QLabel`) | Label widget that elides its text on resize according to a configurable `Qt::TextElideMode`. | clickablelabel.h | Generic upstream Nextcloud widget, no fork-specific branding. | +| linkbutton.h / linkbutton.cpp | `OCC::LinkButton` (extends `QLabel`) | Label styled/behaving as a clickable hyperlink button, emitting `clicked()` on mouse press. | clickablelabel.h | Generic small utility widget; no fork markers found. | +| clickablelabel.h | `ClickableLabel` (extends `QLabel`, header-only) | Minimal clickable label emitting `clicked()` signal on left mouse press. | linkbutton.h | Generic utility widget (note: declared in global namespace, not `OCC::`), no fork-specific branding. | +| ErrorBox.qml | QML `Item` component `errorBox` | Reusable QML error banner (colored stripe, optional close button, wrapped error text) used inside tray/settings QML views. | Style (QML singleton), tray/*.qml | Generic upstream Nextcloud tray QML component; uses `Style.errorBoxBackgroundColor` etc., not directly BaseTheme, so likely shared/unbranded. | +| sesFileIconProvider.h / sesFileIconProvider.cpp | `SesFileIconProvider` (extends `QFileIconProvider`) | Custom file-icon provider overriding `icon()` to supply branded icons for file dialogs/tree views. | basetheme.h (icon paths) | Fork-specific ("ses"-prefixed, BRICKMAKERS custom icon provider not present upstream). | + +## File Activity, Tags & External Editing + +Aktivitäts-/Tag-Anzeige pro Datei, der "Lokal bearbeiten"-Workflow inkl. Server-Verifizierung, sowie systemnahe Integration (Explorer-Kontextmenü/Overlays, Datei-Manager-Öffnen, Tooltip-Updates) — kaum forkspezifischer Code, mit einer Ausnahme. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| fileactivitylistmodel.h/.cpp | `FileActivityListModel` | List model that fetches/shows server activity entries scoped to a single local file path (subclass of `ActivityListModel`). | tray/activitylistmodel.h, accountstate.h | Generic upstream Nextcloud code. | +| filetagmodel.h/.cpp | `FileTagModel` | List model exposing a file's collaborative tags, with `maxTags`/`totalTags` properties for a QML-side "+N" overflow badge and a comma-separated `overflowTagsString()` used as the overflow tooltip, for a given server-relative path and account. | libsync/account.h | Generic upstream Nextcloud code. | +| editlocallyjob.h/.cpp | `EditLocallyJob` | Orchestrates the "Edit Locally" flow for one remote file: locates/sync's the containing folder, locks the file on the server, then opens it locally. | editlocallymanager, folder, folderman, syncengine, systray | Generic upstream Nextcloud code; used by the `nc://` edit-locally URL handler. Connection to `editlocallyverificationjob` is only indirect via the shared `EditLocallyManager` orchestrator, not a direct dependency. | +| editlocallymanager.h/.cpp | `EditLocallyManager` (singleton) | Parses `nc://` edit-locally URLs, verifies the request token via `EditLocallyVerificationJob`, then dispatches to `EditLocallyJob` (or the macOS File Provider variant). | editlocallyjob, editlocallyverificationjob, macOS/fileprovidereditlocallyjob | Generic upstream Nextcloud code; macOS File Provider path is conditional on `BUILD_FILE_PROVIDER_MODULE`. | +| editlocallyverificationjob.h/.cpp | `EditLocallyVerificationJob` | Validates an edit-locally token/relPath against the server before allowing local editing to proceed. | editlocallymanager, editlocallyjob | Generic upstream Nextcloud code. | +| openfilemanager.h/.cpp | `showInFileManager()` (free function) | Cross-platform helper to reveal/pre-select a given local path in the OS file manager (Explorer/Finder/xdg). | — | Generic upstream Nextcloud code. | +| navigationpanehelper.h/.cpp | `NavigationPaneHelper` | Manages Windows Explorer "Cloud Storage Providers" navigation-pane registration/sync for configured folders. | shellextensionsserver, folderman | Contains a branding-relevant check comparing the app's CLSID entry against both the current (branded) app name and a hardcoded `unbrandedApplicationName = "Nextcloud"` — fork-relevant for whitelabel builds coexisting with/migrating from upstream Nextcloud installs. | +| shellextensionsserver.h/.cpp | `ShellExtensionsServer` | Local IPC server (QLocalServer) answering Explorer/Finder shell-extension requests for thumbnails and custom file-state overlay icons. | folderman.h, common/shellextensionutils.h, libsync/vfs/cfapi/shellext/configvfscfapishellext.h | Generic upstream Nextcloud code. | +| tooltipupdater.h/.cpp | `ToolTipUpdater` | QTreeView event filter/helper that live-refreshes a currently displayed tooltip when the underlying model's `Qt::ToolTipRole` data changes (e.g. sync progress). | — | Generic upstream Nextcloud code. | + +## Platform Glue (macOS/Cocoa) & Misc + +macOS/Cocoa-Plattform-Integrations-Shims (Objective-C++-Glue für Autorelease-Pools, URL-Events, App-Nap-Vermeidung, Vordergrund/Dock-Icon-Umschaltung) plus eine unabhängige Qt/QML-Feature-Klasse zum Pollen des Nextcloud-Talk-Call-Status — durchweg unmodifizierter Upstream-Code ohne Whitelabel-Branding. + +| File | Component/Class | Purpose | Related | Notes | +|---|---|---|---|---| +| cocoainitializer.h | `OCC::Mac::CocoaInitializer` | Declares a RAII wrapper (PIMPL) that provides a Cocoa `NSAutoreleasePool` for use in `main()`. | cocoainitializer_mac.mm | Generic Nextcloud upstream (dates to 2014 ownCloud). | +| cocoainitializer_mac.mm | `CocoaInitializer` (impl) + `URLEventHandler` (Obj-C) | Implements the autorelease pool RAII, installs an `NSAppleEventManager` handler to catch custom-URL-scheme open events (workaround for missed `QFileOpenEvent`s), and holds an `NSActivityBackground` assertion for the process lifetime to prevent App Nap/RunningBoard suspension of the background sync agent. | cocoainitializer.h, application.h, editlocallymanager.h | Generic upstream; App Nap handling and Edit-Locally URL handling are Nextcloud desktop features, not whitelabel-specific. | +| foregroundbackground_cocoa.h | `CocoaProcessType` (Obj-C, extends `NSApplication`) | Declares class methods `ToForeground`/`ToBackground` to toggle the app's Cocoa process type (LSUIElement) so the Dock icon/menubar appear or hide. | foregroundbackground_cocoa.mm, foregroundbackground_interface.h | Generic upstream. | +| foregroundbackground_cocoa.mm | `CocoaProcessType` (impl) | Implements `ToForeground`/`ToBackground` via `TransformProcessType` (Carbon Process Manager API) to switch between foreground app and UIElement (menubar-only) modes. | foregroundbackground_cocoa.h | Generic upstream. | +| foregroundbackground_interface.h | `ForegroundBackground` (QObject) | Declares a cross-platform `QObject` event filter interface that triggers foreground/background transitions on window Show/Close events; platform-specific `ToForeground`/`ToBackground` implementations are provided elsewhere. | foregroundbackground_mac.mm, foregroundbackground_cocoa.h | Generic upstream. | +| foregroundbackground_mac.mm | `ForegroundBackground::eventFilter` (macOS impl) | Implements the Qt event filter: on `QEvent::Show` calls `[CocoaProcessType ToForeground]`, on `QEvent::Close` calls `[CocoaProcessType ToBackground]`, bridging Qt window events to the Cocoa process-type toggle. | foregroundbackground_interface.h, foregroundbackground_cocoa.h | Generic upstream. | +| callstatechecker.cpp | `OCC::CallStateChecker` (impl) | Periodically polls the Nextcloud Talk (Spreed) OCS API for a given call token to detect when the call is abandoned or when the local user has joined, then stops the "incoming call" notification accordingly. | callstatechecker.h, account.h, networkjobs.h | Generic upstream Nextcloud Talk integration; unrelated to macOS/Cocoa, grouped here as "misc". | +| callstatechecker.h | `OCC::CallStateChecker` (QObject, declaration) | Declares the QML-exposed properties (`token`, `accountState`, `checking`) and signals/slots used to drive call-state polling and notification dismissal for incoming Talk calls. | callstatechecker.cpp, accountstate.h | Generic upstream. | diff --git a/.claude/context/gui/SesComponents/CLAUDE.md b/.claude/context/gui/SesComponents/CLAUDE.md new file mode 100644 index 0000000000000..dfa0779a19448 --- /dev/null +++ b/.claude/context/gui/SesComponents/CLAUDE.md @@ -0,0 +1,20 @@ +# src/gui/SesComponents + +This folder holds small, fork-specific UI/logic components used across the GUI, kept separate from upstream Nextcloud code so they survive upstream merges with minimal conflicts. "Ses" is this team's internal ticket/component prefix (BRICKMAKERS work on the IONOS/HiDrive Next/STRATO whitelabel fork). All four files here are fork-specific — there is no upstream equivalent folder. + +## Key classes/components + +- `SesErrorBox.qml` — reusable inline error banner (icon + bold "Error" title + wrapped message text), styled via `Style.ses*` tokens; used e.g. in `filedetails/ShareDetailsPage.qml` to show password errors. +- `SesTrayHeader.qml` — custom tray-popup header bar (account menu button, "Website" link, local/team-folder button, optional "featured app"/assistant button); embedded into `tray/MainWindow.qml` as `trayWindowHeaderBackground`, replacing/restyling the upstream Nextcloud tray header row for the whitelabel look. +- `syncdirvalidation.h` / `syncdirvalidation.cpp` — `SyncDirValidator` class: on Windows, checks that a chosen sync directory is not the app's own `%AppData%` roaming path (to avoid users pointing sync at the app's own data folder); on other OSes it's a no-op always returning valid. Used by `folderwizard.cpp` and `wizard/owncloudadvancedsetuppage.cpp` during local-folder selection. + +## How it fits together + +The two QML components are drop-in replacements/extensions for tray and sharing UI, pulled in via relative QML imports (`import "../tray/"` etc.) and the shared `Style` singleton for whitelabel theming; `SyncDirValidator` is an unrelated plain C++ helper reused by folder-setup wizards to validate a picked path. All three are additive fork logic, not modifications of upstream files. + +## Fork-specific notes + +- Entire folder is fork-only (BRICKMAKERS/IONOS-HiDrive Next fork), no upstream Nextcloud equivalent directory exists. +- `SesTrayHeader.qml` specifically substitutes for/duplicates parts of what would otherwise be inline header markup in upstream `tray/MainWindow.qml`. + +*Quelle: src/gui/SesComponents — Stand 2026-08-20, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/branch-changes-vs-develop_stable-4.0.md b/.claude/context/gui/branch-changes-vs-develop_stable-4.0.md new file mode 100644 index 0000000000000..b2cfe776104aa --- /dev/null +++ b/.claude/context/gui/branch-changes-vs-develop_stable-4.0.md @@ -0,0 +1,258 @@ +# Component-Context: alle vom Feature-Branch geänderten GUI-Komponenten (ggü. Basis `develop_stable-4.0`) + +Erzeugt am 2026-08-24 mit dem [component-context](../../skills/component-context/SKILL.md)-Skill, angewendet auf jede echte UI-Komponente (Dialog/Widget/Delegate/QML-View), die im Diff `HEAD` vs. `origin/develop_stable-4.0` unter `src/gui/` auftaucht. + +**Update 2026-08-24 (nach dem Merge):** `feature/SES-578_DarkMode_Tray` wurde per `e7a6ff00a` in `develop_stable-33.0` gemerged (zusammen mit `bugs/SES-596_LINUX_Vereinheitlichung_der_Schriftart`). `HEAD` bezieht sich seither auf `develop_stable-33.0`. Die Analyse wurde wiederholt und gegen den vorherigen Stand (Basis: `feature/SES-578_DarkMode_Tray`-Tip `cba5a70e8`) abgeglichen — Ergebnis siehe **Merge-Konsistenz-Check** unten. Ursprünglich 138 Diff-Dateien → 45 Komponenten; nach dem Merge 145 Diff-Dateien → 51 Komponenten (6 neu, ausschließlich durch SES-596 bedingt, keine Verluste). + +**Wichtig zur Richtung:** `develop_stable-4.0` ist **nicht** ein künftiges Merge-Ziel, sondern die **Basis, von der dieser Feature-Branch abgezweigt wurde** (letzter Entwicklungsstand vor SES-578) — und dieser Branch **wird nicht mehr gepflegt**. Es findet also kein künftiger Merge in diese Richtung statt, auch keiner "irgendwann noch nachzuziehender". Formulierungen wie "noch nicht in `develop_stable-4.0`" sind daher irreführend und wurden unten korrigiert, wo sie das nahelegten. Der Diff zeigt ausschließlich **alles, was dieser Branch seit dem (endgültigen) Abzweigen selbst verändert hat** — nützlich als Übersicht des eigenen Änderungsumfangs (z. B. für Review/PR-Beschreibung), nicht als Merge-Risiko-Einschätzung gegen ein Ziel. Reine Backend-/Model-/Platform-Dateien ohne eigene UI (`accountmanager.cpp`, `folderman.cpp`, `socketapi.cpp`, macOS-FileProvider-Backend-`.mm`, `main.cpp` etc.) wurden bewusst ausgeschlossen. + +**Methodik-Hinweis:** Der Klickpfad/die Einordnung wurde, wo möglich, aus der bestehenden `.claude/context/gui/`-Doku übernommen (dort mit "laut Doku" markiert) statt für jede Komponente einzeln neu aus dem Code hergeleitet — das wäre bei dieser Menge nicht mehr leistbar gewesen. Wo keine bestehende Doku existierte, wurde gezielt nachgeschaut (vermerkt). **Dieses Dokument ist ein einmaliges Sammel-Ergebnis, keine von einem Skill gepflegte Datei** (der `component-context`-Skill selbst schreibt/pflegt bewusst kein eigenes Registry-File — siehe dessen SKILL.md) — es veraltet wie jede Momentaufnahme und wird nicht automatisch nachgezogen. + +## Merge-Konsistenz-Check (Consistency-Methode) + +Um sicherzustellen, dass der Merge `feature/SES-578_DarkMode_Tray` → `develop_stable-33.0` keine SES-578-Arbeit stillschweigend verändert/verloren hat, wurde die Analyse unabhängig wiederholt und systematisch mit dem vorherigen Ergebnis verglichen, statt dem neuen Diff blind zu vertrauen: + +1. **Datei-Diff neu berechnet:** `git diff --name-only HEAD origin/develop_stable-4.0 -- src/gui` vorher (138 Dateien, Basis `cba5a70e8`) gegen nachher (145 Dateien, Basis `e7a6ff00a`) verglichen. Ergebnis: **7 Dateien neu, keine entfernt.** +2. **Jede neu in den Diff gerutschte Datei erklärt** (Klarstellung: keine dieser 7 Dateien wurde neu erstellt — alle existierten schon vorher, teils seit Jahren, z. B. `main.cpp` seit 2011; sie waren nur bis dahin byte-identisch mit `develop_stable-4.0` und sind erst jetzt zum ersten Mal davon abgewichen): `git diff cba5a70e8 origin/develop_stable-4.0 -- ` ergab für alle 7 (`FileDetailsPage.qml`, `main.cpp`, `ActivityItem.qml`, `IconButton.qml`, `SecondaryPillButton.qml`, `TrayFolderListItem.qml`, `TrayFoldersMenuButton.qml`) **0 Diff-Zeilen unmittelbar vor** `70a6a94dd` ("SES-596 fix undefined font fallback on Linux/macOS") — und dieser eine Commit ist laut `git show --stat 70a6a94dd` auch tatsächlich der einzige, der alle 7 anfasst. Kein Zusammenhang mit einer Merge-Konfliktauflösung. +3. **Überlapp mit dem alten 138-Datei-Set geprüft:** `git diff --name-only cba5a70e8 HEAD -- src/gui` ergab 11 seither veränderte Dateien, davon 4 aus dem ursprünglichen Set (`basetheme.h`, `ActivityItemContent.qml`, `CurrentAccountHeaderButton.qml`, `UnifiedSearchResultListItem.qml`). Jede dieser 4 Diffs einzeln gelesen: durchweg additive Font-Fallback-Änderungen (`Utility::isWindows() ? "Segoe UI" : "Open Sans"`, `font.family: Style.sesOpenSansRegular`, `font: parent.font`) — **keine SES-578-Dark-Mode-Logik berührt, keine Farbwerte verändert.** +4. **Fazit:** Merge ist **konsistent und unauffällig** — die einzige Veränderung ggü. dem vorherigen Analysestand ist die vollständig erklärbare, additive SES-596-Font-Fallback-Arbeit. Keine der bestehenden 45 Komponenten-Einträge unten musste inhaltlich korrigiert werden (nur "Letzte Änderungen" ergänzt, wo betroffen). Auffällig, aber unkritisch: `CurrentAccountHeaderButton.qml` — als "funktional inaktiv" dokumentiert, wurde aber trotzdem vom globalen Font-Fallback-Fix mitgezogen (spricht für einen breiten, dateibasierten statt render-baum-basierten Fix-Ansatz in SES-596, keine Reaktivierung der Datei). + +## Legende + +- **Status**: aktiv genutzt | ungenutzt/legacy (aber auf stable geführt) | gelöscht (nur zur Vollständigkeit erwähnt) +- **Diff-Grund**: warum sich die Datei gegenüber der Basis `develop_stable-4.0` geändert hat (SES-578-Dark-Mode-Fix, ältere fork-eigene Abweichung, oder schlicht ein seit dem Abzweigen nachgezogener Versionsstand) + +--- + +## App & Connection Settings (Root-Cluster) + +### AccountSettings (`accountsettings.cpp/.h/.ui`) +**Navigation:** Tray → Account-Menü → Zahnrad/Einstellungen → Settings-Dialog-Toolbar → Account-Tab (pro Account einer) +**Bisherige Entscheidungen:** 2026-08-21 Card-Panel-Hintergründe wieder entfernt (SES-578); `connectionSettingsPanel`-Sichtbarkeitsentscheidung (siehe NetworkSettings unten) wirkt hier rein. +**Doku:** COMPONENTS.md Zeile 14 — "Fork-specific: `whitelabeltheme.h`/`WLTheme`, `#ifndef IONOS_BUILD`-Guard, `STRATO_WL_BUILD`-ExpandMemory-Link". +**Letzte Änderungen:** `cba5a70e8`, `ffeb8e7e4`, `dd2b5fcc4`, `b9e530504` — alle SES-578 Dark-Mode-Feinschliff. +**Diff-Grund:** SES-578-Änderungen seit dem Abzweigen von `develop_stable-4.0` (unmaintained, nichts davon fließt zurück). + +### GeneralSettings (`generalsettings.cpp/.ui`) +**Navigation:** Settings-Dialog-Toolbar → "Allgemein"-Tab +**Bisherige Entscheidungen:** 2026-08-12 zweimal (SES-576: erst als bewusster Redesign-Stand bestätigt, dann strukturell wieder an stable-33.0 angeglichen — erster Eintrag überholt); 2026-08-21 Card-Panel-Revert (SES-578). +**Doku:** COMPONENTS.md Zeile 71 — "prime merge-conflict hotspot", GA4-Tracking auf fast jedem Control, `IONOS_BUILD`/`STRATO_WL_BUILD`-Verzweigungen. +**Letzte Änderungen:** `dd2b5fcc4`, `b9e530504`, `80100217e`, `45446a124`. +**Diff-Grund:** SES-576-Strukturangleichung + SES-578-Dark-Mode — beides nach dem Abzweigen von `develop_stable-4.0` entstanden, kein Rückfluss dorthin vorgesehen. + +### NetworkSettings (`networksettings.cpp/.ui`) +**Navigation:** Settings-Dialog-Toolbar → "Netzwerk"-Tab, bzw. eingebettet als `connectionSettingsPanel` in AccountSettings +**Bisherige Entscheidungen:** **Kontext:** `connectionSettingsPanel` (NetworkSettings-Widget) in AccountSettings sollte nicht mehr sichtbar sein — Eintrag in DECISIONS.md Zeile 9 (Details dort, nicht dupliziert). +**Doku:** COMPONENTS.md Zeile 72 — "mostly generic upstream logic, minor whitelabel touch (`doNotUseProxy()`/`forceSystemNetworkProxy()`)". +**Letzte Änderungen:** überwiegend ältere Palette-/Hintergrund-Fixes (`293ac3836`, `4c79d7586`, `e6b62126b`), keine aktuellen SES-578-Commits. +**Diff-Grund:** ältere, bereits vor SES-578 bestehende Whitelabel-Abweichung. + +### SettingsDialog (`settingsdialog.cpp/.h/.ui`) +**Navigation:** Tray → Account-Menü → Zahnrad, oder Tray → Rechtsklick-Kontextmenü → "Einstellungen" +**Bisherige Entscheidungen:** keine direkten Treffer in DECISIONS.md unter diesem Namen (Kontrast-Fix-Eintrag 2026-08-17 betrifft das Account-Icon in der Toolbar, siehe unten separat). +**Doku:** COMPONENTS.md Zeile 70 — "Heavily fork-specific ... structurally diverges a lot from upstream's simpler settings dialog", bekannter Merge-Hotspot laut `.claude/context/gui/CLAUDE.md`. +**Letzte Änderungen:** `dd2b5fcc4`, `fa36ab8b0` (Min/Max-Buttons wiederhergestellt), `d4351e804`, `ad5512832` — alle SES-578. +**Diff-Grund:** SES-578 Dark-Mode-Arbeit am zentralen Settings-Fenster, größter struktureller Abstand zu `develop_stable-4.0`. + +### FolderStatusDelegate (`folderstatusdelegate.cpp/.h`) +**Navigation:** Settings-Dialog → Account-Tab → Ordnerliste (kein eigener Dialog, nur die Zeilen-Darstellung der Ordnerliste) +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 41 — "Uses whitelabeltheme.h for custom styling"; merge-drift-map: 72% Diff-Anteil, **nicht vollständig geprüft**. +**Letzte Änderungen:** `8534f6865` (Tint-Helper-Konsolidierung), `4bfe75f6b`, `45446a124`. +**Diff-Grund:** SES-578-Feinschliff; siehe merge-drift-map für offenen Vertiefungsbedarf. + +### FolderWizard (`folderwizard.cpp/.h`) +**Navigation:** Tray/Settings → "Ordner hinzufügen" → mehrseitiger Wizard (lokaler Pfad → Remote-Pfad → Selective Sync/VFS) +**Bisherige Entscheidungen:** keine direkten Treffer (aber `ModernStyle`-Regression-Fix `1f735ebbd` "fixing a SES-457 regression" ist im Git-Log dokumentiert). +**Doku:** COMPONENTS.md Zeile 48 — `IONOS_BUILD`-Ifdef für whitelabel-VFS-Checkbox-Setup. +**Letzte Änderungen:** `2d32bc2ab`, `1f735ebbd` — SES-578. +**Diff-Grund:** SES-578-Hintergrund-/Selective-Sync-Header-Fixes. + +### SelectiveSyncDialog (`selectivesyncdialog.cpp`) +**Navigation:** Account-Kontextmenü → "Ordner auswählen", oder Teil des FolderWizard-Flows +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 49 — "Includes whitelabeltheme.h for customizeStyle()". +**Letzte Änderungen:** `2d32bc2ab`, `4bfe75f6b`, `45446a124`. +**Diff-Grund:** SES-578. + +### IgnoreListTableWidget (`ignorelisttablewidget.cpp`) +**Navigation:** Settings-Dialog → Allgemein → "Ignorierte Dateien bearbeiten" → Tabellen-Widget innerhalb des `IgnoreListEditor`-Dialogs +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 51 — "Generic Nextcloud upstream" (Basis-Widget), aber merge-drift-map vermerkt eine echte Logikänderung (`slotAddPattern()` von statischem `QInputDialog::getText()` auf Instanz umgebaut) unter überwiegend additivem Styling-Diff — **Vorsicht bei künftigen Merges**. +**Letzte Änderungen:** `4bfe75f6b`, `45446a124`. +**Diff-Grund:** SES-578 + die o. g. Logikänderung. + +### AddCertificateDialog (`addcertificatedialog.cpp`) +**Navigation:** Login-Flow bei client-zertifikatsbasierter Authentifizierung → Zertifikat-Auswahl-Dialog +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 27 — generisch, keine Fork-Vermerke. +**Letzte Änderungen:** überwiegend alte Merges/SPDX-Migration, keine aktuellen SES-578-Commits. +**Diff-Grund:** reiner Versionsstand-Unterschied zu `develop_stable-4.0` (SES-551-Merge-Historie), keine dark-mode-spezifische Änderung erkennbar — **geringste Priorität dieser Liste**. + +### CaseClashFilenameDialog / InvalidFilenameDialog / ConflictDialog (`caseclashfilenamedialog.cpp`, `invalidfilenamedialog.cpp`, `conflictdialog.cpp`) +**Navigation:** Sync-Konflikt-Benachrichtigung (Tray-Activity oder automatisch bei erkanntem Konflikt) → jeweiliger Rename-/Konflikt-Dialog +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeilen 53/58/59 — generisch upstream, `invalidfilenamedialog.cpp` inkl. `whitelabeltheme.h`. +**Letzte Änderungen:** alle drei zuletzt via `45446a124` (dialogBackgroundColor/titleColor-Rollout SES-578). +**Diff-Grund:** SES-578, gebündelt über den gemeinsamen `dialogBackgroundColor()`/`titleColor()`-Refactor. + +### LegacyAccountSelectionDialog (`legacyaccountselectiondialog.cpp`) +**Navigation:** Erststart/Migration von einem älteren Client → Checklisten-Dialog zur Auswahl der zu importierenden Legacy-Accounts +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 20 — "Fork-specific: styled via whitelabeltheme.h/WLTheme und buttonstyle.h". +**Letzte Änderungen:** `45446a124`, `be5a49f7a` (SES-554), `f389830ee` (SES-493 Legacy-Import-Darkmode). +**Diff-Grund:** SES-578 + ältere SES-493-Dark-Mode-Historie. + +### LinkButton (`linkbutton.cpp/.h`) +**Navigation:** kein eigener Klickpfad — wiederverwendetes Hyperlink-Label, u. a. in GeneralSettings/DataProtection-Seiten eingebettet. +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 133 — **kürzlich korrigiert** (in diesem Gespräch, s. gui-context-refresh-Lauf heute): war fälschlich als "kein Fork-Marker" gelistet, nutzt aber `WLTheme.settingsLinkColor()`. +**Letzte Änderungen:** `cba5a70e8` (Re-Apply on `changeEvent()`, SES-578). +**Diff-Grund:** SES-578. + +### sesSnackBar (`sessnackbar.cpp/.h`) +**Navigation:** kein eigener Klickpfad — erscheint kontextabhängig als Benachrichtigungsleiste (z. B. Fehler-/Erfolgsmeldung) in mehreren Dialogen. +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** COMPONENTS.md Zeile 129 — "Fork-specific, not present upstream". +**Letzte Änderungen:** `cba5a70e8` (Zustand jetzt getrackt, re-styled on `changeEvent()`), `8534f6865`, `4bfe75f6b`. +**Diff-Grund:** fork-only (`n/a` bei stable-33.0 in merge-drift-map) + SES-578-Reaktivität. + +--- + +## Wizard (`src/gui/wizard/`) + +*Basis-Doku bereits vollständig in [`wizard/CLAUDE.md`](wizard/CLAUDE.md).* + +### DataProtectionPage / DataProtectionSettingsPage +**Navigation:** Account-Setup-Wizard, Flow2-Auth-Zweig (`IONOS_BUILD`) → nach Credentials → Datenschutz-Consent-Seite → optional "Einstellungen" → Detail-Seite +**Bisherige Entscheidungen:** keine Treffer zu SES-578; ältere SES-391/SES-522-Einträge im Git-Log (Connect-Mehrfachausführung, STRATO-Übersetzung) — nicht in DECISIONS.md, da reine Bugfixes. +**Doku:** wizard/CLAUDE.md — "whitelabel consent/tracking notice (IONOS/STRATO)". +**Letzte Änderungen:** beide `45446a124` (SES-578). +**Diff-Grund:** SES-578 + fork-only (existiert nicht auf `origin/stable-33.0`, laut merge-drift-map). + +### OwncloudAdvancedSetupPage +**Navigation:** Account-Setup-Wizard → letzte Seite: lokaler Ordner, Sync-Modus, Quota +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** wizard/CLAUDE.md — "final page; local folder picker, sync-everything/selective-sync/VFS radio choice". +**Letzte Änderungen:** `cba5a70e8` (Q_OS_MAC-Guard für `syncModeLabel` entfernt, SES-578). +**Diff-Grund:** SES-578. + +### OwncloudConnectionMethodDialog / OwncloudSetupPage / OwncloudWizard / Flow2AuthCredsPage / Flow2AuthWidget +**Navigation:** Account-Setup-Wizard, jeweils frühe Server-URL-/Auth-Auswahl-Schritte bzw. der Wizard-Container selbst +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** wizard/CLAUDE.md deckt alle fünf ab — größtenteils generischer Upstream-Code, keine SES-578-Berührung. +**Letzte Änderungen:** überwiegend `b7d4a8b53` (SES-551-Merge) oder älter — **keine aktuellen SES-578-Commits**. +**Diff-Grund:** reiner Versionsstand-Unterschied zu `develop_stable-4.0`, nicht dark-mode-getrieben — niedrige Priorität. + +--- + +## SesComponents (fork-eigen, kein Upstream-Pendant) + +### SesErrorBox.qml +**Navigation:** z. B. `filedetails/ShareDetailsPage.qml` bei Passwort-Fehlern +**Bisherige Entscheidungen:** keine direkten Treffer. +**Doku:** SesComponents/CLAUDE.md — "reusable inline error banner". +**Letzte Änderungen:** `8534f6865` (Tint-Helper-Konsolidierung), `cd9d7535e`. +**Diff-Grund:** fork-only. + +### SesTrayHeader.qml +**Navigation:** Tray-Popup-Header (Account-Menü-Button, "Website"-Link, Ordner-Button) +**Shadow-Component:** Fork-Ersatz für `TrayWindowHeader.qml` — siehe shadow-component-watch/registry.md, zuletzt geprüft `568cbe171`, keine offenen Portierungs-Kandidaten. +**Doku:** SesComponents/CLAUDE.md + tray/CLAUDE.md. +**Letzte Änderungen:** `7cc9c7e88` (SES-589). +**Diff-Grund:** fork-only. + +--- + +## Tray (`src/gui/tray/`) + +*Basis-Doku bereits vollständig in [`tray/CLAUDE.md`](tray/CLAUDE.md).* + +### MainWindow.qml +**Navigation:** Tray-Icon-Klick → gesamtes Popup-Fenster +**Bisherige Entscheidungen:** 2026-08-17 `panelBackgroundColor()` entfernt, Settings/Tray-Hintergrund vereinheitlicht. +**Letzte Änderungen:** `26fe47ea7` (SES-579, Unified Search reaktiviert). +**Diff-Grund:** mehrere SES-579/SES-578-Runden. + +### TrayWindowAccountMenu.qml / UserLine.qml +**Navigation:** Tray → Header → Account-Button → Dropdown mit Account-Zeilen +**Shadow-Component:** Fork-Ersatz für `CurrentAccountHeaderButton.qml` — registriert, zuletzt geprüft `60e0cb196`, keine offenen Punkte (User-Status-Feature bewusst nicht portiert, SES-50). +**Bisherige Entscheidungen:** Chevron-Dark-Mode-Fix (`9647f1174`), Status-Feature-Abweichung (SES-50), beide 2026-08-17/18. +**Letzte Änderungen:** `9647f1174`, `78b411a57` (SES-579), `7de66b861`/`b84dca7b4` (SES-579 Layout-Fixes). +**Diff-Grund:** SES-578/579 kombiniert, hoher Diff-Anteil laut merge-drift-map (`UserLine.qml` ~110%). + +### AccountMenuItem.qml / ActivityItemContent.qml / SyncStatus.qml / UnifiedSearchInputContainer.qml / UnifiedSearchResultListItem.qml / UserStatusMessageView.qml +**Navigation:** jeweils Teil des Tray-Popup-Baums (Account-Menü-Zeile, Activity-Feed-Eintrag, Sync-Status-Icon, Unified-Search-Feld/-Ergebnis, User-Status-Anzeige) — Details siehe tray/CLAUDE.md. +**Bisherige Entscheidungen:** Unified-Search-Reaktivierung 2026-08-18 (SES-579) betrifft `UnifiedSearchInputContainer.qml` direkt. +**Letzte Änderungen:** alle zuletzt via `f3477411d` (SES-578 Tray-Dark-Mode) oder `26fe47ea7` (SES-579); `ActivityItemContent.qml`/`UnifiedSearchResultListItem.qml` zusätzlich `70a6a94dd` (SES-596 Font-Fallback, siehe Merge-Konsistenz-Check). +**Diff-Grund:** SES-578/579 (+ SES-596 bei den beiden genannten). + +### CurrentAccountHeaderButton.qml / TrayWindowHeader.qml (Legacy) +**Status:** laut tray/CLAUDE.md **ungenutzt** (nicht im aktiven Render-Baum), aber **bewusst nicht gelöscht** — `origin/stable-33.0` führt beide Dateien noch aktiv (Entscheidung 2026-08-17, "Grundsatz: toter Code bleibt erhalten, solange stable ihn noch führt"). +**Letzte Änderungen:** `CurrentAccountHeaderButton.qml` zusätzlich `70a6a94dd` (SES-596 Font-Fallback — mitgezogen, obwohl inaktiv, siehe Merge-Konsistenz-Check). +**Diff-Grund:** rein struktureller Altbestand, keine aktive Weiterentwicklung nötig — **niedrigste Priorität**, ggf. sogar ignorierbar für einen `develop_stable-4.0`-Abgleich, da funktional inaktiv. + +### Neu im Diff seit dem Merge (nicht neu erstellt, s. Merge-Konsistenz-Check Punkt 2): ActivityItem.qml / IconButton.qml / SecondaryPillButton.qml / TrayFolderListItem.qml / TrayFoldersMenuButton.qml +**Navigation:** Teil des Tray-Popup-Baums — Activity-Feed-Zeile, generischer Icon-Button-Baustein, sekundäre Pill-Button-Variante, Ordner-Picker-Popup-Zeile bzw. -Button (Details siehe tray/CLAUDE.md, alle dort bereits als Bausteine gelistet). +**Bisherige Entscheidungen:** keine Treffer (nur eine beiläufige Erwähnung von `TrayFoldersMenuButton.qml` als Analogie-Beispiel im Chevron-Fix-Eintrag vom 2026-08-18, keine eigene Entscheidung dazu). +**Letzte Änderungen:** ausschließlich `70a6a94dd` (SES-596 Font-Fallback) — vorher gab es zu diesen 5 Dateien keinen Unterschied zu `develop_stable-4.0`. +**Diff-Grund:** ausschließlich SES-596, nicht SES-578 — vor dem Merge nicht im Diff, da diese Dateien bis dahin identisch mit der Basis waren. + +--- + +## macOS FileProvider UI (`src/gui/macOS/ui/`) + +*Basis-Doku bereits vollständig in [`macOS/CLAUDE.md`](macOS/CLAUDE.md).* + +### FileProviderSettings.qml / FileProviderFileDelegate.qml / FileProviderStorageInfo.qml / FileProviderSyncStatus.qml / FileProviderEvictionDialog.qml +**Navigation:** Settings-Dialog → Account-Tab → "Virtual Files"-Einstellungen (macOS-only) bzw. Datei-Detailansicht/Eviction-Liste +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** macOS/CLAUDE.md — Backing-Klassen `FileProviderSettingsController`/`FileProviderItemMetadata`. +**Letzte Änderungen:** überwiegend ältere Button-Farb-Fixes (`f05f53c56` SES-482, `996ae46c6` SES-467) oder generische Upstream-Fixes (`9d2333c4e`, `89104e77b`) — **keine aktuellen SES-578-Commits**. +**Diff-Grund:** reiner Versionsstand-Unterschied, macOS-only, niedrige Priorität für diesen (Windows-fokussierten) Dark-Mode-Branch. + +--- + +## Sonstiges + +### ShareeSearchField.qml (`filedetails/`) +**Navigation:** Datei-Detailansicht → Freigabe-Tab → Personen-Suchfeld +**Bisherige Entscheidungen:** 2026-08-18 Unified-Search-Wiederaktivierung referenziert dieses Feld als Farbvorbild. +**Letzte Änderungen:** `cd9d7535e` (SES-578). +**Diff-Grund:** SES-578 + Namespace-Rename (merge-drift-map: "mittel"-Risiko). + +### FileDetailsPage.qml (`filedetails/`, neu seit dem Merge) +**Navigation:** Datei-Detailansicht (Freigabe-/Aktivitäts-Tab-Container) +**Bisherige Entscheidungen:** keine Treffer. +**Letzte Änderungen:** ausschließlich `70a6a94dd` (SES-596 Font-Fallback) — vorher kein Unterschied zu `develop_stable-4.0`. +**Diff-Grund:** ausschließlich SES-596. + +### FileActionsWindow.qml (`integration/`) +**Navigation:** Tray/Explorer-Kontextmenü → serverdefinierte Datei-Aktion → Popup-Fenster mit Aktionsliste +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** integration/CLAUDE.md — "fairly new, generic Nextcloud functionality, not obviously fork-specific". +**Letzte Änderungen:** generische Upstream-Fixes (`dc844a287`, `cfbd9042f`), keine SES-578-Berührung. +**Diff-Grund:** reiner Versionsstand-Unterschied, niedrige Priorität. + +### WebFlowCredentialsDialog (`creds/`) +**Navigation:** Login/Re-Auth-Flow → Anmeldedialog (hostet Flow2AuthWidget oder WebEngine-View) +**Bisherige Entscheidungen:** keine Treffer. +**Doku:** creds/CLAUDE.md — "styles itself via whitelabeltheme.h/WLTheme ... BRICKMAKERS whitelabel-branding code". +**Letzte Änderungen:** generische Upstream-Fixes, keine SES-578-Berührung. +**Diff-Grund:** reiner Versionsstand-Unterschied. + +--- + +## Zusammenfassung: Änderungsumfang dieses Branches gegenüber seiner Basis `develop_stable-4.0` + +Diese Einordnung beschreibt **wie viel eigene Arbeit** in jeder Komponente steckt, seit der Branch von `develop_stable-4.0` abgezweigt wurde — z. B. relevant für eine Review-/PR-Beschreibung oder um abzuschätzen, welche Komponenten am stärksten vom SES-578-Umbau betroffen sind. **Keine Merge-Zielrichtung, keine Konflikt-Prognose** (dafür `stable-merge-check`/`merge-drift-map` gegen den tatsächlichen Referenz-Branch `stable-33.0`). + +**SES-578-Kernarbeit** (größter Änderungsumfang ggü. der Basis, überwiegend Dark-Mode-getrieben): SettingsDialog, GeneralSettings, AccountSettings, sesSnackBar, LinkButton, FolderStatusDelegate, MainWindow.qml, TrayWindowAccountMenu.qml/UserLine.qml, SesErrorBox.qml, ShareeSearchField.qml, DataProtectionPage/-SettingsPage. + +**SES-578 mitgezogen, aber selbst nicht Kernthema**: FolderWizard, SelectiveSyncDialog, IgnoreListTableWidget, CaseClashFilenameDialog/InvalidFilenameDialog/ConflictDialog, LegacyAccountSelectionDialog, OwncloudAdvancedSetupPage, restliche Tray-QML-Dateien (AccountMenuItem, ActivityItemContent, SyncStatus, UnifiedSearch*, UserStatusMessageView). + +**Änderung liegt vor SES-578** (ältere, seit dem Abzweigen von `develop_stable-4.0` bereits bestehende Abweichung, keine aktuelle SES-578-Berührung): AddCertificateDialog, NetworkSettings, alle macOS-FileProvider-QML-Dateien, FileActionsWindow.qml, WebFlowCredentialsDialog, OwncloudConnectionMethodDialog/OwncloudSetupPage/OwncloudWizard/Flow2AuthCredsPage/Flow2AuthWidget. + +**Bewusst funktional inaktiv, nicht anfassen**: CurrentAccountHeaderButton.qml, TrayWindowHeader.qml (auf stable noch geführt, bewusst nicht gelöscht). + +**Neu seit dem Merge, ausschließlich SES-596** (Font-Fallback-Fix, keine Dark-Mode-Berührung): FileDetailsPage.qml, ActivityItem.qml, IconButton.qml, SecondaryPillButton.qml, TrayFolderListItem.qml, TrayFoldersMenuButton.qml. diff --git a/.claude/context/gui/cloudproviders/CLAUDE.md b/.claude/context/gui/cloudproviders/CLAUDE.md new file mode 100644 index 0000000000000..2056cb62b52b9 --- /dev/null +++ b/.claude/context/gui/cloudproviders/CLAUDE.md @@ -0,0 +1,19 @@ +# src/gui/cloudproviders + +Linux-only desktop integration with the freedesktop.org `libcloudproviders`/GNOME Online Accounts style "cloud providers" D-Bus interface, letting file managers (e.g. GNOME Files/Nautilus) show native sync status and a context menu for each configured Nextcloud account/folder. Compiled only when CMake finds libcloudproviders (`WITH_LIBCLOUDPROVIDERS`, see `src/gui/CMakeLists.txt`). + +## Key classes/components + +- `CloudProviderManager` (`cloudprovidermanager.h/.cpp`) — owns the D-Bus name/provider exporter (`g_bus_own_name`), keeps a `QMap` in sync with `FolderMan`'s folder list (`slotFolderListChanged`), creating/destroying one wrapper per sync folder. +- `CloudProviderWrapper` (`cloudproviderwrapper.h/.cpp`) — wraps one `OCC::Folder` as a `CloudProvidersAccountExporter`: builds its GMenu (open desktop app, open in browser, recent files, pause sync, help/settings/logout/quit), an action group handling those GActions, and pushes live status text from `ProgressDispatcher`/`Folder` signals (sync started/finished/paused, progress). +- `cloudproviderconfig.h.in` — CMake-configured header defining the D-Bus bus name/object path constants (`LIBCLOUDPROVIDERS_DBUS_BUS_NAME/OBJECT_PATH`) used by the manager. + +## How it fits together + +`CloudProviderManager` is instantiated once (from `owncloudgui.cpp`) and acts as the top-level bridge between OCC's `FolderMan`/`Folder` model and glib/GIO's cloud-providers D-Bus API; it creates a `CloudProviderWrapper` per folder, and each wrapper independently listens to its `Folder`'s Qt signals and translates them into GObject/GVariant calls understood by GNOME's file manager integration. + +## Fork-specific notes + +- Entirely generic/upstream-style Nextcloud Linux integration (SPDX headers say "Nextcloud GmbH and contributors", 2017) — not fork-specific, though it does read `APPLICATION_NAME`/`APPLICATION_ICON_NAME` theme macros so the exported label/icon follow whitelabel branding. + +*Quelle: src/gui/cloudproviders — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/creds/CLAUDE.md b/.claude/context/gui/creds/CLAUDE.md new file mode 100644 index 0000000000000..d3c9123af834f --- /dev/null +++ b/.claude/context/gui/creds/CLAUDE.md @@ -0,0 +1,22 @@ +# src/gui/creds + +This folder implements account authentication and credential storage for the desktop client: obtaining a username/password (or app password/OAuth-style token) from the server, persisting it securely in the OS keychain, and re-authenticating GUI dialogs when credentials become invalid. It builds on the credential abstractions in `src/libsync/creds` (`AbstractCredentials`, `HttpCredentials`) and adds the GUI-facing dialogs/flows. + +## Key classes/components + +- `CredentialsFactory` (`credentialsfactory.h/.cpp`) — factory function `create(type)` that instantiates the right `AbstractCredentials` subclass ("http" → `HttpCredentialsGui`, "webflow" → `WebFlowCredentials`, else `DummyCredentials`). +- `HttpCredentialsGui` (`httpcredentialsgui.h/.cpp`) — GUI variant of basic-auth `HttpCredentials`; shows a `QInputDialog` password prompt (`askFromUser`) and builds the "request an app password" link for older server versions. +- `Flow2Auth` (`flow2auth.h/.cpp`) — implements Nextcloud "Login Flow v2": POSTs to `/index.php/login/v2`, opens the browser (or copies the link) for the user to authorize, then polls the server until an app password is returned; emits `result()`/`statusChanged()`. +- `WebFlowCredentials` (`webflowcredentials.h/.cpp`) — `AbstractCredentials` implementation for the "webflow" auth type; manages username/password/client-cert storage in the keychain (via `KeychainChunk` read/write jobs, chunked to work around Windows credential size limits), injects Basic-Auth headers and client SSL certs through a custom `WebFlowCredentialsAccessManager` (subclass of `AccessManager`), and triggers re-auth (`askFromUser`) by opening `WebFlowCredentialsDialog`. +- `WebFlowCredentialsDialog` (`webflowcredentialsdialog.h/.cpp`) — the `QDialog` shown for (re-)login; hosts either a `Flow2AuthWidget` (Login Flow v2) or, if built `WITH_WEBENGINE`, an embedded `WebView` for the legacy web login flow; forwards captured credentials via `urlCatched()`. + +## How it fits together + +`CredentialsFactory` picks the credentials class based on the account's stored auth `type`. `WebFlowCredentials` is the modern default; when it needs interactive login it opens `WebFlowCredentialsDialog`, which in turn drives `Flow2Auth` (or the WebEngine view) to obtain a token/app-password, then reports back through `slotAskFromUserCredentialsProvided`, after which `WebFlowCredentials::persist()` writes everything to the keychain via `KeychainChunk` jobs. `HttpCredentialsGui` is a simpler legacy path used for plain HTTP basic auth. + +## Fork-specific notes + +- `WebFlowCredentialsDialog` styles itself via `whitelabeltheme.h`/`WLTheme` (dialog background, fonts, colors) instead of upstream Nextcloud's default Qt styling — this is BRICKMAKERS whitelabel-branding code. +- `Flow2Auth`, `HttpCredentialsGui`, `WebFlowCredentials`, and `CredentialsFactory` are otherwise essentially unmodified upstream Nextcloud/ownCloud logic (Login Flow v2, keychain persistence, HTTP basic auth). + +*Quelle: src/gui/creds — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/filedetails/CLAUDE.md b/.claude/context/gui/filedetails/CLAUDE.md new file mode 100644 index 0000000000000..94513ae3c569f --- /dev/null +++ b/.claude/context/gui/filedetails/CLAUDE.md @@ -0,0 +1,71 @@ +# src/gui/filedetails + +QML + C++ backend for the "file details" side panel/window: a per-file view showing file +info (icon, size, modified date, lock state, tags) and a "Sharing" tab for managing +Nextcloud shares (links, users/groups, permissions, passwords, expiry, notes). Opened from +the tray context menu or Explorer/Finder overlay context menu for a selected synced file. + +## Key classes/components + +- **`FileDetails`** (`filedetails.h/cpp`) – C++ model backing the header area: resolves a + local path to its `Folder`/`SyncJournalFileRecord`, exposes name/size/last-changed/icon, + file-lock status (polls every 6s while locked), and the file's `FileTagModel` + (from `gui/filetagmodel.h`). Also exposes `sharingAvailable` (server capability check). +- **`ShareModel`** (`sharemodel.h/cpp`, large — 1502 lines in the .cpp, ~1770 combined) – the core sharing model: + `QAbstractListModel` wrapping `ShareManager` to list/create/delete/modify shares + (link, user/group, team, room, remote), handles password/expiry/note/permission toggles, + placeholder rows for "create link share" / secure-file-drop, and "shared with me" info. + Views: **`ShareView.qml`** (tab body: owner banner, `ShareeSearchField`, list of shares) + and **`ShareDelegate.qml`** (compact per-share row) / **`ShareDetailsPage.qml`** + (full-screen detail editor for one share, pushed onto the `StackView` when a share row + is tapped). +- **`SortedShareModel`** (`sortedsharemodel.h/cpp`) – `QSortFilterProxyModel` ordering + `ShareModel` rows (e.g. placeholder/internal links first) for `ShareView`'s `ListView`. +- **`ShareeModel`** (`shareemodel.h/cpp`) – autocomplete model that queries the server for + users/groups/teams to share with (local + "search globally" modes). View: + **`ShareeSearchField.qml`** (search `TextField` + popup) with **`ShareeDelegate.qml`** + as the suggestion row. +- **`DateFieldBackend`** (`datefieldbackend.h/cpp`, `OCC::Quick` namespace) – small helper + exposing a `QDate` with min/max bounds and locale string parsing for QML date entry; + used by **`NCInputDateField.qml`** (expiry-date picker in `ShareDetailsPage`). +- **`NCInputTextField.qml` / `NCInputTextArea.qml` / `NCTabButton.qml`** – small generic + styled input controls (password/note fields, permission-mode tabs) shared by + `ShareDetailsPage.qml` and `ShareView.qml`'s password dialog. +- **`FileTag.qml`** – single colored tag chip rendered in the file-details header, driven + by `FileDetails.fileTagModel`. +- **`FileActivityView.qml`** – wraps the generic `ActivityList` (from `../tray`) with a + `FileActivityListModel` filtered to one file's activity; an "Activity" tab intended as a + sibling of the sharing tab (see fork note below). +- **`FileDetailsPage.qml` → `FileDetailsView.qml` → `FileDetailsWindow.qml`** – the + assembly chain: `FileDetailsPage` is a `Page` with the header (icon/name/size/lock/tags) + and a `SwipeView` for the tab content; `FileDetailsView` wraps it in a `StackView` (so + `ShareDetailsPage` can be pushed on top); `FileDetailsWindow` wraps that in a standalone + `ApplicationWindow`. + +## How it fits together + +`Systray`/`ownCloudGui` (see `src/gui/systray.cpp`) instantiate `FileDetailsWindow.qml` +for a given `accountState`/`localPath` when the user picks "Share…" or "Activity" from a +file's tray/overlay context menu, and route `Systray::showFileDetailsPage` signals to +switch tabs on an already-open window. Inside, `FileDetailsPage` owns one `FileDetails` +instance (header data) and hosts tab content in a `SwipeView`; each `ShareDelegate` row +can push a `ShareDetailsPage` onto the shared `rootStackView` for full editing of that +share. `ShareModel` and `ShareeModel` are independent QAbstractListModels created per view +instance, parameterized by `accountState`/`localPath`. + +## Fork-specific notes + +- QML files import `com.strato.hidrivenext.desktopclient` (this build's whitelabel QML + module name) rather than a generic Nextcloud module name — check the active branding + target's module alias when porting code between forks. +- `FileDetailsPage.qml`'s `SwipeView` currently wires up only the `ShareView` tab; the + `Connections` handler still branches on `Systray.FileDetailsPage.Activity` and + references `fileActivityView.swipeIndex`, but no `FileActivityView` item exists in the + `SwipeView` — the Activity tab appears to have been stripped from the UI in this fork + while `FileActivityView.qml`/`FileActivityListModel` wiring in `systray.cpp` was left + in place (likely dead code path — candidate for cleanup, verify usages before removing). +- Styling throughout (`Style.ses*` properties, `SesErrorBox`, `../SesComponents`) is + fork-specific "SES" theming layered over otherwise-generic upstream Nextcloud sharing + logic (`ShareModel`, `ShareeModel`, `ShareManager` itself live in `src/gui` more broadly). + +*Quelle: src/gui/filedetails — Stand 2026-08-20, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/ga4/CLAUDE.md b/.claude/context/gui/ga4/CLAUDE.md new file mode 100644 index 0000000000000..5ab796d095866 --- /dev/null +++ b/.claude/context/gui/ga4/CLAUDE.md @@ -0,0 +1,19 @@ +# src/gui/ga4 + +Google Analytics 4 (Measurement Protocol) telemetry integration for the fork, used to send anonymous usage/click events from the desktop client. Files carry "This file is part of Nextcloud Destop - Ionos HiDrive Next" headers (sic, typo in the original) and explicit "Modifications" notes, meaning this is an adapted version of an (upstream/older ownCloud-style) GA integration, reworked to target the newer GA4 `/g/collect` endpoint instead of the old Universal Analytics Measurement Protocol. + +## Key classes/components + +- `GAnalytics` (`ganalytics.h/.cpp`) — thread-safe **singleton** (`GAnalytics::getInstance()`) public API: configure measurement ID, client ID, account, send interval, enable/disable, anonymize IPs, log level; `sendEvent()`/`sendEventImmediatley()` queue events. Delegates all real work to a private `GAnalyticsWorker`. +- `GAnalyticsWorker` (`ganalytics_worker.h/.cpp`) — the actual implementation ("d-pointer" of `GAnalytics`, declared `friend`): owns a `QQueue` of pending events, a `QTimer` that periodically POSTs them via `OCC::Account::sendRawRequest`, builds the GA4 query string (measurement id, client/session ids, user agent, screen resolution, language, event name, engagement time…), and drops events older than 4 hours. +- `DataCollectionWrapper` (`datacollectionwrapper.h/.cpp`) — the app-facing wrapper actually called from GUI code (settings, login/logout); exposes enums `TrackingPage`/`TrackingEvent`/`TrackingElement` (e.g. `GeneralSettings`, `PrivacyPolicy`, `AutoStart`) and slots `login()`, `accountRemoved()`, `clicked()`, `opened()`; maps them to string event/page names and forwards to `GAnalytics::getInstance()`. Also hardcodes the per-platform/per-buildtype GA4 measurement IDs (`GA_MEASUREMENT_ID`) in `initDataCollection()`. + +## How it fits together + +GUI code (settings dialogs, `application.cpp`) talks only to `DataCollectionWrapper`, which forwards typed tracking calls to the `GAnalytics` singleton, which hands them to its `GAnalyticsWorker` for queuing/batching/HTTP delivery to `www.google-analytics.com`. `setSendData(bool)` toggles the whole pipeline via the user's telemetry opt-in setting. + +## Fork-specific notes + +- The whole folder is a fork addition on top of Nextcloud (no such telemetry exists upstream); explicit code comments mark it as ported/modified for GA4 (a `Modifications:` block comment listing `* - Changed the usage of Measurement Protocol to the GA4 API`), and measurement IDs / `TODO SES-169` comments confirm it's BRICKMAKERS/IONOS-owned code. + +*Quelle: src/gui/ga4 — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/integration/CLAUDE.md b/.claude/context/gui/integration/CLAUDE.md new file mode 100644 index 0000000000000..77035e4adcbfb --- /dev/null +++ b/.claude/context/gui/integration/CLAUDE.md @@ -0,0 +1,18 @@ +# src/gui/integration + +Implements the "server-defined file actions" feature: lets the Nextcloud server describe custom context-menu actions for specific file types (via capabilities), and shows/executes them in a small popup window from the systray/shell-integration context menu. This is fairly new, generic Nextcloud functionality (2025 SPDX headers, `namespace OCC`), not obviously fork-specific. + +## Key classes/components + +- `FileActionsModel` (`fileactionsmodel.h/.cpp`) — `QAbstractListModel` (namespace `OCC`) driven by `accountState`, `localPath`, `fileId`, `remoteItemPath` properties. On `parseEndpoints()` it reads `account->capabilities().fileActionsByMimeType(mimeType)` to build a list of `FileAction` (icon/name/url/method/params), issues the chosen action as a `JsonApiJob` on `createRequest(row)`, and parses the server's JSON reply in `processRequest()` into a displayable `Response` (label + URL) shown as a clickable result button. +- `FileActionsWindow.qml` — the popup `ApplicationWindow` (frameless or normal window depending on `Systray.useNormalWindow`) that binds a `FileActionsModel`, lists available actions in a `ListView`, and shows the response link/button after an action completes; closes and opens the response URL in the browser via `Systray.openUrlInBrowser`. + +## How it fits together + +`systray.cpp`/`owncloudgui.cpp` create/show `FileActionsWindow.qml` (registered as a QML type) for a given file, which instantiates `FileActionsModel` bound to the current `AccountState`/file path/`fileId`; the model fetches server-capability-defined actions, executes the selected one via a network job, and reports results back into the QML window. `folderman.h` is referenced for locating the sync folder/journal record for a given local path (to resolve file IDs and remote paths). + +## Fork-specific notes + +- Nothing here appears fork-specific except cosmetic reuse of shared `Style`/whitelabel QML singletons (`import Style`, `com.nextcloud.desktopclient`); the feature itself, its model, and its network protocol look like generic (recent) upstream-style Nextcloud functionality carried into the fork. + +*Quelle: src/gui/integration — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/macOS/CLAUDE.md b/.claude/context/gui/macOS/CLAUDE.md new file mode 100644 index 0000000000000..362c24a4ec943 --- /dev/null +++ b/.claude/context/gui/macOS/CLAUDE.md @@ -0,0 +1,29 @@ +# src/gui/macOS + +macOS-specific GUI integration code: bridges between the cross-platform Qt/C++ desktop client and Apple's native APIs (FileProvider/VFS extension, XPC, sandboxed single-instance IPC). Files come in three flavors: plain `.h`/`.cpp` (portable, built on all platforms but effectively no-ops elsewhere), `.h`/`.mm` (Objective-C++ implementation of a C++-declared class, macOS-only), and `.m` (pure Objective-C). You only need to touch this folder when building, debugging, or extending macOS virtual-files/FileProvider behavior, the Finder-integrated settings UI, or macOS app-lifecycle quirks (single-instance, sandboxing). + +## Key classes/components + +- **`FileProvider`** (`fileprovider.h/_mac.mm`) — top-level singleton (`Mac::FileProvider::instance()`) that owns and wires up the domain manager, socket server, XPC client, and service; `available()` gates the feature off below macOS Sonoma. +- **`FileProviderDomainManager`** (`fileproviderdomainmanager.h/.mm` — no `_mac` suffix on the `.mm`) — creates/removes/reconnects one `NSFileProviderDomain` per account (the actual VFS mount registered with macOS); pImpl'd via nested `MacImplementation` to hide Objective-C from the header. +- **`FileProviderXPC`** + **`FileProviderXPCUtils`** (`fileproviderxpc_mac.h/.mm`, `fileproviderxpc_mac_utils.h/.mm`) — client→extension communication over XPC (domain reachability checks, authentication, ignore-list push, dirty-data queries); `FileProviderXPCUtils` is a namespace, not a class, and does the raw `NSXPCConnection`/`NSFileProviderManager` plumbing. +- **`FileProviderSocketServer`** / **`FileProviderSocketController`** (`fileprovidersocket*.h/.cpp/.mm`) — legacy extension→client channel over a `QLocalServer` UNIX socket (one controller per connection); handles auth requests and ignore-list delivery from the extension; marked TODO to be replaced by XPC. +- **`FileProviderService`** + `AppProtocol`/`ClientCommunicationProtocol` (`fileproviderservice.h/.mm`, symlinked `AppProtocol.h`/`ClientCommunicationProtocol.h`) — implements the app-side XPC service (`AppProtocol`) that extensions call into to report sync status or request the "file actions" dialog; the two protocol headers are symlinks into `shell_integration/MacOSX/.../FileProviderExt/Services/`, the single source of truth shared by app and extension. +- **`FileProviderSettingsController`** (`fileprovidersettingscontroller.h/_mac.mm` + `ui/FileProviderSettings.qml`) — singleton backing the "Virtual files settings" QML page; enables/disables VFS per account and migrates legacy (non-sandboxed) deployments via `migrateToAppSandbox()`. +- **`FileProviderItemMetadata`** (`fileprovideritemmetadata.h/.cpp/_mac.mm` + `ui/FileProviderFileDelegate.qml`) — `Q_GADGET` wrapper exposing `NSFileProviderItem` metadata (sync status, dates, owner, etc.) to QML for the file-details/eviction list. +- **`FileProviderEditLocallyJob`** (`fileprovidereditlocallyjob.h/.cpp/_mac.mm`) — resolves a relative path to an `ocId` via the server, then asks the FileProvider domain to open that item locally ("Edit locally" feature, macOS specialization of the cross-platform job). +- **`FileProviderUtils`** (`fileproviderutils.h/_mac.mm`) — grab-bag of Objective-C++ helpers (domain identifier validation, log/support directory lookup, sync `NSFileProviderManager` lookup); explicitly documented as "only use from `*_mac.mm` files". +- **`ProgressObserver`** (`progressobserver.h/.m`) — plain Objective-C KVO helper that observes an `NSProgress` and forwards changes via a block callback; used where upload/download progress needs bridging from Apple APIs. +- **`SingleInstanceManager`** (`singleinstancemanager_mac.h/.mm`) — replaces `KDSingleApplication` on macOS with a `QLocalSocket`-based guard in the sandbox container directory, working around a macOS 13 Ventura socket-path-length/sandbox bug; API-compatible with `KDSingleApplication` so `Application` needs no logic changes. + +## How it fits together + +`FileProvider` is the entry point instantiated once by `Application` on startup; it owns a `FileProviderDomainManager` (one VFS domain per account), a `FileProviderSocketServer` (legacy inbound channel), and a `FileProviderXPC` client plus `FileProviderService` (XPC-based bidirectional channel, the intended long-term replacement for sockets). The FileProvider *extension* itself lives outside this folder (`shell_integration/MacOSX/...`); this folder is the *app-side* half of that relationship, sharing protocol headers (`AppProtocol.h`, `ClientCommunicationProtocol.h`) with it via symlink. `FileProviderSettingsController` and `FileProviderItemMetadata` expose this machinery to the Qt/QML settings UI in `ui/`. `FileProviderUtils` and `FileProviderEditLocallyJob` are consumers/helpers built on top of the domain manager. `ProgressObserver` and `SingleInstanceManager` are unrelated, small standalone macOS-platform helpers used elsewhere in `gui/`. + +## Fork-specific vs. upstream + +- The FileProvider/XPC/VFS architecture, `KDSingleApplication` workaround rationale, and overall class layout mirror upstream Nextcloud desktop's macOS integration. +- Fork-specific: XPC/service identifiers are rebranded per whitelabel (e.g. `com.ionos.hidrivenext.desktopclient.ClientCommunicationService`), and the QML files import `com.strato.hidrivenext.desktopclient` — confirming this is the IONOS/HiDrive Next/STRATO whitelabel build, not vanilla Nextcloud branding. Qt logging-category rebranding to `hidrivenext.gui.*` is inconsistent/incomplete though: only some files (`fileprovider_mac.mm`, `fileprovideritemmetadata_mac.mm`, `fileprovidersettingscontroller_mac.mm`, `fileproviderutils_mac.mm`, `fileproviderxpc_mac*.mm`) use it, while others (`fileproviderdomainmanager.mm`, `fileproviderservice.mm`, `singleinstancemanager_mac.mm`, `fileprovidereditlocallyjob.cpp`, `fileprovidersocketcontroller.cpp`, `fileprovidersocketserver.cpp`) still use the generic `nextcloud.gui.*` category. +- `SingleInstanceManager` is a newer, fork-authored addition not present in upstream, added specifically to fix a macOS 13 sandbox/socket-path bug in `KDSingleApplication`. (Its SPDX header is dated 2026, but that's not a unique marker — `fileproviderservice.h/.mm` also carry a 2026 SPDX date.) + +*Quelle: src/gui/macOS — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/socketapi/CLAUDE.md b/.claude/context/gui/socketapi/CLAUDE.md new file mode 100644 index 0000000000000..87303be6292ba --- /dev/null +++ b/.claude/context/gui/socketapi/CLAUDE.md @@ -0,0 +1,21 @@ +# src/gui/socketapi + +Implements the local IPC server that talks to the OS shell-integration extensions (Explorer overlay-icon handler on Windows, Finder Sync extension on macOS, Nautilus/file-manager extensions on Linux), providing per-file sync status and context-menu actions. This is core, largely upstream Nextcloud infrastructure. + +## Key classes/components + +- `SocketApi` (`socketapi.h/.cpp`) — `QObject` owning a `QLocalServer` (named pipe on Windows, App-Group socket on macOS, runtime-dir socket on Linux/BSD). Accepts client connections (`SocketListener` per socket), parses newline-delimited text commands (`COMMAND:argument`) and dispatches them via Qt meta-object reflection to `command_XXX` slots (e.g. `command_RETRIEVE_FILE_STATUS`, `command_SHARE`, `command_ENCRYPT`, `command_LOCK_FILE`, `command_MAKE_AVAILABLE_LOCALLY`, `command_GET_MENU_ITEMS`, plus `ASYNC_*` job-based commands for GUI testing). Also broadcasts `STATUS`/`UPDATE_VIEW`/`REGISTER_PATH` push messages to all connected listeners as folders sync. +- `socketapi_p.h` — private helper types: `BloomFilter` (compact probabilistic set used to avoid pushing STATUS updates for unmonitored directories), `SocketListener` (per-connection wrapper with `sendMessage`/`sendWarning`/`sendError` and directory-interest tracking), `ListenerClosure` (one-shot callback-as-slot helper), and `SocketApiJob` (represents one async `ASYNC_*` request/response with `resolve`/`reject`, used for GUI-testing commands and richer JSON-based commands). +- `socketapi.cpp` — the bulk of the logic: `FileData` helper (resolves a local path to its `Folder`, relative/server paths, journal record), share/leave-share/encrypt/activity/file-actions request handling, context-menu building (`sendSharingContextMenuOptions`, `sendFileActionsContextMenuOptions`, `sendEncryptFolderCommandMenuEntries`, `sendLockFile*MenuEntries`), conflict resolution, file lock/unlock, direct-editing (`command_EDIT`), and private-link copy/email/open. +- `socketapi_mac.mm` — macOS-only Objective-C++ providing `socketApiSocketUrl()`, which resolves the App Group container URL for the local socket path (needed because macOS sandboxing requires shared App Group storage instead of a plain runtime-dir path). +- `CMakeLists.txt` — adds `socketapi.h/.cpp` always, and `socketapi_mac.mm` only when building on Apple platforms. + +## How it fits together + +`SocketApi` is the single long-lived server object (created and owned by `FolderMan`, see `folderman.cpp::_socketApi.reset(new SocketApi)`), and each shell-extension process connects as a client and speaks the line-based `command_*` protocol; `FolderMan`/`Folder`/journal DB provide the data `SocketApi` needs to answer status queries and build context menus, while `SyncFileItem`/`SyncJournalFileRecord`/`ShareManager`/`EncryptFolderJob`/`ConflictDialog` are invoked to actually perform requested actions. + +## Fork-specific notes + +- Mostly upstream Nextcloud code; the one visible fork-specific touch is the `#ifndef IONOS_BUILD` guard around the "Activity" context-menu item in `command_GET_MENU_ITEMS`, which suppresses that menu entry specifically in the IONOS/whitelabel build. + +*Quelle: src/gui/socketapi — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/tray/CLAUDE.md b/.claude/context/gui/tray/CLAUDE.md new file mode 100644 index 0000000000000..c32d44a7d433b --- /dev/null +++ b/.claude/context/gui/tray/CLAUDE.md @@ -0,0 +1,42 @@ +# src/gui/tray + +This folder implements the Nextcloud Desktop **system tray popup**: the tray icon's dropdown window (account switcher, activity/notification feed, unified search, sync status, group-folder shortcuts, incoming-call/talk-reply dialogs). C++ classes expose Qt models/objects to QML (registered under the `com.strato.hidrivenext.desktopclient` module); the `.qml` files render them. The window is created/owned by `Systray` (src/gui/systray.cpp, outside this folder), which loads `MainWindow.qml`. + +## Key C++ backing classes +- **`UserModel`/`User`** (usermodel.h/.cpp) — list model of connected accounts; `User` wraps one `AccountState` and exposes name/avatar/status/sync-status/group-folders/assistant (AI) state to QML. Central hub other tray objects hang off of (`getActivityModel()`, `getUnifiedSearchResultsListModel()`). +- **`UserAppsModel`** — list of the account's server "apps" shown in the apps menu. +- **`ActivityListModel`** (+ `SortedActivityListModel` proxy) — feed of notifications/sync results/errors/activities per account; backs `ActivityList.qml`/`ActivityItem*.qml`. Data rows are `Activity`/`ActivityLink`/`PreviewData` (activitydata.h). +- **`ServerNotificationHandler`** (notificationhandler.cpp) — fetches server notifications via OCS API and feeds them into `User`/`ActivityListModel`. +- **`SyncStatusSummary`** — aggregates folder sync progress/state across the current account; drives `SyncStatus.qml`. +- **`UnifiedSearchResultsListModel`** + `UnifiedSearchResult` — server-wide search results; backs `UnifiedSearchInputContainer.qml`/`UnifiedSearchResult*.qml` (re-enabled and restyled for the whitelabel design as of SES-579, after being hidden since SES-589). +- **`TalkReply`** — sends Talk chat replies from notification/activity items; used with `TalkReplyTextField.qml`. +- **`ImageProvider`/`AsyncImageResponse`, `TrayImageProvider`, `SvgImageProvider`** — async QML image providers for avatars, server icons, and recolored SVGs. + +## Key QML components (MainWindow tree) +- **`MainWindow.qml`** — the tray popup `ApplicationWindow`; hosts header, sync-status warning banner, activity list, unified search, user-status/file-details drawers, and the AI assistant panel. Reacts to `UserModel`/`Systray` signals. +- **`HeaderLogo.qml`** + **`SesTrayHeader.qml`** (in `../SesComponents/`, fork-specific) — the actual active header bar, containing `TrayWindowAccountMenu`, the website `HeaderButton`, `TrayFoldersMenuButton`, and the apps menu. +- **`TrayWindowAccountMenu.qml`** + **`UserLine.qml`** — current-account button and its dropdown menu (per-account rows via `Instantiator` over `UserModel`, add-account/pause-sync/settings/exit entries). +- **`AccountMenuItem.qml`** — generic styled menu-item row reused inside the account menu and elsewhere (e.g. `ActivityItemContent.qml`). +- **`ActivityList.qml`** / **`ActivityItem.qml`** / **`ActivityItemContent.qml`** / **`ActivityItemActions.qml`** / **`ActivityItemContextMenu.qml`** — the notification/activity feed list and its per-row rendering, action buttons, and context menu. +- **`SyncStatus.qml`** (wraps `NC.SyncStatusSummary`) + **`NCBusyIndicator.qml`** / **`NCProgressBar.qml`** — sync spinner/progress shown in the header area. +- **`TrayFoldersMenuButton.qml`** + **`TrayFolderListItem.qml`** + **`AutoSizingMenu.qml`** — "open local/group folder" button and its folder-picker popup menu. +- **`UnifiedSearchInputContainer.qml`** + **`UnifiedSearchResult*.qml`** (`Item`, `ListItem`, `SectionItem`, `Skeleton*`, `NothingFound`, `FetchMoreTrigger`, `PlaceholderView`) — unified search box and results list; active in `MainWindow.qml` (SES-579 restyled the field/hover state to match the fork's design — background/border/text colors like `ShareeSearchField`, `Style.sesHover` instead of `palette.highlight`). +- **`CallNotificationDialog.qml`** — incoming Talk call popup; **`EncryptionTokenDiscoveryDialog.qml`** / **`EditFileLocallyLoadingDialog.qml`** — misc modal dialogs launched from activities. +- Small shared building blocks: `HeaderButton.qml`, `IconButton.qml`, `PrimaryPillButton.qml`/`SecondaryPillButton.qml`, `EnforcedPlainTextLabel.qml`, `ListItemLineAndSubline.qml`, `NCIconWithBackgroundImage.qml`. +- **Legacy/unused (superseded by the fork's `SesTrayHeader`)**: `TrayWindowHeader.qml` (upstream header variant) and `CurrentAccountHeaderButton.qml` (upstream account button, only referenced by `TrayWindowHeader.qml`) are still present but not instantiated from `MainWindow.qml`'s actual render tree — kept from upstream merges, replaced functionally by `TrayWindowAccountMenu.qml`/`UserLine.qml`. (Note: `TrayWindowHeaderBar.qml` does not exist in this fork.) + +## How it fits together +`Systray` creates the QML engine and loads `MainWindow.qml`, which is shown/hidden on tray-icon click. Inside, `HeaderLogo` + `SesTrayHeader` render the header; `SesTrayHeader` instantiates `TrayWindowAccountMenu`, which opens a `Menu` populated by an `Instantiator` over the global `UserModel` singleton, one `UserLine` per account, plus add-account/pause/settings/exit items. Below the header, `SyncStatus` shows aggregate sync progress, and the main body is an `ActivityList` bound to `UserModel.currentUser.getActivityModel()` (via `SortedActivityListModel`), rendering `ActivityItem`/`ActivityItemContent` rows whose actions/links come from `ActivityListModel`/`ActivityLink`. Switching the current account (`UserModel.currentUserId`) swaps which `User`'s activity/search/sync models the whole window is bound to. Drawers (`UserStatusSelectorPage`, `FileDetailsView`) and dialogs (`CallNotificationDialog`, `EncryptionTokenDiscoveryDialog`) are opened on demand from `Systray`/`UserModel` signals. + +## Fork-specific vs upstream +- All `.qml` files import the fork's `com.strato.hidrivenext.desktopclient` QML module (HiDrive Next/Strato branding) rather than a generic Nextcloud one, and use `Style.ses*` properties for fork theming (colors, fonts, icon sizes) defined in `theme/Style/Style.qml`. +- `SesTrayHeader.qml` (in `../SesComponents/`) is a fork-only replacement for upstream's `TrayWindowHeader.qml`; `TrayWindowAccountMenu.qml`/`UserLine.qml` are fork-only replacements for upstream's `CurrentAccountHeaderButton.qml`, tied to tickets like `SES-459`/`SES-511`/`SES-589`. +- `AsyncImageResponse` (asyncimageresponse.h/.cpp) pulls in `SesFileIconProvider` (`sesFileIconProvider.h`) for fork-specific file-type icon rendering. +- Some upstream features are explicitly disabled in this fork via `visible: false // SES-4 removed` (e.g. the "Sync now" button in `SyncStatus.qml`). Unified Search was previously in this category (hidden since SES-589) but was re-enabled and restyled for the whitelabel design in SES-579 — its C++ backing models (`UnifiedSearchResultsListModel`, etc.) are active again, not unused. +- `BaseTheme`'s colors are now dark-mode-reactive (`themeColorsChanged` signal, `themedColor(light, dark)` helper, see `../CLAUDE.md`/`COMPONENTS.md` root docs); the tray (`SES-578`) was the reference implementation other dialogs' dark-mode support was modeled on. +- Generic/unmodified upstream Nextcloud logic: `ActivityListModel`/`activitydata`, `ServerNotificationHandler`, `SyncStatusSummary`, `UserModel`/`User` core account logic, `TalkReply`, and the various list-model/image-provider plumbing. + +## Merge-Risiko-Hinweis +Dieser Ordner enthält mehrere der historisch häufigsten Merge-Konflikt-Stellen des Forks (`MainWindow.qml`, `TrayWindowAccountMenu.qml`, `UserLine.qml`) — bei Änderungen hier den [stable-merge-check-Skill](../../../skills/stable-merge-check/SKILL.md) nutzen. + +*Quelle: src/gui/tray — Stand 2026-08-19, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/updater/CLAUDE.md b/.claude/context/gui/updater/CLAUDE.md new file mode 100644 index 0000000000000..20233b9e63639 --- /dev/null +++ b/.claude/context/gui/updater/CLAUDE.md @@ -0,0 +1,24 @@ +# src/gui/updater + +This folder implements the client's self-update mechanism: periodic checks against an update server, platform-specific handling of the result (silent notification, NSIS/MSI installer download+run on Windows, Sparkle framework on macOS), and parsing of the legacy ownCloud XML update-info format. + +## Key classes/components + +- `Updater` (`updater.h/.cpp`) — abstract base class/singleton (`Updater::instance()`); builds the update-check URL (`updateUrl()`, from `APPLICATION_UPDATE_URL` plus query params like version/platform/OS/channel/oem), and factory-selects the concrete updater per platform in `create()`. Also has version-integer helpers (`Helper::versionToInt`, `currentVersionToInt`, `stringVersionToInt`). +- `OCUpdater` (`ocupdater.h/.cpp`) — base class using ownCloud's proprietary XML update-info format; performs the network GET, tracks a `DownloadState` state machine (CheckingServer/UpToDate/Downloading/DownloadComplete/DownloadFailed/DownloadTimedOut/UpdateOnlyAvailableThroughSystem), and exposes `statusString()` for UI display. +- `NSISUpdater` (in `ocupdater.h/.cpp`) — Windows-only `OCUpdater` subclass; downloads the `.exe`/`.msi` installer, stores it, and on next startup (`handleStartup`) launches it (via `QProcess`/PowerShell wrapper for MSI) and shows "new version"/"update failed" dialogs. +- `PassiveUpdateNotifier` (in `ocupdater.h/.cpp`) — Linux/fallback `OCUpdater` subclass that only notifies the user of an available update (relies on the OS package manager for actual installation); also detects if the on-disk binary changed under a running process and requests an app restart. +- `SparkleUpdater` (`sparkleupdater.h`, `sparkleupdater_mac.mm`) — macOS `Updater` implementation wrapping the Sparkle framework (`SPUStandardUpdaterController`); `SparkleInterface`/`NCSparkleUpdaterDelegate` bridge Sparkle's Objective-C delegate callbacks to Qt signals (`statusChanged`) and an internal `State` enum (Idle/Working/AwaitingUserInput). +- `UpdaterScheduler` (in `ocupdater.h/.cpp`) — `QObject` that owns the recurring `QTimer`, checks `ConfigFile` for skip/auto-update settings, and calls `Updater::instance()->backgroundCheckForUpdate()`; forwards `newUpdateAvailable`/`requestRestart` signals to the app. +- `UpdateInfo` (`updateinfo.h/.cpp`) — generated (kxml_compiler-style) XML data class parsing `` responses into version/versionString/web/downloadUrl fields; used by `OCUpdater`. + +## How it fits together + +`Updater::instance()` (created via `Updater::create()`) picks `NSISUpdater` (Windows), `SparkleUpdater` or `PassiveUpdateNotifier` (macOS, depending on Sparkle availability/entitlement), or `PassiveUpdateNotifier` (Linux/other). `UpdaterScheduler` drives periodic `backgroundCheckForUpdate()` calls and relays announcements to the GUI. The non-Sparkle updaters all funnel through `OCUpdater`, which fetches XML from the update URL, parses it via `UpdateInfo`, and lets the subclass decide what to do (`versionInfoArrived`). + +## Fork-specific notes + +- The update server URL is brand-specific and set at CMake configure time — see `IONOS.cmake` (`APPLICATION_UPDATE_URL = https://customerupdates.nextcloud.com/client/`, set identically for both "strato" and "ionos" `WHITELABEL_NAME`, and only inside the `LOCALBUILD` branch — production builds presumably get this from an external CMake cache variable) vs. `NEXTCLOUD.cmake` (`https://updates.nextcloud.org/client/`, set unconditionally), compiled into `config.h` as `APPLICATION_UPDATE_URL` and read in `Updater::updateUrl()`. +- The updater C++ logic itself (`OCUpdater`, `NSISUpdater`, `PassiveUpdateNotifier`, `SparkleUpdater`, `UpdateInfo`) is largely unmodified upstream Nextcloud/ownCloud code; only user-facing strings reference `Theme::instance()->appNameGUI()` for branding. + +*Quelle: src/gui/updater — Stand 2026-08-17, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/context/gui/wizard/CLAUDE.md b/.claude/context/gui/wizard/CLAUDE.md new file mode 100644 index 0000000000000..f163a54b88423 --- /dev/null +++ b/.claude/context/gui/wizard/CLAUDE.md @@ -0,0 +1,76 @@ +# src/gui/wizard + +The "new connection" / account-setup wizard: a `QWizard` (`OwncloudWizard`) that walks a user +from a welcome screen through server URL entry, credential/login, optional data-protection +consent, terms-of-service acceptance, and local/remote folder configuration to finish adding an +account. Pages are plain `QWizardPage` subclasses registered by page ID (see +`WizardCommon::Pages` in owncloudwizardcommon.h) and are owned/created by `OwncloudWizard`. + +## Key classes/components + +- **owncloudwizard.h/.cpp** — `OwncloudWizard`: the `QWizard` container; owns and registers all + pages, holds shared state (account, client cert/key data, flow/VFS flags), relays signals + (`determineAuthType`, `connectToOCUrl`, `createLocalAndRemoteFolders`, `basicSetupFinished`). +- **owncloudwizardcommon.h/.cpp** — `WizardCommon` namespace: shared `Pages` enum (page-ID + ordering), style helpers (title templates, error/hint label styling) used by all pages. +- **welcomepage.h/.cpp/.ui** — `WelcomePage`: first page; branding slideshow + "Log in" / + "Create account" buttons; picks next page (server setup, or webview for registration). +- **owncloudsetuppage.h/.cpp** + **owncloudsetupnocredspage.ui** — `OwncloudSetupPage`: server + URL entry, proxy settings button, client-certificate handling (`AddCertificateDialog`), + triggers `DetermineAuthTypeJob` and branches `nextId()` to HTTP creds / Flow2 / WebView page. +- **owncloudconnectionmethoddialog.h/.cpp/.ui** — small modal shown when a plain-HTTP/self-signed + server is detected, letting the user choose No-TLS vs. client-side TLS vs. going back. +- **abstractcredswizardpage.h/.cpp** — `AbstractCredentialsWizardPage`: base class for + credential-entry pages; default `nextId()` routes to Terms-of-Service (if required) or + Advanced Setup, and exposes `getCredentials()`. +- **owncloudhttpcredspage.h/.cpp/.ui** — `OwncloudHttpCredsPage`: username/password form for + Basic auth servers. +- **flow2authcredspage.h/.cpp** + **flow2authwidget.h/.cpp/.ui** — `Flow2AuthCredsPage` (wizard + page) hosts `Flow2AuthWidget`, which drives OAuth2/LoginFlowV2 (`Flow2Auth`): shows a browser + link + polls for completion. +- **webview.h/.cpp/.ui** + **webviewpage.h/.cpp** — `WebView` (embedded `QWebEngineView`, + WITH_WEBENGINE only) and `WebViewPage` wrapping it, used for browser-based login/registration + flows that can't use Flow2. +- **termsofservicewizardpage.h/.cpp** + **termsofservicecheckwidget.h/.cpp/.ui** — + `TermsOfServiceWizardPage` hosts `TermsOfServiceCheckWidget`, which polls whether the user has + accepted ToS in the browser before letting the wizard proceed. +- **dataprotectionpage.h/.cpp/.ui** — `DataProtectionPage`: whitelabel consent/tracking notice + (IONOS/STRATO), branches to `Page_AdvancedSetup` or `Page_DataProtectionSettings`. +- **dataprotectionsettingspage.h/.cpp/.ui** — `DataProtectionSettingsPage`: detailed + cookie/tracking preference toggles shown only if the user opens "settings" from the consent page. +- **owncloudadvancedsetuppage.h/.cpp/.ui** — `OwncloudAdvancedSetupPage`: final page; local + folder picker, sync-everything/selective-sync/virtual-files radio choice, quota lookup, avatar + fetch; terminal page (`nextId()` returns -1). +- **wizardproxysettingsdialog.h/.cpp** + **proxysettings.ui** — `WizardProxySettingsDialog`: + modal for manual proxy host/port/credentials, reachable from the server-setup page. +- **slideshow.h/.cpp** — `SlideShow`: generic animated image/label carousel widget used on the + welcome page. +- **linklabel.h/.cpp** — `LinkLabel`: clickable/underline-on-hover `QLabel` used for hyperlink- + style text throughout the wizard. + +## How it fits together + +`OwncloudWizard` registers all pages against `WizardCommon::Pages` IDs and lets each page's +`nextId()` decide the actual route. Typical flow: `Page_Welcome` → `Page_ServerSetup` (URL entry, +emits the `determineAuthType` signal, handled outside this folder in +`owncloudsetupwizard.cpp::slotDetermineAuthType` which runs the actual `DetermineAuthTypeJob`) → +depending on the detected auth type: `Page_HttpCreds` (Basic), `Page_Flow2AuthCreds` +(OAuth2/LoginFlowV2), or `Page_WebView` (browser-based, non-Flow2 builds). From a credentials +page, `AbstractCredentialsWizardPage::nextId()` (used by `Page_HttpCreds`) sends the user to +`Page_TermsOfService` if the server requires ToS acceptance, else to `Page_AdvancedSetup` — or, +if `useVirtualFileSyncByDefault()`, straight to the wizard end (`-1`), skipping Advanced Setup +entirely. The `Page_DataProtection` detour (with an optional further detour to +`Page_DataProtectionSettings`) exists only in the **Flow2-specific override** +(`Flow2AuthCredsPage::nextId()`, `#ifdef IONOS_BUILD`) — Basic-auth users never see it. +`Page_AdvancedSetup` is the terminal page that creates local/remote folders and finishes the +wizard (`basicSetupFinished`). + +## Fork-specific vs upstream +- `dataprotectionpage.*`, `dataprotectionsettingspage.*`, and IONOS/STRATO logos + and copy inside `welcomepage.cpp`, `flow2authwidget.cpp` (guarded by `IONOS_WL_BUILD`/ + `STRATO_WL_BUILD`/`IONOS_BUILD`) are whitelabel-only additions layered on top of upstream + Nextcloud; `whitelabeltheme.h` (`WLTheme`) supplies fork-specific styling. +- Everything else (welcome/setup/creds/webview/advanced-setup/proxy pages, slideshow, linklabel) + is generic upstream Nextcloud wizard code shared by all brand variants. + +*Quelle: src/gui/wizard — Stand 2026-08-20, automatisch erstellt, bitte gegenlesen.* diff --git a/.claude/skills/color-scheme-map/COLOR_MAP.md b/.claude/skills/color-scheme-map/COLOR_MAP.md new file mode 100644 index 0000000000000..3dbbf72ca709a --- /dev/null +++ b/.claude/skills/color-scheme-map/COLOR_MAP.md @@ -0,0 +1,335 @@ +# Farbschema-Landkarte + +Wird vom [color-scheme-map](SKILL.md)-Skill Komponente für Komponente befüllt — kein Vollscan, sondern nur die Komponenten, die tatsächlich geprüft wurden. + +**Hinweis (2026-08-21):** `StratoTheme::dialogBackgroundColor()` (stratotheme.h:17-19) war zwischenzeitlich im Light Mode auf `#FAFAFA` geändert, ist jetzt aber bewusst wieder auf `#F7F7F9` zurückgestellt — damit teilen sich Settings-Seiten und Tray-`MainWindow` (`WLTheme.trayBackgroundColor()`, stratotheme.h:33-34, ebenfalls `#F7F7F9`/`#1F2024`) exakt denselben Hintergrund in beiden Modi. Alle Abschnitte dieser Karte, die `dialogBackgroundColor()` referenzieren, sind mit diesem Wert wieder konsistent. + +## Tray (`src/gui/tray/*.qml` + `src/gui/SesComponents/SesTrayHeader.qml`) + +*Zuletzt geprüfter Commit: `9647f1174` (2026-08-18)* + +Der Tray war laut Doku die Referenzimplementierung für SES-578 — entsprechend ist die Grundstruktur solide: eine kleine Menge zentraler `Style.*`-Properties (aus `theme/Style/Style.qml`, letztlich `WLTheme`/`Theme` gespeist) wird von praktisch allen ~35 QML-Dateien wiederverwendet, statt dass jede Datei eigene Werte hätte. + +### Gemeinsame Style-Properties (in fast allen Komponenten verwendet) + +| Property | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---| +| `Style.sesBackgroundColor` | `#F7F7F9` | `#1F2024` | `WLTheme.trayBackgroundColor()`, stratotheme.h:33 | ✅ theme-aware | +| `Style.sesBorderColor` | `#8493B3` | `#454C5E` | `WLTheme.trayBorderColor()`, stratotheme.h:25 | ✅ theme-aware | +| `Style.sesTrayFontColor` | `#2F2F70` | `#C9CBEF` | `WLTheme.trayFontColor()`, stratotheme.h:21 | ✅ theme-aware | +| `Style.sesMenuBorder` | `#2E4360` | `#5B7699` | inline `Theme.darkMode ? ... : ...`, Style.qml:273 | ✅ theme-aware | +| `Style.sesHover` | `#F2F5F8` | `#2D3138` | inline Ternary, Style.qml:263 | ✅ theme-aware | +| `Style.sesSelectedColor` | `#F4F7FA` | `#333844` | inline Ternary, Style.qml:266 | ✅ theme-aware | +| `Style.sesButtonPressed` | `#D6D6E4` | `#3A3B52` | `WLTheme.toolButtonPressedColor()`, stratotheme.h:128 | ✅ theme-aware | +| `Style.sesAccountMenuHover` | `#EDEEF3` | `#282A36` | `WLTheme.toolButtonHoveredColor()`, stratotheme.h:124 | ✅ theme-aware | +| `Style.sesIconDarkColor` | `#2F2F70` | `#C9CBEF` | `WLTheme.iconDarkColor()`, stratotheme.h:144 | ✅ theme-aware | +| `Style.sesIconColor` | `#2f2f70` | `#C9CBEF` | `WLTheme.buttonIconColor()`, stratotheme.h:108 | ✅ theme-aware | +| `Style.sesActionHover` | `#eeeff9` | `#2A2B3D` | `WLTheme.buttonHoveredColor()`, stratotheme.h:116 | ✅ theme-aware | +| `Style.sesActionPressed` | `#D6D6E4` | `#3A3B52` | `WLTheme.buttonPressedColor()`, stratotheme.h:120 | ✅ theme-aware | +| `Style.sesWhite` | `#FFFFFF` | = `sesBackgroundColor` | inline Ternary, Style.qml:260 | ✅ theme-aware | +| `Style.sesPillButtonPrimaryBackgroundColor` | `#272CB2` | `#5B60D6` | `WLTheme.pillButtonPrimaryColor()`, stratotheme.h:92 | ✅ theme-aware | +| `Style.sesPillButtonSecondaryBackgroundColor` / `Style.buttonBackgroundColor` | `#E4E4ED` | `#2E2F3D` | `WLTheme.pillButtonSecondaryColor()`, stratotheme.h:96 | ✅ theme-aware | +| `Style.sesPillButtonBorderColor` | `#FFFFFF` | `#FFFFFF` (identisch) | `WLTheme.pillButtonBorderColor()`, stratotheme.h:100 — Kommentar in basetheme.h:382/stratotheme.h:72 bestätigt "stays white in both modes" | ℹ️ bewusst fix | +| `Style.ncTextColor` / `Style.ncSecondaryTextColor` | = `sesTrayFontColor` | = `sesTrayFontColor` | Alias, Style.qml:16/18 | ✅ theme-aware | +| `Style.backgroundColor` | `#FFFFFF` | `#1E2126` | inline Ternary, Style.qml:22 | ✅ theme-aware | +| `Style.errorBoxBackgroundColor` (MainWindow.qml:305, Sync-Warnbanner) | `Qt.rgba(0.89,0.18,0.18,1)` fix | identisch | Style.qml:33, per Konvention bewusst fix (wie `ErrorBox.qml`/`SesErrorBox.qml`) | ℹ️ bewusst fix | +| `Style.ncTextBrightColor` (EncryptionTokenDiscoveryDialog.qml:32, `brightText`-Palette-Rolle) | `"white"` fix | identisch | Style.qml:17, keine Begründung im Code | ⚠️ Bruch: kein Dark-Wert | +| `Style.currentUserHeaderColor`/`Style.accentColor` (Fallback `ncBlue`, nur wenn noch kein Account eingerichtet) | `APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR` (Compile-Konstante) | identisch | `Theme::wizardHeaderBackgroundColor()`, theme.cpp:784 — `Q_PROPERTY(... CONSTANT)`, keine `themedColor()`-Anbindung | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert (Edge-Case, nur vor der ersten Account-Einrichtung sichtbar) | +| `Style.adjustedCurrentUserHeaderColor` | `Qt.darker(currentUserHeaderColor, 1.5)` | `Qt.lighter(currentUserHeaderColor, 2)` | inline Ternary, Style.qml:29 | ✅ theme-aware (Formel reagiert, Basiswert ist aber meist der dynamische Server-Header, s.o.) | + +### Ambient `palette.*`-Bindungen + +`MainWindow.qml:40-41` setzt am Wurzel-`ApplicationWindow` `palette.base: Style.sesBackgroundColor` und `palette.windowText: Style.sesTrayFontColor` — dank QMLs Context-Property-Vererbung erben **alle** Kind-Items diese beiden Rollen automatisch, ohne dass jede Datei sie einzeln setzen muss (anders als auf der QWidget-Seite, wo genau diese Weitergabe in `AccountSettings`/`SettingsDialog` zuvor fehlte). `palette.base`/`palette.windowText` gelten daher überall im Baum als ✅ theme-aware, auch wenn einzelne Dateien nur `palette.base`/`palette.windowText` referenzieren, ohne es selbst zu setzen (z. B. `NCBusyIndicator.qml:13`, `ListItemLineAndSubline.qml:23`, `TrayWindowHeader.qml:80/90/123`, `TrayFolderListItem.qml:15`, `UnifiedSearchPlaceholderView.qml:23`, `UnifiedSearchResultNothingFound.qml:23`). + +Andere Palette-Rollen (`palette.dark`, `palette.light`, `palette.highlight`, `palette.mid`, `palette.brightText`, `palette.alternateBase`) werden **nicht** in `MainWindow.qml` überschrieben — sie referenzieren die native Qt-Palette direkt (z. B. `NCProgressBar.qml:21/34`, `ListItemLineAndSubline.qml:24`, `TalkReplyTextField.qml:27/29/45/46`, `UnifiedSearchResultItem.qml:29/30`, `UnifiedSearchResultFetchMoreTrigger.qml:20`, `EditFileLocallyLoadingDialog.qml:17/41/43/64`, `EncryptionTokenDiscoveryDialog.qml:59/80`, `MainWindow.qml:649/662/686/699/713/843`). Status **ℹ️ ambient** — verlässt sich auf Qt's natives `QStyleHints::colorScheme()`-Palette-Tracking, das laut früherer Analyse dieser Sitzung auf Windows 11 grundsätzlich funktioniert. + +### Komponenten-spezifische Brüche + +| Fundstelle | Wert | Status | Notiz | +|---|---|---|---| +| `MainWindow.qml:409` (`border.color`, KI-Assistent-Reset-Bestätigungsdialog) | `"#808080"` fix | ⚠️ Bruch: kein Dark-Wert | Einziger Dialog-Rahmen im Tray, der nicht über `Style.sesBorderColor`/`Style.sesMenuBorder` läuft — wirkt im Dark Mode vermutlich zu hell. | +| `MainWindow.qml:420` (`DropShadow.color`) | `"#80000000"` fix | ℹ️ bewusst fix | Schlagschatten bleiben konventionell schwarz/halbtransparent unabhängig vom Theme — unkritisch. | +| `CallNotificationDialog.qml:132` (`darkenerRect`, Hintergrundbild-Abdunklung) | `"black"`, `opacity: 0.4` fix | ℹ️ bewusst fix | Abdunklung eines Hintergrund-**Fotos** (Anruferbild), kein UI-Chrome — theme-unabhängig sinnvoll. | +| `PrimaryPillButton.qml:26` (Button-Text) | `"white"` fix | ⚠️ Bruch: kein Dark-Wert | Kein Kommentar; praktisch vermutlich unkritisch, da der Pill-Hintergrund (`sesPillButtonPrimaryBackgroundColor`, `#272CB2`/`#5B60D6`) in beiden Modi kräftig genug für weißen Text ist — aber nicht wie z. B. `buttonstyle.h`/`titleColor()` bewusst dokumentiert. | +| `CurrentAccountHeaderButton.qml`, `TrayWindowHeader.qml` | — | — nicht bewertet | Laut `tray/CLAUDE.md` Legacy/unbenutzt (durch `SesTrayHeader.qml`/`TrayWindowAccountMenu.qml` ersetzt, nicht im aktiven Render-Baum) — bewusst von dieser Prüfung ausgeschlossen. | + +### Ausgeschlossen (keine echten Farbwerte) + +Mehrere `color: "white"`-Treffer sind reine `OpacityMask`-Maskenformen (`visible: false`, z. B. `ActivityItemContent.qml:69`, `CallNotificationDialog.qml:114/171`, `UnifiedSearchResultItemSkeleton.qml:77/120/156`, `UnifiedSearchResultItemSkeletonContainer.qml:56`) — sie definieren nur die Maskenform für ein rundes/geformtes Bild, werden nie selbst gerendert, und sind daher kein Dark-Mode-Thema. + +**Brüche (Zusammenfassung):** +1. `Style.ncTextBrightColor` fix `"white"` (Style.qml:17) — betrifft `brightText`-Palette-Rolle in `EncryptionTokenDiscoveryDialog.qml`. +2. `Theme::wizardHeaderBackgroundColor()` (`Q_PROPERTY CONSTANT`, theme.cpp:784) — Fallback-Farbe für `ncBlue`/`accentColor`/`currentUserHeaderColor`, nur relevant bevor ein Account eingerichtet ist. +3. `MainWindow.qml:409` — Dialograhmen `#808080`, einziger nicht themefähiger Rahmen im Tray. +4. `PrimaryPillButton.qml:26` — weißer Button-Text ohne Dark-Variante/Begründung, praktisch aber unkritisch. + +## AccountSettings (`src/gui/accountsettings.cpp/.h/.ui`) + +*Zuletzt geprüfter Commit: working tree auf `0b9c2b950` (2026-08-21, noch uncommitted)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | accountsettings.cpp:605, 998, 1178, 1917, 1932, 1936 | `#F7F7F9` (identisch mit Tray `trayBackgroundColor()`, siehe Hinweis oben) | `#1F2024` | `StratoTheme::dialogBackgroundColor()`, stratotheme.h:17-19 (überschreibt `themedColor("#FFFFFF","#1E2126")` in basetheme.h:441) | ✅ theme-aware | +| `WLTheme.titleColor()` | accountsettings.cpp:997, 1177, 1431, 1925, 1928 | `#000000` | `#D6E4F5` | `themedColor()` in basetheme.h:318 (keine Strato-Override) | ✅ theme-aware | +| `WLTheme.settingsLinkColor()` | accountsettings.cpp:1342, 1367, 1920 | `#272CB2` | `#5B60D6` | `StratoTheme::settingsLinkColor()`, stratotheme.h:37 | ✅ theme-aware | +| `WLTheme.trayBackgroundColor()` | accountsettings.cpp:736 | `#F7F7F9` | `#1F2024` | `StratoTheme::trayBackgroundColor()`, stratotheme.h:33 | ✅ theme-aware | +| `WLTheme.menuBorderColor()` | accountsettings.cpp:737 | `#2E4360` | `#5B7699` | `themedColor()` in basetheme.h:449 (keine Strato-Override) | ✅ theme-aware | +| `WLTheme.menuTextColor()` | accountsettings.cpp:738 | `#29294d` | `#C9CBEF` | `StratoTheme::menuTextColor()`, stratotheme.h:132 | ✅ theme-aware | +| `WLTheme.menuSelectedItemColor()` | accountsettings.cpp:740 | `#D6D6E4` | `#282A36` | `StratoTheme::menuSelectedItemColor()`, stratotheme.h:136 | ✅ theme-aware | +| `WLTheme.menuPressedItemColor()` | accountsettings.cpp:741 | `#5A6782` | `#3A3B52` | `StratoTheme::menuPressedItemColor()`, stratotheme.h:148 | ✅ theme-aware | +| `WLTheme.menuPressedTextColor()` | accountsettings.cpp:739 | `#FFFFFF` | `#FFFFFF` (identisch) | `StratoTheme::menuPressedTextColor()`, stratotheme.h:140 — **fixer Rückgabewert, kein `themedColor()`** | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert — Regression ggü. `basetheme.h:457` (`themedColor("#001B41","#D6E4F5")`), das Strato hier mit einem festen `"#FFFFFF"` überschreibt | +| Fixe Fehler-Box: `color:#ffffff; background-color:#bb4d4d;` + Link `#c1c8e6` | accountsettings.cpp:1337-1339, 1350, 1362-1364, 1375 (`showConnectionLabel`, Fehlerfall) | — | — | Hartcodiert, per Kommentar (Z. 1333) explizit als "fixed, self-contained contrast pair - deliberately not themed" begründet | ℹ️ bewusst fix | +| `color: black; background: lightgrey;` | accountsettings.cpp:1280 (E2E-Mnemonic-Dialog, `ui.lineEdit`) | `black`/`lightgrey` (fix) | identisch | Hartcodiert, keine Begründung im Code | ⚠️ Bruch: kein Dark-Wert | +| `color: red` | accountsettings.ui:352 (`selectiveSyncNotification`) | `red` (fix) | identisch | Im `.ui`-Formular hartcodiert, in `customizeStyle()`/sonst im `.cpp` nie überschrieben | ⚠️ Bruch: kein Dark-Wert | +| `palette().highlight().color()` → `toolTipStyle` | accountsettings.cpp:1930-1932 | — | — | `QPalette`-Rolle, aber Ergebnis wird nie per `setStyleSheet()` angewendet | ℹ️ ambient — vermutlich **dead code** (außerhalb des Farbschema-Scopes, aber hier notiert: `color`/`toolTipStyle` werden berechnet und nie verwendet) | + +**Brüche:** +1. `StratoTheme::menuPressedTextColor()` (stratotheme.h:140) gibt fest `"#FFFFFF"` zurück statt `themedColor(light, dark)` — im Kontextmenü (accountsettings.cpp:739) betrifft das den Text von `QMenu::item:pressed`. Da der zugehörige Hintergrund (`menuPressedItemColor()`) im Light-Mode `#5A6782` ist, bleibt es noch lesbar, ist aber nicht bewusst getestet/geplant — eher eine übersehene Vereinfachung. +2. `ui.lineEdit` im E2E-Mnemonic-Dialog (accountsettings.cpp:1280) ist fest hellgrau/schwarz, unabhängig vom OS-Theme — fällt in einem sonst abgedunkelten Dialog optisch heraus. +3. `selectiveSyncNotification` (accountsettings.ui:352) hat fest `color: red` ohne jede Theme-Anbindung — funktional meist noch lesbar (Rot auf Hell/Dunkel), aber nicht über `themedColor()` geführt wie der Rest der Seite. + +**Hinweis (2026-08-21):** `WLTheme.panelBackgroundColor()` für die Card-Panels `accountStatusPanel`/`fileProviderPanel`/`syncFoldersPanel`/`connectionSettingsPanel` (aus Commit `b9e530504`) wurde wieder entfernt — Referenzscreenshot des Nutzers (stable-33.0-Optik) zeigt einen durchgängig einheitlichen Settings-Hintergrund ohne abgesetzte Panels. Getter + Q_PROPERTY komplett aus `basetheme.h` entfernt (ungenutzt). Der Dateibaum (`_folderList`) in dieser Komponente ist von dieser Betrachtung bewusst ausgenommen — wird separat behandelt. Durch das Entfernen von zwei Blöcken in `basetheme.h` (Q_PROPERTY-Zeile + Kommentar/Methode) können sich Zeilenangaben zu `basetheme.h` in anderen, noch nicht seit diesem Commit neu geprüften Abschnitten dieser Karte um 1-7 Zeilen verschoben haben — bei Bedarf beim nächsten Check der jeweiligen Komponente korrigieren. + +## Dialog-Familie: caseclashfilenamedialog / conflictdialog / foldercreationdialog / invalidfilenamedialog / legacyaccountselectiondialog + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +Alle fünf Dialoge teilen dasselbe Whitelabel-Muster (Commit `45446a124` "roll out dialogBackgroundColor()/titleColor() dark-mode reuse across dialogs", Basis `bb543a166`): `#include "buttonstyle.h"`/`"whitelabeltheme.h"`, `buttonStyle`-Property auf den Buttons, eine `customizeStyle()`-Methode, die per `setStyleSheet()`/`setPalette()` dieselben vier Getter kombiniert. **Alle fünf rufen `customizeStyle()` nur einmal im Konstruktor auf, keiner hängt es an `changeEvent()`/`paletteChanged`** — ein Theme-Wechsel bei bereits offenem Dialog würde ihn nicht neu einfärben (übergreifende Beobachtung, kein Einzel-Bruch). + +| Property | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | `#F7F7F9` | `#1F2024` | `StratoTheme::dialogBackgroundColor()`, stratotheme.h:17-18 | ✅ theme-aware | +| `WLTheme.titleColor()` | `#000000` | `#D6E4F5` | `themedColor()` basetheme.h:319-320 (keine Strato-Override) | ✅ theme-aware | +| `WLTheme.folderWizardPathColor()` | `#97A3B4` | `#A8B4C6` | `themedColor()` basetheme.h:327-328 (keine Strato-Override) | ✅ theme-aware | +| `WLTheme.menuBorderColor()` | `#2E4360` | `#5B7699` | `themedColor()` basetheme.h:457-458 (keine Strato-Override) | ✅ theme-aware | + +Fundstellen: `caseclashfilenamedialog.cpp:296,297,302,311,316,317` · `conflictdialog.cpp:186,187,192` (+ zwei `Q_OS_MAC`-only QCheckBox-Styles `titleColor()` bei 202/206) · `foldercreationdialog.cpp:106,118,123,124` (`dialogBackgroundColor()` hier direkt per `setPalette(QPalette(QPalette::Window, …))`) · `invalidfilenamedialog.cpp:331,336,345,350,351` · `legacyaccountselectiondialog.cpp:25,67,72,74`. + +**Brüche:** keine — alle Treffer ✅ theme-aware. + +**Hinweis (kein Farb-, sondern Merge-Risiko-Fund):** `foldercreationdialog.cpp` hat neben dem Styling auch echte Logik verändert (`accept()`-Guard `QDir(fullPath).exists()` entfernt, `ui->labelErrorMessage`→`ui->errorSnackbar` umbenannt) — siehe [[project_ionos-build-dead-branch-pattern]]-artige Kategorie "unauffällige Nebenänderung im selben Commit", separat in DRIFT_MAP.md als **hoch** eingestuft. Lohnt eine eigene Prüfung, ob der entfernte Guard beabsichtigt war. + +## GeneralSettings (`src/gui/generalsettings.cpp`) + +*Zuletzt geprüfter Commit: working tree auf `0b9c2b950` (2026-08-21, noch uncommitted)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | generalsettings.cpp:869 | `#F7F7F9` (identisch mit Tray `trayBackgroundColor()`, siehe Hinweis oben) | `#1F2024` | `StratoTheme::dialogBackgroundColor()`, stratotheme.h:17-19 | ✅ theme-aware | +| `WLTheme.titleColor()` | generalsettings.cpp:872,876,880,884,887,901,904,907 | `#000000` | `#D6E4F5` | `themedColor()` basetheme.h:320 | ✅ theme-aware | +| `WLTheme.folderWizardSubtitleColor()` | generalsettings.cpp:895,898 | `#104996` | `#5FA8E0` | `themedColor()` basetheme.h:324 | ✅ theme-aware | + +**Brüche:** keine. (Hinweis: `WLTheme.panelBackgroundColor()` für QGroupBox-Card-Panels wurde am 2026-08-20 in Commit `b9e530504` eingeführt und am 2026-08-21 wieder entfernt — Referenzscreenshot des Nutzers zeigt für die Settings-Seite einen durchgängig einheitlichen Hintergrund ohne abgesetzte Panels, analog zur stable-33.0-Optik. Getter + Q_PROPERTY komplett aus `basetheme.h` entfernt, da sonst ungenutzt.) + +## IgnoreListTableWidget (`src/gui/ignorelisttablewidget.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | ignorelisttablewidget.cpp:210,232,267,296 | `#F7F7F9` | `#1F2024` | stratotheme.h:18 | ✅ theme-aware | +| `WLTheme.titleColor()` | ignorelisttablewidget.cpp:211,217,226,233,238,265,278 | `#000000` | `#D6E4F5` | basetheme.h:320 | ✅ theme-aware | +| `WLTheme.folderWizardPathColor()` | ignorelisttablewidget.cpp:287 | `#97A3B4` | `#A8B4C6` | basetheme.h:328 | ✅ theme-aware | +| `WLTheme.menuBorderColor()` | ignorelisttablewidget.cpp:292 | `#2E4360` | `#5B7699` | basetheme.h:458 | ✅ theme-aware | + +Kommentar bei Zeile 296 begründet bewusst, dass das Eingabefeld sich nur über den Rahmen absetzt statt über eine eigene Füllfarbe. + +**Brüche:** keine. + +## Systray-Kontextmenü (`src/gui/systray.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.trayBackgroundColor()` | systray.cpp:237 | `#F7F7F9` | `#1F2024` | stratotheme.h:34 | ✅ theme-aware | +| `WLTheme.menuBorderColor()` | systray.cpp:238 | `#2E4360` | `#5B7699` | basetheme.h:458 | ✅ theme-aware | +| `WLTheme.menuTextColor()` | systray.cpp:242 | `#29294d` | `#C9CBEF` | stratotheme.h:133 | ✅ theme-aware | +| `WLTheme.menuSelectedItemColor()` | systray.cpp:243 | `#D6D6E4` | `#282A36` | stratotheme.h:137 | ✅ theme-aware | +| `WLTheme.menuPressedItemColor()` | systray.cpp:244 | `#5A6782` | `#3A3B52` | stratotheme.h:149 | ✅ theme-aware | +| `WLTheme.menuPressedTextColor()` | systray.cpp:245 | `#FFFFFF` | `#FFFFFF` (identisch) | `StratoTheme::menuPressedTextColor()`, stratotheme.h:141 — fixer Rückgabewert, kein `themedColor()` | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | + +**Brüche:** `menuPressedTextColor()` (stratotheme.h:141) — derselbe bereits unter AccountSettings dokumentierte Bruch, hier im Tray-Kontextmenü bestätigt (zweite Verwendungsstelle desselben Getters). + +## ButtonStyle (`src/gui/buttonstyle.h`, fork-eigen) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +Reine Weiterleitung an `WLTheme`-Getter (`PrimaryButtonStyle`/`SecondaryButtonStyle`/`MoreOptionsButtonStyle`), keine eigenen Hex-Werte. + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `buttonPrimaryColor()` | 75,80,119 | `#272CB2` | `#5B60D6` | stratotheme.h:50 | ✅ theme-aware | +| `buttonPrimaryHoverColor()` | 86,91 | `#2944CC` | `#2944CC` (identisch) | `StratoTheme`-Override stratotheme.h:55, eigener TODO-Kommentar "no established Strato dark counterpart" | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert (offen bekannt) | +| `buttonPrimaryPressedColor()` | 97,102 | `#272CB2` | `#5B60D6` | stratotheme.h:59 | ✅ theme-aware | +| `buttonDisabledColor()` | 108,113,202,207,297,302 | `#EDEEF3` | `#282A36` | stratotheme.h:89 | ✅ theme-aware | +| `buttonPrimaryFocusedBorderColor()` | 124 | `#CDD5E3` | `#FFFFFF` | stratotheme.h:63 | ✅ theme-aware | +| `buttonDisabledFontColor()` | 130,224,319 | `#BDBDBD` | `#5A5D63` | basetheme.h:415 | ✅ theme-aware | +| `white()` | 135,141,146,213,235,240,308 | `#FFFFFF` | identisch | basetheme.h:435, fixer Wert | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | +| `buttonSecondaryColor()` | 169 | `#F7F7F9` | `#2E2F3D` | stratotheme.h:68 | ✅ theme-aware | +| `buttonSecondaryBorderColor()` | 174,185,196 | `#CDD5E3` | `#FFFFFF` | stratotheme.h:73 | ✅ theme-aware | +| `buttonSecondaryHoverColor()` | 180 | `#EDEEF3` | `#282A36` | stratotheme.h:77 | ✅ theme-aware | +| `buttonSecondaryPressedColor()` | 191 | `#D6D6E4` | `#3A3B52` | stratotheme.h:81 | ✅ theme-aware | +| `buttonSecondaryFocusedBorderColor()` | 218 | `#8493B3` | `#454C5E` | stratotheme.h:85 | ✅ theme-aware | +| `titleColor()` | 229 | `#000000` | `#D6E4F5` | basetheme.h:320 | ✅ theme-aware | +| `dialogBackgroundColor()` | 264,269 | `#F7F7F9` | `#1F2024` | stratotheme.h:18 | ✅ theme-aware | +| `buttonHoveredColor()` | 275,280 | `#eeeff9` | `#2A2B3D` | stratotheme.h:117 | ✅ theme-aware | +| `buttonPressedColor()` | 286,291 | `#D6D6E4` | `#3A3B52` | stratotheme.h:121 | ✅ theme-aware | +| `black()` | 313,324 | `#000000` | identisch | basetheme.h:439, fixer Wert | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert — **Zeile 324 (`MoreOptionsButtonStyle::buttonFontColor()`) sitzt auf `dialogBackgroundColor()` dunkel `#1F2024`, also potenziell fast-schwarzer Text auf fast-schwarzem Grund** | +| `buttonIconColor()` | 330 | `#2f2f70` | `#C9CBEF` | stratotheme.h:109 | ✅ theme-aware | +| `buttonIconHoverColor()` | 335 | `#2f2f70` | `#C9CBEF` | stratotheme.h:113 | ✅ theme-aware | + +**Brüche:** +1. `buttonPrimaryHoverColor()` (stratotheme.h:55) — kein Dark-Wert, laut eigenem TODO noch offen (Design-Input fehlt). +2. `white()`/`black()` (basetheme.h:435/439) — fixe Werte, betreffen v. a. `MoreOptionsButtonStyle::buttonFontColor()` (Zeile 324): **schwarzer Text auf `dialogBackgroundColor()` im Dark Mode (`#1F2024`) — praktisch vermutlich unlesbar, konkretester Kontrast-Fund dieser Analyse-Runde.** + +## SettingsDialog (`src/gui/settingsdialog.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | 532,546-547,553-555,558-561 (nur `IONOS_BUILD`) | `#F7F7F9` (identisch mit Tray `trayBackgroundColor()`, siehe Hinweis oben) | `#1F2024` | stratotheme.h:17-19 | ✅ theme-aware | +| `WLTheme.menuBorderColor()` (rechter Rahmen `#settings_navigation_scroll`, neu am 2026-08-21) | 559-561 | `#2E4360` (identisch mit Tray `Style.sesMenuBorder`) | `#5B7699` | basetheme.h:457-458 (keine Strato-Override) | ✅ theme-aware | +| `WLTheme.toolButtonHoveredColor()` | 533 | `#EDEEF3` | `#282A36` | stratotheme.h:124-125 | ✅ theme-aware | +| `WLTheme.toolButtonPressedColor()` | 534 | `#D6D6E4` | `#3A3B52` | stratotheme.h:128-129 | ✅ theme-aware | +| `WLTheme.menuSelectedItemColor()` | 535 | `#D6D6E4` | `#282A36` | stratotheme.h:136-137 | ✅ theme-aware | +| `WLTheme.buttonSecondaryBorderColor()` | 538 | `#CDD5E3` | `#FFFFFF` | stratotheme.h:71-73 | ✅ theme-aware | +| `WLTheme.titleColor()` (als `highlightTextColor`) | 539 | `#000000` | `#D6E4F5` | basetheme.h:319-320 | ✅ theme-aware | +| `WLTheme.menuTextColor()` | 544 | `#29294d` | `#C9CBEF` | stratotheme.h:132-133 | ✅ theme-aware | +| `palette(highlight)`/`palette(highlighted-text)` (`TOOLBAR_CSS`, `IONOS_BUILD`) | 118 | OS-Theme | OS-Theme | ambient | ℹ️ ambient — verifiziert reaktiv über `changeEvent()`→`customizeStyle()` (306-312) | +| `palette(window)`/`palette(alternate-base/light)` (`BACKGROUND_PALETTE`, Nicht-`IONOS_BUILD`-Zweig) | 583-596 | OS-Theme | OS-Theme | ambient | ℹ️ ambient — verifiziert reaktiv | +| `palette().color(QPalette::WindowText/Window)` (Avatar-Ring/Glyph) | 468,684,688 | OS-Theme | OS-Theme | ambient, mit Begründungskommentar | ℹ️ ambient — verifiziert reaktiv | +| `Theme::createColorAwareIcon(iconPath, palette())` | 560,609,666,697 | OS-Theme | OS-Theme | ambient | ℹ️ ambient — verifiziert reaktiv | + +**Brüche:** keine. + +## FolderStatusDelegate (`src/gui/folderstatusdelegate.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `option.palette` (Zeilentext) | 285,287,334 | OS-Theme | OS-Theme | Qt liefert `option.palette` pro Paint-Aufruf neu | ℹ️ ambient (zwangsläufig aktuell) | +| `WLTheme.warningBorderColor()` | 355 | `#F4BFAB` | `#C98F5E` | basetheme.h:521-522 | ✅ theme-aware | +| `WLTheme.errorBorderColor()` | 358 | `#FF004C` | `#FF6688` | stratotheme.h:152-153 | ✅ theme-aware | +| `WLTheme.infoBorderColor()` | 384 | `#11C7E6` | `#4DD9F0` | basetheme.h:529-530 | ✅ theme-aware | +| `WLTheme.dialogBackgroundColor()` (Fortschrittsleisten-Base) | 425 | `#F7F7F9` | `#1F2024` | stratotheme.h:17-18 | ✅ theme-aware | +| `WLTheme.syncProgressColor()` (Fortschrittsleisten-Highlight) | 426 | `#009850` | identisch | `StratoTheme::syncProgressColor()`, stratotheme.h:45-47 — fixer Rückgabewert, überschreibt `basetheme.h:368-369` (`themedColor("#359ada","#4FB6F0")`) | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | +| `BaseTheme::tintedFillFromBorder(borderColor)` (Fehler/Warn/Info-Box-Füllung) | 330 | abgeleitet | abgeleitet | Formel auf o.g. Border-Farben, Kommentar 326-329 begründet bewusste Ableitung statt fixer Pastelltöne | ✅ theme-aware | + +**Brüche:** `StratoTheme::syncProgressColor()` (stratotheme.h:45-47) fest `#009850` — betrifft die Sync-Fortschrittsleiste; Grün bleibt im Dark Mode vermutlich noch kontrastreich genug, aber nicht bewusst dokumentiert. + +## FolderWizard (`src/gui/folderwizard.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | 186,216,256,290,301,722,757,967 | `#F7F7F9` | `#1F2024` | stratotheme.h:17-18 | ✅ theme-aware | +| `WLTheme.titleColor()` | 189,201,289,304,677,686,756,760 | `#000000` | `#D6E4F5` | basetheme.h:319-320 | ✅ theme-aware | +| `WLTheme.folderWizardSubtitleColor()` | 193-196,673,763-766 | `#104996` | `#5FA8E0` | basetheme.h:323-324 | ✅ theme-aware | +| `WLTheme.folderWizardPathColor()` | 210,295 | `#97A3B4` | `#A8B4C6` | basetheme.h:327-328 | ✅ theme-aware | +| `WLTheme.menuBorderColor()` | 215,300 | `#2E4360` | `#5B7699` | basetheme.h:457-458 | ✅ theme-aware | +| `WLTheme.white()` (Button-Text, nur `Q_OS_MAC`) | 223,310 | `#FFFFFF` | identisch | basetheme.h:434-436, fixer Wert | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | + +**Brüche:** `white()` (basetheme.h:434-436) — nur macOS-Zweig, vermutlich unkritisch (analog zum bekannten `menuPressedTextColor()`-Muster), aber nicht als bewusste Dark-Mode-Ausnahme kommentiert. + +## OwncloudGui (`src/gui/owncloudgui.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +**Keine Farbtreffer.** Alle `WLTheme.*`-Aufrufe (`syncOfflineIcon`, `syncSyncingIcon`, `syncPausedIcon`, `syncSuccessIcon`, `syncWarningIcon`) liefern Icon-Dateipfade, keine Farbwerte. + +## SelectiveSyncDialog (`src/gui/selectivesyncdialog.cpp`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.dialogBackgroundColor()` | 81,105,122,138,582,598 | `#F7F7F9` | `#1F2024` | stratotheme.h:17-18 | ✅ theme-aware | +| `WLTheme.titleColor()` | 80,115,127,134,569 | `#000000` | `#D6E4F5` | basetheme.h:319-320 | ✅ theme-aware | +| `WLTheme.white()` (OK-Button-Text) | 591 | `#FFFFFF` | identisch | basetheme.h:434-436, fixer Wert | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | +| `palette(dark)` (Header-Trennlinie, nur `Q_OS_MAC`) | 109 | OS-Theme | OS-Theme | ambient, Kommentar 107-108 bewusst analog zum Tray | ℹ️ ambient (bewusst) | + +**Brüche:** `white()` (basetheme.h:434-436), gleiches Muster wie `folderwizard.cpp`. + +**Hinweis:** `customizeStyle()` wird nur im Konstruktor aufgerufen (Zeile 576), nicht an `changeEvent()` gehängt — anders als `settingsdialog.cpp`. Farbwerte selbst sind theme-aware definiert, werden aber bei einem Theme-Wechsel während der Dialog offen ist ggf. nicht neu angewendet. + +## Wizard: DataProtection-Seiten (`src/gui/wizard/dataprotectionpage.cpp`, `dataprotectionsettingspage.cpp`, fork-eigen) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.titleColor()` | dataprotectionpage.cpp:83 · dataprotectionsettingspage.cpp:96,108,121 | `#000000` | `#D6E4F5` | basetheme.h:319-320 | ✅ theme-aware | +| `WLTheme.folderWizardSubtitleColor()` | dataprotectionsettingspage.cpp:76,86 | `#104996` | `#5FA8E0` | basetheme.h:323-324 | ✅ theme-aware | + +**Brüche:** keine. + +## SesErrorBox.qml / ShareeSearchField.qml (FileDetailsPage-Umfeld) + +*Zuletzt geprüfter Commit: `8534f6865` (2026-08-20) — heute im selben Lauf per SES-578 gefixt (Commits `cd9d7535e`, `8534f6865`)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `Style.tintedFill(Style.sesErrorBoxBorder, 0.2)` (Hintergrund) | SesErrorBox.qml:34 | `#FF004C`@20% | `#FF6688`@20% | `Style.tintedFill()` Style.qml:231, neuer gemeinsamer Helfer `BaseTheme::tintedFillFromBorder` | ✅ theme-aware | +| `Style.sesErrorBoxBorder` | SesErrorBox.qml:35 | `#FF004C` | `#FF6688` | `WLTheme.trayErrorBorderColor()`, stratotheme.h:156-157 | ✅ theme-aware | +| `Style.sesErrorBoxText` | SesErrorBox.qml:63 | `#CC0052` | `#FF8FB3` | `WLTheme.trayErrorTextColor()`, stratotheme.h:160-161 | ✅ theme-aware | +| `Style.sesSearchFieldContent` (Placeholder/Icon-Tint) | ShareeSearchField.qml:34,116 | `#97A3B4` | `#B7C1CE` | inline `Theme.darkMode`-Ternary, Style.qml:280 | ✅ theme-aware | +| `Style.sesTrayFontColor` (Text/Cursor) | ShareeSearchField.qml:43,44 | `#2F2F70` | `#C9CBEF` | stratotheme.h:21-22 | ✅ theme-aware | +| `Style.sesBackgroundColor` (Field-/Popup-Background) | ShareeSearchField.qml:93,173 | `#F7F7F9` | `#1F2024` | stratotheme.h:33-34 | ✅ theme-aware | +| `Style.sesMenuBorder` (Field-/Popup-Border) | ShareeSearchField.qml:94,174 | `#2E4360` | `#5B7699` | inline Ternary, Style.qml:279 | ✅ theme-aware | +| `Style.sesHover` (Vorschlagsliste-Highlight) | ShareeSearchField.qml:197 | `#F2F5F8` | `#2D3138` | inline Ternary, Style.qml:269 | ✅ theme-aware | +| `palette.placeholderText` (Busy-Indicator/Clear-Icon-Tint) | ShareeSearchField.qml:132,154 | — | — | ambient, kein eigener Wert | ℹ️ ambient, unverifiziert — weder `FileDetailsPage.qml` noch `ShareDetailsPage.qml` rebinden `placeholderText` | + +**Brüche:** keine (beide Dateien wurden in dieser Session bereits korrigiert). Einzige offene Frage: `palette.placeholderText` ist ambient/unverifiziert, praktisch niedrige Priorität. + +## UserStatusMessageView.qml + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `Style.ncBlue` (Feldrahmen bei `showBorder`) | 65 | `#0082c9` | identisch | `Theme::wizardHeaderBackgroundColor()`, theme.cpp:784, `Q_PROPERTY CONSTANT` | ⚠️ Bruch: Getter selbst hat keinen Dark-Wert | +| `palette.dark` (Feldrahmen-Fallback, Emoji-Dialog-Border) | 65,96 | — | — | ambient | ℹ️ ambient, unverifiziert — `MainWindow.qml` rebindet nur `base`/`windowText`, `dark` bleibt System-Palette | +| `palette.button` (Feld-Hintergrund) | 69 | — | — | ambient | ℹ️ ambient, unverifiziert | +| `palette.base` (Emoji-Dialog-Hintergrund) | 94 | — | — | ambient | ℹ️ ambient, verifiziert — `MainWindow.qml:40` setzt `palette.base` an der Wurzel | +| `Style.sesTrayFontColor` (ComboBox-Indicator-Icon-Tint, neu) | 192 | `#2F2F70` | `#C9CBEF` | stratotheme.h:21-22 | ✅ theme-aware | + +**Brüche:** `Style.ncBlue`/`Theme::wizardHeaderBackgroundColor()` (theme.cpp:784) — derselbe bereits im Tray-Abschnitt dokumentierte Bruch, hier als zweite Verwendungsstelle bestätigt. + +## Application (`src/gui/application.cpp`/`.h`) + +*Zuletzt geprüfter Commit: `9d3af4e33` (2026-08-20)* + +**Keine Farbtreffer.** Die Datei behandelt Style-Auswahl (`sesStyle`-Base-Style, s. Fix von heute) und natives Dark-Titlebar-Chrome (`applyImmersiveDarkMode()`, DWM-Attribut, kein Farbwert) — beides betrifft Dark-Mode-*Verhalten*, aber keine `WLTheme`/Hex-Farbwerte im Sinne dieser Karte. + +## SesSnackBar-Header (`src/gui/sessnackbar.h`) + +*Zuletzt geprüfter Commit: `2f77361ec` (2026-08-20)* + +**Keine Farbtreffer in der Header-Datei** — nur Deklarationen (`updateStyleSheet(QColor)`, `errorStyle()`/`warningStyle()`/`successStyle()`). Die tatsächlichen Farbwerte stehen in `src/gui/sessnackbar.cpp` (fork-only) — dort bislang noch nicht geprüft, offen für eine künftige Erfassung bei Bedarf. + +## Ergänzung Tray-QML (AccountMenuItem, ActivityItemContent, TrayWindowAccountMenu, UnifiedSearchInputContainer, UserLine) + +*Sanity-Check-Commit: `2f77361ec` (2026-08-20)* + +Sanity-Check gegen die bestehende Tray-Sektion (s.o.) durchgeführt — keine neuen Farbmuster gefunden, die nicht bereits über die dort erfassten `Style.*`-Properties abgedeckt sind. `UnifiedSearchInputContainer.qml` hat neu `palette.placeholderText` durch `Style.sesSearchFieldContent`/`Style.sesTrayFontColor` ersetzt (jetzt ✅ theme-aware statt ambient). Keine neuen Brüche. + +## Format je Komponente + +```markdown +## (``) + +*Zuletzt geprüfter Commit: `` ()* + +| Property | Fundstelle | Light-Wert | Dark-Wert | Quelle | Status | +|---|---|---|---|---|---| +| `WLTheme.titleColor()` | accountsettings.cpp:1922 | `#000000` | `#D6E4F5` | `themedColor()` in basetheme.h:318 | ✅ theme-aware | +| ... | ... | ... | ... | ... | ⚠️ Bruch: kein Dark-Wert | + +**Brüche:** kurze Liste der ⚠️-Zeilen mit Einschätzung/Notiz, oder "keine". +``` + +Status-Werte: ✅ theme-aware · ⚠️ Bruch (kein Dark-Wert / Getter selbst ohne Dark-Wert) · ℹ️ bewusst fix (mit Begründung) · ℹ️ ambient (hängt von Palette-Propagation ab, ungeprüft/geprüft vermerken). diff --git a/.claude/skills/color-scheme-map/SKILL.md b/.claude/skills/color-scheme-map/SKILL.md new file mode 100644 index 0000000000000..2963d1cc50ea7 --- /dev/null +++ b/.claude/skills/color-scheme-map/SKILL.md @@ -0,0 +1,56 @@ +--- +name: color-scheme-map +description: Pflegt eine Farbschema-Landkarte für src/gui/-Komponenten in COLOR_MAP.md — pro Komponente, welches Farb-Property sie verwendet, mit Light-/Dark-Wert und Quelle (themedColor() vs. hartcodiert vs. QPalette-Rolle) — um gezielt nach Dark-Mode-Brüchen suchen zu können. Wird bewusst Komponente für Komponente befüllt, kein Vollscan in einem Lauf. Nur manuell auslösen. Trigger — "erfasse die Farben von ", "Farbschema-Map aktualisieren", "welche Farb-Properties nutzt ", "such nach Dark-Mode-Brüchen in ", nach Abschluss von Theming-Arbeiten (z. B. SES-578-artig) an einer einzelnen Komponente. +--- + +# Color-Scheme-Map + +Beim Dark-Mode-Umbau (SES-578 u. ä.) taucht dieselbe Frage ständig neu auf: welches `WLTheme`/`BaseTheme`-Property färbt dieses Widget, hat es überhaupt einen Dark-Wert, oder hängt die Farbe an einer nicht mehr propagierten `QPalette`-Rolle? Bisher wurde das jedes Mal per Ad-hoc-Grep/Agent neu ermittelt. Dieser Skill baut dafür eine dauerhafte Landkarte auf: Komponente → verwendete Farb-Properties → Light-/Dark-Wert → Quelle → Status. + +**Bewusst inkrementell, eine Komponente pro Aufruf** — keine Vollscan-Aktion über alle ~450 Dateien in `src/gui/`. Die Karte wächst mit jeder Komponente, die tatsächlich bearbeitet/geprüft wurde, nicht spekulativ im Voraus. + +## Datei + +- [COLOR_MAP.md](COLOR_MAP.md) — ein Abschnitt pro Komponente: Tabelle (Property | Datei:Zeile | Light-Wert | Dark-Wert | Quelle | Status) plus eine Zeile "zuletzt geprüfter Commit", analog zum `registry.md`-Muster von [shadow-component-watch](../shadow-component-watch/SKILL.md). + +## Ablauf + +1. **Komponente identifizieren.** Name/Klasse/Datei(en) vom Nutzer übernehmen. Bei vager Beschreibung wie bei [component-context](../component-context/SKILL.md) zunächst per Grep in `.claude/context/gui/**/CLAUDE.md`/`COMPONENTS.md` auflösen. Bei mehreren Kandidaten nachfragen statt zu raten. + +2. **Prüfen, ob schon ein Abschnitt existiert** (`Grep COLOR_MAP.md`): + - Vorhanden, mit "zuletzt geprüfter Commit" → nur inkrementell nachziehen: `git log ..HEAD -- `. Keine neuen Commits → nichts tun, das dem Nutzer sagen. Neue Commits → nur die dort geänderten Zeilen neu bewerten (Schritt 4), nicht die ganze Komponente neu scannen. + - Noch nicht vorhanden → Vollerfassung dieser Komponente (Schritte 3–5). + +3. **Scannen.** In den Komponentendateien (`.cpp`/`.h`/`.ui`/`.qml`) nach Farb-relevanten Mustern grep'en: + - `WLTheme\.\w+\(` / `WLTheme\.\w+` (C++ und QML) + - `themedColor\(` (direkter Aufruf, selten außerhalb von basetheme.h/stratotheme.h/ionostheme.h) + - `QColor\(` / `#[0-9a-fA-F]{6}` (hartcodierte Hex-Werte) + - `palette\(` (Qt-Stylesheet-CSS-Funktion) / `QPalette::\w+` (C++ Palette-Rolle) + - `Style\.\w+` (QML-Singleton) / `Qt\.rgba\(` / `Qt\.lighter\(` / `Qt\.darker\(` / `Qt\.tint\(` + - `setStyleSheet\(` -Aufrufe, die `color:`/`background-color:`/`border-color:` enthalten + +4. **Jeden Treffer auflösen und einordnen:** + - Ruft einen `WLTheme`-Getter, der intern `themedColor(light, dark)` nutzt → beide Hex-Werte aus `basetheme.h`/`stratotheme.h`/`ionostheme.h` übernehmen, Status **✅ theme-aware**. + - Hartcodierter Einzelwert (Hex/`QColor(...)`/`Qt.rgba(...)` ohne `Theme.darkMode`/`Style.darkMode`-Verzweigung) → Status **⚠️ Bruch: kein Dark-Wert**, außer ein Kommentar begründet das explizit als bewusst (z. B. "fixed contrast pair, deliberately not themed" wie bei den Error-Styles) → dann **ℹ️ bewusst fix**, mit Begründung in der Notiz-Spalte. + - `palette(...)`/`QPalette::\w+` ohne eigene Farbwerte → Status **ℹ️ ambient**, mit Vermerk, ob das Widget nachweislich eine aktuelle Palette bekommt (kurzer Blick auf `customizeStyle()`/`changeEvent()` der Komponente bzw. ihres Parents) oder ob das ungeprüft ist. + - Aufruf eines anderen `WLTheme`-Getters ohne erkennbares `themedColor` (z. B. weil der Getter selbst noch nicht auf `themedColor` umgestellt wurde) → Status **⚠️ Bruch: Getter selbst hat keinen Dark-Wert**, Fundstelle in `basetheme.h`/`stratotheme.h` mit angeben. + +5. **`COLOR_MAP.md` schreiben/ergänzen:** neuen Abschnitt (oder aktualisierte Zeilen im bestehenden) für die Komponente anlegen, "zuletzt geprüfter Commit" auf den aktuellen `HEAD` der geprüften Dateien setzen. + +6. **Brüche hervorheben.** Am Ende dem Nutzer explizit alle **⚠️**-Zeilen dieser Komponente nennen — das ist der eigentliche Zweck der Karte, nicht nur die Tabelle selbst. + +7. **Nicht automatisch fixen.** Gefundene Brüche nur melden und in der Karte vermerken; ein Fix läuft wie gewohnt erst nach Bestätigung durch den Nutzer als eigener Schritt. + +## Abgrenzung + +- **gui-context-refresh**: Zweck/Struktur/Zusammenspiel einer Komponente — dieser Skill befasst sich ausschließlich mit ihren Farb-Properties, nicht mit ihrer sonstigen Doku. +- **stable-merge-check**: ob eine Farb-/Style-Änderung strukturell von `stable-x.y` abweicht — dieser Skill bewertet nur, ob eine Farbe dark-mode-tauglich ist, nicht das Merge-Risiko. +- **component-context**: bündelt vorhandenen Kontext vor einer Änderung, liest aber nur — dieser Skill ist die einzige Quelle, die die Farb-Landkarte selbst schreibt/pflegt. +- **merge-drift-map**: gleiches Muster (inkrementelle, dauerhafte Landkarte), aber für Abweichung von `stable-x.y` statt Farb-Properties — unabhängige Dimension, kein Zusammenhang zwischen den beiden Karten. + +## Nicht in diesem Skill enthalten + +- Kein Vollscan aller Komponenten in einem Lauf — bewusst Komponente für Komponente, wie vom Nutzer festgelegt. +- Kein automatisches Fixen gefundener Brüche. +- Keine Bewertung, ob eine Farbwahl *gestalterisch* richtig ist (Kontrast/Barrierefreiheit) — nur, ob überhaupt ein Dark-Wert existiert und woher er kommt. +- Kein periodischer/automatischer Trigger — der Nutzer entscheidet, wann eine Komponente erfasst/aktualisiert wird. diff --git a/.claude/skills/component-context/SKILL.md b/.claude/skills/component-context/SKILL.md new file mode 100644 index 0000000000000..92cf35197a5f1 --- /dev/null +++ b/.claude/skills/component-context/SKILL.md @@ -0,0 +1,72 @@ +--- +name: component-context +description: Sammelt auf Zuruf vorhandenen Kontext zu EINER konkreten GUI-Komponente, bevor sie ausgeblendet oder in einem Subdialog geändert wird — aus DECISIONS.md, .claude/context/gui/, shadow-component-watch/registry.md, Git-Historie und einem per Code-Analyse ermittelten Klickpfad, wo die Komponente in der laufenden App zu finden ist. Schreibt nichts, liest nur und fasst zusammen. Nur manuell auslösen. Trigger — "gib mir Kontext zu ", "was wissen wir schon über ", "bevor ich anfasse", "History zu ", "wurde schon mal ausgeblendet/geändert", "wo finde ich in der App", "wie komme ich zu ", "Klickpfad zu ". +--- + +# Component-Context + +Beim UI-Theming (Ausblenden von Komponenten, Änderungen in Subdialogen) mit viel KI-Unterstützung geht schnell die Übersicht verloren, was an einer Komponente schon passiert ist — und wo sie in der laufenden App überhaupt zu finden ist. Dieser Skill sammelt vorhandenen Kontext zu **genau einer** Komponente aus bereits existierenden Quellen und ermittelt zusätzlich einen Klickpfad zur Komponente in der App, bevor du dort weiterarbeitest. + +**Wichtig:** Dieser Skill schreibt selbst nichts fest und pflegt kein eigenes Registry-File. Er liest ausschließlich aus den Quellen, die [decision-log](../decision-log/SKILL.md), [gui-context-refresh](../gui-context-refresh/SKILL.md) und [shadow-component-watch](../shadow-component-watch/SKILL.md) ohnehin schon pflegen, plus Git-Historie live zusammen. + +## Ablauf + +1. **Komponente identifizieren.** Name/Klasse/Datei vom Nutzer übernehmen. Bei vager Beschreibung ("das Tray-Kontextmenü") zunächst per Grep in `.claude/context/gui/**/CLAUDE.md` und `COMPONENTS.md` nach passenden Zeilen suchen, um Datei(en) und Klassenname aufzulösen. Bei mehreren Kandidaten kurz nachfragen statt zu raten. + +2. **Decision-Log durchsuchen** (nur gezielt, nicht komplett lesen): `Grep .claude/skills/decision-log/DECISIONS.md`. Bei Treffer(n) den jeweiligen Abschnitt lesen — liefert das "Warum" zu bewussten Ausblend-/Entfernungs-/Abweichungsentscheidungen. + +3. **Kontext-Doku nachschlagen**: passenden Abschnitt in `.claude/context/gui//CLAUDE.md` oder die zugehörige Zeile in `.claude/context/gui/COMPONENTS.md` lesen (nicht die ganze Datei). Liefert Zweck, Zusammenspiel mit anderen Klassen, "Fork-spezifisch vs. Upstream"-Einordnung, ggf. Notes zu Dead Code. Auf das dort vermerkte "Stand"-Datum achten und im Ergebnis mit angeben, damit klar ist, wie aktuell die Doku ist. + +4. **Shadow-Component-Check**: `Grep .claude/skills/shadow-component-watch/registry.md`. Falls die Komponente ein Fork-Replacement für eine Upstream-Komponente ist, kurz auf offene Einträge in `CHANGELOG.md` desselben Skills hinweisen. + +5. **Git-Historie der Datei(en)**: `git log --oneline -n 10 -- ` für einen schnellen Überblick, wer/wann zuletzt daran gearbeitet hat. Bei Bedarf (Nutzer fragt "wann wurde das ausgeblendet") gezielt `git log -S"" -- ` nachschieben. + +6. **Nicht selbst tief in Merge-Risiko einsteigen.** Falls die Frage in Richtung "wie riskant ist das für den nächsten stable-Merge" geht, das kurz benennen und auf [stable-merge-check](../stable-merge-check/SKILL.md) verweisen statt die Analyse hier zu duplizieren. + +7. **Klickpfad in der laufenden App ermitteln** (rein statisch per Code-Analyse, kein App-Start nötig): + - **Aufrufstelle(n) finden**: repo-weit grep nach Instanziierung/Anzeige der Komponente — `new (`, `::` gefolgt von `show()`/`exec()`/`open()`, sowie in QML ` {`, `Loader { source: ".../.qml" }`, `sourceComponent:`. + - **Pro Aufrufstelle den Auslöser identifizieren**: die umgebende Funktion/den Slot ansehen — meist ein `connect(, &...::triggered/clicked, ...)` in C++ oder ein `onClicked:`/`onTriggered:` in QML. + - **Sichtbares Label des Auslösers extrahieren**: in der Nähe nach `tr("...")`/`i18n(...)` (C++) bzw. der `text:`-Property (QML) suchen — das ist der Text, den der Nutzer tatsächlich anklickt. + - **Nach oben verfolgen** (max. 3–4 Ebenen): denselben Schritt für den umgebenden Dialog/das umgebende Fenster wiederholen, bis ein bekannter Top-Level-Einstieg erreicht ist (Tray-Icon-Menü, `MainWindow.qml`/Tray-Popup, Settings-Dialog-Tab, Setup-Wizard-Schritt, Explorer/Finder-Kontextmenü). Die Ordnerzuordnung aus `.claude/context/gui/CLAUDE.md` (welcher Unterordner/Cluster) als Abkürzung/Plausibilitätscheck nutzen, ersetzt aber nicht das Nachverfolgen der konkreten Aufrufkette. + - **Mehrere Einstiegspunkte** (Komponente von mehreren Stellen erreichbar, z. B. Dialog sowohl aus Tray als auch aus Kontextmenü öffenbar): alle auflisten, nicht nur einen auswählen. + - **Plattform-Unterschiede** (macOS/Windows/Linux-Guards in der Aufrufkette) kurz vermerken, falls vorhanden. + - **Ehrlich bleiben bei Unsicherheit**: lässt sich der Pfad nicht eindeutig statisch ermitteln (z. B. dynamisch aus Server-Capabilities generierte Menüeinträge, stark verschachtelte Delegates), das offen so benennen ("Klickpfad nicht eindeutig ermittelbar, vermutete Region: ...") statt einen Pfad zu erfinden — Datei/Klasse zum manuellen Nachschlagen trotzdem angeben. + - Kein automatischer App-Start, kein Screenshot — das Ergebnis ist ein Text-Breadcrumb zum manuellen Nachklicken. + +8. **Zusammenfassen**, kompakt und mit Quellenangabe pro Punkt (Datei:Zeile, DECISIONS.md-Datum, Commit-Hash), Format z. B.: + + ```markdown + ## Kontext: + + **Datei(en):** ... + **Status:** unangetastet | ausgeblendet seit ... | zuletzt geändert am ... + + **Navigation in der App:** Tray-Icon (Rechtsklick) > Konto-Menü > Einstellungen (Zahnrad) > Tab "Allgemein" > ... + (bei mehreren Einstiegspunkten alle auflisten; bei Unsicherheit klar als "vermutet" kennzeichnen) + + **Bisherige Entscheidungen:** (aus DECISIONS.md, oder "keine Einträge gefunden") + **Doku:** (Zweck/Zusammenspiel aus context/gui, Stand-Datum) + **Shadow-Component:** (falls zutreffend, sonst weglassen) + **Letzte Änderungen:** (2–3 relevante Commits) + + **Hinweis:** (z. B. "keine Vorgeschichte gefunden — Neuland" oder "Merge-Risiko nicht geprüft, siehe stable-merge-check" oder "nach dieser Änderung ggf. decision-log-Eintrag sinnvoll") + ``` + +9. Wenn zu keiner Quelle etwas gefunden wird, das explizit sagen ("keine Vorgeschichte gefunden") statt die Abwesenheit von Treffern zu verschweigen — das ist selbst eine nützliche Information (Neuland, kein bekanntes Risiko). Gleiches gilt für den Klickpfad: "nicht ermittelbar" ist eine gültige und nützliche Antwort. + +## Abgrenzung + +- **decision-log**: hält das "Warum" von Ausblend-/Entfernungs-/Abweichungsentscheidungen fest. Dieser Skill liest daraus, schreibt aber nichts hinein — wird nach einer Änderung ein Eintrag fällig, decision-log dafür vorschlagen, nicht hier nachbilden. +- **gui-context-refresh**: pflegt die Strukturdoku unter `.claude/context/gui/`. Dieser Skill liest nur daraus und aktualisiert sie nicht. +- **stable-merge-check**: für Merge-Risiko-Analyse im Detail und Post-Merge-Diagnose. Bei Bedarf verlinken statt inhaltlich duplizieren. +- **shadow-component-watch**: für laufende Überwachung von Fork-Ersatzkomponenten gegen Upstream. Dieser Skill prüft nur kurz, ob die Komponente dort registriert ist. +- **color-scheme-map**: für die Farb-Property-Landkarte (welches Theme-Property, Light-/Dark-Wert, Bruch oder nicht) einer Komponente — dieser Skill liest davon nichts mit, bei Bedarf explizit verweisen. + +## Nicht in diesem Skill enthalten + +- Kein automatisches Loggen von Entscheidungen (macht decision-log). +- Keine Aktualisierung der Kontext-Doku (macht gui-context-refresh). +- Kein eigenes Registry-/Cache-File — jede Abfrage sammelt live aus den vorhandenen Quellen, es gibt nichts, das veralten kann außer den Quellen selbst (das gilt auch für den Klickpfad: wird bei jeder Abfrage neu aus dem Code hergeleitet, nicht zwischengespeichert). +- Kein automatisiertes Starten/Steuern/Screenshotten der laufenden App — dafür gibt es in diesem Projekt noch keine belastbare Grundlage (native Qt/QML-App, kein etabliertes UI-Automation-Setup). Der Klickpfad ist ein textueller Wegweiser zum manuellen Nachklicken, kein Beweis, dass er exakt stimmt — bei Unsicherheit im Ergebnis klar kennzeichnen. +- Keine Sammel-Tour über mehrere Komponenten (z. B. "alle in diesem Branch geänderten Stellen") — bewusst auf eine Komponente pro Abfrage begrenzt. +- Keine proaktive Auslösung — der Nutzer entscheidet bewusst, wann er Kontext abruft. diff --git a/.claude/skills/decision-log/DECISIONS.md b/.claude/skills/decision-log/DECISIONS.md new file mode 100644 index 0000000000000..1260015e12df9 --- /dev/null +++ b/.claude/skills/decision-log/DECISIONS.md @@ -0,0 +1,121 @@ +# Decision Log + +Chronologisches Protokoll: Komponenten-Ausblenden/-Entfernen-Entscheidungen und bewusste Nähe/Abweichung zu stable-x.y. Format, Ablauf und **was hier NICHT reingehört** (Bugfixes, Build-/Kompilierbarkeits-Fixes, reine Aufräumarbeiten) siehe [SKILL.md](SKILL.md). + +**Hinweis zum Bestand unten:** Die Einträge bis 2026-08-17 (exkl. dem Avatar-Icon-Kontrast-Eintrag) wurden am 2026-08-18 rückwirkend aus 10 vergangenen Session-Transkripten per Agent-Analyse rekonstruiert, dann am selben Tag auf Boris' engeren Maßstab hin gefiltert (ursprünglich 26 Kandidaten, u. a. Bugfixes/Build-Fixes/reine Aufräumarbeiten entfernt) — bitte gegenlesen. + +## 2026-08-11 — Verbindungseinstellungen-Panel ausblenden statt entfernen + +**Kontext:** `connectionSettingsPanel` (NetworkSettings-Widget) in AccountSettings sollte nicht mehr sichtbar sein. + +**Entscheidung:** Nur `setVisible(false)` (accountsettings.cpp), Code/Widget bleibt vollständig bestehen — analog zum bestehenden `fileProviderPanel`-Muster. + +**Verworfene Alternative:** Komponente/Code entfernen — abgelehnt, um die Nähe zu `stable-x.y` zu halten: stable führt diese Komponente weiterhin aktiv, ein Entfernen würde den Diff unnötig vergrößern und jeden künftigen Merge hier komplizierter machen als ein einfaches Sichtbarkeits-Flag. + +**Status:** aktiv + +## 2026-08-12 — GeneralSettings.ui zunächst als bewusster Redesign-Stand bestätigt (SES-576) + +**Kontext:** Merge-Risiko-Analyse zeigte tiefgreifenden Strukturumbau von `generalsettings.ui` gegenüber stable-33.0 (Row-Neunummerierung, Grid→VBox, `aboutAndUpdatesGroupBox` aufgesplittet). + +**Entscheidung:** Umbau zunächst als gewollte Redesign-Entscheidung bestätigt und belassen, obwohl das künftige Merges konfliktträchtig macht. + +**Status:** überholt durch den Eintrag "GeneralSettings.ui strukturell wieder an stable-33.0 angeglichen" (selber Tag) + +## 2026-08-12 — GeneralSettings.ui strukturell wieder an stable-33.0 angeglichen (SES-576) + +**Kontext:** Nach genauerer Prüfung: Widget-/Layout-Namen wichen so stark ab (`advanced_groupBox` vs. `advancedGroupBox`, andere Layout-Klasse), dass jede Zeile künftiger stable-Merges kollidieren würde. + +**Entscheidung:** Namen/Nummerierung an stable-33.0 angeglichen, eigene BRICKMAKERS-Sektionen ans Ende gehängt statt eingemischt, äußere Layout-Klasse zurück auf `QGridLayout`. + +**Verworfene Alternative(n):** Reparenting der BRICKMAKERS-Widgets zur Laufzeit in C++ (mehr Boilerplate, keine Designer-Vorschau); eigener Wrapper `GeneralSettingsPage` um unangetastete `GeneralSettings` — zu großer Umbau, widerspricht "so nah wie möglich an stable-33.0 ohne viel Arbeit". + +**Status:** aktiv + +## 2026-08-14 — Merge-robuste Zwei-Loop-Struktur für Avatar-Reapply in `customizeStyle()` (SES-576) + +**Kontext:** Server-Avatar im Settings-Dialog war per `#ifndef IONOS_BUILD` deaktiviert (Regression aus SES-457-Whitelabel-Umbau); nach Entfernen des Guards musste `customizeStyle()` das Avatar nach jedem Theme-Icon-Reset erneut anwenden. + +**Entscheidung:** Zwei Schleifen behalten (generischer Icon-Reset, dann Avatar-Reapply über bestehendes `updateAccountAvatar()`) statt einer zusammengeführten Ein-Loop-Version mit Reverse-Lookup. + +**Verworfene Alternative:** Ein-Loop-Version mit Reverse-Lookup-Map — sauberer, aber hätte `AvatarJob::makeCircularAvatar` im IONOS-Zweig dupliziert statt die gemeinsame Funktion zu nutzen; künftige stable-33.0-Änderungen an `updateAccountAvatar()` würden dann still divergieren statt automatisch übernommen zu werden. + +**Status:** aktiv + +## 2026-08-17 — Verwaiste `TrayWindowHeaderBar.qml` gelöscht + +**Kontext:** Beim Aufbau des `shadow-component-watch`-Registry fiel eine dritte, ähnlich benannte Datei im selben Ordner auf. + +**Entscheidung:** Gelöscht — per `-S`-Pickaxe über die komplette Historie bestätigt: nie in `resources.qrc`, nie aus `MainWindow.qml`/`TrayWindowHeader.qml`/`SesTrayHeader.qml` referenziert, nie in `origin/stable-33.0` existent. Reines totes Fork-Artefakt ohne Upstream-Pendant. + +**Status:** aktiv + +## 2026-08-17 — Grundsatz: toter Code bleibt erhalten, solange stable-33.0 ihn noch führt (SES-576) + +**Kontext:** `AuthenticationDialog`, `PasswordInputDialog`, `TrayWindowHeader.qml`, `CurrentAccountHeaderButton.qml` u. a. wirken im Fork ungenutzt (naheliegend: als Cleanup entfernen). Anders als bei `TrayWindowHeaderBar.qml` (s.o.) führt `origin/stable-33.0` diese Dateien aber weiterhin aktiv. + +**Entscheidung:** Kein Löschversuch. `stable-merge-check`-Skill um eine Prüfung erweitert: vor jeder Löschempfehlung per `git show origin/:` prüfen, ob stable die Datei noch führt. Falls ja: nur als "im Fork ungenutzt, bewusst erhalten für Merge-Kompatibilität" dokumentieren, nicht löschen. + +**Verworfene Alternative:** Sofortige Entfernung aus Code-Hygiene-Gründen — verworfen, vergrößert den Diff zu stable statt ihn zu verkleinern. Boris: "ich möchte hier keinen weiteren diff zum stable-33.0 riskieren" / "Nähe zu stable-33.0 schlägt Code-Hygiene." (Gleiches Prinzip wie beim Verbindungseinstellungen-Panel-Eintrag oben, hier auf tote Dateien statt ein UI-Panel angewendet — siehe auch [[feedback_removal-safety-criteria]].) + +**Status:** aktiv + +## 2026-08-17 — User-Status-Feature (Set status/Status message/Indikator) als bewusste Abweichung dokumentiert (SES-50) + +**Kontext:** Beim `shadow-component-watch`-Review stellte sich heraus, dass in `UserLine.qml`/`TrayWindowAccountMenu.qml` nicht nur der Online-Status-Indikator (bekannter `SES-50`-Kommentar), sondern auch beide Menüeinträge ("Set status", "Status message") komplett unverdrahtet sind — kein UI-Weg mehr, den Status zu setzen. + +**Entscheidung:** Von Boris bestätigt: beabsichtigt, da das Produkt keine Collaboration-Features hat und Status daher keinen Sinn ergibt. Gesamter Feature-Komplex (Indikator + beide Menüeinträge) als eine zusammengehörige, bewusste Abweichung in `registry.md` dokumentiert, damit künftige Skill-Läufe das nicht erneut als offenen Punkt aufwerfen. + +**Status:** aktiv + +## 2026-08-17 — Kontrast-Fix für Account-Icon im Settings-Dialog: avatar-spezifisch statt generischer stable-Mechanismus (SES-576) + +**Kontext:** Account-Button in `settingsdialog.cpp`s Toolbar hatte zu wenig Kontrast — sowohl das SVG-Fallback-Glyph (Marken-Navy) als auch das nachgeladene Server-Avatar/Initialen-Bild. + +**Entscheidung:** Avatar-spezifischer Fix in `settingsdialog.cpp` (Kreis/Rand in `palette(WindowText)`), statt den generischen, für alle Icons geltenden Mechanismus aus stable-33.0 zu übernehmen. Committed als `ef168b21d`. + +**Verworfene Alternative:** Invertierungs-Logik aus stable-33.0s `Theme::createColorAwareIcon` (`src/libsync/theme.cpp`) wiederherstellen. Verworfen: unsere Marken-SVGs sind Navy statt Schwarz/Weiß, RGB-Invertierung ergibt nur einen unbefriedigenden Khakiton statt Kontrast. Von Boris nach Test verworfen — festgehalten, damit dieser Weg nicht erneut versucht wird. + +**Offener Punkt:** `Theme::createColorAwareIcon` ignoriert `palette` fork-weit komplett (nicht nur fürs Avatar-Icon) — relevant, falls an anderer Stelle ebenfalls über mangelnden Icon-Kontrast diskutiert wird. + +**Status:** aktiv + +## 2026-08-18 — Unified Search im Tray wieder aktiviert (SES-579) + +**Kontext:** Die Unified-Search-Leiste (`UnifiedSearchInputContainer.qml`) im Tray war seit `SES-589` per `visible: false` ausgeblendet (Commit `f545c860a`, "removed searchbar") — laut Commit-Message zurückgestellt, weil sie damals kein direktes `develop_stable-4.0`-Element war. + +**Entscheidung:** Sichtbarkeit wiederhergestellt und ins Whitelabel-Design integriert: Such-/Ergebnis-Hover-Styling an fork-eigene Farben/Komponenten angeglichen (Hintergrund/Rand/Text wie `ShareeSearchField`, `Style.sesHover` statt `palette.highlight`, einheitliche Eingabefeldhöhe). Commit `26fe47ea7`. Die zugehörigen C++-Modelle (`UnifiedSearchResultsListModel` etc.) waren die ganze Zeit unverändert im Baum vorhanden und sind jetzt wieder aktiv genutzt statt totem Code. + +**Status:** aktiv + +## 2026-08-18 — Chevron im Account-Umschalter: generisches Icon statt Marken-SVG (SES-578) + +*Rekonstruiert am 2026-08-19 nach versehentlichem Datenverlust (Branch-Wechsel hat den ursprünglichen, noch nicht committeten Eintrag überschrieben) — aus dem Kontext-Fragment dieses Gesprächs und der Commit-Message von `9647f1174` zusammengesetzt, nicht das Original-Wording.* + +**Kontext:** Im Zuge der Dark-Mode-Unterstützung für den Tray fiel auf, dass der Chevron-Pfeil im Account-Umschalter (`TrayWindowAccountMenu.qml`) im Dark Mode nicht die Farbe wechselt. Ursache: `source: Style.sesChevron` lädt die markenspezifische Asset-Datei `ses-chevron.svg` (`WLTheme.chevronIcon()`) direkt — deren Farbe ist fest ins SVG kodiert. Der `image://svgimage-custom-color/`-Provider, der Icons dynamisch nach `Style.sesTrayFontColor` einfärbt, kann nur Dateien im generischen `:/client/theme/`-Verzeichnis finden, keine markenspezifischen `ses/`-Assets. + +**Entscheidung:** Auf generisches `caret-down.svg` über den `svgimage-custom-color`-Provider umgestellt, getönt mit `Style.sesTrayFontColor` — gleiches Muster wie `TrayFoldersMenuButton.qml`/`CurrentAccountHeaderButton.qml`. Commit `9647f1174`. + +**Offener Punkt:** Icon-Form weicht leicht vom bisherigen Marken-Chevron ab — ggf. später durch einen passenden Marken-Asset von Design ersetzen. + +**Status:** aktiv + +## 2026-08-20 — isWindows11OrGreater() bleibt lokal in theme.cpp, verifiziert gegen stable-33.0 (SES-578) + +**Kontext:** Erwogen, den Windows-11-Versionscheck aus `theme.cpp` in einen wiederverwendbaren `Utility::isWindows11OrGreater()`-Helper zu extrahieren, in der Annahme, das sei näher an stable-33.0. Direkter Abgleich gegen den lokalen `stable-33.0`-Branch zeigt aber: dort ist `isWindows11OrGreater()` selbst eine lokale Funktion in `theme.cpp` (Zeile 62), kein `Utility::`-Helper. + +**Entscheidung:** Als lokale Funktion in `theme.cpp` belassen, exakt wie in stable-33.0 — keine Code-Änderung nötig, nur verifiziert. + +**Verworfene Alternative:** Extraktion nach `Utility::isWindows11OrGreater()` — hätte die Struktur von stable-33.0 weg bewegt statt angenähert. + +**Status:** aktiv + +## 2026-08-21 — Card-Panel-Hintergründe in GeneralSettings/AccountSettings wieder entfernt (SES-578) + +**Kontext:** Commit `b9e530504` hatte per neuem `panelBackgroundColor()`-Getter abgesetzte Karten-Hintergründe für die QGroupBox/QFrame-Panels eingeführt, mit der Begründung, das entspreche stable-33.0s gruppierter Panel-Optik. Ein vom Nutzer bereitgestellter Vergleichsscreenshot (Referenz-Build 4.0.11) zeigte für die Settings-Seite jedoch einen durchgängig einheitlichen Hintergrund ohne abgesetzte Karten. + +**Entscheidung:** `panelBackgroundColor()` (Getter + `Q_PROPERTY`) sowie beide Verwendungsstellen (`generalsettings.cpp`, `accountsettings.cpp`) vollständig entfernt. Seitenhintergrund läuft jetzt einheitlich über `dialogBackgroundColor()`, zusätzlich im Light Mode auf denselben Wert wie Tray `trayBackgroundColor()` (`#F7F7F9`) vereinheitlicht — Settings und Tray-`MainWindow` teilen sich damit exakt dasselbe Hintergrund-Farbschema in beiden Modi. + +**Verworfene Alternative:** Nur Kontrastrichtung/-stärke des Panel-Getters reparieren (Light heller, Dark massiv dunkler als die Seite — inkonsistent zwischen den Modi) statt die Panels komplett zu entfernen. Verworfen, da der Referenzscreenshot gar keine abgesetzten Panels zeigt; die stable-33.0-Analogie aus `b9e530504` traf also nicht zu. + +**Status:** aktiv diff --git a/.claude/skills/decision-log/SKILL.md b/.claude/skills/decision-log/SKILL.md new file mode 100644 index 0000000000000..5c4a6bf746823 --- /dev/null +++ b/.claude/skills/decision-log/SKILL.md @@ -0,0 +1,73 @@ +--- +name: decision-log +description: Hält Entscheidungen zum Ausblenden/Entfernen ganzer Komponenten sowie bewusste Abweichungen von stable-x.y in DECISIONS.md fest, inkl. Begründung — bewusst eng gefasst, siehe stable-merge-check für die Detail-Merge-Risiko-Analyse. Kein allgemeines Änderungsprotokoll: Bugfixes, Kompilierbarkeits-Fixes und reine Vereinheitlichungs-/Aufräum-Refactorings gehören NICHT hinein. Proaktiv nutzen: wenn eine Komponente bewusst ausgeblendet statt entfernt wird (oder umgekehrt), oder eine Struktur bewusst nah an/abweichend von stable-x.y gehalten wird, einen Eintrag vorschlagen bzw. anlegen, ohne dass explizit danach gefragt werden muss. Auch manuell auslösbar. Trigger — "halte das im Entscheidungs-Log fest", "log diese Entscheidung", "warum haben wir X ausgeblendet/entfernt", "warum weichen wir hier von stable ab". +--- + +# Decision Log + +Ziel ist **nicht** ein allgemeines Änderungsprotokoll, sondern gezielt zwei Dinge nachvollziehbar zu machen: + +1. Warum eine ganze Komponente/ein ganzes Feature **ausgeblendet statt entfernt** wurde (oder umgekehrt entfernt statt nur ausgeblendet) — und was das für Nähe/Abstand zu stable-x.y bedeutet. +2. Warum eine Struktur bewusst **nah an stable-x.y gehalten** oder bewusst **davon abgewichen** wurde — relevant für spätere Merge-Konflikte oder Diagnose "was hat der Merge kaputt gemacht". + +Ohne Protokoll geht diese Begründung verloren, und ein späteres Gespräch riskiert, eine bereits geprüfte Abweichung erneut aufzurollen oder eine bewusst ausgeblendete Komponente versehentlich zu entfernen (oder umgekehrt). + +Dieser Skill pflegt dafür ein einziges, chronologisches Protokoll in [DECISIONS.md](DECISIONS.md). + +## Wann ein Eintrag sinnvoll ist + +- Eine **ganze Komponente/ein ganzes Feature** wird ausgeblendet statt entfernt (Code bleibt bestehen, nur `setVisible(false)`/Guard) — oder umgekehrt tatsächlich entfernt/gelöscht. +- Eine Struktur wird bewusst **nah an stable-x.y gehalten oder bewusst davon abgewichen**, mit Auswirkung auf künftige Merge-Konflikte (Überschneidung mit [stable-merge-check](../stable-merge-check/SKILL.md) — dort ggf. kurz verweisen statt inhaltlich zu duplizieren). +- Toter Code/eine Komponente wird bewusst **erhalten**, weil stable-x.y sie noch führt (Merge-Kompatibilität), obwohl sie im Fork ungenutzt wirkt. + +**Nicht** eintragen (auch wenn eine Alternative erwogen wurde): +- Bugfixes jeder Art, auch wenn dabei eine Alternative verworfen wurde. +- Anpassungen, die nur die Kompilierbarkeit/den Build wiederherstellen (Includes, Case-Sensitivity, fehlende Pakete, Toolchain-Probleme). +- Rein mechanische Aufräumarbeiten (verwaiste Einzeldeklarationen, tote Funktionen ohne Komponentencharakter) — auch wenn "entfernt statt reaktiviert" wie eine Entscheidung aussieht, zählt das hier nicht, solange kein ganzes Feature/keine ganze Komponente betroffen ist. +- Reine Vereinheitlichungs-/Konsistenz-Refactorings ohne Bezug zu stable-x.y-Abweichung. +- Dev-Tooling/Build-Umgebung (Linux-Setup, CraftRoot, IDE-Konfiguration) — nichts davon betrifft Produktcode oder stable-Nähe. +- Triviale Ein-Weg-Fixes, reine Tippfehler-Korrekturen, Entscheidungen ohne echte Alternative. + +**Faustregel bei Unsicherheit:** Würde ein späterer stable-x.y-Merge oder eine "warum ist Komponente X weg/da" Frage von diesem Eintrag profitieren? Wenn nein, gehört es nicht ins Log. + +## Knapp halten (Token-Budget) + +Diese Datei wird potenziell bei jedem neuen Eintrag gelesen — sie soll nicht zur Kopie des Gesprächsverlaufs werden. + +- Jedes Feld (`Kontext`/`Entscheidung`/`Verworfene Alternative(n)`) **1–3 Sätze**, kein Fließtext-Absatz. Wenn es mehr braucht, gehört das Detail in den Commit/PR, nicht ins Log — hier nur die Kurzfassung + Referenz. +- Keine Code-Blöcke, keine Diffs, keine kompletten Funktionskörper zitieren. Stattdessen Datei:Zeile oder Commit-Hash referenzieren. +- Aufzählungen statt Prosa, wo es passt. +- Nicht dieselbe Begründung in mehreren Einträgen wiederholen — auf den früheren Eintrag verlinken (`siehe Eintrag vom YYYY-MM-DD`) statt sie erneut auszuformulieren. + +## Ablauf + +1. **Proaktiv erkennen**: Wenn im Gespräch eine Komponente ausgeblendet statt entfernt wird (oder umgekehrt), oder eine Struktur bewusst nah an/abweichend von stable-x.y gehalten wird, kurz anbieten oder direkt einen Eintrag ergänzen ("Ich halte das im Entscheidungs-Log fest.") — nicht erst warten, bis explizit danach gefragt wird. Bei allem anderen (Bugfixes, Build-Fixes, Aufräumarbeiten) nicht proaktiv anbieten, siehe "Wann ein Eintrag sinnvoll ist" oben. +2. **Vor einem neuen Eintrag gezielt prüfen, ob es schon einen zum selben Thema gibt** — nicht die komplette Datei lesen, sondern per Grep nach dem Thema/Dateinamen/Funktionsnamen suchen (z. B. `Grep "avatar" DECISIONS.md`). Nur bei echtem Treffer den betroffenen Abschnitt gezielt lesen. Das hält den Kontextverbrauch auch bei einer langen Datei klein. +3. **Neuen Abschnitt anhängen** (chronologisch, ans Ende) im Format: + + ```markdown + ## YYYY-MM-DD — Kurzer Titel (Ticket-Referenz falls vorhanden) + + **Kontext:** 1–3 Sätze: Problem/Symptom, Auslöser. + + **Entscheidung:** 1–3 Sätze: was umgesetzt/gewählt wurde, ggf. Commit-Hash. + + **Verworfene Alternative(n):** 1–3 Sätze pro Alternative: was geprüft wurde und der konkrete Grund der Ablehnung. + + **Status:** aktiv | überholt durch [Datum/Titel] | offen + ``` + + Nicht jedes Feld ist immer nötig — "Verworfene Alternative(n)" nur, wenn es welche gab. +4. **Bei späterer Revision eines alten Eintrags**: nicht überschreiben, sondern neuen Abschnitt anhängen und im alten Eintrag `Status: überholt durch [Datum/Titel]` ergänzen, damit die Historie nachvollziehbar bleibt (wie ein Git-Log, nicht wie eine editierbare Wahrheit). +5. Kurz mit dem Nutzer bestätigen, dass der Eintrag so passt, besonders bei der Formulierung des "Warum" — das ist der Teil, der später zählt. + +## Archivierung + +Wenn `DECISIONS.md` über ca. 25 Einträge oder 300 Zeilen wächst: die ältesten Einträge mit `Status: überholt durch ...` (deren Begründung durch einen neueren Eintrag ohnehin abgedeckt ist) nach `DECISIONS-archive.md` verschieben, im Hauptfile nur einen Ein-Zeiler-Verweis lassen (`## YYYY-MM-DD — Titel → siehe DECISIONS-archive.md`). Aktive/offene Einträge bleiben immer im Hauptfile. Nur nach kurzer Rücksprache mit dem Nutzer durchführen, nicht automatisch. + +## Abgrenzung + +- Für stable-x.y-Vergleichs-/Merge-Risiko-Entscheidungen im Detail: [stable-merge-check](../stable-merge-check/SKILL.md). +- Für Protokoll zu Fork-Ersatzkomponenten vs. Upstream-Original: [shadow-component-watch](../shadow-component-watch/CHANGELOG.md). +- Für gebündelten Kontextabruf zu einer einzelnen Komponente (inkl. Auszug aus diesem Log): [component-context](../component-context/SKILL.md) — liest hier nur, schreibt nichts. +- Dieser Skill dupliziert deren Inhalte nicht, sondern verlinkt bei Bedarf darauf. diff --git a/.claude/skills/gui-context-refresh/SKILL.md b/.claude/skills/gui-context-refresh/SKILL.md new file mode 100644 index 0000000000000..e7e596f521140 --- /dev/null +++ b/.claude/skills/gui-context-refresh/SKILL.md @@ -0,0 +1,40 @@ +--- +name: gui-context-refresh +description: Aktualisiert die AI-lesbare Dokumentation von src/gui unter .claude/context/gui/ (CLAUDE.md je Unterordner + COMPONENTS.md für den flachen Wurzelbereich), indem nur die seit dem letzten Scan tatsächlich geänderten Bereiche neu erfasst werden. Nur manuell auslösen. Trigger — "aktualisiere die gui-doku", "gui context map ist veraltet", "gui-Doku auf den neuesten Stand bringen", nach größeren Umbauten/Merges in src/gui. +--- + +# GUI-Context-Refresh + +Hält `.claude/context/gui/` (siehe dessen [CLAUDE.md](../../context/gui/CLAUDE.md)) aktuell, ohne bei jedem Aufruf alle ~450 Dateien in `src/gui/` neu zu scannen. Wird **nur manuell** aufgerufen, nicht automatisch bei jeder Session. + +## Ablauf + +1. **Referenzstruktur einlesen**: Liste alle Dateien unter `.claude/context/gui/` (`CLAUDE.md` je Unterordner + `COMPONENTS.md`). Jede trägt am Ende eine Zeile `*Quelle: src/gui/ — Stand YYYY-MM-DD, ...*`. Dieses Datum ist der Ausgangspunkt. + +2. **Strukturabgleich zuerst** (billig, immer machen): + - `find src/gui -maxdepth 1 -type d` gegen die Liste der dokumentierten Unterordner abgleichen. + - Neuer Unterordner ohne `CLAUDE.md`-Gegenstück? → Dem Nutzer melden, danach wie in Schritt 4 (Vollscan) behandeln. + - Dokumentierter Unterordner, der nicht mehr existiert? → Nutzer fragen, ob die verwaiste Doku-Datei gelöscht werden soll (nicht automatisch löschen). + +3. **Änderungs-Check pro Bereich** (git-basiert, kein Neuscan): + - Pro Unterordner: `git log --oneline --since=" 00:00:00" -- src/gui/` + - Für den flachen Wurzelbereich (COMPONENTS.md): `git log --oneline --since=" 00:00:00" -- src/gui/*.cpp src/gui/*.h src/gui/*.ui src/gui/*.qml src/gui/*.mm` (Shell-Glob, matcht nur Dateien direkt in `src/gui/`, nicht in Unterordnern — im Bash-Tool ausführen, nicht PowerShell, damit die Glob-Expansion greift). + - **Wichtig:** immer `" 00:00:00"` an das Stand-Datum anhängen. `--since=` ohne Uhrzeit interpretiert Git als "dieses Datum, aktuelle Uhrzeit" — an genau dem Tag, an dem der Refresh läuft (Stand-Datum = heute), filtert das sonst stillschweigend alle bereits heute gemachten Commits heraus und täuscht "keine Änderungen" vor. + - Keine Treffer → Bereich überspringen, nichts anfassen. + +4. **Bei Treffern: Umfang der Änderung einschätzen, dann passend reagieren**: + - **Kleine Änderung** (grob: ≤ 3 betroffene Dateien, keine neuen/gelöschten Dateien, keine neuen Top-Level-Klassen): gezielt nachziehen — `git diff -- ` lesen und nur die betroffenen Zeilen/Bullet-Points im bestehenden `CLAUDE.md`/`COMPONENTS.md`-Abschnitt per Edit anpassen. Kein Agent nötig für triviale Fälle. + - **Größere/strukturelle Änderung** (neue Dateien, gelöschte Dateien, umbenannte Kernklassen, oder viele betroffene Dateien): den betroffenen Bereich komplett neu scannen wie beim Erstaufbau — ein Explore-Agent mit demselben Muster wie in der ursprünglichen Erstellung (Zweck, Kernklassen, Zusammenspiel, Fork-spezifisch vs. Upstream) über den Ordner laufen lassen und die zugehörige Datei komplett neu schreiben. Für den Wurzelbereich: nur den/die betroffenen Cluster in `COMPONENTS.md` neu scannen, nicht die ganze Tabelle. + - Bei Unsicherheit, welche Kategorie zutrifft: lieber der größeren Kategorie zuordnen (Vollscan des Bereichs) statt eine unvollständige gezielte Anpassung zu riskieren. + +5. **Datum aktualisieren**: Nach jedem tatsächlich geänderten Bereich die `*Quelle: ... Stand YYYY-MM-DD*`-Zeile auf das heutige Datum setzen. Unveränderte Bereiche behalten ihr altes Datum (zeigt ehrlich, wie alt die Doku dort wirklich ist). + +6. **Zusammenfassung geben**: Am Ende auflisten, welche Bereiche unverändert übersprungen, welche gezielt nachgezogen und welche komplett neu gescannt wurden. Neue/verwaiste Unterordner aus Schritt 2 erneut hervorheben, falls der Nutzer sie noch nicht behandelt hat. + +## Nicht in diesem Skill enthalten + +- Kein automatisches Löschen verwaister Dateien (immer erst fragen). +- Kein periodischer/automatischer Trigger — der Nutzer entscheidet bewusst, wann aktualisiert wird. +- Prüft nicht inhaltlich, ob die *Fork-spezifisch vs. Upstream*-Einordnungen noch stimmen, wenn sich am Referenz-Branch selbst etwas geändert hat (dafür ist [stable-merge-check](../stable-merge-check/SKILL.md) zuständig). +- Liefert keinen gebündelten Kontext zu einer einzelnen Komponente (Doku + Decision-Log + Git-Historie zusammen) — dafür [component-context](../component-context/SKILL.md), der diese Doku nur liest. +- Pflegt keine Farbschema-Landkarte (welches Theme-Property, Light-/Dark-Wert, Bruch oder nicht) — dafür [color-scheme-map](../color-scheme-map/SKILL.md). diff --git a/.claude/skills/merge-drift-map/DRIFT_MAP.md b/.claude/skills/merge-drift-map/DRIFT_MAP.md new file mode 100644 index 0000000000000..2dd455b016012 --- /dev/null +++ b/.claude/skills/merge-drift-map/DRIFT_MAP.md @@ -0,0 +1,67 @@ +# Merge-Drift-Landkarte + +Wird vom [merge-drift-map](SKILL.md)-Skill Datei für Datei/Komponente für Komponente befüllt — kein Vollscan, sondern nur tatsächlich geprüfte Stellen. Trackt nur den **Ist-Zustand** je Datei, keine Verlaufs-/Rundenhistorie — eine erneute Prüfung aktualisiert die bestehende Zeile statt einen neuen Abschnitt anzuhängen. + +## Legende + +**Diff-Anteil** (geänderte Zeilen ÷ Zeilen auf `stable-33.0`): wie *oft* ein Merge hier vermutlich einen Textkonflikt auslöst — sagt nichts darüber, wie gefährlich der wäre. + +**Qualitative Einstufung** (niedrig/mittel/hoch, aus [stable-merge-check](../stable-merge-check/SKILL.md)s Workflow A): wie *gefährlich* eine Abweichung tatsächlich ist — additive Whitelabel-Ergänzungen sind unkritisch, umbenannte/umstrukturierte Stellen können hart kollidieren oder sich sogar unbemerkt falsch zusammenführen. + +Kombiniert: niedrig+niedrig = unauffällig, hoch+niedrig = viele harmlose Konflikte, niedrig+hoch = seltene, aber gefährliche Stellen, hoch+hoch = besondere Vorsicht. + +## Erfasste Dateien + +*Zuletzt aktualisiert: eigener Commit `0b9c2b950`, stable-Seite `28fdc6898`/`9e65fef46` (je Zeile s. Spalte, sofern abweichend geprüft)* + +| Datei | Auf stable? | Diff-Anteil | Qualitative Einstufung | Bewusst/Drift | Zuletzt geprüft | Bemerkung | +|---|---|---|---|---|---|---| +| `src/gui/settingsdialog.cpp` | ja | ~49% (298/607) | **hoch** | gemischt | `9d3af4e33` / `9e65fef46` | bekannter Merge-Hotspot, `IONOS_BUILD`-Verzweigungen | +| `src/gui/application.cpp` | ja | ~35% (424/1205) | **hoch** | bewusst | `2f77361ec` / `9e65fef46` | Änderungen über nahezu die gesamte Datei verteilt (23 Hunks: Konstruktor, Destruktor, `setupAccountsAndFolders`, `setupLogging`, `event()`); mehrere Hunks mit Netto-Zeilenreduktion (echte Restrukturierung, nicht nur Anbau) — u. a. DWM-Dark-Titlebar, `sesStyle`-Konstruktion, ShellExtensionsServer | +| `src/gui/foldercreationdialog.cpp` | ja | ~74% (64/86) | **hoch** | gemischt | `2f77361ec` / `9e65fef46` | Styling-Teil additiv/bewusst, aber `accept()`-Guard `QDir(fullPath).exists()` entfernt + `ui->labelErrorMessage`→`ui->errorSnackbar` umbenannt — echte Logik-/Identifier-Änderung, höchstes Konfliktrisiko dieser Dialog-Gruppe | +| `src/gui/generalsettings.cpp` | ja | ~55% (451/827) | **hoch** | bewusst | `2f77361ec` / `9e65fef46` | großflächig über Konstruktor/Destruktor/fast alle Slots verteilt, u. a. Identifier-Rename `advanced_groupBox`→`advancedGroupBox` (Merge-Angleichung) und neue Card-Panel-Optik | +| `src/gui/tray/ActivityItemContent.qml` | ja | ~45% (137/307) | **hoch** | bewusst | `2f77361ec` / `9e65fef46` | `Button`→eigene `IconButton`-Komponente, komplett neue MenuItem-Hintergrund-Delegates mit Hover/Pressed/Tooltip — echte Restrukturierung | +| `src/gui/tray/UserLine.qml` | ja | **~110%** (286/261) | **hoch** | bewusst | `2f77361ec` / `9e65fef46` | Diffgröße übersteigt die gesamte stable-Datei — `Menu`→eigenes `Popup`, Avatar-Status-Overlay entfernt, `Button`→`IconButton`, Farblogik von Palette-Kaskaden auf `Style.ses*` — praktisch Neuimplementierung; am stärksten umgebaute Datei der gesamten Karte | +| `src/gui/folderstatusdelegate.cpp` | ja | ~72% (349/482) | **nicht vollständig geprüft** | — | `9d3af4e33` / `9e65fef46` | nur SES-578-Teiländerung inhaltlich angesehen, nicht volle Historie — Kandidat für eine vertiefte `stable-merge-check`-Runde | +| `src/gui/ignorelisttablewidget.cpp` | ja | ~76% (146/193) | mittel | bewusst | `2f77361ec` / `9e65fef46` | Großteil reiner Anbau (`customize*Style()`), aber `slotAddPattern()` von statischem `QInputDialog::getText()` auf Instanz mit `exec()`/`textValue()` umgebaut — echte Logikänderung | +| `src/gui/filedetails/ShareeSearchField.qml` | ja | ~13% (31/232) | mittel | gemischt | `2f77361ec` / `9e65fef46` | Namespace-Rename (projektweite Whitelabel-Umbenennung, systemweite Konfliktstelle) + bewusste SES-578-Dark-Mode-Fixes; vormals palette-basierte Werte durch `Style.*` ersetzt | +| `src/gui/UserStatusMessageView.qml` | ja | ~4% (10/229) | mittel | gemischt | `2f77361ec` / `9e65fef46` | Namespace-Rename (gleiche systemweite Konfliktlinie) + reine Ergänzung eines `indicator: Image {}`-Blocks | +| `theme/Style/Style.qml` | ja | ~60% (130/216) | niedrig | bewusst (Whitelabel) | `9d3af4e33` / `9e65fef46` | fast nur zusätzliche `ses*`-Properties, Kernfunktionen unverändert | +| `src/gui/folderwizard.cpp` | ja | ~63% (467/736) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | additive Whitelabel-Styles + eine bewusste Bugfix-Zeile (`ModernStyle`) | +| `src/gui/accountsettings.cpp` | ja | ~30% (586/1927) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | Struktur deckungsgleich, nur Branding-Werte | +| `src/gui/legacyaccountselectiondialog.cpp` | ja | ~61% (31/51) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | hoher Prozentsatz nur wegen kleiner Ausgangsdatei; inhaltlich reine Additionen | +| `src/gui/selectivesyncdialog.cpp` | ja | ~19% (106/553) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | fast rein additiv | +| `src/gui/conflictdialog.cpp` | ja | ~21% (36/175) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | reine Additionen, macOS-only QCheckBox-Styles | +| `src/gui/caseclashfilenamedialog.cpp` | ja | ~17% (47/280) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | reine Additionen | +| `src/gui/invalidfilenamedialog.cpp` | ja | ~16% (51/314) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | fast nur Additionen, 2 Deletions rein kosmetisch | +| `src/gui/tray/UnifiedSearchInputContainer.qml` | ja | ~15% (15/98) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | reine Ergänzungen (Hintergrund-Rectangle, Farbzuweisungen) | +| `src/gui/application.h` | ja | ~2% (4/162) | niedrig | bewusst | `2f77361ec` / `9e65fef46` | reine Methodendeklarations-Additionen (`eventFilter`, `startTracking`/`stopTracking`) | +| `theme.cpp` (`src/libsync/theme.cpp`) | ja | ~17% (195/1156) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | vollständig gegen stable durchgegangen | +| `src/gui/owncloudgui.cpp` | ja | ~15% (115/760) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | | +| `src/libsync/theme.h` | ja | ~3% (23/682) | niedrig | bewusst | `9d3af4e33` / `9e65fef46` | | +| `src/gui/systray.cpp` | ja | ~3,6% (39/1086) | niedrig | bewusst | `0b9c2b950` / `28fdc6898` | fast rein additiv (neuer `darkModeChanged`-Connect, `WLTheme`-Styling fürs Kontextmenü); eine kleine unabhängige Logik-Ergänzung (`localPath.isEmpty()`-Guard in `presentShareViewInTray`), selbst additiv, keine bestehende Zeile geändert | +| `src/gui/CMakeLists.txt` | ja | ~7,3% (55/758) | niedrig | bewusst | `0b9c2b950` / `28fdc6898` | praktisch nur `list(APPEND client_SRCS ...)`-Blöcke für neue Whitelabel-/SES-Dateien plus ein neues `SKIP_MACDEPLOYQT`-Option; eine unabhängige Quoting-Korrektur bei `-executable=` | +| `src/common/utility.h` | ja | **0%** | keins | — | `9d3af4e33` / `9e65fef46` | identisch zu stable (frühere `isWindows11OrGreater()`-Verlagerung wieder zurückgerollt) | +| `src/common/utility_win.cpp` | ja | **0%** | keins | — | `9d3af4e33` / `9e65fef46` | dito | +| `src/gui/buttonstyle.h` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `9d3af4e33` | | +| `src/gui/stratotheme.h` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `9d3af4e33` | | +| `src/gui/basetheme.h` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `9d3af4e33` | | +| `src/gui/sessnackbar.cpp` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `9d3af4e33` | | +| `src/gui/sessnackbar.h` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | | +| `src/gui/SesComponents/SesErrorBox.qml` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | | +| `src/gui/wizard/dataprotectionpage.cpp` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | | +| `src/gui/wizard/dataprotectionsettingspage.cpp` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | | +| `src/gui/tray/AccountMenuItem.qml` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | SES-578-Neubau | +| `src/gui/tray/TrayWindowAccountMenu.qml` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `2f77361ec` | SES-578-Neubau | +| `src/gui/sesstyle.cpp` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `0b9c2b950` | zentrale `sesStyle`-Implementierung (269 Zeilen), Kern der SES-578-Theming-Arbeit | +| `src/gui/sesstyle.h` | **nein** | n/a (fork-only) | keins von stable-Seite | bewusst | `0b9c2b950` | Header dazu (78 Zeilen) | + +## Auffällige Kombinationen + +- **hoch+hoch:** keine aktuell in der Karte. +- **hoch Diff-Anteil + niedriges/mittleres Risiko** (häufige, aber überwiegend harmlose Konflikte zu erwarten): `folderstatusdelegate.cpp` (72%, noch nicht vollständig qualitativ geprüft), `ignorelisttablewidget.cpp` (76%), `folderwizard.cpp` (63%), `legacyaccountselectiondialog.cpp` (61%), `Style.qml` (60%). +- **niedrig Diff-Anteil + hoch Risiko** (selten, aber gefährlich): keine aktuell in der Karte — die frühere `isWindows11OrGreater()`-Verlagerung wäre ein Beispiel gewesen, ist aber bereits zurückgerollt (daher jetzt 0% bei `utility.h`/`utility_win.cpp`). +- **Versteckte Logikänderungen unter additivem Styling-Diff:** `foldercreationdialog.cpp` und `ignorelisttablewidget.cpp` — bei künftigen Merges nicht nur auf Konflikt-Menge, sondern auf den Inhalt der geänderten Zeilen achten. +- **Ungünstigste Kombination (hoher Diff-Anteil + hohes Risiko):** `settingsdialog.cpp`, `application.cpp`, `foldercreationdialog.cpp`, `generalsettings.cpp`, `ActivityItemContent.qml`, `UserLine.qml` — verdienen vor jedem `stable-33.0`-Merge besondere Aufmerksamkeit. + +Damit sind alle 46 von SES-578-Commits berührten `src/gui/`- und `src/libsync/`-Dateien in dieser Karte erfasst. diff --git a/.claude/skills/merge-drift-map/SKILL.md b/.claude/skills/merge-drift-map/SKILL.md new file mode 100644 index 0000000000000..3efc6966cdece --- /dev/null +++ b/.claude/skills/merge-drift-map/SKILL.md @@ -0,0 +1,60 @@ +--- +name: merge-drift-map +description: Pflegt eine Landkarte in DRIFT_MAP.md, die pro Datei/Klasse zeigt, wie stark sie gegenüber stable-33.0 abweicht — als zwei getrennte Werte: Diff-Anteil (wie oft ein Merge hier vermutlich kollidiert) und qualitative Risiko-Einstufung aus stable-merge-check (wie gefährlich eine Kollision wäre). Wird bewusst Datei für Datei/Komponente für Komponente befüllt, kein Vollscan. Nur manuell auslösen. Trigger — "wie stark weicht von stable ab", "Drift-Map aktualisieren", "Merge-Risiko-Landkarte", "wo sind Brüche zu stable-33.0 zu erwarten". +--- + +# Merge-Drift-Map + +`stable-merge-check` beantwortet die Frage "wie riskant ist diese eine Datei" sehr gut, aber nur als einmalige Antwort im Gespräch — sie geht danach verloren. Dieser Skill hält das Ergebnis dauerhaft fest, damit sich über die Zeit eine Übersicht aufbaut, welche Dateien wie stark umgebaut sind und wo bei künftigen `stable-x.y`-Merges am ehesten Ärger zu erwarten ist. + +**Zwei getrennte Werte, bewusst nicht zu einer Kennzahl verschmolzen** (Diskussion dazu: reine Diff-Größe allein ist irreführend — `folderwizard.cpp` hat einen sehr hohen Diff-Anteil und ist trotzdem unkritisch, während die frühere `isWindows11OrGreater()`-Verlagerung nur 3 Zeilen Diff war, aber ein echter struktureller Rückschritt gegenüber stable): + +- **Diff-Anteil** (geänderte Zeilen ÷ Zeilen auf `stable-33.0`): wie *oft* ein Merge hier vermutlich einen Textkonflikt auslöst — sagt nichts darüber, wie gefährlich der wäre. +- **Qualitative Einstufung** (niedrig/mittel/hoch, aus [stable-merge-check](../stable-merge-check/SKILL.md)s Workflow A): wie *gefährlich* eine Abweichung tatsächlich ist — additive Whitelabel-Ergänzungen sind unkritisch, umbenannte/umstrukturierte Stellen können hart kollidieren oder sich sogar unbemerkt falsch zusammenführen. + +Kombiniert: niedrig+niedrig = unauffällig, hoch+niedrig = viele harmlose Konflikte, niedrig+hoch = seltene, aber gefährliche Stellen, hoch+hoch = besondere Vorsicht. + +**Bewusst inkrementell, Datei/Komponente für Komponente** — kein Vollscan über das ganze Repo in einem Lauf. + +## Datei + +- [DRIFT_MAP.md](DRIFT_MAP.md) — Legende (die Erläuterung oben) einmal am Anfang, danach **eine einzige, flache Tabelle** über alle bisher geprüften Dateien: Datei | Auf stable? | Diff-Anteil | Qualitative Einstufung | Bewusst/Drift | zuletzt geprüfter Commit (eigener und stable-Seite) | Bemerkung. **Kein Verlaufs-/Rundenprotokoll**: eine erneute Prüfung überschreibt die bestehende Zeile der Datei, statt einen neuen datierten Abschnitt ("Runde 2", "Erstbefüllung" o. ä.) anzuhängen. Die Karte zeigt nur den Ist-Zustand — Historie über verschiedene Repo-Stände pflegt der Nutzer bewusst an anderer Stelle selbst. + +## Referenz-Branch + +Wie [stable-merge-check](../stable-merge-check/SKILL.md): Branch aus dessen `reference-branch.txt` lesen, dem Nutzer nennen, vorher `git fetch origin ` (rein lesend). + +## Ablauf + +1. **Datei(en)/Komponente identifizieren.** Vom Nutzer übernehmen, bei vager Beschreibung wie bei [component-context](../component-context/SKILL.md) über `.claude/context/gui/**/CLAUDE.md`/`COMPONENTS.md` auflösen. + +2. **Prüfen, ob schon erfasst** (`Grep DRIFT_MAP.md`): + - Vorhanden mit "zuletzt geprüfter Commit" (eigene Seite und stable-Seite) → nur inkrementell nachziehen: `git log ..HEAD -- ` und `git log ..origin/ -- `. Keine neuen Commits auf beiden Seiten → nichts tun. Neue Commits → nur die betroffene Zeile neu bewerten, nicht die ganze Datei neu rechnen. + - Noch nicht erfasst → Vollerfassung (Schritte 3–5). + +3. **Existenz prüfen:** `git cat-file -e origin/:`. Existiert die Datei auf stable nicht → Diff-Anteil ist **n/a (fork-only)**, qualitative Einstufung **keins von stable-Seite**, fertig — kein Konflikt möglich, den es nicht schon durch die reine Existenz der Datei gäbe. + +4. **Diff-Anteil berechnen** (existiert die Datei): + - Zeilenzahl auf stable: `git show origin/: | wc -l` + - Änderungsumfang: `git diff --shortstat origin/ HEAD -- ` + - Anteil = (insertions + deletions) ÷ Zeilenzahl auf stable, grob gerundet in Prozent. + +5. **Qualitative Einstufung ermitteln:** Workflow A aus [stable-merge-check](../stable-merge-check/SKILL.md) anwenden (nicht hier duplizieren) — `git diff origin/ -- ` lesen, niedrig/mittel/hoch einordnen, bewusst vs. Drift vermerken. Bei Unsicherheit oder wenn nur ein Teil der Datei bisher inhaltlich geprüft wurde, ehrlich **"nicht vollständig geprüft"** statt eine Einstufung zu raten. + +6. **`DRIFT_MAP.md` schreiben/aktualisieren:** neue Zeile in der einen Tabelle anlegen, oder bei einer erneuten Prüfung die bestehende Zeile der Datei überschreiben (nicht duplizieren) — beide "zuletzt geprüft"-Commits (eigener HEAD, stable-Seite) auf den aktuellen Stand setzen. + +7. **Auffällige Kombinationen hervorheben.** Am Ende dem Nutzer explizit die hoch+hoch-Fälle nennen (besondere Vorsicht) sowie neue niedrig+hoch-Fälle (selten, aber gefährlich) — das sind die eigentlich interessanten Ergebnisse, nicht die Tabelle an sich. + +## Abgrenzung + +- **stable-merge-check**: liefert die qualitative Methodik und macht die tiefergehende Post-Merge-Diagnose (Workflow B) — dieser Skill übernimmt nur deren Ergebnis in die Landkarte, dupliziert die Methode nicht. +- **color-scheme-map**: gleiches Muster (inkrementelle, dauerhafte Landkarte), aber für Farb-Properties statt Merge-Drift — unabhängige Dimension, kein Zusammenhang zwischen den beiden Karten. +- **gui-context-refresh**: Struktur/Zweck einer Komponente — nicht deren Abweichung von stable. + +## Nicht in diesem Skill enthalten + +- Keine eigene Risikomethodik — reine Diff-Größe wird nie allein als Risikoaussage verwendet, immer nur neben der qualitativen Einstufung aus `stable-merge-check`. +- Kein Vollscan aller Dateien in einem Lauf. +- Kein automatisches Fixen gefundener Risiken. +- Kein periodischer/automatischer Trigger. +- Keine Verlaufs-/Rundenhistorie in `DRIFT_MAP.md` (kein "Runde 1/2/3", keine datierten Abschnitte) — nur der Ist-Zustand pro Datei. Historie über verschiedene Repo-Stände pflegt der Nutzer bewusst an anderer Stelle selbst. diff --git a/.claude/skills/shadow-component-watch/CHANGELOG.md b/.claude/skills/shadow-component-watch/CHANGELOG.md new file mode 100644 index 0000000000000..f52e0d2abbb13 --- /dev/null +++ b/.claude/skills/shadow-component-watch/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog: Shadow-Component-Watch + +Chronologisches Protokoll jeder Prüfung. Referenz-Branch beim jeweiligen Lauf siehe `../stable-merge-check/reference-branch.txt` (Stand zum Prüfzeitpunkt: `stable-33.0`). + +## 2026-08-17 — Erstaufbau der Registry + +**Paar: `TrayWindowHeader.qml` ↔ `SesTrayHeader.qml`** +Geprüfte Commits (komplette bisherige Historie, da Erstaufbau): `01e099546`, `6475c51ab`, `1ae041da0`, `19d70cdaa`, `568cbe171`. +- Featured-App-Signal (`featuredAppButtonClicked`), Property-Umbenennungen (`isFeaturedAppEnabled` → `isNcAssistantEnabled` → `isAssistantEnabled`), i18n-String "team folder" — **bereits portiert**, exakt übereinstimmend im aktuellen `SesTrayHeader.qml`. +- "More apps"-Menü inkl. Icon-Ladepfad-Fix (`19d70cdaa`) — **bewusst nicht portiert**: das ganze Multi-App-Menü existiert im Fork nicht (Ticket `SES-4`, einzelner "Website"-Button stattdessen), Fix daher gegenstandslos. + +**Paar: `CurrentAccountHeaderButton.qml` ↔ `TrayWindowAccountMenu.qml`** +Geprüfte Commits (komplette bisherige Historie, da Erstaufbau): u. a. `60e0cb196` (color fix), `7f9d6a869` (`parentBackgroundColor` an `UserLine`), `bcb421cab`/`adaaf3634` (Status-Hintergrund), `de066e6b9` (anySyncFolders-Guard), `7ce4f78c4` (Sync-Error-Indikator in Account-Zeile). +- **`de066e6b9`** (Pause/Resume-Sync-Menüeintrag nur sichtbar wenn `Systray.anySyncFolders`) — fehlte in `TrayWindowAccountMenu.qml`. **Verdict: nachgezogen** — `enabled`/`visible: Systray.anySyncFolders` an `syncPauseButton` ergänzt (zugehörige C++-Property existierte bereits in `systray.h`/`.cpp`, keine neue Logik nötig). +- Online-Status-Indikator (farbiger Punkt) — im Fork explizit mit `visible: false // SES-50 Remove Inidcator` deaktiviert. **Verdict: bewusst nicht portiert** (Ticket `SES-50`). +- Dynamische Menübreite nach breitestem Eintrag vs. feste `Style.sesAccountMenuWidth` — **Verdict: bewusstes Design** (passt zum restlichen statischen Ses-Farbsystem), kein Ticket-Bezug bekannt, aber nicht überraschend genug für einen Portierungs-Kandidaten. +- `parentBackgroundColor`-Weiterreichung für kontrastabhängige Hintergründe — **Verdict: bewusstes Design**, entfällt weil der Fork statische `Style.ses*`-Farben statt dynamischer QPalette nutzt. +- Sync-Error-Indikator in der Account-Zeile (`7ce4f78c4`) — bereits vorhanden in `UserLine.qml` (`syncStatusColumn`/`syncStatusIndicator`). **Verdict: bereits portiert.** + +**Sonstiges:** `TrayWindowHeaderBar.qml` (dritte "Karteileiche" im selben Ordner) wurde im Rahmen dieser Analyse gelöscht — kein Eintrag in der Registry, da reines Fork-Artefakt ohne jemaliges Upstream-Pendant (nie in `resources.qrc`, nie aus `MainWindow.qml`/`TrayWindowHeader.qml`/`SesTrayHeader.qml` referenziert). + +**Korrektur (siehe Eintrag unten, selber Tag):** Der oben gesetzte "Zuletzt geprüft"-Marker `de066e6b9` für `CurrentAccountHeaderButton.qml` war zu optimistisch — es wurden bei der Erstaufbau nur die Top-5-Commits plus der eine für `anySyncFolders` relevante Commit inhaltlich geprüft, nicht die komplette Spanne bis dorthin. Der erste reguläre Lauf des Skills (unten) hat das nachgeholt. + +## 2026-08-17 — Erster regulärer Lauf: Nachhol-Review der Lücke + +Referenz-Branch: `origin/stable-33.0` (`git fetch` durchgeführt). + +**Paar: `TrayWindowHeader.qml` ↔ `SesTrayHeader.qml`** +`git log 568cbe171..origin/stable-33.0 -- src/gui/tray/TrayWindowHeader.qml` → keine neuen Commits. **Kein Handlungsbedarf**, Stand unverändert. + +**Paar: `CurrentAccountHeaderButton.qml` ↔ `TrayWindowAccountMenu.qml`/`UserLine.qml`** +`git log de066e6b9..origin/stable-33.0 -- ...` ergab überraschend 24 bisher ungeprüfte Commits (2025-03-19 bis 2026-02-11) — die Erstaufbau-Prüfung hatte diese Lücke übersehen (s. Korrektur oben). Alle 24 durchgesehen: +- Dynamische Text-/Icon-Farben über `palette`/`Style.currentUserHeaderTextColor` (`2e321d238`, `896cd1196`, `0e346e8d3`), Status-Icon-Hintergrund/-Position (`0f84c803f`, `cf326c1c7`, `5248b59a1`, `bcb421cab`, `adaaf3634`), `parentBackgroundColor` fürs Status-Icon (`7f9d6a869`, `60e0cb196`) — **bewusst nicht portiert**: hängt alles am Online-Status-Indikator, der im Fork komplett deaktiviert ist (`SES-50`). `parentBackgroundColor` existiert als Property in `UserLine.qml`, wird aber nirgends gesetzt/genutzt — totes Überbleibsel derselben Abschaltung, keine Aktion nötig. +- SPDX-Lizenzkopf-Migration (`00994aa9e`) — rein Lizenzmetadaten, N/A. +- Label-Alignment/Spacing-Feintuning (`db23bd6f4`) — bezieht sich auf `CurrentAccountHeaderButton`s eigene Column-Struktur (separates Server-Label unterhalb des Namens); der Fork hat diese Zeile anders aufgebaut (Server erscheint nur als Fallback in der Status-Message-Zeile) — nicht 1:1 portierbar, geringer visueller Wert, **kein Portierungs-Kandidat**. +- `onObjectAdded`/`onObjectRemoved`-Deprecation-Fix (`6a40c0e48`, explizite Funktionsparameter statt injizierter Signal-Parameter) — **bereits portiert**, wenn auch mit anderer Syntax: `TrayWindowAccountMenu.qml` nutzt bereits `(index, object) => ...`-Arrow-Functions, was dieselbe Qt-Deprecation-Warnung vermeidet. +- Menübreite-Auto-Sizing-Weiterentwicklung (`8145c1ef7`, `b26c20e64`, Breiten-Teil von `7ce4f78c4`) sowie `addAccountButton`-Icon-Sizing-Verfeinerungen (`3adeb70af`, `c69d8edc9`, `53a165f56`, `adaa54cce`) — **bewusstes Design**: Fork nutzt feste `Style.sesAccountMenuWidth` und eine generische, geteilte `AccountMenuItem`-Komponente statt pro-Button-Maßen: kein Nachzug nötig, bereits als bekannte Abweichung in der Registry vermerkt. +- Windows-Ausnahme für Highlight-Textfarbe (`aa7ee7478`, `((...) && Qt.platform.os !== "windows")`) sowie Kontrastfarben-Logik (`be3a001ed`) — **bereits portiert**: exakt dieses Muster steckt in `UserLine.qml`s `statusItemColor`. +- Elide-Ergänzung für Menüeinträge (`d354b329f`) — abgedeckt durch `AccountMenuItem.qml`s eigenes Text-Handling, **kein Handlungsbedarf** erkennbar. +- **`1e7f20641`** ("fix: restore user status message type import") führt `onShowUserStatusMessageSelector` samt `userStatusDrawer.openUserStatusMessageDrawer(model.index)` wieder ein. Nachrecherchiert: in `origin/stable-33.0` hat jede Account-Zeile im "More actions"-Menü zwei getrennte, immer sichtbare Einträge — "Set status" (→ `showUserStatusSelector`, öffnet den Presence-Picker) und "Status message" (→ `showUserStatusMessageSelector`, Shortcut direkt zum Message-Editor; beides dieselbe `UserStatusSelectorPage.qml`, nur unterschiedlicher Einstiegs-`mode`). Im Fork sind **beide** Signale tot — `git grep` nach tatsächlichen Aufrufen (nicht nur Deklarationen) findet in ganz `src/gui/tray` keinen einzigen; `UserLine.qml`s "More actions"-Menü enthält nur noch `logInOutButton`/`removeAccountButton`, "Set status" und "Status message" wurden komplett entfernt (Commit `bb543a166`/`c89b7d636`, "SES-457 - Squashed commit"). + - **Verdict: bewusst nicht portiert.** Von Boris bestätigt (2026-08-17): das Produkt hat keine Collaboration-Features, User-Status ergibt daher grundsätzlich keinen Sinn — gilt für den kompletten Feature-Komplex (Indikator **und** beide Menüeinträge), auch wenn nur der Indikator einen expliziten `SES-50`-Kommentar im Code trägt. `registry.md` entsprechend zusammengefasst, damit künftige Läufe das als eine Abweichung behandeln statt es erneut als offenen Punkt zu melden. + +**Ergebnis:** keine offenen Punkte mehr — alles bereits portiert oder nachvollziehbar bewusst nicht portiert (Status-Feature komplett, Produktentscheidung "keine Collaboration-Features"). + +## 2026-08-19 — Routine-Check + +Referenz-Branch: `origin/stable-33.0` (`git fetch` durchgeführt). + +**Paar: `TrayWindowHeader.qml` ↔ `SesTrayHeader.qml`** +`git log 568cbe171..origin/stable-33.0 -- src/gui/tray/TrayWindowHeader.qml` → keine neuen Commits. **Kein Handlungsbedarf**, Stand unverändert. + +**Paar: `CurrentAccountHeaderButton.qml` ↔ `TrayWindowAccountMenu.qml`/`UserLine.qml`** +`git log 60e0cb196..origin/stable-33.0 -- src/gui/tray/CurrentAccountHeaderButton.qml` → keine neuen Commits. **Kein Handlungsbedarf**, Stand unverändert. + +**Ergebnis:** Upstream `stable-33.0` hat seit dem letzten Lauf keine neuen Commits an beiden beobachteten Dateien — reiner Bestätigungs-Check ohne Änderungen an der Registry. diff --git a/.claude/skills/shadow-component-watch/SKILL.md b/.claude/skills/shadow-component-watch/SKILL.md new file mode 100644 index 0000000000000..a532fd7790a74 --- /dev/null +++ b/.claude/skills/shadow-component-watch/SKILL.md @@ -0,0 +1,44 @@ +--- +name: shadow-component-watch +description: Überwacht Fork-eigene UI-Komponenten, die eine Upstream-Komponente funktional ersetzt haben (Paare in registry.md, z. B. TrayWindowHeader.qml → SesTrayHeader.qml), auf funktionale Änderungen im upstream stable-Branch an der verwaisten Originaldatei und pflegt ein Änderungsprotokoll (CHANGELOG.md) über bewusste vs. offene Abweichungen. Nur manuell auslösen. Trigger — "prüfe die shadow components", "sind unsere Ersatzkomponenten noch aktuell", "wurden funktionale Änderungen aus stable übernommen", "shadow component check", nach einem größeren stable-Merge, wenn im Gespräch eine weitere "durch eigene Entwicklung ersetzte" Komponente identifiziert wird. +--- + +# Shadow-Component-Watch + +Manche Fork-Bereiche haben eine Upstream-Komponente nicht angepasst, sondern komplett durch eine eigene Entwicklung ersetzt (Beispiel: `TrayWindowHeader.qml`/`CurrentAccountHeaderButton.qml` → `SesTrayHeader.qml`/`TrayWindowAccountMenu.qml`). Die alten Upstream-Dateien bleiben oft unbenutzt im Baum liegen (kompiliert, aber nicht mehr aus dem Render-Baum erreichbar) und werden bei jedem stable-Merge weiter von Nextcloud gepflegt — funktionale Fixes/Verbesserungen dort (nicht nur Styling) laufen sonst ins Leere, weil niemand hinschaut. + +Dieser Skill hält eine Registry solcher Paare, prüft inkrementell (nur neue Commits seit der letzten Prüfung) auf funktionale Änderungen, und protokolliert jede Prüfung inkl. Entscheidung in einem Changelog — damit bewusste Abweichungen nachvollziehbar bleiben und nicht bei jeder Anfrage neu diskutiert werden müssen. + +## Dateien in diesem Skill + +- [registry.md](registry.md) — Liste der bekannten Paare (Upstream-Datei ↔ Fork-Ersatz), inkl. Notizen zu bekannten bewussten Abweichungen und dem zuletzt geprüften Upstream-Commit je Datei. +- [CHANGELOG.md](CHANGELOG.md) — chronologisches Protokoll jeder Prüfung: welche Commits geprüft wurden, und ob die Änderung "bereits portiert", "bewusst nicht portiert" (mit Grund/Ticket) oder "offener Portierungs-Kandidat" ist. + +## Referenz-Branch + +Dieser Skill nutzt denselben Referenz-Branch wie [stable-merge-check](../stable-merge-check/SKILL.md) — dort in `reference-branch.txt` nachsehen, nicht dupliziert pflegen. Vor der Analyse `git fetch origin ` ausführen. + +## Ablauf + +1. **Referenz-Branch** aus `../stable-merge-check/reference-branch.txt` lesen und dem Nutzer nennen. +2. **`registry.md` einlesen** — für jedes Paar: Upstream-Datei(en), Fork-Ersatz-Datei(en), zuletzt geprüfter Commit. +3. **Pro Paar, neue Commits seit letzter Prüfung ermitteln** (rein inkrementell, kein Vollscan der Historie): + - `git log --oneline ..origin/ -- ` + - Keine neuen Commits → Paar überspringen, nichts anfassen. +4. **Jeden neuen Commit einordnen:** + - `git show -- ` lesen. + - **Rein kosmetisch/i18n/Refactor ohne Verhaltensänderung** (Property-Umbenennung ohne Semantikänderung, Textänderung, Formatierung) → kurz vermerken, kein Handlungsbedarf. + - **Funktionale Änderung** (neue Bedingung/Guard, neues Signal, geändertes Verhalten, neue sichtbare Property): prüfen, ob das Verhalten im Fork-Ersatz bereits existiert — gezielt nach dem betroffenen Property-/Funktionsnamen in der Fork-Ersatzdatei suchen (Grep), ggf. auch in mitbenutzten geteilten Komponenten (z. B. `UserLine.qml`, das von mehreren Menü-Varianten genutzt wird). + - **Bereits vorhanden** → "bereits portiert" vermerken, fertig. + - **Fehlt, aber erkennbar bewusst** (Fork-Code hat an der fachlich passenden Stelle einen Kommentar mit Ticket-Referenz wie `//SES-4 removed`, `//SES-50`, oder das ganze Feature ist nachweislich per Produktentscheidung nicht vorhanden) → als "bewusst nicht portiert" mit Begründung/Ticket vermerken. + - **Fehlt ohne erkennbaren Grund** → dem Nutzer als offenen Portierungs-Kandidaten vorschlagen, mit kurzer Einschätzung des Aufwands. Nicht automatisch patchen — erst nach Bestätigung umsetzen (wie bei Workflow A in `stable-merge-check`). +5. **Ergebnis mit dem Nutzer besprechen**, bestätigte Portierungen umsetzen. +6. **`CHANGELOG.md` ergänzen**: neuen Abschnitt mit Datum anlegen, pro Paar die geprüften Commits + Verdict auflisten (siehe Format in der Datei). Auch "bewusst nicht portiert"-Fälle explizit eintragen, nicht nur die nachgezogenen — genau das macht das Changelog wertvoll für spätere Nachvollziehbarkeit. +7. **`registry.md` aktualisieren**: "zuletzt geprüfter Commit" je Upstream-Datei auf den neuesten tatsächlich geprüften Commit setzen. +8. **Neue Paare aufnehmen**: Wenn der Nutzer im Gespräch eine weitere "durch eigene Entwicklung ersetzte" Komponente identifiziert, die noch nicht in `registry.md` steht, anbieten sie dort mit Startzustand (aktueller HEAD-Commit der Upstream-Datei als "zuletzt geprüft") einzutragen. + +## Nicht in diesem Skill enthalten + +- Keine automatische/periodische Ausführung — bewusst nutzergetriggert, typischerweise nach einem stable-Merge oder wenn der Verdacht aufkommt, dass eine ersetzte Komponente hinterherhinkt. +- Kein automatisches Patchen ohne Bestätigung. +- Prüft nicht, ob ein Paar selbst noch aktuell/korrekt ist (z. B. ob der Fork-Ersatz zwischenzeitlich erneut umbenannt wurde) — dafür `registry.md` bei Bedarf manuell korrigieren oder [gui-context-refresh](../gui-context-refresh/SKILL.md) für die generelle Strukturdoku nutzen. diff --git a/.claude/skills/shadow-component-watch/registry.md b/.claude/skills/shadow-component-watch/registry.md new file mode 100644 index 0000000000000..bc2ec09580c5f --- /dev/null +++ b/.claude/skills/shadow-component-watch/registry.md @@ -0,0 +1,14 @@ +# Registry: Upstream-Datei ↔ Fork-Ersatz + +Jede Zeile ist ein Paar: eine Upstream-Komponente, die im Fork durch eine eigene Entwicklung ersetzt wurde und selbst nicht mehr aus dem aktiven Render-Baum erreichbar ist, plus die Fork-Datei(en), die sie funktional ersetzen. "Zuletzt geprüft" ist der Commit-Hash **auf dem Referenz-Branch** (siehe `../stable-merge-check/reference-branch.txt`), bis zu dem die Historie der Upstream-Datei bereits durchgesehen wurde — beim nächsten Lauf wird nur `..origin/` betrachtet. + +## src/gui/tray + +| Upstream-Datei | Fork-Ersatz | Bekannte bewusste Abweichungen | Zuletzt geprüft (Upstream-Commit) | +|---|---|---|---| +| `src/gui/tray/TrayWindowHeader.qml` | `src/gui/SesComponents/SesTrayHeader.qml` | "More apps"-Menü (mehrere externe Apps aus `UserAppsModel`, je eigenem Icon) entfernt, ersetzt durch einzelnen statischen "Website"-Button — Ticket `SES-4` | `568cbe171` (2026-04-19, "chore(i18n): Change group folder to team folder") | +| `src/gui/tray/CurrentAccountHeaderButton.qml` | `src/gui/tray/TrayWindowAccountMenu.qml` (+ `src/gui/tray/UserLine.qml`, geteilt mit anderen Aufrufern, selbst aber schon lange vom Upstream-Pendant abgedriftet — kein reines Duplikat) | Gesamtes User-Status-Feature (Presence-Picker + Status-Message) bewusst nicht vorhanden — Produkt hat keine Collaboration-Features, Status ergibt daher keinen Sinn (bestätigt von Boris am 2026-08-17). Betrifft: Status-Indikator auf dem Avatar (Ticket `SES-50`, Code-Kommentar vorhanden) UND die beiden Menüeinträge "Set status"/"Status message" in `UserLine.qml` (entfernt im Zuge von `SES-457`, kein expliziter Ticket-Kommentar im Code, aber dieselbe Produktentscheidung) — beide beim Prüfen als eine zusammengehörige Abweichung behandeln, nicht als zwei getrennte. Dynamische Menübreite (breitester Eintrag) durch feste `Style.sesAccountMenuWidth` ersetzt; `parentBackgroundColor`-Weiterreichung für kontrastabhängige Hintergründe entfällt (Fork nutzt statisches `Style.ses*`-Farbsystem statt QPalette) | `60e0cb196` (2026-02-11, "color fix" — kompletter Rückstand bis `de066e6b9` am 2026-08-17 nachgeholt geprüft) | + +## Neues Paar eintragen + +Beim Auffinden eines weiteren "durch eigene Entwicklung ersetzten" Paars: Zeile mit Upstream-Datei, Fork-Ersatz und dem aktuellen HEAD-Commit der Upstream-Datei auf dem Referenz-Branch (`git log -1 --format=%h origin/ -- `) als Startwert für "Zuletzt geprüft" ergänzen. diff --git a/.claude/skills/stable-merge-check/SKILL.md b/.claude/skills/stable-merge-check/SKILL.md new file mode 100644 index 0000000000000..c7179127cd6ea --- /dev/null +++ b/.claude/skills/stable-merge-check/SKILL.md @@ -0,0 +1,64 @@ +--- +name: stable-merge-check +description: Vergleicht GUI-/Code-Änderungen in diesem Fork mit dem konfigurierten upstream stable-x.y-Branch, um Merge-Konflikt-Risiken durch Strukturabweichungen zu erkennen, merge-robuste Lösungsvorschläge zu machen, und um nach einem Merge kaputte Stellen (doppelte Layouts, verlorenes/zerschossenes Styling, kaputte Features) durch Vergleich mit beiden Merge-Elternversionen zu diagnostizieren. Trigger — "vergleiche mit stable", "Merge-Konflikt-Risiko", "nach dem Merge kaputt", "Struktur-Analyse gegen stable-33.0", "was hat der Merge kaputt gemacht", "merge-robuste Lösung", UI sieht nach einem Merge falsch/zerschossen aus, doppelte setLayout()-Warnungen, fehlende Elemente nach Merge (Search Bar, Quota-Anzeige, Avatar/Initialen). +--- + +# Stable-Merge-Check + +Dieses Repo ist ein Whitelabel-Fork von Nextcloud Desktop (u.a. für IONOS/HiDrive Next, BRICKMAKERS-Kontext). Es wird regelmäßig gegen den upstream `stable-x.y`-Branch von Nextcloud gemerged. Zwei wiederkehrende Probleme: + +1. Eigene GUI-Anpassungen weichen strukturell so stark vom stable-Branch ab, dass künftige Merges unnötig konfliktträchtig werden. +2. Nach einem Merge ist etwas kaputt (doppelte Widgets/Layouts, verlorenes Styling, fehlende Features), weil der Merge beide Seiten unglücklich zusammengeführt hat. + +Dieser Skill deckt beide Fälle ab. + +## Referenz-Branch + +Der Vergleichs-Branch steht in [reference-branch.txt](reference-branch.txt) (aktuell `stable-33.0`). Er gilt für einen ganzen Entwicklungszyklus und wird bewusst nicht automatisch ermittelt. + +- **Vor jeder Analyse**: Datei lesen und dem Nutzer explizit mitteilen, gegen welchen Branch gerade verglichen wird (z. B. "Ich vergleiche gegen `origin/stable-33.0`."), damit das nie stillschweigend passiert. +- **Wenn der Nutzer signalisiert, dass sich der relevante stable-Branch geändert hat** (neuer Release-Branch geschnitten, z. B. "wir sind jetzt auf stable-34.0"): kurz bestätigen lassen, dann `reference-branch.txt` aktualisieren. Das ist eine geteilte, eingecheckte Einstellung fürs ganze Team — nicht ungefragt ändern. +- Vor dem eigentlichen Vergleich `git fetch origin ` ausführen (rein lesend, unkritisch), damit nicht gegen einen veralteten lokalen Stand verglichen wird. Falls `origin/` lokal nicht existiert, das dem Nutzer melden statt zu raten. + +## Workflow A — Vorab-Struktur-/Merge-Risiko-Analyse + +Einsatz: Der Nutzer arbeitet an einer GUI-Datei/Komponente (typischerweise unter `src/gui/`, oft `.ui`, `.qml`, `.cpp`) und will wissen, wie riskant aktuelle/geplante Änderungen für künftige Merges sind, oder will die Struktur bewusst näher an stable heranziehen. + +1. Referenz-Branch ansagen (s.o.). +2. Betroffene Datei(en)/Klasse identifizieren — aus IDE-Selection/offener Datei übernehmen, sonst nachfragen. +3. Vergleichen: + - `git diff origin/ -- ` für den reinen Inhalt. + - `git log --oneline origin/ -- ` bzw. `git log --oneline -- ` um zu verstehen, wann/warum divergiert wurde. +4. Abweichungen einordnen, nicht nur auflisten: + - **Niedriges Risiko**: reine Ergänzungen (neue Property, neues optionales Element) ohne Umbau bestehender Struktur. + - **Mittleres Risiko**: umbenannte/verschobene Bezeichner, die an anderer Stelle im Code referenziert werden (vorher per Grep prüfen, ob es weitere Verwendungsstellen gibt). + - **Hohes Risiko**: Umstrukturierung bestehender Layout-Hierarchien/Reihenfolgen (z. B. andere Gruppierung von Widgets, dynamisches statt fixes Spacing, verschobene Sections) — das sind erfahrungsgemäß die Stellen, an denen künftige stable-Merges kollidieren. + - Für jede Abweichung kurz einordnen, ob sie **bewusst** ist (Branding/Feature-Unterschied dieses Forks, z. B. entfernter "Neues Konto"-Button, andere Datenschutz-Formulierung) oder **zufällige Drift**. + - Wenn eine Abweichung darauf hindeutet, dass eine Datei/Komponente durch eine neuere ersetzt wurde und ungenutzt wirkt (z. B. altes Widget nicht mehr im aktiven Render-Baum): **vor einer Entfernungsempfehlung reposweit per Grep** (Dateiname, Klassenname, QML-Typname) auf verbleibende Referenzen prüfen — nicht allein aus dem Struktur-Diff auf "toter Code" schließen. + - **Entscheidend zusätzlich: prüfen, ob `origin/` dieselbe Datei/Klasse noch führt und aktiv nutzt** (`git show origin/:`, ggf. dort ebenfalls auf Referenzen grep'en). Ziel ist maximale Nähe zu stable, nicht minimaler eigener Code: + - Führt stable die Datei noch aktiv → **nicht löschen**, auch wenn sie im Fork ungenutzt wirkt. Entfernen würde den Diff zu stable vergrößern statt verkleinern. Stattdessen nur als "im Fork ungenutzt, aber bewusst erhalten für Merge-Kompatibilität mit stable" dokumentieren. + - Hat stable die Datei ebenfalls entfernt/nicht mehr aktiv verdrahtet → Entfernung bringt uns näher an stable; nur dann Löschung als merge-robust vorschlagen, weiterhin nur nach Bestätigung durch den Nutzer. + - Bei Unsicherheit: nicht löschen. Nähe zu stable schlägt Code-Hygiene. +5. Höchstens 2–3 Lösungsvorschläge machen, nicht mehr. Jeder Vorschlag mit einer knappen Aufwand-vs-Konfliktrisiko-Einschätzung. + - Standard-Bias: Die Option bevorzugen, die die Struktur so nah wie möglich an stable hält, auch wenn sie gerade etwas mehr Aufwand bedeutet — das zahlt sich bei jedem künftigen Merge aus. Optionen, die zwar einfach umzusetzen sind, aber die Struktur dauerhaft von stable wegbewegen, explizit als "hoher Wartungsaufwand bei künftigen Merges" kennzeichnen statt sie gleichwertig zu präsentieren. + - Erst nach Bestätigung durch den Nutzer implementieren, nicht direkt lospatchen. +6. Bei größeren strukturellen Umbauten: nach jedem stabilen Zwischenschritt einen Checkpoint-Commit vorschlagen ("soll ich das erstmal committen, bevor wir weitermachen?"), damit Zwischenstände nicht verloren gehen. + +## Workflow B — Post-Merge-Schadensdiagnose + +Einsatz: Nach einem Merge von `origin/` funktioniert/sieht etwas nicht mehr richtig aus (Qt-Warnung zu doppeltem Layout, zerschossenes Styling, fehlende Suchleiste/Quota-Anzeige, kaputtes Feature). + +1. Referenz-Branch ansagen (s.o.). +2. Merge-Commit identifizieren: `git log --merges -1 -- ` (oder den Commit-Hash vom Nutzer erfragen, falls nicht der letzte). +3. Beide Merge-Elternversionen der betroffenen Datei/Stelle gegenüberstellen: + - `git show ^1:` + - `git show ^2:` + - `git show :` (das tatsächliche Merge-Ergebnis) + - Klassisches Muster: beide Elternversionen haben unabhängig voneinander dieselbe Art Änderung eingebaut (z. B. je ein `setLayout()`-Aufruf), und der Merge hat beide behalten statt nur einen. +4. Mit der konkreten Fehlerbeschreibung des Nutzers abgleichen (Fehlermeldung, Screenshot-Beschreibung, betroffenes UI-Element) — nicht raten, sondern die exakte Stelle über `git blame` auf der aktuellen Datei lokalisieren, um zu sehen, welche Zeile von welcher Seite stammt. +5. Den konkreten Fix vorschlagen (i. d. R. die überzählige/widersprüchliche Zeile entfernen) und kurz erklären, welches Merge-Muster dazu geführt hat, damit der Nutzer es beim nächsten Mal selbst erkennt. +6. Nach dem Fix: kurz gegen `origin/` und den vorherigen (funktionierenden) Stand gegenchecken, dass keine weiteren Stellen betroffen sind. + +## Verwandte Skills + +- [merge-drift-map](../merge-drift-map/SKILL.md) hält das Ergebnis von Workflow A dauerhaft in einer Landkarte fest (Diff-Anteil + diese Risiko-Einstufung), statt es nur einmalig im Gespräch zu liefern. diff --git a/.claude/skills/stable-merge-check/reference-branch.txt b/.claude/skills/stable-merge-check/reference-branch.txt new file mode 100644 index 0000000000000..f1f276e91e48a --- /dev/null +++ b/.claude/skills/stable-merge-check/reference-branch.txt @@ -0,0 +1 @@ +stable-33.0 diff --git a/.githooks/lib-translation-merge.sh b/.githooks/lib-translation-merge.sh new file mode 100644 index 0000000000000..bbc9b6ee7f55b --- /dev/null +++ b/.githooks/lib-translation-merge.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Shared by .githooks/post-merge and .githooks/post-commit. +# Detects whether a commit message denotes a merge of a stable-* branch +# (the NC release base) and, if so, re-runs the STRATO/IONOS translation +# merge (translation_scripts/merge_translation.py) against it so +# translations/client_*.ts stay in sync. + +translation_merge_run_if_stable() { + hook_name="$1" + merge_msg="$2" + repo_root=$(git rev-parse --show-toplevel) + log_file="$repo_root/.githooks/post-merge.log" + + # Matches "Merge branch 'stable-33.0'" or "Merge remote-tracking branch 'origin/stable-33.0'" + branch=$(printf '%s' "$merge_msg" | grep -oE "Merge (branch|remote-tracking branch) '[^']*stable-[0-9]+\.[0-9]+[^']*'" | head -1 | sed -E "s/.*'([^']*)'.*/\1/") + + if [ -z "$branch" ]; then + return 0 + fi + + # GUI clients (GitKraken etc.) often don't surface hook stdout anywhere visible, + # so mirror everything into a log file as well - that's the reliable record. + { + echo "" + echo "===== $(date '+%Y-%m-%d %H:%M:%S') $hook_name: detected merge of '$branch' =====" + } >> "$log_file" + + echo "$hook_name: detected merge of '$branch' - re-running STRATO/IONOS translation merge..." + echo "$hook_name: full log also written to .githooks/post-merge.log" + + command -v python3 >/dev/null 2>&1 && PY=python3 || PY=python + + # 'auto' runs steps 1-5 directly against the already-merged working tree + # (which now contains both the NC base and our STRATO source) - unlike + # 'all', it skips step 0's git-worktree checkout of the clean NC branch, + # which only feeds a "which keys are ours vs NC's" diagnostic in the final + # report and isn't needed for the merge itself. No worktree also means no + # more Windows temp-dir races. + # + # Capture via command substitution (not a pipe) so $? below is the real + # python exit status - portable across dash/bash. + output=$(cd "$repo_root/translation_scripts" && "$PY" merge_translation.py auto 2>&1) + status=$? + + echo "$output" + echo "$output" >> "$log_file" + + if [ "$status" -ne 0 ]; then + msg="$hook_name: translation merge script reported problems (exit $status). Review translations/ before committing." + echo "$msg" >&2 + else + msg="$hook_name: translation merge finished. Review 'git status'/'git diff' in translations/ and commit manually." + echo "$msg" + fi + echo "$msg" >> "$log_file" +} diff --git a/.githooks/post-commit b/.githooks/post-commit new file mode 100644 index 0000000000000..28c5620d7fd3d --- /dev/null +++ b/.githooks/post-commit @@ -0,0 +1,20 @@ +#!/bin/sh +# Runs after every `git commit`. Git only invokes post-merge for CONFLICT-FREE +# merges - if a merge had conflicts, it's concluded by an explicit `git commit` +# once conflicts are resolved, which fires post-commit instead of post-merge. +# This hook catches that case: if HEAD is a merge commit (>=2 parents) whose +# message names a stable-* branch, re-run the STRATO/IONOS translation merge. +# +# Enable once per clone with: +# git config core.hooksPath .githooks + +parent_count=$(($(git rev-list --parents -n 1 HEAD | wc -w) - 1)) +if [ "$parent_count" -lt 2 ]; then + exit 0 +fi + +repo_root=$(git rev-parse --show-toplevel) +. "$repo_root/.githooks/lib-translation-merge.sh" +translation_merge_run_if_stable "post-commit" "$(git log -1 --pretty=%B)" + +exit 0 diff --git a/.githooks/post-merge b/.githooks/post-merge new file mode 100644 index 0000000000000..543f521c2338b --- /dev/null +++ b/.githooks/post-merge @@ -0,0 +1,21 @@ +#!/bin/sh +# Runs after a CONFLICT-FREE `git merge` / `git pull`. Git does NOT invoke this +# hook if the merge produced conflicts - see .githooks/post-commit for that case. +# +# If the merge brought in a stable-* branch (the NC release base), automatically +# re-run the STRATO/IONOS translation merge (translation_scripts/merge_translation.py) +# against that branch so translations/client_*.ts stay in sync. +# +# Enable once per clone with: +# git config core.hooksPath .githooks + +# $1 is 1 for a squash merge (no merge commit / message to inspect) - skip. +if [ "$1" = "1" ]; then + exit 0 +fi + +repo_root=$(git rev-parse --show-toplevel) +. "$repo_root/.githooks/lib-translation-merge.sh" +translation_merge_run_if_stable "post-merge" "$(git log -1 --pretty=%B)" + +exit 0 diff --git a/.github/workflows/check-translations.yml b/.github/workflows/check-translations.yml index f6d48eb26afda..6c5c7b9ac8bc4 100644 --- a/.github/workflows/check-translations.yml +++ b/.github/workflows/check-translations.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Check Elipsis run: | - lupdate -no-obsolete src/gui/ src/cmd/ src/common/ $crashreporter src/csync/ src/libsync/ $resources -ts ../ci-client.ts + lupdate -no-obsolete src/gui/ src/cmd/ src/common/ src/csync/ src/libsync/ $resources -ts ../ci-client.ts if [ $(grep '\.\.\.' ../ci-client.ts | wc -l) -ne 0 ]; then echo "English source contains three consecutive dots. Unicode … should be used instead" exit -1 diff --git a/.github/workflows/clang-tidy-review.yml b/.github/workflows/clang-tidy-review.yml index 188ef96af7d47..1a036b6668346 100644 --- a/.github/workflows/clang-tidy-review.yml +++ b/.github/workflows/clang-tidy-review.yml @@ -3,8 +3,7 @@ name: clang-tidy-review on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: clang-tidy: diff --git a/.github/workflows/linux-clang-compile-tests.yml b/.github/workflows/linux-clang-compile-tests.yml index d447033d650b6..61a6a33e30eb0 100644 --- a/.github/workflows/linux-clang-compile-tests.yml +++ b/.github/workflows/linux-clang-compile-tests.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Linux Clang compilation and tests on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: Linux Clang compilation and tests diff --git a/.github/workflows/linux-gcc-compile-tests.yml b/.github/workflows/linux-gcc-compile-tests.yml index 98872c3d054ef..d0a4c4fba3f83 100644 --- a/.github/workflows/linux-gcc-compile-tests.yml +++ b/.github/workflows/linux-gcc-compile-tests.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Linux GCC compilation and tests on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: Linux GCC compilation and tests diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 84148fb823bd5..dabf4b34928f1 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -2,8 +2,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: SonarCloud analysis on: - pull_request: - types: [opened, synchronize, reopened] + workflow_dispatch: jobs: build: name: SonarCloud analysis diff --git a/.github/workflows/windows-build-and-test.yml b/.github/workflows/windows-build-and-test.yml index a6d0570478c49..5b1ee5d0f0cf7 100644 --- a/.github/workflows/windows-build-and-test.yml +++ b/.github/workflows/windows-build-and-test.yml @@ -2,6 +2,11 @@ # SPDX-License-Identifier: GPL-2.0-or-later name: Windows Build and Test on: + workflow_dispatch: + # push: + # branches: + # - master + # - develop pull_request: types: [opened, synchronize, reopened] jobs: diff --git a/.gitignore b/.gitignore index 921157a060d18..6d78d72e582f6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ # SPDX-FileCopyrightText: 2011 ownCloud GmbH # SPDX-License-Identifier: GPL-2.0-or-later .gitmodules +.githooks/post-merge.log *build*/ *flymake* CMakeLists.txt.user* @@ -22,7 +23,7 @@ t1.cfg ## Ignore Visual Studio Code config & environment files .vs/ -.vscode/ +# .vscode/ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. @@ -193,7 +194,12 @@ convert.exe *-w10startmenu.png *state-*.png theme.qrc +ionos-theme.qrc *.AppImage # Swift Package Manager Build Artifacts .swiftpm_codeql_detected_source_root + +.idea/ + +shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000000000..3f0c64ea209b8 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,14 @@ +{ + "configurations": [ + { + "name": "Linux", + "compileCommands": [ + "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/compile_commands.json" + ], + "intelliSenseMode": "linux-gcc-x64", + "cStandard": "c17", + "cppStandard": "c++17" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000..ebf989268ef95 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,114 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "(Linux-IONOS-RelWithDebInfo) Launch HiDriveNext", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/linux-GCC-x64/RelWithDebInfo/bin/IONOS_HiDrive_Next", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "LD_LIBRARY_PATH", + "value": "/home/chaetty/CraftRoot/bin:${env:LD_LIBRARY_PATH}" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + "MIMode": "gdb", + "setupCommands": [ + { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } + ] + }, + { + "name": "(Linux-STRATO-RelWithDebInfo) Launch HiDriveNext", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/build/linux-GCC-x64/RelWithDebInfo/bin/STRATO_HiDrive_Next", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "LD_LIBRARY_PATH", + "value": "/home/chaetty/CraftRoot/bin:${env:LD_LIBRARY_PATH}" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + "MIMode": "gdb", + "setupCommands": [ + { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true } + ] + }, + { + "name": "(RelWithDebInfo) Launch IONOS HiDriveNext", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/bin/ionos-hidrive-next.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + }, + { + "name": "(RelWithDebInfo) Launch STRATO HiDriveNext", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/bin/strato-hidrive-next.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + { "name": "QML_IMPORT_TRACE", "value": "1" }, + { "name": "QT_LOGGING_RULES", "value": "qt.qml.debug=true" } + ], + }, + { + "name": "(RelWithDebInfo) Launch NextCloud", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/RelWithDebInfo/bin/nextcloud.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + ], + }, + { + "name": "(Release) Launch NextCloud", + "type": "cppvsdbg", + "request": "launch", + "program": "${workspaceFolder}/../build/win32-MSVC-x64/Release/bin/nextcloud.exe", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}", + "environment": [ + { + "name": "PATH", + "value": "C:/CraftRoot/bin;C:/Craft64/bin;%PATH%" + }, + ], + }, + ] +} \ No newline at end of file diff --git a/.vscode/nc-desktop-snippets.code-snippets b/.vscode/nc-desktop-snippets.code-snippets new file mode 100644 index 0000000000000..920dea7cb70f6 --- /dev/null +++ b/.vscode/nc-desktop-snippets.code-snippets @@ -0,0 +1,30 @@ +{ + // Place your nc-desktop workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + + "DebugLog": { + "scope": "cpp", + "prefix": "dlog", + "body": [ + "char buffer$1[256];", + "sprintf(buffer$1, \"$2\\n\", $3);", + "OutputDebugStringA(buffer$1);" + ], + "description": "Debug log output" + } + +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..ffc4046d987dc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,24 @@ +{ + "cmake.generator": "Ninja", + "cmake.configureSettings": { + "CMAKE_PREFIX_PATH": [ + "C:/CraftRoot", + "C:/CraftRoot/dev-utils/bin", + "C:/Craft64", + "C:/Craft64/dev-utils/bin" + ], + "BUILD_TESTING":"OFF", + "LOCALBUILD": "ON", + //"WHITELABEL_NAME": "strato", + "WHITELABEL_NAME": "ionos", + "CMAKE_CXX_CLANG_TIDY": "", + "DO_NOT_USE_PROXY": "ON", + }, + "cmake.buildArgs": ["-j", "16"], + "cmake.buildDirectory": "${workspaceFolder}/../build/${buildKitTargetOs}-${buildKitVendor}-${buildKitTargetArch}/${buildType}", + "files.associations": { + "qwizardpage": "cpp", + "xutility": "cpp", + "*.ts": "xml" + }, +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000000000..153627d3103a2 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,37 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clean RelWithDebInfo", + "type": "shell", + "command": "powershell", + "args": [ + "-NoProfile", + "-Command", + "Remove-Item -Recurse -Force ..\\build\\win32-MSVC-x64\\RelWithDebInfo" + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "A task to clean the build directory using PowerShell" + }, + { + "label": "clean Release", + "type": "shell", + "command": "powershell", + "args": [ + "-NoProfile", + "-Command", + "Remove-Item -Recurse -Force ..\\build\\win32-MSVC-x64\\Release" + ], + "problemMatcher": [], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "A task to clean the build directory using PowerShell" + }, + ] +} \ No newline at end of file diff --git a/.vscode/wsl-nc-desktop.code-workspace b/.vscode/wsl-nc-desktop.code-workspace new file mode 100644 index 0000000000000..b890d2f8b5101 --- /dev/null +++ b/.vscode/wsl-nc-desktop.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": ".." + } + ] +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ed437c0342b50..ebaaba3b82950 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,8 @@ cmake_policy(SET CMP0071 NEW) # Enable use of QtQuick compiler/generated code project(client) +add_compile_definitions(IONOS_BUILD) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) if(APPLE) set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum OSX deployment version") endif() @@ -15,10 +17,10 @@ set(CMAKE_CXX_STANDARD_REQUIRED 20) include(FeatureSummary) -find_program(CLANG_TIDY_EXE NAMES "clang-tidy") -if (CLANG_TIDY_EXE) - set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables) -endif() +# find_program(CLANG_TIDY_EXE NAMES "clang-tidy") +# if (CLANG_TIDY_EXE) +# set(CMAKE_CXX_CLANG_TIDY ${CLANG_TIDY_EXE} -checks=-*,modernize-use-auto,modernize-use-using,modernize-use-nodiscard,modernize-use-nullptr,modernize-use-override,cppcoreguidelines-pro-type-static-cast-downcast,modernize-use-default-member-init,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-init-variables) +# endif() if(CMAKE_BUILD_TYPE STREQUAL "Debug") set(CMAKE_XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME NO) @@ -35,21 +37,21 @@ set(REQUIRED_QT_VERSION "6.8.0") # CfAPI Shell Extensions set( CFAPI_SHELL_EXTENSIONS_LIB_NAME CfApiShellExtensions ) - -set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}") -set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" ) +# Defaults for Nextcloud; overridden per whitelabel in IONOS.cmake +set( CFAPI_SHELLEXT_APPID_REG "{E314A650-DCA4-416E-974E-18EA37C213EA}") set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "1E62D59A-6EA4-476C-B707-4A32E88ED822" ) +set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "6FF9B5B6-389F-444A-9FDD-A286C36EA079" ) +set( APPLICATION_URI_HANDLER_SCHEME "nc") + +include(${CMAKE_SOURCE_DIR}/IONOS.cmake) + +set( CFAPI_SHELLEXT_APPID_DISPLAY_NAME "${APPLICATION_NAME} CfApi Shell Extensions" ) set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID}}" ) set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Custom State Handler" ) - -set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "6FF9B5B6-389F-444A-9FDD-A286C36EA079" ) set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID_REG "{${CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID}}" ) set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_DISPLAY_NAME "${APPLICATION_NAME} Thumbnail Handler" ) -# URI Handler Scheme for Local File Editing -set( APPLICATION_URI_HANDLER_SCHEME "nc") - # Default suffix if the theme doesn't define one if(NOT DEFINED APPLICATION_VIRTUALFILE_SUFFIX) set(APPLICATION_VIRTUALFILE_SUFFIX "${APPLICATION_SHORTNAME}_virtual" CACHE STRING "Virtual file suffix (not including the .)") @@ -99,10 +101,6 @@ include(ECMEnableSanitizers) include(ECMCoverageOption) -if(NOT CRASHREPORTER_EXECUTABLE) - set(CRASHREPORTER_EXECUTABLE "${APPLICATION_EXECUTABLE}_crash_reporter") -endif() - include(Warnings) if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU") @@ -120,15 +118,6 @@ include_directories( ${CMAKE_CURRENT_BINARY_DIR}/src ) -# disable the crashreporter if libcrashreporter-qt is not available or we're building for ARM -if( CMAKE_SYSTEM_PROCESSOR MATCHES "arm" OR NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/3rdparty/libcrashreporter-qt/CMakeLists.txt") - set( WITH_CRASHREPORTER OFF ) -endif() - -if(NOT WITH_CRASHREPORTER) - message(STATUS "Build of crashreporter disabled.") -endif() - include(GNUInstallDirs) include(DefineInstallationPaths) include(GenerateExportHeader) @@ -237,6 +226,14 @@ if(OWNCLOUD_5XX_NO_BLACKLIST) add_definitions(-DOWNCLOUD_5XX_NO_BLACKLIST=1) endif() +if(APPLE) + if(APPLICATION_NAME STREQUAL "STRATO HiDrive Next") + set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "6UU53QCET7." CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) + else() + set( SOCKETAPI_TEAM_IDENTIFIER_PREFIX "5TDLCVD243." CACHE STRING "SocketApi prefix (including a following dot) that must match the codesign key's TeamIdentifier/Organizational Unit" ) + endif() +endif() + if(BUILD_CLIENT) OPTION(GUI_TESTING "Build with gui introspection features of socket api" OFF) diff --git a/CPackOptions.cmake.in b/CPackOptions.cmake.in index d38c4bf28b81b..472974145bcc5 100644 --- a/CPackOptions.cmake.in +++ b/CPackOptions.cmake.in @@ -14,7 +14,7 @@ endif(CPACK_GENERATOR MATCHES "NSIS") set( CMAKE_SOURCE_DIR @CMAKE_SOURCE_DIR@ ) set( CMAKE_BINARY_DIR @CMAKE_BINARY_DIR@ ) -include("${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake") +include("${CMAKE_SOURCE_DIR}/IONOS.cmake") set( BUILD_OWNCLOUD_OSX_BUNDLE @BUILD_OWNCLOUD_OSX_BUNDLE@) if(APPLE AND NOT BUILD_OWNCLOUD_OSX_BUNDLE) diff --git a/IONOS.cmake b/IONOS.cmake new file mode 100644 index 0000000000000..0641da2e22ab6 --- /dev/null +++ b/IONOS.cmake @@ -0,0 +1,76 @@ +option(LOCALBUILD "Local developer build" OFF) + +if(LOCALBUILD) + ## Only needed for local build + message(STATUS "Building in LOCAL mode") + + set( APPLICATION_VIRTUALFILE_SUFFIX "${APPLICATION_SHORTNAME}_virtual" CACHE STRING "Virtual file suffix (not including the .)" FORCE) + + ## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen" + if(WIN32) + # Context Menu + set( WIN_SHELLEXT_CONTEXT_MENU_GUID "{6B16FF7B-F242-4CE3-8FB9-F06EF127E0DC}" ) + + # Overlays + set( WIN_SHELLEXT_OVERLAY_GUID_ERROR "{243D887B-9F74-41DD-BACA-BC5501AF10AC}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_OK "{2D88D499-3272-4A76-84BF-D252254B40D6}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_OK_SHARED "{7BEF6B56-5B5B-4284-A70C-56D62254C97A}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_SYNC "{5F2F493D-A683-426F-925E-4CA25F17C4A9}" ) + set( WIN_SHELLEXT_OVERLAY_GUID_WARNING "{7F256BB6-29D2-4E40-A6C4-E5E756E64C82}" ) + endif() + + if("${WHITELABEL_NAME}" STREQUAL "strato") + set( APPLICATION_REV_DOMAIN "com.strato.hidrivenext.desktopclient" ) + set( APPLICATION_NAME "STRATO HiDrive Next" ) + set( APPLICATION_SHORTNAME "STRATOHiDriveNext" ) + set( APPLICATION_EXECUTABLE "strato-hidrive-next" ) + set( APPLICATION_CONFIG_NAME "STRATO-HiDrive-Next" ) + set( APPLICATION_ICON_NAME "strato_hidrive_next" ) + set( APPLICATION_DOMAIN "strato.com" ) + set( APPLICATION_UPDATE_URL "https://customerupdates.nextcloud.com/client/" CACHE STRING "URL for updater" FORCE) + set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" FORCE) + set( APPLICATION_SERVER_URL "https://storage.ionos.fr" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" FORCE) + elseif("${WHITELABEL_NAME}" STREQUAL "ionos") + set( APPLICATION_REV_DOMAIN "com.ionos.hidrivenext.desktopclient" ) + set( APPLICATION_NAME "IONOS HiDrive Next" ) + set( APPLICATION_SHORTNAME "IONOSHiDriveNext" ) + set( APPLICATION_EXECUTABLE "ionos-hidrive-next" ) + set( APPLICATION_CONFIG_NAME "IONOS-HiDrive-Next" ) + set( APPLICATION_ICON_NAME "ionos_hidrive_next" ) + set( APPLICATION_DOMAIN "ionos.com" ) + set( APPLICATION_UPDATE_URL "https://customerupdates.nextcloud.com/client/" CACHE STRING "URL for updater" FORCE) + set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" FORCE) + set( APPLICATION_SERVER_URL "https://storage.ionos.fr" CACHE STRING "URL for the server to use. If entered, the UI field will be pre-filled with it" FORCE) + endif() + +endif() + +string(TOLOWER "${APPLICATION_NAME}" app_name_lower) +if(APPLE AND app_name_lower MATCHES "hidrive") + set(APPLICATION_ICON_NAME "${APPLICATION_EXECUTABLE}-macOS") + message("Using macOS-specific application icon: ${APPLICATION_ICON_NAME}") +endif() + +if(APPLICATION_NAME STREQUAL "STRATO HiDrive Next") + set( APPLICATION_VENDOR "STRATO GmbH" ) + set( APPLICATION_REV_DOMAIN "com.strato.hidrivenext.desktopclient" ) + set( WIN_MSI_UI_DIALOG_BMP "dialog_strato.bmp" ) + # MSI Upgrade Code (without brackets) + set( WIN_MSI_UPGRADE_CODE "F125E371-1E5D-424C-B738-70447A65F08C" ) + add_compile_definitions(STRATO_WL_BUILD) + set( CFAPI_SHELLEXT_APPID_REG "{7D9A4B29-A898-4425-8CC6-E12C45D39D78}") + set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "02D43837-7958-4DFB-9CB3-B7FB10276012" ) + set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "57822EDA-7186-4AD6-A113-5A7471A1F917" ) + set( APPLICATION_URI_HANDLER_SCHEME "strato") +elseif(APPLICATION_NAME STREQUAL "IONOS HiDrive Next") + set( APPLICATION_VENDOR "IONOS SE" ) + set( APPLICATION_REV_DOMAIN "com.ionos.hidrivenext.desktopclient" ) + set( WIN_MSI_UI_DIALOG_BMP "dialog_ionos.bmp" ) + # MSI Upgrade Code (without brackets) + set( WIN_MSI_UPGRADE_CODE "6C9E5670-E8A9-4BBD-9BDF-D003794AC177" ) + add_compile_definitions(IONOS_WL_BUILD) + set( CFAPI_SHELLEXT_APPID_REG "{8DA6F907-784F-4A7A-A704-83342A8C205E}") + set( CFAPI_SHELLEXT_CUSTOM_STATE_HANDLER_CLASS_ID "A748032D-2EBF-40A3-BDC1-94C19D28A108" ) + set( CFAPI_SHELLEXT_THUMBNAIL_HANDLER_CLASS_ID "F8CE69E3-B804-4738-BF31-3CBD0E9A6674" ) + set( APPLICATION_URI_HANDLER_SCHEME "ionos") +endif() \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000..eda7bae4c9726 --- /dev/null +++ b/LICENSE @@ -0,0 +1,73 @@ +Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2022-2024 chinchilla + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000000..59ff260650176 --- /dev/null +++ b/NOTICE @@ -0,0 +1,18 @@ +Nextcloud Desktop - Ionos HiDrive Next + +This project includes software developed by chinchilla, available at: +[https://github.com/564398053/Qt-GoogleAnalytics4]. + +Bundled Components: + Qt-GoogleAnalytics4 + ga4/ganalytics.cpp + ga4/ganalytics_worker.cpp + Copyright 2022 chinchilla + Licensed under the Apache License, Version 2.0. + You may obtain a copy of the license at: + [http://www.apache.org/licenses/LICENSE-2.0]. + + +Important Note: +The rest of this project is not licensed under the Apache License, Version 2.0. It remains under the original license as specified in the original repository: +[https://github.com/IONOS-Productivity/nc-desktop]. \ No newline at end of file diff --git a/NextcloudCPack.cmake b/NextcloudCPack.cmake index d0e0f2da99791..75a072340b76a 100644 --- a/NextcloudCPack.cmake +++ b/NextcloudCPack.cmake @@ -5,7 +5,7 @@ include( InstallRequiredSystemLibraries ) set( CPACK_PACKAGE_CONTACT "Dominik Schmidt " ) -include("${CMAKE_SOURCE_DIR}/NEXTCLOUD.cmake") +include("${CMAKE_SOURCE_DIR}/IONOS.cmake") include( VERSION.cmake ) set( CPACK_PACKAGE_VERSION_MAJOR ${MIRALL_VERSION_MAJOR} ) diff --git a/VERSION.cmake b/VERSION.cmake index 422c52f2f38d1..6536a264b3c36 100644 --- a/VERSION.cmake +++ b/VERSION.cmake @@ -29,6 +29,7 @@ set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MAJOR 28) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_MINOR 0) set(NEXTCLOUD_SERVER_VERSION_MOUNT_ROOT_PROPERTY_SUPPORTED_PATCH 3) +set(MIRALL_VERSION_SUFFIX "stable") # ------------------------------------ # Define default suffixes if not set diff --git a/admin/osx/TransifexStringCatalogSanitizer/.gitignore b/admin/osx/TransifexStringCatalogSanitizer/.gitignore new file mode 100644 index 0000000000000..920d21189f4e7 --- /dev/null +++ b/admin/osx/TransifexStringCatalogSanitizer/.gitignore @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: GPL-2.0-or-later + +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm +.netrc diff --git a/admin/osx/TransifexStringCatalogSanitizer/Package.swift b/admin/osx/TransifexStringCatalogSanitizer/Package.swift new file mode 100644 index 0000000000000..312d8790c325d --- /dev/null +++ b/admin/osx/TransifexStringCatalogSanitizer/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TransifexStringCatalogSanitizer", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "TransifexStringCatalogSanitizer", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), + ] +) diff --git a/admin/osx/TransifexStringCatalogSanitizer/README.md b/admin/osx/TransifexStringCatalogSanitizer/README.md new file mode 100644 index 0000000000000..fd8d4258c9fe8 --- /dev/null +++ b/admin/osx/TransifexStringCatalogSanitizer/README.md @@ -0,0 +1,15 @@ +# Transifex String Catalog Sanitizer + +Sanitize Xcode string catalogs which were pulled from a Transifex online resource. + +## Usage + +See the integrated help for up to date reference. + +```sh +swift run TransifexStringCatalogSanitizer --help +``` + +## Development + +This Swift command-line utility can be easily run and debugged from Xcode. diff --git a/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizer.swift b/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizer.swift new file mode 100644 index 0000000000000..8f13585b190c1 --- /dev/null +++ b/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizer.swift @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +import ArgumentParser +import Foundation + +@main +struct TransifexStringCatalogSanitizer: ParsableCommand { + @Argument(help: "The string catalog file to sanitize.") + var input: String + + mutating func run() throws { + let url = URL(fileURLWithPath: input) + let data = try Data(contentsOf: url) + + guard var root = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.jsonObject + } + + guard var strings = root["strings"] as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.missingStrings + } + + try sanitizeStrings(&strings) + + // Update the root with modified strings + root["strings"] = strings + + // Write the processed data back to the original file + let processedData = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) + try processedData.write(to: url) + } + + private func sanitizeStrings(_ strings: inout [String: Any]) throws { + for key in strings.keys.sorted() { + print("💬 \"\(key)\"") + + guard var string = strings[key] as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.missingString + } + + guard var localizations = string["localizations"] as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.missingLocalizations + } + + try sanitizeLocalizations(&localizations) + + // Update the string with modified localizations + string["localizations"] = localizations + strings[key] = string + } + } + + private func sanitizeLocalizations(_ localizations: inout [String: Any]) throws { + var localizationsToRemove: [String] = [] + + for localeCode in localizations.keys.sorted() { + guard let localization = localizations[localeCode] as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.missingLocalization + } + + guard let stringUnit = localization["stringUnit"] as? [String: Any] else { + throw TransifexStringCatalogSanitizerError.missingStringUnit + } + + guard let value = stringUnit["value"] as? String else { + throw TransifexStringCatalogSanitizerError.missingValue + } + + if value.isEmpty { + print("\t❌ \(localeCode): empty, will be removed") + localizationsToRemove.append(localeCode) + } else { + print("\t✅ \(localeCode): \"\(value)\"") + } + } + + // Remove empty localizations + for localeCode in localizationsToRemove { + localizations.removeValue(forKey: localeCode) + } + } +} diff --git a/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizerError.swift b/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizerError.swift new file mode 100644 index 0000000000000..f94dd99ff27a4 --- /dev/null +++ b/admin/osx/TransifexStringCatalogSanitizer/Sources/TransifexStringCatalogSanitizer/TransifexStringCatalogSanitizerError.swift @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-2.0-or-later + +enum TransifexStringCatalogSanitizerError: Error { + case jsonObject + case missingLocalization + case missingLocalizations + case missingString + case missingStrings + case missingStringUnit + case missingValue +} diff --git a/admin/osx/ionos_macmaker/mac_craft.sh b/admin/osx/ionos_macmaker/mac_craft.sh new file mode 100755 index 0000000000000..0ac49038fdd4c --- /dev/null +++ b/admin/osx/ionos_macmaker/mac_craft.sh @@ -0,0 +1,274 @@ +#!/bin/bash + + +# This script is used to build the Mac OS X version of the IONOS client. +# Parse the command line arguments +while getopts "b:p:s:n:k:q:civtuw" opt; do + case ${opt} in + b )REL_BASE_DIR=$OPTARG;; + p )REL_PATH_TO_PKG=$OPTARG ;; + s )IONOS_TEAM_IDENTIFIER=$OPTARG ;; + n )NC_TEAM_IDENTIFIER=$OPTARG ;; + k )SPARKLE_KEY=$OPTARG ;; # not used + q )KEYCHAIN_PROFILE_OVERRIDE=$OPTARG ;; + c )CLEAN_REBUILD=true ;; + i )PACKAGE_INSTALLER=true ;; + v )VERBOSE=true ;; + t )TEAM_PATCHING=true ;; + u )BUILD_UPDATER=true ;; + w )USE_STRATO=true ;; + \? ) + echo "Usage: mac_craft.sh [-b ] [-p ] [-s ] [-n ] [-k ] [-q ] [-c CLEAN_REBUILD] [-i PACKAGE_INSTALLER] [-v VERBOSE] [-t TEAM_PATCHING] [-u BUILD_UPDATER] [-w USE_STRATO]" + exit 1 + ;; + esac +done + +if [ "$VERBOSE" = true ]; then + echo "VERBOSE MODE" + set -xe +fi + +if [ "$TEAM_PATCHING" == "true" ] && [ -z "$NC_TEAM_IDENTIFIER" ]; then + echo "Patching aktivated, but NC_TEAM_IDENTIFIER not set. Exiting." + exit 0 +fi + +# Check if CODE_SIGN_IDENTITY is set, if not exit +if [ -z "$IONOS_TEAM_IDENTIFIER" ]; then + echo "IONOS_TEAM_IDENTIFIER not set. Exiting." + exit 0 +fi + +if [ -z "$REL_BASE_DIR" ]; then + echo "REL_BASE_DIR not set. Exiting." + exit 0 +fi + +if [ -z "$REL_PATH_TO_PKG" ]; then + echo "REL_PATH_TO_PKG not set. Exiting." + exit 0 +fi + +# Some variables +BASE_DIR="$( cd "$REL_BASE_DIR" && pwd )" +PATH_TO_PKG="$( realpath "$REL_PATH_TO_PKG")" + +PKG_FULLNAME=$(basename "$PATH_TO_PKG") +PKG_FILENAME="${PKG_FULLNAME%.pkg}" +if [ "$USE_STRATO" = true ]; then + PRODUCT_NAME="STRATO HiDrive Next" + UNDERSCORE_PRODUCT_NAME="STRATO_HiDrive_Next" + CODE_SIGN_IDENTITY="STRATO AG ($IONOS_TEAM_IDENTIFIER)" + KEYCHAIN_PROFILE="STRATO AG HiDrive Next" +else + PRODUCT_NAME="IONOS HiDrive Next" + UNDERSCORE_PRODUCT_NAME="IONOS_HiDrive_Next" + CODE_SIGN_IDENTITY="IONOS SE ($IONOS_TEAM_IDENTIFIER)" + KEYCHAIN_PROFILE="IONOS SE HiDrive Next" +fi + +if [ -n "$KEYCHAIN_PROFILE_OVERRIDE" ]; then + KEYCHAIN_PROFILE="$KEYCHAIN_PROFILE_OVERRIDE" +fi + +INSTALLER_CERT="Developer ID Installer: $CODE_SIGN_IDENTITY" +APPLICATION_CERT="Developer ID Application: $CODE_SIGN_IDENTITY" + +WORK_DIR="ex" +EXTRACTED_DIR="${BASE_DIR%/}/$WORK_DIR" + +PRODUCT_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload/Applications +SCRIPTS_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Scripts +INNER_PKG=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg +PAYLOAD_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload +INSTALLER_PKG=${BASE_DIR%/}/INSTALLER.pkg +APP_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +ADMIN_OSX="$( cd "$SCRIPT_DIR/.." && pwd )/macosx.entitlements.cmake" +MACCRAFTER_DIR="$( cd "$SCRIPT_DIR/../mac-crafter" && pwd )" + + +# Sparkle Variables +PACKAGE_PATH="${BASE_DIR%/}/$PKG_FILENAME.resigned.pkg" +SPARKLE_TBZ_PATH="${PACKAGE_PATH}.tbz" +SPARKLE_DIR="${BASE_DIR%/}/sparkle" +SPARKLE_DOWNLOAD_URI="https://github.com/sparkle-project/Sparkle/releases/download/1.27.3/Sparkle-1.27.3.tar.xz" + + +echo "Expanding original package..." + +if [ -d "$EXTRACTED_DIR" ]; then + + echo "$EXTRACTED_DIR already exits." + + if [ "$CLEAN_REBUILD" = true ]; then + echo "Clean Rebuild Enabled - Deleting folder: $EXTRACTED_DIR" + rm -rf "$EXTRACTED_DIR" + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" + fi +else + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" +fi + +# --------------------------------------------------- +# Patch Team Identifier + +# check wether patching is needed. ".com" is important because otherwise the ID in the signature will be found + +if [[ -n "$TEAM_PATCHING" ]]; then + echo "Team Patching Enabled - Start Patching Detection" + + PLIST_MATCHES=$(find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + BIN_MATCHES=$(find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + + if [[ "$PLIST_MATCHES" -gt 0 || "$BIN_MATCHES" -gt 0 ]]; then + # Ensure both IDs are same lengt + if [[ ${#NC_TEAM_IDENTIFIER} -ne ${#IONOS_TEAM_IDENTIFIER} ]]; then + echo "NC_TEAM_IDENTIFIER and IONOS_TEAM_IDENTIFIER must be the same length for binary-safe patching." + open $BASE_DIR + exit 1 + fi + + if [[ "$PLIST_MATCHES" -gt 0 ]]; then + # --- Replace in .plist files (plain XML) --- + echo "Replacing Team Identifier in .plist files..." + find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER" {} \; -exec sed -i '' "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" {} \; + fi + + if [[ "$BIN_MATCHES" -gt 0 ]]; then + # Find and patch all binaries containing the old ID + find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER" {} \; -print | while read -r file; do + echo "Patching Team Identifier in $file" + perl -pi -e "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" "$file" + done + fi + else + echo "Nothing to patch" + fi +fi + +# --------------------------------------------------- +# Sign the client + + + +echo "start signing the client" + +swift run --package-path "$MACCRAFTER_DIR" \ + mac-crafter codesign \ + -c "$CODE_SIGN_IDENTITY" \ + -e "$ADMIN_OSX" \ + "$APP_PATH" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$APP_PATH" 2>&1 | grep -q "TeamIdentifier=$IONOS_TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + open $BASE_DIR + exit 0 +fi + +# --------------------------------------------------- +# Installer + +echo "start building the installer" + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "Renew BOM" +mkbom "$PAYLOAD_DIR" "$INNER_PKG/Bom" +echo "Reassembling the package..." +(cd "$PAYLOAD_DIR" && \ + find . | cpio -o --format odc | gzip -c) > $PAYLOAD_DIR.new + +rm -rf $PAYLOAD_DIR +mv $PAYLOAD_DIR.new $PAYLOAD_DIR + +(cd $EXTRACTED_DIR && \ + pkgutil --flatten $UNDERSCORE_PRODUCT_NAME.pkg $UNDERSCORE_PRODUCT_NAME.pkg.flat) + +rm -rf $INNER_PKG +mv $INNER_PKG.flat $INNER_PKG + +productsign --timestamp --sign "$INSTALLER_CERT" \ + $INNER_PKG \ + $INNER_PKG.signed + +rm -rf $INNER_PKG +mv $INNER_PKG.signed $INNER_PKG + +(cd $BASE_DIR && productbuild \ + --distribution ex/Distribution \ + --resources ex/Resources \ + --package-path ex \ + $INSTALLER_PKG.unsigned) + +productsign --timestamp --sign "$INSTALLER_CERT" $INSTALLER_PKG.unsigned "$PACKAGE_PATH" + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait "$PACKAGE_PATH"\ + --keychain-profile "$KEYCHAIN_PROFILE") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + open $BASE_DIR + exit 1 +fi + +# staple +xcrun stapler staple "$PACKAGE_PATH" +xcrun stapler validate "$PACKAGE_PATH" + + +# Sparkle + +SPARKLE_TBZ_PATH="${PACKAGE_PATH}.tbz" + +echo "Creating Sparkle package archive: $SPARKLE_TBZ_PATH" + +# Load Sparkle + +if [ "$BUILD_UPDATER" == "true" ]; then + echo "Creating Sparkle package archive: $SPARKLE_TBZ_PATH" + + if [ -d "$SPARKLE_DIR" ]; then + + echo "$SPARKLE_DIR already exits." + echo "Deleting..." + rm -rf "$SPARKLE_DIR" + fi + + echo "Download Sparkle" + + mkdir -p $SPARKLE_DIR + wget $SPARKLE_DOWNLOAD_URI -O ${SPARKLE_DIR%/}/Sparkle.tar.xz + tar -xvf ${SPARKLE_DIR%/}/Sparkle.tar.xz -C $SPARKLE_DIR + + if tar cf "$SPARKLE_TBZ_PATH" -C "$(dirname "$PACKAGE_PATH")" "$(basename "$PACKAGE_PATH")"; then + echo "✅ Sparkle package created successfully." + else + echo "❌ Could not create Sparkle package tbz!" >&2 + exit 1 + fi + + echo "Signing Sparkle package: $SPARKLE_TBZ_PATH" + if "${SPARKLE_DIR%/}/bin/sign_update" "$SPARKLE_TBZ_PATH"; then + echo "✅ Sparkle package signed successfully." + else + echo "❌ Could not sign Sparkle package tbz!" >&2 + exit 1 + fi + +fi + +open $BASE_DIR + diff --git a/admin/osx/ionos_macmaker/readme.md b/admin/osx/ionos_macmaker/readme.md new file mode 100644 index 0000000000000..fe0eebfce38d4 --- /dev/null +++ b/admin/osx/ionos_macmaker/readme.md @@ -0,0 +1,270 @@ +# start.sh — Local Debug Builds + +> **This script is for local development only.** While it technically contains signing and installer packaging code, those paths are not maintained for release use. For production signing, notarization, and installer creation use [mac_craft.sh](#mac_craftsh) instead. + +`start.sh` compiles the client from source using CMake and Ninja, deploys it into a product directory via `mac-crafter`, and immediately launches it for testing — all in one step. It is the primary entry point for a developer working locally on the IONOS HiDrive Next macOS client. + +--- + +## Usage + +```bash +./start.sh -a -b [options] +``` + +`-a` and `-b` are always required. + +### Options + +| Flag | Argument | Description | +|------|----------|-------------| +| `-a` | `` | Target CPU architecture — `arm64` or `x86_64` (required) | +| `-b` | `` | Build output directory (required) | +| `-s` | `` | Local signing identity — use your personal **Apple Development** cert. If omitted, signing is skipped | +| `-c` | | Clean rebuild — wipe the build directory before building | +| `-f` | | Build the FileProvider extension | +| `-o` | | Build as an OSX bundle (`.app`) instead of a standalone binary | +| `-r` | | Launch the app automatically after the build completes | +| `-u` | | Download and integrate Sparkle (only meaningful on clean rebuilds) | +| `-m` | | Skip `macdeployqt` | + +### Recommended local build workflow + +This is the standard way to build the client locally. + +**First build — always do a clean build:** +```bash +./start.sh -b ~/repos/build/desktop -a "x86_64" -f -m -o -r -c +``` + +**All subsequent builds — omit `-c`:** +```bash +./start.sh -b ~/repos/build/desktop -a "x86_64" -f -m -o -r +``` + +Skipping `-c` reuses the existing build directory, so only changed files are recompiled — this is significantly faster. + +### The `-m` flag and starting the app + +`-m` skips `macdeployqt`, which copies all Qt frameworks and plugins into the `.app` bundle. This makes the build much faster, but has an important consequence: + +| | With `-m` (recommended for dev) | Without `-m` | +|---|---|---| +| Build time | Fast | Very long | +| Start from Finder | **Does not work** — Qt dependencies are missing inside the bundle | Works | +| Start via `-r` | Works — `launch_app` sets the required library paths | Works | + +**When using `-m`, the easiest way to start the app is `-r`**, which sets all required Craft library paths before launching. + +Alternatively, you can set the paths manually in your terminal and launch from there: + +```bash +export CRAFT="$HOME/Craft64" +export DYLD_LIBRARY_PATH="$CRAFT/lib" +export DYLD_FRAMEWORK_PATH="$CRAFT/lib" +export QT_PLUGIN_PATH="$CRAFT/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="$CRAFT/plugins/platforms" +export QML2_IMPORT_PATH="$CRAFT/qml" +"$HOME/repos/build/desktop/bin/IONOS HiDrive Next.app/Contents/MacOS/IONOS HiDrive Next" +``` + +## What it does + +1. Kills any running instance of the client +2. Runs `cmake` to configure the build (always reconfigures) +3. Runs `ninja install` to build and deploy into `/product` +4. If `-s` is omitted — opens the product directory in Finder; launches the app if `-r` is set +5. If `-s` is provided — signs frameworks, plugins, and the `.app` bundle with the given identity, then verifies the TeamIdentifier + +## What it does NOT do (use mac_craft.sh instead) + +The script contains `-i` (installer packaging) and notarization code, but these are **not suitable for release builds**: + +* The installer call (`create_mac.sh`) has the IONOS team ID hardcoded +* The notarization step also uses a hardcoded IONOS keychain profile +* No white-label / STRATO support + +For anything that produces a deliverable — signing a release build, building an installer, notarizing, or creating a Sparkle archive — use **mac_craft.sh**. + +## Requirements + +* macOS with Xcode tools installed +* Craft64 installed at `~/Craft64` +* `cmake`, `ninja`, `ccache` +* Optionally: a personal **Apple Development** certificate for local signing + +--- + +# mac_craft.sh + +This script automates the build and signing process for the **IONOS HiDrive Next** and **STRATO HiDrive Next** macOS client installers. It takes an existing `.pkg` package, optionally patches team identifiers, resigns the app, reassembles the installer, notarizes it with Apple, and finally creates a **Sparkle update package** for distribution. + +--- + +## Features + +* **Expand** a given `.pkg` file into a working directory +* **Patch** identifiers if required (team patching mode) +* **Resign** the app with the correct `Developer ID` certificates +* **Reassemble** the installer package with updated files +* **Notarize and staple** the final package with Apple's notarization service +* **Build Sparkle update** archives for app updates +* **White-label support** for STRATO HiDrive Next via `-w` + +--- + +## Usage + +```bash +./mac_craft.sh [-b ] [-p ] [-s ] [-n ] [-k ] [-q ] [-c] [-i] [-v] [-t] [-u] [-w] +``` + +### Options + +| Flag | Argument | Description | +|------|----------|-------------| +| `-b` | `` | Base directory for build and output | +| `-p` | `` | Path to the original `.pkg` installer | +| `-s` | `` | Apple Developer Team ID of the signing entity (required) | +| `-n` | `` | Original Nextcloud Team ID (required when using `-t`) | +| `-k` | `` | Sparkle signing key (**currently unused**) | +| `-q` | `` | Override the notarytool keychain profile name (optional) | +| `-c` | | Clean rebuild — delete the old extracted directory before expanding | +| `-i` | | Enable installer packaging (script exits after signing if omitted) | +| `-v` | | Verbose mode — print debug output (`set -xe`) | +| `-t` | | Enable team patching — replaces `NC_TEAM_IDENTIFIER` with the signing Team ID in plists and binaries | +| `-u` | | Build and sign a Sparkle updater archive | +| `-w` | | White-label mode: build **STRATO HiDrive Next** instead of IONOS HiDrive Next | + +--- + +## Brand defaults + +The following values are set automatically based on whether `-w` is passed: + +| | IONOS (default) | STRATO (`-w`) | +|--|-----------------|----------------| +| Product name | `IONOS HiDrive Next` | `STRATO HiDrive Next` | +| Code sign identity | `IONOS SE ()` | `STRATO AG ()` | +| Keychain profile | `IONOS SE HiDrive Next` | `STRATO AG HiDrive Next` | + +Use `-q` to override the keychain profile name when the stored credentials use a different name. + +### Setting up notarization credentials + +Each brand requires its own notarytool keychain profile to be set up once per machine by a member of the corresponding Apple Developer team: + +```bash +# IONOS +xcrun notarytool store-credentials "IONOS SE HiDrive Next" \ + --apple-id "developer@ionos.com" \ + --team-id "" \ + --password "xxxx-xxxx-xxxx-xxxx" + +# STRATO +xcrun notarytool store-credentials "STRATO AG HiDrive Next" \ + --apple-id "developer@strato.com" \ + --team-id "" \ + --password "xxxx-xxxx-xxxx-xxxx" +``` + +The password is an **app-specific password** generated at [appleid.apple.com](https://appleid.apple.com) — not the Apple ID login password. The Apple ID used must be a member of the respective Apple Developer team. + +--- + +## Workflow + +1. **Expand Original Package** + + * The `.pkg` is expanded into a working directory (`pkgutil --expand-full`). + * If `-c` is set, any previous working directory is removed first. + +2. **Patch Identifiers (Optional)** + + * If `-t` is used, the script searches `.plist` and binary files for the old Nextcloud team identifier. + * It replaces it with the signing `TEAM_IDENTIFIER` passed via `-s`. + * Both IDs must have the same character length to ensure safe binary patching. + +3. **Resign Application** + + * The client `.app` is signed using the `mac-crafter` tool. + * Codesign identity is brand-dependent (see [Brand defaults](#brand-defaults) above). + * Verifies that the signed app's `TeamIdentifier` matches the expected one. + +4. **Reassemble Installer** + + * Recreates the package payload (`mkbom`, `cpio`, `pkgutil --flatten`). + * Signs the inner package and final installer using the brand's `Developer ID Installer` certificate. + * Uses `productbuild` and `productsign` to generate the final signed package. + +5. **Notarization & Stapling** + + * Submits the package to Apple's Notary Service (`xcrun notarytool`). + * Uses the brand's keychain profile (overridable via `-q`). + * Waits for the result and validates acceptance. + * Applies a **staple** to the installer (`xcrun stapler staple`). + +6. **Sparkle Update Build (Optional)** + + * Downloads Sparkle if not already present. + * Archives the signed package as `.tbz`. + * Signs the archive using Sparkle's `sign_update` tool. + +--- + +## Examples + +**IONOS build:** +```bash +./mac_craft.sh \ + -b /Users/developer/build \ + -p ./IONOS.pkg \ + -s ABC123XYZ \ + -n OLDTEAMID \ + -i -c -t -u -v +``` + +**STRATO build:** +```bash +./mac_craft.sh \ + -b /Users/developer/build \ + -p ./STRATO.pkg \ + -s DEF456UVW \ + -n OLDTEAMID \ + -i -c -t -u -v -w +``` + +**With custom keychain profile:** +```bash +./mac_craft.sh \ + -b /Users/developer/build \ + -p ./STRATO.pkg \ + -s DEF456UVW \ + -i -w -q "My Custom Profile" +``` + +--- + +## Requirements + +* macOS with Xcode tools installed +* Apple Developer account with: + * **Developer ID Application** certificate for the signing entity + * **Developer ID Installer** certificate for the signing entity +* Notarytool keychain profile stored on the build machine (see [Setting up notarization credentials](#setting-up-notarization-credentials)) +* `mac-crafter` tool (Swift package, provided by Nextcloud) +* System tools: `pkgutil`, `productbuild`, `productsign`, `notarytool`, `stapler`, `xcrun`, `grep`, `sed`, `mkbom`, `cpio`, `gzip` +* `wget`, `tar`, `perl` for Sparkle integration + +--- + +## Output + +* Final notarized installer: + ``` + /.resigned.pkg + ``` +* Sparkle update archive (if `-u` enabled): + ``` + /.resigned.pkg.tbz + ``` diff --git a/admin/osx/ionos_macmaker/sign.sh b/admin/osx/ionos_macmaker/sign.sh new file mode 100755 index 0000000000000..f781a41c9cb4d --- /dev/null +++ b/admin/osx/ionos_macmaker/sign.sh @@ -0,0 +1,212 @@ +#!/bin/bash + +recursive_sign(){ + local path="$1" + local extension="${path##*.}" + if [[ "$extension" == "dylib" || "$extension" == "framework" || "$extension" == "appex" ]]; then + echo "Signing directory: $path" + codesign -s "$2" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "${path}" + fi +} + +export -f recursive_sign + +sign_folder_content(){ + local folder="$1" + local identity="$2" + local entitlements="$3" + codesign -s "$identity" --force $entitlements --verbose=4 --deep --options=runtime --timestamp "${folder}" +} + +export -f sign_folder_content + +# This script is used to build the Mac OS X version of the IONOS client. +# Parse the command line arguments +while getopts "b:p:s:civt" opt; do + case ${opt} in + b )BASE_DIR=$OPTARG;; + p )PATH_TO_PKG=$OPTARG ;; + s )CODE_SIGN_IDENTITY=$OPTARG ;; + c )CLEAN_REBUILD=true ;; + i )PACKAGE_INSTALLER=true ;; + v )VERBOSE=true ;; + t )TEAM_PATCHING=true ;; + \? ) + echo "Usage: sign.sh [-b ] [-p ] [-s ] [-c clean-rebuild] [-i build-installer] [-v verbose]" + exit 1 + ;; + esac +done + +if [ "$VERBOSE" = true ]; then + set -xe +fi + +# Some variables +PKG_FULLNAME=$(basename "$PATH_TO_PKG") +PKG_FILENAME="${PKG_FULLNAME%.pkg}" +PRODUCT_NAME="IONOS HiDrive Next" +UNDERSCORE_PRODUCT_NAME="IONOS_HiDrive_Next" + +IONOS_TEAM_IDENTIFIER="5TDLCVD243" +NC_TEAM_IDENTIFIER="NKUJUXUJ3B" +INSTALLER_CERT="Developer ID Installer: $CODE_SIGN_IDENTITY" +APPLICATION_CERT="Developer ID Application: $CODE_SIGN_IDENTITY" + +WORK_DIR="ex" +EXTRACTED_DIR="${BASE_DIR%/}/$WORK_DIR" + +PRODUCT_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload/Applications +SCRIPTS_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Scripts +INNER_PKG=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg +PAYLOAD_DIR=$EXTRACTED_DIR/$UNDERSCORE_PRODUCT_NAME.pkg/Payload +INSTALLER_PKG=$BASE_DIR/INSTALLER.pkg +APP_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app + +echo "Expanding original package..." + +if [ -d "$EXTRACTED_DIR" ]; then + + echo "$EXTRACTED_DIR already exits." + + if [ "$CLEAN_REBUILD" = true ]; then + echo "Clean Rebuild Enabled - Deleting folder: $EXTRACTED_DIR" + rm -rf "$EXTRACTED_DIR" + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" + fi +else + pkgutil --expand-full "$PATH_TO_PKG" "$EXTRACTED_DIR" +fi + +# --------------------------------------------------- +# Patch Team Identifier + +# check wether patching is needed. ".com" is important because otherwise the ID in the signature will be found + +if [[ -n "$TEAM_PATCHING" ]]; then + PLIST_MATCHES=$(find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + BIN_MATCHES=$(find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER.com" {} \; -print | wc -l) + + if [[ "$PLIST_MATCHES" -gt 0 || "$BIN_MATCHES" -gt 0 ]]; then + # Ensure both IDs are same lengt + if [[ ${#NC_TEAM_IDENTIFIER} -ne ${#IONOS_TEAM_IDENTIFIER} ]]; then + echo "NC_TEAM_IDENTIFIER and IONOS_TEAM_IDENTIFIER must be the same length for binary-safe patching." + open $BASE_DIR + exit 1 + fi + + if [[ "$PLIST_MATCHES" -gt 0 ]]; then + # --- Replace in .plist files (plain XML) --- + echo "Replacing Team Identifier in .plist files..." + find "$APP_PATH" -name "*.plist" -exec grep -q "$NC_TEAM_IDENTIFIER" {} \; -exec sed -i '' "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" {} \; + fi + + if [[ "$BIN_MATCHES" -gt 0 ]]; then + # Find and patch all binaries containing the old ID + find "$APP_PATH" -type f -exec grep -q --binary-files=text "$NC_TEAM_IDENTIFIER" {} \; -print | while read -r file; do + echo "Patching Team Identifier in $file" + perl -pi -e "s/$NC_TEAM_IDENTIFIER/$IONOS_TEAM_IDENTIFIER/g" "$file" + done + fi + fi +fi +# --------------------------------------------------- +# Sign the client +# CODE_SIGN_IDENTITY="Developer ID Application: IONOS SE (5TDLCVD243)" + +# Check if CODE_SIGN_IDENTITY is set, if not exit +if [ -z "$CODE_SIGN_IDENTITY" ]; then + echo "Code sign identity not set. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "start signing the client" + +CLIENT_CONTENTS_DIR=$APP_PATH/Contents +CLIENT_FRAMEWORKS_DIR=$CLIENT_CONTENTS_DIR/Frameworks +CLIENT_RESOURCES_DIR=$CLIENT_CONTENTS_DIR/Resources +CLIENT_PLUGINS_DIR=$CLIENT_CONTENTS_DIR/PlugIns + +for script in $SCRIPTS_DIR/*; do + echo "→ Signing script: $script" + codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --options=runtime --timestamp "$script" +done + +find "$CLIENT_FRAMEWORKS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_PLUGINS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_RESOURCES_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "$APP_PATH" + +# Sign the client ---- Still needed? +find "$CLIENT_CONTENTS_DIR/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY" "$entitlements" ' _ {} "$CODE_SIGN_IDENTITY" "--preserve-metadata=entitlements" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$APP_PATH" 2>&1 | grep -q "TeamIdentifier=$IONOS_TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + open $BASE_DIR + exit 0 +fi + +# --------------------------------------------------- +# Installer + +echo "start building the installer" + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $BASE_DIR + exit 0 +fi + +echo "Renew BOM" +mkbom $PAYLOAD_DIR $INNER_PKG/Bom +echo "Reassembling the package..." +(cd $PAYLOAD_DIR && \ + find . | cpio -o --format odc | gzip -c) > $PAYLOAD_DIR.new + +rm -rf $PAYLOAD_DIR +mv $PAYLOAD_DIR.new $PAYLOAD_DIR + +(cd $EXTRACTED_DIR && \ + pkgutil --flatten $UNDERSCORE_PRODUCT_NAME.pkg $UNDERSCORE_PRODUCT_NAME.pkg.flat) + +rm -rf $INNER_PKG +mv $INNER_PKG.flat $INNER_PKG + +productsign --timestamp --sign 'Developer ID Installer: IONOS SE (5TDLCVD243)' \ + $INNER_PKG \ + $INNER_PKG.signed + +rm -rf $INNER_PKG +mv $INNER_PKG.signed $INNER_PKG + +(cd $BASE_DIR && productbuild \ + --distribution ex/Distribution \ + --resources ex/Resources \ + --package-path ex \ + $INSTALLER_PKG.unsigned) + +productsign --timestamp --sign 'Developer ID Installer: IONOS SE (5TDLCVD243)' $INSTALLER_PKG.unsigned "$BASE_DIR$PKG_FILENAME.resigned.pkg" + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait "$BASE_DIR$PKG_FILENAME.resigned.pkg"\ + --keychain-profile "IONOS SE HiDrive Next") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + open $BASE_DIR + exit 1 +fi + +# staple +xcrun stapler staple "$BASE_DIR$PKG_FILENAME.resigned.pkg" +xcrun stapler validate "$BASE_DIR$PKG_FILENAME.resigned.pkg" + +open $BASE_DIR + diff --git a/admin/osx/ionos_macmaker/start.sh b/admin/osx/ionos_macmaker/start.sh new file mode 100755 index 0000000000000..03f39f8d9c822 --- /dev/null +++ b/admin/osx/ionos_macmaker/start.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +recursive_sign(){ + local path="$1" + local extension="${path##*.}" + if [[ "$extension" == "dylib" || "$extension" == "framework" || "$extension" == "appex" ]]; then + echo "Signing directory: $path" + codesign -s "$2" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "${path}" + fi +} + +export -f recursive_sign + +sign_folder_content(){ + local folder="$1" + local identity="$2" + local entitlements="$3" + codesign -s "$identity" --force $entitlements --verbose=4 --deep --options=runtime --timestamp "${folder}" +} + +export -f sign_folder_content + +launch_app() { + export CRAFT="$HOME/Craft64" + export DYLD_LIBRARY_PATH="$CRAFT/lib" + export DYLD_FRAMEWORK_PATH="$CRAFT/lib" + export QT_PLUGIN_PATH="$CRAFT/plugins" + export QT_QPA_PLATFORM_PLUGIN_PATH="$CRAFT/plugins/platforms" + export QML2_IMPORT_PATH="$CRAFT/qml" + "$BUILD_DIR/bin/IONOS HiDrive Next.app/Contents/MacOS/IONOS HiDrive Next" +} + +# This script is used to build the Mac OS X version of the IONOS client. +set -xe + +# Parse the command line arguments +while getopts "a:b:s:cifoumr" opt; do + case ${opt} in + a ) ARCHITECTURE=$OPTARG ;; + b ) BUILD_DIR=$OPTARG ;; + s ) CODE_SIGN_IDENTITY=$OPTARG ;; + c ) CLEAN_REBUILD=true ;; + i ) PACKAGE_INSTALLER=true ;; + f ) BUILD_FILEPROVIDER=true ;; + o ) OSX_BUNDLE=true ;; + u ) BUILD_UPDATER=true ;; + m ) SKIP_MACDEPLOY=true ;; + r ) RUN_AFTER_BUILD=true ;; + \? ) + echo "Usage: start.sh -a -b [-s ] [-c] [-f] [-i] [-m] [-o] [-r] [-u]" + exit 1 + ;; + esac +done + +# Set the deployment target +export MACOSX_DEPLOYMENT_TARGET=13.0 +# export MACOSX_DEPLOYMENT_TARGET=10.15 + +# Some variables +# The product name depends on whether we build an OSX bundle or not. +# See src/gui/CMakeLists.txt: OUTPUT_NAME is set to APPLICATION_NAME (bundle) or APPLICATION_EXECUTABLE (non-bundle). +if [ "$OSX_BUNDLE" == "true" ]; then + PRODUCT_NAME="IONOS HiDrive Next" +else + PRODUCT_NAME="ionos-hidrive-next" +fi +REPO_ROOT_DIR="../../.." +CRAFT_DIR=~/Craft64 +PRODUCT_DIR=$BUILD_DIR/product +TEAM_IDENTIFIER="5TDLCVD243" + +# Check if the client is running and kill it +# This is necessary to avoid issues with replacement of the bundle file +if pgrep -x "$PRODUCT_NAME" >/dev/null; then + killall "$PRODUCT_NAME" +fi + +# Check if BUILD_DIR is set, so we don't accidentally delete the whole filesystem +if [ -z "$BUILD_DIR" ]; then + echo "Build dir not set. Add -b to the command." + exit 0 +fi + +# Check if ARCHITECTURE is set +if [ -z "$ARCHITECTURE" ]; then + echo "ARCHITECTURE not set. Add -a to the command." + exit 0 +fi + +# Check if BUILD_DIR exists. If not, create it. If so, clear it. +if [ ! -d $BUILD_DIR ]; then + mkdir -p $BUILD_DIR +else + if [ $CLEAN_REBUILD = true ]; then + rm -rf $BUILD_DIR/* + fi +fi + +# Check if Craft dir exists, if not exit +if [ ! -d $CRAFT_DIR ]; then + echo "Craft dir not found. Exiting." + exit 1 +fi + +# Load Sparkle +SPARKLE_DIR=$BUILD_DIR/sparkle +SPARKLE_DOWNLOAD_URI="https://github.com/sparkle-project/Sparkle/releases/download/1.27.3/Sparkle-1.27.3.tar.xz" + +if [ "$CLEAN_REBUILD" == "true" ] && [ "$BUILD_UPDATER" == "true" ]; then + mkdir -p $SPARKLE_DIR + wget $SPARKLE_DOWNLOAD_URI -O $SPARKLE_DIR/Sparkle.tar.xz + tar -xvf $SPARKLE_DIR/Sparkle.tar.xz -C $SPARKLE_DIR + + # Sign Sparkle + if [ -n "$CODE_SIGN_IDENTITY" ]; then + SPARKLE_FRAMEWORK_DIR=$SPARKLE_DIR/Sparkle.framework + find "$SPARKLE_FRAMEWORK_DIR/Resources/Autoupdate.app/Contents/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" + codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=4 --deep --options=runtime --timestamp "$SPARKLE_FRAMEWORK_DIR/Sparkle" + fi +fi + +# Nach Zeile ~133, vor dem eigentlichen Build-Befehl (ninja/cmake --build) +# QML-Ressourcen invalidieren damit Änderungen erkannt werden +# find "$BUILD_DIR" -name "*.qmlc" -delete +# find "$BUILD_DIR" -name "qrc_*.cpp" -delete + +# Build the client +# Only reconfigure if this is a clean build or no CMakeCache exists yet +# if [ "$CLEAN_REBUILD" == "true" ] || [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then + cmake -S $REPO_ROOT_DIR/ -B $BUILD_DIR \ + -G Ninja \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DQT_TRANSLATIONS_DIR=$REPO_ROOT_DIR/translations \ + -DCMAKE_INSTALL_PREFIX=$PRODUCT_DIR \ + -DBUILD_TESTING=OFF \ + -DBUILD_UPDATER=$(if [ $BUILD_UPDATER == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DMIRALL_VERSION_BUILD=`date +%Y%m%d` \ + -DMIRALL_VERSION_SUFFIX="stable" \ + -DBUILD_OWNCLOUD_OSX_BUNDLE=$(if [ $OSX_BUNDLE == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DCMAKE_OSX_ARCHITECTURES=$ARCHITECTURE \ + -DBUILD_FILE_PROVIDER_MODULE=$(if [ $BUILD_FILEPROVIDER == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DCMAKE_PREFIX_PATH=$CRAFT_DIR \ + -DSPARKLE_LIBRARY=$SPARKLE_DIR/Sparkle.framework \ + -DSOCKETAPI_TEAM_IDENTIFIER_PREFIX="$TEAM_IDENTIFIER." \ + -DARG_SIDEBAR_ICONS=ON \ + -DLOCALBUILD=ON \ + -DSKIP_MACDEPLOYQT=$(if [ $SKIP_MACDEPLOY == true ]; then echo "ON"; else echo "OFF"; fi) \ + -DWHITELABEL_NAME=ionos \ + -DDO_NOT_USE_PROXY=ON + +# fi +ninja -C $BUILD_DIR install -v + +# --------------------------------------------------- +# Sign the client +# CODE_SIGN_IDENTITY="Developer ID Application: IONOS SE (5TDLCVD243)" + +# Check if CODE_SIGN_IDENTITY is set, if not skip signing +if [ -z "$CODE_SIGN_IDENTITY" ]; then + echo "Code sign identity not set. Skipping signing." + open $PRODUCT_DIR + [ "$RUN_AFTER_BUILD" == "true" ] && launch_app + exit 0 +fi + +PRODUCT_PATH=$PRODUCT_DIR/$PRODUCT_NAME.app + +CLIENT_CONTENTS_DIR=$PRODUCT_PATH/Contents +CLIENT_FRAMEWORKS_DIR=$CLIENT_CONTENTS_DIR/Frameworks +CLIENT_PLUGINS_DIR=$CLIENT_CONTENTS_DIR/PlugIns +CLIENT_RESOURCES_DIR=$CLIENT_CONTENTS_DIR/Resources + +find "$CLIENT_FRAMEWORKS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_PLUGINS_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" +find "$CLIENT_RESOURCES_DIR" -print0 | xargs -0 -I {} bash -c 'recursive_sign "$@" "$CODE_SIGN_IDENTITY"' _ {} "$CODE_SIGN_IDENTITY" + +codesign -s "$CODE_SIGN_IDENTITY" --force --preserve-metadata=entitlements --verbose=0 --deep --options=runtime "$PRODUCT_PATH" + + +# Sign the client +find "$CLIENT_CONTENTS_DIR/MacOS" -mindepth 1 -print0 | xargs -0 -I {} bash -c 'sign_folder_content "$@" "$CODE_SIGN_IDENTITY" "$entitlements" ' _ {} "$CODE_SIGN_IDENTITY" "--preserve-metadata=entitlements" + +# Validate that the key used for signing the binary matches the expected TeamIdentifier +# needed to pass the SocketApi through the sandbox for communication with virtual file system +if ! codesign -dv "$PRODUCT_PATH" 2>&1 | grep -q "TeamIdentifier=$TEAM_IDENTIFIER"; then + echo "TeamIdentifier does not match. Exiting." + exit 0 +fi + +# --------------------------------------------------- +# Installer + +# Build the installer, if enabled +if [ -z "$PACKAGE_INSTALLER" ]; then + echo "Installer packaging not enabled. Exiting." + open $PRODUCT_DIR + [ "$RUN_AFTER_BUILD" == "true" ] && launch_app + exit 0 +fi + +# package +$BUILD_DIR/admin/osx/create_mac.sh "$PRODUCT_DIR" "$BUILD_DIR" 'Developer ID Installer: IONOS SE (5TDLCVD243)' + +# notariaze +# Extract package filename from filesystem per .pkg extension +PACKAGE_FILENAME=$(ls $PRODUCT_DIR/*.pkg) + +# catch the output of the notarytool command +OUTPUT=$(xcrun notarytool submit --wait $PACKAGE_FILENAME\ + --keychain-profile "IONOS SE HiDrive Next") + +SUBMISSION_STATUS=$(echo $OUTPUT | grep -o 'status: [^ ]*' | cut -d ' ' -f 2) + +# Check if the notarization was successful +if [ $SUBMISSION_STATUS != "Accepted" ]; then + echo "Notarization failed. Exiting." + exit 1 +fi + +# staple +xcrun stapler staple $PACKAGE_FILENAME +xcrun stapler validate $PACKAGE_FILENAME + +open $PRODUCT_DIR +[ "$RUN_AFTER_BUILD" == "true" ] && launch_app diff --git a/admin/osx/mac-crafter/Package.resolved b/admin/osx/mac-crafter/Package.resolved deleted file mode 100644 index abc7f48333ef2..0000000000000 --- a/admin/osx/mac-crafter/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "42f510a9baeebb4b09104db0837f6dd21eba28f9233a7e61bb67fb3885667f9c", - "pins" : [ - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "011f0c765fb46d9cac61bca19be0527e99c98c8b", - "version" : "1.5.1" - } - } - ], - "version" : 3 -} diff --git a/admin/osx/mac-crafter/Sources/Commands/Build.swift b/admin/osx/mac-crafter/Sources/Commands/Build.swift index 3e30f582b3e82..7359d329b599b 100644 --- a/admin/osx/mac-crafter/Sources/Commands/Build.swift +++ b/admin/osx/mac-crafter/Sources/Commands/Build.swift @@ -49,7 +49,7 @@ struct Build: AsyncParsableCommand { var buildType = "RelWithDebInfo" @Option(name: [.long], help: "The application's branded name.") - var appName = "Nextcloud" + var appName = "IONOS HiDrive Next" @Option(name: [.long], help: "Sparkle release.") var sparkleRelease = SparkleRepository.defaultRelease @@ -519,4 +519,4 @@ struct Build: AsyncParsableCommand { Log.info("Flattened DWARF entry: \(entry.lastPathComponent)") } } -} +} \ No newline at end of file diff --git a/admin/osx/macosx.entitlements.cmake b/admin/osx/macosx.entitlements.cmake index 685551b6d12a6..43aa7d5641549 100644 --- a/admin/osx/macosx.entitlements.cmake +++ b/admin/osx/macosx.entitlements.cmake @@ -19,6 +19,5 @@ @APPLICATION_REV_DOMAIN@-spks @APPLICATION_REV_DOMAIN@-spki -@DEBUG_ENTITLEMENTS@ diff --git a/admin/win/msi/CMakeLists.txt b/admin/win/msi/CMakeLists.txt index 8b1770629ae91..97e2ffe2cda12 100644 --- a/admin/win/msi/CMakeLists.txt +++ b/admin/win/msi/CMakeLists.txt @@ -29,9 +29,10 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/make-msi.bat EnsureACL.js Platform.wxi - Nextcloud.wxs + Ionos.wxs ${CMAKE_CURRENT_BINARY_DIR}/RegistryCleanup.vbs RegistryCleanupCustomAction.wxs - gui/banner.bmp - gui/dialog.bmp + gui/banner_2.bmp + gui/dialog_ionos.bmp + gui/dialog_strato.bmp DESTINATION msi/) diff --git a/admin/win/msi/Ionos.wxs b/admin/win/msi/Ionos.wxs new file mode 100644 index 0000000000000..583fb7b886065 --- /dev/null +++ b/admin/win/msi/Ionos.wxs @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") + + + (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") + + + (SCHEDULE_REBOOT=1) OR NOT (UILevel=2) + + + + $(var.AppName) + $(var.AppIcon) + $(var.AppHelpLink) + $(var.AppInfoLink) + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 1 + + + WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed + + Removing previous installation + Trying to terminate application process of previous installation + Removing sync folders from Explorer's Navigation Pane + + + + + + + NOT (LAUNCH=0) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (NO_SHELL_EXTENSIONS=1) + + + + + (NO_START_MENU_SHORTCUTS=1) + + + + + (NO_DESKTOP_SHORTCUT=1) + + + + diff --git a/admin/win/msi/Nextcloud.wxs b/admin/win/msi/Nextcloud~~.wxs similarity index 100% rename from admin/win/msi/Nextcloud.wxs rename to admin/win/msi/Nextcloud~~.wxs diff --git a/admin/win/msi/OEM.wxi.in b/admin/win/msi/OEM.wxi.in index 85b8b71e1d510..52fd7f60b5ad2 100644 --- a/admin/win/msi/OEM.wxi.in +++ b/admin/win/msi/OEM.wxi.in @@ -39,7 +39,7 @@ - - + + diff --git a/admin/win/msi/gui/banner_2.bmp b/admin/win/msi/gui/banner_2.bmp new file mode 100644 index 0000000000000..5270a99dea084 Binary files /dev/null and b/admin/win/msi/gui/banner_2.bmp differ diff --git a/admin/win/msi/gui/dialog_ionos.bmp b/admin/win/msi/gui/dialog_ionos.bmp new file mode 100644 index 0000000000000..b870b472556df Binary files /dev/null and b/admin/win/msi/gui/dialog_ionos.bmp differ diff --git a/admin/win/msi/gui/dialog_strato.bmp b/admin/win/msi/gui/dialog_strato.bmp new file mode 100644 index 0000000000000..e697e11f72423 Binary files /dev/null and b/admin/win/msi/gui/dialog_strato.bmp differ diff --git a/admin/win/msi/make-msi.bat.in b/admin/win/msi/make-msi.bat.in index 98731a15215ec..2347091ca9608 100644 --- a/admin/win/msi/make-msi.bat.in +++ b/admin/win/msi/make-msi.bat.in @@ -107,7 +107,7 @@ exit 0 set codepage=%~1 echo :: Compiling MSI project for codepage !codepage! - "%WIX%\bin\candle.exe" -dcodepage=!codepage! -dProductCode=%PRODUCTCODE% -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Nextcloud.wxs RegistryCleanupCustomAction.wxs + "%WIX%\bin\candle.exe" -dcodepage=!codepage! -dProductCode=%PRODUCTCODE% -dPlatform=%BuildArch% -arch %BuildArch% -dHarvestAppDir="%HarvestAppDir%" -ext WixUtilExtension NCMsiHelper.wxs WinShellExt.wxs collect.wxs Ionos.wxs RegistryCleanupCustomAction.wxs if %ERRORLEVEL% neq 0 exit %ERRORLEVEL% echo/ @@ -121,7 +121,7 @@ exit 0 set culture=%%c echo :: Linking MSI project for !culture! - "%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:!culture!;en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Nextcloud.wixobj RegistryCleanupCustomAction.wixobj -out "!culture!_@MSI_INSTALLER_FILENAME@" + "%WIX%\bin\light.exe" -sw1076 -ext WixUIExtension -ext WixUtilExtension -cultures:!culture!;en-us NCMsiHelper.wixobj WinShellExt.wixobj collect.wixobj Ionos.wixobj RegistryCleanupCustomAction.wixobj -out "!culture!_@MSI_INSTALLER_FILENAME@" if %ERRORLEVEL% neq 0 exit %ERRORLEVEL% if NOT "!culture!" == "en-us" ( diff --git a/cmake/modules/MacOSXBundleInfo.plist.in b/cmake/modules/MacOSXBundleInfo.plist.in index a94e577d5d8fd..9750b29a62082 100644 --- a/cmake/modules/MacOSXBundleInfo.plist.in +++ b/cmake/modules/MacOSXBundleInfo.plist.in @@ -42,10 +42,8 @@ SUShowReleaseNotes - SUPublicDSAKeyFile - dsa_pub.pem SUPublicEDKey - c3RcfDWDayvsYSZW8FhZN1UOJhvPVN30zleb4zOqbtU= + FQ8Dq6AiSDDv4XpnyJ3b6mQBFYLPKgj9ziEg/+VNGHg= SUEnableInstallerLauncherService UTExportedTypeDeclarations diff --git a/config.h.in b/config.h.in index 3159cdb9233b5..8bb73c8af1cc8 100644 --- a/config.h.in +++ b/config.h.in @@ -11,6 +11,7 @@ #cmakedefine THEME_INCLUDE @THEME_INCLUDE@ #cmakedefine APPLICATION_NAME "@APPLICATION_NAME@" +#cmakedefine WHITELABEL_BRAND "@WHITELABEL_BRAND@" #cmakedefine APPLICATION_VENDOR "@APPLICATION_VENDOR@" #cmakedefine APPLICATION_DOMAIN "@APPLICATION_DOMAIN@" #cmakedefine APPLICATION_REV_DOMAIN "@APPLICATION_REV_DOMAIN@" diff --git a/craftmaster.ini b/craftmaster.ini index 526b99e6f06d6..f1318f0f7d52a 100644 --- a/craftmaster.ini +++ b/craftmaster.ini @@ -41,7 +41,6 @@ binary/mysql.useMariaDB = False libs/qt6.version = 6.10.2 craft/craft-blueprints-kde.revision = stable-33.0 libs/zlib.version = 1.3.1 -libs/openssl.version = 3.5.7 [windows-msvc2022_64-cl] Packager/PackageType = NullsoftInstallerPackager diff --git a/doc/ADR/20241007_TrackingWithGA4.md b/doc/ADR/20241007_TrackingWithGA4.md new file mode 100644 index 0000000000000..8422656e3ddc1 --- /dev/null +++ b/doc/ADR/20241007_TrackingWithGA4.md @@ -0,0 +1,32 @@ +# Use Tracking with Google Analytics 4 + +## Status + +proposed + +## Context + +The web front end already tracks user interactions with GA4. To track interactions in the client software as well, an implementation with GA should also be done here. + +## Decision + +Since the use of GA4 in desktop software and the use of the pure API are not natively supported, a reverse-engineered solution had to be used (see https://ga4mp.dev/#/), similar to the use of the GA4 Measurement Protocol, which, however, does not provide a complete replacement. + +This approach implements GA4 tracking based on HTTP POST requests. + +## Implementation Details + +The implementation consists of three classes: + +* GAnalyticsWorker +* GAnalytics +* DataCollectionWrapper + +The DataCollectionWrapper is the outermost layer and is used by the application to track actions and events. The DataCollectionWrapper provides enums for various tracking pages (areas of the application, such as GeneralSettings or UserSettings) and tracking elements (specific buttons, checkboxes, or similar items). +GAnalytics acts as an intermediary between the outer layer (DataCollectionWrapper) and the communication logic. Various variables are also set here. When a tracking call is made, it is forwarded to the GAnalyticsWorker, where it is queued. + +The GAnalyticsWorker contains a queue, a message loop, and a QNetworkAccessManager. At fixed intervals, the queue is checked for tracking calls, which are then sent to the GA4 interface. If multiple calls are present, the connection is kept alive until all tracking calls have been sent. + +## Consequences + +This approach allows client-side tracking using GA4, bridging the gap between web and desktop tracking. The modular design simplifies maintenance by separating tracking logic from communication handling. Usage is straightforward; the application simply calls the DataCollectionWrapper at points where tracking should occur, ensuring that actions and events are recorded seamlessly. However, relying on a reverse-engineered solution introduces risks of future incompatibility with GA4 updates. Managing a queue and network connection adds complexity, and there might be latency when batching tracking calls. Overall, the solution balances functionality with maintainability but carries some technical risks. diff --git a/doc/ADR/20250212_UseCostomizationService.md b/doc/ADR/20250212_UseCostomizationService.md new file mode 100644 index 0000000000000..2131d2188ea8f --- /dev/null +++ b/doc/ADR/20250212_UseCostomizationService.md @@ -0,0 +1,69 @@ +# Use Customization Service + +## Status + +accepted + +## Context + +Nextcloud (NC) offers a customization service. The service is a Nextcloud white-labeling offering. It allows customers and their forks to be redesigned according to their own specifications. There are two levels. + +### Brander + +Here, the icons, names, and technical aspects are being adjusted to release a simply redesigned, color-enhanced Nextcloud. + +### Actual Customization Service + +This will integrate deeper changes into the branded client. This could be anything, in principle. + +For this to work we need to create Pull-Requests against the branches of the nextcloud repo. These pull requests can not depend on the same code line. + +More information can be read [here](https://portal.nextcloud.com/article/Branding/Customization-Service) + +### Workflow on the Repo + +A PR always has a source branch and a target branch. The target branch must be `master` in our case. Source branches can follow our established workflow. However, there are some conditions: + +- Concurrent work on different features must be possible. +- Local builds must be possible for testing purposes. +- A nightly/onPush build might be desirable. +- It must be ensured that everyone has an up-to-date status with all PRs. + +There are two possible Solutions: + +1. We use one Pull-Request containing all changes. +2. We use multiple Pull requests containing changes grouped by files. + +## Decision + +We will use only one PR containing all our changes. + +At this point in time, it would be too much work to split all Changes into separate branches/PRs. + +## Implementation Details + +### Branching + +- We use the master branch only for pulling the updated nc/master branch. +- We have a separate develop branch to develop changes. +- We create a PR to nc/masetr from develop. + +## Consequences + +### Positive + +- We can use our existing git-workflow +- We have a simple repo-structure with only 2 really needed branches +- It is easy to build locally, because all changes are on one branch +- Less Branches to backport +- We do not need to sort every change to a separate PR + +### Negative + +- Merge conflicts can be complex +- The PR will contain all the changes, this could be overwhelming to check + +### Further Thoughts + +- We need to see how this works in practice +- This is not final \ No newline at end of file diff --git a/doc/ADR/_template.md b/doc/ADR/_template.md new file mode 100644 index 0000000000000..0f2aef85df5b5 --- /dev/null +++ b/doc/ADR/_template.md @@ -0,0 +1,30 @@ +# ADR template by Michael Nygard + +This is the template in [Documenting architecture decisions - Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). +You can use [adr-tools](https://github.com/npryce/adr-tools) for managing the ADR files. + +We extended this Template with ```Implementation Details``` (optional) + +In each ADR file, write these sections: + +# Title + +## Status + +What is the status, such as proposed, accepted, rejected, deprecated, superseded, etc.? + +## Context + +What is the issue that we're seeing that is motivating this decision or change? + +## Decision + +What is the change that we're proposing and/or doing? + +## Implementation Details + +(Optional) How was this Decision implemented. I.e. Uml Diagrams, Code etc. + +## Consequences + +What becomes easier or more difficult to do because of this change? diff --git a/find_qml_imports.py b/find_qml_imports.py new file mode 100644 index 0000000000000..0c7dbce5a48a7 --- /dev/null +++ b/find_qml_imports.py @@ -0,0 +1,29 @@ +import os +import sys +import re + +def find_imports_in_qml(root_dir, import_prefix): + pattern = re.compile(r'^\s*import\s+' + re.escape(import_prefix) + r'.*$', re.MULTILINE) + matches = [] + for subdir, _, files in os.walk(root_dir): + for file in files: + if file.endswith('.qml'): + file_path = os.path.join(subdir, file) + try: + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + for match in pattern.finditer(content): + matches.append((file_path, match.group().strip())) + except Exception: + pass # Ignore files that can't be processed + print(f"Found {len(matches)} matching import lines:") + for file_path, line in matches: + print(f"{file_path}: {line}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python find_qml_imports.py ") + sys.exit(1) + import_prefix = sys.argv[1] + root_dir = sys.argv[2] + find_imports_in_qml(root_dir, import_prefix) diff --git a/fonts/OpenSans-Bold.ttf b/fonts/OpenSans-Bold.ttf new file mode 100644 index 0000000000000..efdd5e84a0397 Binary files /dev/null and b/fonts/OpenSans-Bold.ttf differ diff --git a/fonts/OpenSans-BoldItalic.ttf b/fonts/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000000000..9bf9b4e97b657 Binary files /dev/null and b/fonts/OpenSans-BoldItalic.ttf differ diff --git a/fonts/OpenSans-ExtraBold.ttf b/fonts/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000000..67fcf0fb2af0c Binary files /dev/null and b/fonts/OpenSans-ExtraBold.ttf differ diff --git a/fonts/OpenSans-ExtraBoldItalic.ttf b/fonts/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000000000..086722809c74d Binary files /dev/null and b/fonts/OpenSans-ExtraBoldItalic.ttf differ diff --git a/fonts/OpenSans-Italic.ttf b/fonts/OpenSans-Italic.ttf new file mode 100644 index 0000000000000..117856707b9b8 Binary files /dev/null and b/fonts/OpenSans-Italic.ttf differ diff --git a/fonts/OpenSans-Light.ttf b/fonts/OpenSans-Light.ttf new file mode 100644 index 0000000000000..6580d3a169e89 Binary files /dev/null and b/fonts/OpenSans-Light.ttf differ diff --git a/fonts/OpenSans-LightItalic.ttf b/fonts/OpenSans-LightItalic.ttf new file mode 100644 index 0000000000000..1e0c3319810ad Binary files /dev/null and b/fonts/OpenSans-LightItalic.ttf differ diff --git a/fonts/OpenSans-Regular.ttf b/fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000000000..29bfd35a2bfdd Binary files /dev/null and b/fonts/OpenSans-Regular.ttf differ diff --git a/fonts/OpenSans-SemiBold.ttf b/fonts/OpenSans-SemiBold.ttf new file mode 100644 index 0000000000000..54e7059cf3635 Binary files /dev/null and b/fonts/OpenSans-SemiBold.ttf differ diff --git a/fonts/OpenSans-SemiBoldItalic.ttf b/fonts/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 0000000000000..aebcf1421226d Binary files /dev/null and b/fonts/OpenSans-SemiBoldItalic.ttf differ diff --git a/ionos-theme.qrc b/ionos-theme.qrc new file mode 100644 index 0000000000000..2377ef3b72aa5 --- /dev/null +++ b/ionos-theme.qrc @@ -0,0 +1,88 @@ + + + theme/colored/ionos-data-protection-logo.png + theme/colored/strato-data-protection-logo.png + theme/ses/ses-IONOS-logo.svg + theme/colored/IONOS_logo_w_suffix_frontend.png + theme/colored/IONOS_logo_w_suffix_frontend@2x.png + theme/colored/IONOS_logo_w_suffix_frontend.svg + fonts/OpenSans-SemiBold.ttf + fonts/OpenSans-Regular.ttf + theme/ses/ses-accountDelete.svg + theme/ses/ses-activityDelete.svg + theme/ses/ses-accountLogout.svg + theme/ses/ses-accountPause.svg + theme/ses/ses-accountQuit.svg + theme/ses/ses-accountResume.svg + theme/ses/ses-activity.svg + theme/ses/ses-chevron.svg + theme/ses/ses-clipboard.svg + theme/ses/ses-lightClipboard.svg + theme/ses/ses-darkPlus.svg + theme/ses/ses-lightPlus.svg + theme/ses/ses-addlivebackup.svg + theme/ses/ses-file.svg + theme/ses/ses-folderIcon.svg + theme/ses/ses-folderIconBright.svg + theme/ses/ses-syncstate-success.svg + theme/ses/ses-syncstate-syncing.svg + theme/ses/ses-syncstate-paused.svg + theme/ses/ses-syncstate-warning.svg + theme/ses/ses-syncstate-error.svg + theme/ses/ses-state-offline.svg + theme/ses/ses-snackbar-success.svg + theme/ses/ses-snackbar-warning.svg + theme/ses/ses-snackbar-error.svg + theme/ses/ses-more.svg + theme/ses/ses-more-hover.svg + theme/ses/ses-questionmark.svg + theme/ses/ses-refresh.svg + theme/ses/ses-settings.svg + theme/ses/ses-settingsAvatar.svg + theme/ses/ses-settingsAvatarRound.svg + theme/ses/ses-info.svg + theme/ses/ses-syncArrows.svg + theme/ses/ses-external.svg + theme/ses/ses-website.svg + theme/ses/strato/ses-STRATO-logo.svg + theme/colored/STRATO_logo_w_suffix_frontend.png + theme/colored/STRATO_logo_w_suffix_frontend@2x.png + theme/colored/STRATO_logo_w_suffix_frontend.svg + theme/ses/strato/ses-accountDelete.svg + theme/ses/strato/ses-activityDelete.svg + theme/ses/strato/ses-accountLogout.svg + theme/ses/strato/ses-accountPause.svg + theme/ses/strato/ses-accountQuit.svg + theme/ses/strato/ses-accountResume.svg + theme/ses/strato/ses-activity.svg + theme/ses/strato/ses-chevron.svg + theme/ses/strato/ses-clipboard.svg + theme/ses/strato/ses-lightClipboard.svg + theme/ses/strato/ses-darkPlus.svg + theme/ses/strato/ses-lightPlus.svg + theme/ses/strato/ses-addlivebackup.svg + theme/ses/strato/ses-file.svg + theme/ses/strato/ses-folderIcon.svg + theme/ses/strato/ses-folderIconBright.svg + theme/ses/strato/ses-syncstate-success.svg + theme/ses/strato/ses-syncstate-syncing.svg + theme/ses/strato/ses-syncstate-paused.svg + theme/ses/strato/ses-syncstate-warning.svg + theme/ses/strato/ses-syncstate-error.svg + theme/ses/strato/ses-state-offline.svg + theme/ses/strato/ses-snackbar-success.svg + theme/ses/strato/ses-snackbar-warning.svg + theme/ses/strato/ses-snackbar-error.svg + theme/ses/strato/ses-more.svg + theme/ses/strato/ses-more-hover.svg + theme/ses/strato/ses-questionmark.svg + theme/ses/strato/ses-refresh.svg + theme/ses/strato/ses-settings.svg + theme/ses/strato/ses-settingsAvatar.svg + theme/ses/strato/ses-settingsAvatarRound.svg + theme/ses/strato/ses-info.svg + theme/ses/strato/ses-syncArrows.svg + theme/ses/strato/ses-external.svg + theme/ses/strato/ses-website.svg + + diff --git a/ionos-theme.qrc.in b/ionos-theme.qrc.in new file mode 100644 index 0000000000000..2377ef3b72aa5 --- /dev/null +++ b/ionos-theme.qrc.in @@ -0,0 +1,88 @@ + + + theme/colored/ionos-data-protection-logo.png + theme/colored/strato-data-protection-logo.png + theme/ses/ses-IONOS-logo.svg + theme/colored/IONOS_logo_w_suffix_frontend.png + theme/colored/IONOS_logo_w_suffix_frontend@2x.png + theme/colored/IONOS_logo_w_suffix_frontend.svg + fonts/OpenSans-SemiBold.ttf + fonts/OpenSans-Regular.ttf + theme/ses/ses-accountDelete.svg + theme/ses/ses-activityDelete.svg + theme/ses/ses-accountLogout.svg + theme/ses/ses-accountPause.svg + theme/ses/ses-accountQuit.svg + theme/ses/ses-accountResume.svg + theme/ses/ses-activity.svg + theme/ses/ses-chevron.svg + theme/ses/ses-clipboard.svg + theme/ses/ses-lightClipboard.svg + theme/ses/ses-darkPlus.svg + theme/ses/ses-lightPlus.svg + theme/ses/ses-addlivebackup.svg + theme/ses/ses-file.svg + theme/ses/ses-folderIcon.svg + theme/ses/ses-folderIconBright.svg + theme/ses/ses-syncstate-success.svg + theme/ses/ses-syncstate-syncing.svg + theme/ses/ses-syncstate-paused.svg + theme/ses/ses-syncstate-warning.svg + theme/ses/ses-syncstate-error.svg + theme/ses/ses-state-offline.svg + theme/ses/ses-snackbar-success.svg + theme/ses/ses-snackbar-warning.svg + theme/ses/ses-snackbar-error.svg + theme/ses/ses-more.svg + theme/ses/ses-more-hover.svg + theme/ses/ses-questionmark.svg + theme/ses/ses-refresh.svg + theme/ses/ses-settings.svg + theme/ses/ses-settingsAvatar.svg + theme/ses/ses-settingsAvatarRound.svg + theme/ses/ses-info.svg + theme/ses/ses-syncArrows.svg + theme/ses/ses-external.svg + theme/ses/ses-website.svg + theme/ses/strato/ses-STRATO-logo.svg + theme/colored/STRATO_logo_w_suffix_frontend.png + theme/colored/STRATO_logo_w_suffix_frontend@2x.png + theme/colored/STRATO_logo_w_suffix_frontend.svg + theme/ses/strato/ses-accountDelete.svg + theme/ses/strato/ses-activityDelete.svg + theme/ses/strato/ses-accountLogout.svg + theme/ses/strato/ses-accountPause.svg + theme/ses/strato/ses-accountQuit.svg + theme/ses/strato/ses-accountResume.svg + theme/ses/strato/ses-activity.svg + theme/ses/strato/ses-chevron.svg + theme/ses/strato/ses-clipboard.svg + theme/ses/strato/ses-lightClipboard.svg + theme/ses/strato/ses-darkPlus.svg + theme/ses/strato/ses-lightPlus.svg + theme/ses/strato/ses-addlivebackup.svg + theme/ses/strato/ses-file.svg + theme/ses/strato/ses-folderIcon.svg + theme/ses/strato/ses-folderIconBright.svg + theme/ses/strato/ses-syncstate-success.svg + theme/ses/strato/ses-syncstate-syncing.svg + theme/ses/strato/ses-syncstate-paused.svg + theme/ses/strato/ses-syncstate-warning.svg + theme/ses/strato/ses-syncstate-error.svg + theme/ses/strato/ses-state-offline.svg + theme/ses/strato/ses-snackbar-success.svg + theme/ses/strato/ses-snackbar-warning.svg + theme/ses/strato/ses-snackbar-error.svg + theme/ses/strato/ses-more.svg + theme/ses/strato/ses-more-hover.svg + theme/ses/strato/ses-questionmark.svg + theme/ses/strato/ses-refresh.svg + theme/ses/strato/ses-settings.svg + theme/ses/strato/ses-settingsAvatar.svg + theme/ses/strato/ses-settingsAvatarRound.svg + theme/ses/strato/ses-info.svg + theme/ses/strato/ses-syncArrows.svg + theme/ses/strato/ses-external.svg + theme/ses/strato/ses-website.svg + + diff --git a/ionos.qrc b/ionos.qrc new file mode 100644 index 0000000000000..0cc12434b4814 --- /dev/null +++ b/ionos.qrc @@ -0,0 +1,12 @@ + + + src/gui/tray/HeaderLogo.qml + src/gui/SesComponents/SesErrorBox.qml + src/gui/SesComponents/SesTrayHeader.qml + src/gui/tray/TrayWindowAccountMenu.qml + src/gui/tray/PrimaryPillButton.qml + src/gui/tray/SecondaryPillButton.qml + src/gui/tray/IconButton.qml + src/gui/tray/AccountMenuItem.qml + + diff --git a/shell_integration/MacOSX/CMakeLists.txt b/shell_integration/MacOSX/CMakeLists.txt index 1e02a6faf1459..689563727ab55 100644 --- a/shell_integration/MacOSX/CMakeLists.txt +++ b/shell_integration/MacOSX/CMakeLists.txt @@ -45,6 +45,7 @@ if(APPLE) "OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}" "OC_APPLICATION_NAME=${APPLICATION_NAME}" "OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}" + "USER_HEADER_SEARCH_PATHS=${CMAKE_BINARY_DIR}" COMMENT building macOS File Provider extension VERBATIM) @@ -62,6 +63,7 @@ if(APPLE) "OC_APPLICATION_VENDOR=${APPLICATION_VENDOR}" "OC_APPLICATION_NAME=${APPLICATION_NAME}" "OC_APPLICATION_REV_DOMAIN=${APPLICATION_REV_DOMAIN}" + "USER_HEADER_SEARCH_PATHS=${CMAKE_BINARY_DIR}" DEPENDS mac_fileproviderplugin COMMENT building macOS File Provider UI extension VERBATIM) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h index 008232c86be84..ac68533a8c0e2 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExt-Bridging-Header.h @@ -8,5 +8,6 @@ #import "Services/AppProtocol.h" #import "Services/ClientCommunicationProtocol.h" +#import "config.h" #endif /* FileProviderExt_Bridging_Header_h */ diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift index 3fe270b06a0f4..d902057436d37 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension+CustomActions.swift @@ -12,7 +12,7 @@ extension FileProviderExtension: NSFileProviderCustomAction { completionHandler: @escaping ((any Error)?) -> Void ) -> Progress { switch actionIdentifier.rawValue { - case "com.nextcloud.desktopclient.FileProviderExt.FileActionsAction": + case "com.ionos.hidrive.desktopclient.FileProviderExt.FileActionsAction": guard let itemIdentifier = itemIdentifiers.first else { logger.error("Failed to get first item identifier for file actions action.") completionHandler(NSFileProviderError(.noSuchItem)) @@ -46,13 +46,13 @@ extension FileProviderExtension: NSFileProviderCustomAction { } return Progress() - case "com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction": + case "com.ionos.hidrive.desktopclient.FileProviderExt.KeepDownloadedAction": return performKeepDownloadedAction( keepDownloaded: true, onItemsWithIdentifiers: itemIdentifiers, completionHandler: completionHandler ) - case "com.nextcloud.desktopclient.FileProviderExt.AutoEvictAction": + case "com.ionos.hidrive.desktopclient.FileProviderExt.AutoEvictAction": return performKeepDownloadedAction( keepDownloaded: false, onItemsWithIdentifiers: itemIdentifiers, diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift index f221d277e1828..964ca18ab705a 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/FileProviderExtension.swift @@ -170,6 +170,7 @@ import OSLog } let progress = Progress() + Task { progress.totalUnitCount = 1 if let item = await Item.storedItem(identifier: identifier, account: ncAccount, remoteInterface: ncKit, dbManager: dbManager, log: log), item.metadata.deleted == false { diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist index 533faaa375bbf..c8cb64375ef8c 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Info.plist @@ -41,7 +41,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayKeepDownloaded == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderExt.KeepDownloadedAction + $(OC_APPLICATION_REV_DOMAIN).FileProviderExt.KeepDownloadedAction NSExtensionFileProviderActionName Always keep downloaded @@ -49,7 +49,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayAllowAutoEvicting == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderExt.AutoEvictAction + $(OC_APPLICATION_REV_DOMAIN).FileProviderExt.AutoEvictAction NSExtensionFileProviderActionName Allow automatic freeing up space diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift index 940d5c947755c..c244ab25819df 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderExt/Services/FPUIExtensionService.swift @@ -9,7 +9,9 @@ import NextcloudKit /// /// This does not need to be branded because it is scoped by the app-specific file provider domains in the file system already. /// -let fpUiExtensionServiceName = NSFileProviderServiceName("com.nextcloud.desktopclient.FPUIExtensionService") +let fpUiExtensionServiceName = NSFileProviderServiceName( + "com.ionos.hidrivenext.desktopclient.FPUIExtensionService" +) /// /// The requirements of the service exposed and dedicated to the file provider user interface extension. diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift index ec4671d70e9f7..afc1fc3cfbd5c 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/DocumentActionViewController.swift @@ -64,13 +64,13 @@ class DocumentActionViewController: FPUIActionExtensionViewController { logger?.info("Preparing action: \(actionIdentifier)") switch (actionIdentifier) { - case "com.nextcloud.desktopclient.FileProviderUIExt.ShareAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.ShareAction": prepare(childViewController: ShareViewController(itemIdentifiers, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.LockFileAction": prepare(childViewController: LockViewController(itemIdentifiers, locking: true, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction": + case "com.ionos.hidrivenext.desktopclient.FileProviderUIExt.UnlockFileAction": prepare(childViewController: LockViewController(itemIdentifiers, locking: false, serviceResolver: serviceResolver, log: log)) - case "com.nextcloud.desktopclient.FileProviderUIExt.EvictAction": + case "com.ionos.hidrive.desktopclient.FileProviderUIExt.EvictAction": evict(itemsWithIdentifiers: itemIdentifiers, inDomain: domain); extensionContext.completeRequest(); default: diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist index dbb88fab631c8..f60616edbfa2e 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Info.plist @@ -22,26 +22,26 @@ NSExtensionFileProviderActions - NSExtensionFileProviderActionActivationRule - SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked != nil && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.UnlockFileAction + $(OC_APPLICATION_REV_DOMAIN).FileProviderUIExt.UnlockFileAction NSExtensionFileProviderActionName Unlock file + NSExtensionFileProviderActionActivationRule + SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked != nil && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 0 NSExtensionFileProviderActionActivationRule - SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked == nil && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 0 - NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.LockFileAction + SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.locked == nil && !($fileproviderItem.contentType.identifier UTI-CONFORMS-TO "public.folder") ).@count > 0 NSExtensionFileProviderActionName Lock file + NSExtensionFileProviderActionIdentifier + $(OC_APPLICATION_REV_DOMAIN).FileProviderUIExt.LockFileAction NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.displayShare == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.ShareAction + $(OC_APPLICATION_REV_DOMAIN).FileProviderUIExt.ShareAction NSExtensionFileProviderActionName Share options @@ -49,7 +49,7 @@ NSExtensionFileProviderActionActivationRule SUBQUERY ( fileproviderItems, $fileproviderItem, $fileproviderItem.userInfo.downloaded == true ).@count > 0 NSExtensionFileProviderActionIdentifier - com.nextcloud.desktopclient.FileProviderUIExt.EvictAction + $(OC_APPLICATION_REV_DOMAIN).FileProviderUIExt.EvictAction NSExtensionFileProviderActionName Free up space diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Localizable.xcstrings b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Localizable.xcstrings index 416ac4d1c1fe3..71b6c52510d84 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Localizable.xcstrings +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Localizable.xcstrings @@ -632,6 +632,124 @@ } } }, + "Circle share (%@)" : { + "comment" : "Text label", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kreis teilen (%@)" + } + }, + "de_DE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kreis-Freigabe (%@)" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circle share (%@)" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Recurso compartido de Círculo (%@)" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Partage Cercle (%@)" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartición de círculo (%@)" + } + }, + "pt_BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Compartilhamento de círculo (%@)" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takım paylaşımı (%@)" + } + }, + "zh_HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "圓形共享 (%@)" + } + } + } + }, + "Circle share icon" : { + "comment" : "Accessibility description for image", + "localizations" : { + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kreisförmiges Freigabesymbol" + } + }, + "de_DE" : { + "stringUnit" : { + "state" : "translated", + "value" : "Symbol für Kreis-Freigabe" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Circle share icon" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícono de compartir con el círculo" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icône de partage Cercle" + } + }, + "gl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Icona de compartición de círculo" + } + }, + "pt_BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ícone de compartilhamento de círculo" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Takım paylaşımı simgesi" + } + }, + "zh_HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "圓形共享圖標" + } + } + } + }, "Close" : { "comment" : "Button title", "extractionState" : "manual", diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift index 523da80a0068d..bb30a3b346974 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Locking/LockViewController.swift @@ -184,7 +184,7 @@ class LockViewController: NSViewController { user: account.username, userId: account.id, password: account.password, - userAgent: "Nextcloud-macOS/FileProviderUIExt", + userAgent: "IONOS HiDrive Next/FileProviderUIExt", groupIdentifier: "" ) diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift index 5d1436b7ac3bd..23019b377ca47 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareOptionsView.swift @@ -194,6 +194,12 @@ class ShareOptionsView: NSView { caps.defaultPermissions & NKShare.Permission.update.rawValue != 0 ? .on : .off + groupShareMenuItem.isHidden = true + emailShareMenuItem.isHidden = false + federatedCloudShareMenuItem.isHidden = true + teamShare.isHidden = true + talkConversationShare.isHidden = true + switch type { case .publicLink: passwordProtectCheckbox.isHidden = false diff --git a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift index e4cdc5fba85bc..734ef7cdd2765 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift +++ b/shell_integration/MacOSX/NextcloudIntegration/FileProviderUIExt/Sharing/ShareTableViewDataSource.swift @@ -47,7 +47,7 @@ class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDele } } - private(set) var userAgent: String = "Nextcloud-macOS/FileProviderUIExt" + private(set) var userAgent: String = "IONOS HiDrive Next/FileProviderUIExt" private(set) var account: Account? { didSet { diff --git a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m index 46fb9a2343589..fc53c66753858 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m +++ b/shell_integration/MacOSX/NextcloudIntegration/FinderSyncExt/FinderSync.m @@ -60,6 +60,16 @@ - (instancetype)init [syncController setBadgeImage:warning label:@"Ignored" forBadgeIdentifier:@"IGNORE+SWM"]; [syncController setBadgeImage:error label:@"Error" forBadgeIdentifier:@"ERROR+SWM"]; + // The Mach port name needs to: + // - Be prefixed with the code signing Team ID + // - Then infixed with the sandbox App Group + // - The App Group itself must be a prefix of (or equal to) the application bundle identifier + // We end up in the official signed client with: 9B5WD74GWJ.com.owncloud.desktopclient.socket + // With ad-hoc signing (the '-' signing identity) we must drop the Team ID. + // When the code isn't sandboxed (e.g. the OC client or the legacy overlay icon extension) + // the OS doesn't seem to put any restriction on the port name, so we just follow what + // the sandboxed App Extension needs. + // https://developer.apple.com/library/mac/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW24 NSURL *container = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:groupIdentifier]; NSURL *socketPath = [container URLByAppendingPathComponent:@"s" isDirectory:NO]; diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj index c5739990c9e36..9b5ff55f19a63 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.pbxproj @@ -232,6 +232,10 @@ C2B573F11B1DAD6400303B36 /* warning.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = warning.iconset; path = ../../icons/nopadding/warning.iconset; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + AAFDB1862ECB63AD00C26AE7 /* src */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); name = src; path = /Users/iva/Projekte/nextcloud/desktop/src; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 538E396427F4765000FA63D5 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 22d6a07900a11..de4e8597f2b45 100644 --- a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,13 +1,22 @@ { - "originHash" : "61feeb4866ca418188987b3ae0c9a2032cac13e1662ac72932283925957aa395", + "originHash" : "54aeaa2601e8f07807ca90e701c536f2fef2ac3e2d52169e4828143e05e59cad", "pins" : [ { "identity" : "alamofire", "kind" : "remoteSourceControl", "location" : "https://github.com/Alamofire/Alamofire", "state" : { - "revision" : "e938f8c66708e7352fc7e3512647fa54255b267a", - "version" : "5.11.2" + "revision" : "7595cbcf59809f9977c5f6378500de2ad73b7ddb", + "version" : "5.12.0" + } + }, + { + "identity" : "codescanner", + "kind" : "remoteSourceControl", + "location" : "https://github.com/twostraws/CodeScanner.git", + "state" : { + "revision" : "5e886430238944c7200fc9e10dbf2d9550dba865", + "version" : "2.5.2" } }, { @@ -24,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nextcloud/NextcloudKit", "state" : { - "revision" : "6f25e101f5833d4d01961c5012acb37d526fffd2", - "version" : "7.2.8" + "revision" : "6d21f1258d1fb67b510c3b66129e73d86114fe14", + "version" : "7.4.1" } }, { @@ -33,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "4cc46f8607516226e5465062fdacd088fbd94552", - "version" : "20.1.4" + "revision" : "b4192c46305570577c4d08df790fddec5ab3aa04", + "version" : "20.1.5" } }, { @@ -42,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-swift.git", "state" : { - "revision" : "600e187711e5fa4e8e2c0429cacee27e8e44f112", - "version" : "20.0.4" + "revision" : "ca03df491ec5e4bb8af0bca1e9957d08c10ec2da", + "version" : "20.0.5" } }, { diff --git a/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme new file mode 100644 index 0000000000000..0e76b4c2e7cf0 --- /dev/null +++ b/shell_integration/MacOSX/NextcloudIntegration/NextcloudIntegration.xcodeproj/xcshareddata/xcschemes/NextcloudDev.xcscheme @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shell_integration/windows/WinShellExt.wxs.in b/shell_integration/windows/WinShellExt.wxs.in index a23bc12cf3031..b424ab0a67d45 100644 --- a/shell_integration/windows/WinShellExt.wxs.in +++ b/shell_integration/windows/WinShellExt.wxs.in @@ -31,11 +31,11 @@ There is a limit in Windows (oh wonder^^) so that only the first 15 extensions get invoked, this is why to use that dirty little trick to get ahead ;) See: https://docs.microsoft.com/en-us/windows/win32/shell/context-menu-handlers?redirectedfrom=MSDN#employing-the-verb-selection-model --> - - - - - + + + + + diff --git a/src/common/utility.cpp b/src/common/utility.cpp index 317ada0e4505f..d6500bcf8bb4a 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -166,8 +166,8 @@ QByteArray Utility::userAgentString() QByteArray Utility::friendlyUserAgentString() { - const auto pattern = QStringLiteral("%1 (Desktop Client - %2)"); - const auto userAgent = pattern.arg(QSysInfo::machineHostName(), platform()); + const auto pattern = QStringLiteral("%1 (%2 Desktop Client - %3)"); + const auto userAgent = pattern.arg(QSysInfo::machineHostName(), qApp->applicationName(), platform()); return userAgent.toUtf8(); } diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 9aabf0a93c03d..75c8c64460cdd 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -142,7 +142,10 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const QString &path, bool exclu if (bname.startsWith(QLatin1String(".owncloudsync.log"), Qt::CaseInsensitive)) { // ".owncloudsync.log*" return CSYNC_FILE_SILENTLY_EXCLUDED; } - if (bname.startsWith(QLatin1String(".nextcloudsync.log"), Qt::CaseInsensitive)) { // ".nextcloudsync.log*" + if (bname.startsWith(QLatin1String(".hidrivenextsync.log"), Qt::CaseInsensitive)) { // ".hidrivenextsync.log*" + return CSYNC_FILE_SILENTLY_EXCLUDED; + } + if (bname.startsWith(QLatin1String(".nextcloudpermissions.log"), Qt::CaseInsensitive)) { // ".nextcloudpermissions.log*" return CSYNC_FILE_SILENTLY_EXCLUDED; } if (bname.startsWith(QLatin1String(".nextcloudpermissions.log"), Qt::CaseInsensitive)) { // ".nextcloudpermissions.log*" diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 7f45acfeba5ed..614a73e3c55f9 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -9,6 +9,9 @@ find_package(KF6GuiAddons) if(CMAKE_BUILD_TYPE MATCHES Debug) add_definitions(-DQT_QML_DEBUG) endif() +if(CMAKE_BUILD_TYPE MATCHES "RelWithDebInfo") + add_definitions(-DBUILDTYPE_RELWITHDEBINFO) +endif() IF(BUILD_UPDATER) add_subdirectory(updater) @@ -21,6 +24,7 @@ if(APPLICATION_NAME STREQUAL "Nextcloud") endif() configure_file(${CMAKE_SOURCE_DIR}/theme.qrc.in ${CMAKE_SOURCE_DIR}/theme.qrc) +configure_file(${CMAKE_SOURCE_DIR}/ionos-theme.qrc.in ${CMAKE_SOURCE_DIR}/ionos-theme.qrc) set(theme_dir ${CMAKE_SOURCE_DIR}/theme) set(client_UI_SRCS @@ -53,7 +57,12 @@ set(client_UI_SRCS wizard/proxysettings.ui ) -qt_add_resources(client_UI_SRCS ../../resources.qrc ${CMAKE_SOURCE_DIR}/theme.qrc) +list(APPEND client_UI_SRCS + wizard/dataprotectionpage.ui + wizard/dataprotectionsettingspage.ui +) + +qt_add_resources(client_UI_SRCS ../../resources.qrc ../../ionos.qrc ${CMAKE_SOURCE_DIR}/theme.qrc ${CMAKE_SOURCE_DIR}/ionos-theme.qrc) set(client_SRCS accountmanager.h @@ -266,6 +275,40 @@ if (NOT DISABLE_ACCOUNT_MIGRATION) ) endif() +list(APPEND client_SRCS + clickablelabel.h + buttonstyle.h + ionostheme.h + whitelabeltheme.h + stratotheme.h + basetheme.h + wizard/dataprotectionpage.h + wizard/dataprotectionpage.cpp + wizard/dataprotectionsettingspage.h + wizard/dataprotectionsettingspage.cpp + ga4/datacollectionwrapper.cpp + ga4/datacollectionwrapper.h + ga4/ganalytics_worker.cpp + ga4/ganalytics_worker.h + ga4/ganalytics.cpp + ga4/ganalytics.h + sessnackbar.h + sessnackbar.cpp + sesstyle.h + sesstyle.cpp + linkbutton.h + linkbutton.cpp + buttonstylestrategy.h + sesFileIconProvider.h + sesFileIconProvider.cpp + pushbuttonstylehelper.h + pushbuttonstylehelper.cpp + moreoptionsbuttonstylehelper.h + moreoptionsbuttonstylehelper.cpp + SesComponents/syncdirvalidation.h + SesComponents/syncdirvalidation.cpp +) + if (WITH_WEBENGINE) list(APPEND client_SRCS wizard/webviewpage.h @@ -544,6 +587,11 @@ if(KF6GuiAddons_FOUND) add_definitions(-DHAVE_KGUIADDONS) endif() +if(WIN32) + # DwmSetWindowAttribute(), used to give native window title bars a dark mode. + target_link_libraries(nextcloudCore PUBLIC dwmapi) +endif() + add_subdirectory(socketapi) # skip unity inclusion for files which cause problems with a CMake unity build @@ -700,7 +748,8 @@ endif() # if building a bundle or not and the install_qt4_executable needs to be called afterwards # # OSX: Run macdeployqt for src/gui and for src/cmd using the -executable option -if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY) +option(SKIP_MACDEPLOYQT "Skip macdeployqt post-build step for faster incremental build" OFF) +if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY AND NOT SKIP_MACDEPLOYQT) # macdeployqt (POST_BUILD) replaces the build-tree RPATH with @loader_path/../Frameworks. # Setting BUILD_WITH_INSTALL_RPATH prevents CMake from separately tracking the build-tree # RPATH and then trying to delete it at cmake --install time (which would fail because @@ -731,7 +780,7 @@ if(BUILD_OWNCLOUD_OSX_BUNDLE AND NOT BUILD_LIBRARIES_ONLY) "$/../.." -qmldir=${CMAKE_SOURCE_DIR}/src/gui -always-overwrite - -executable="$/${cmd_NAME}" + -executable=$/${cmd_NAME} -libpath=${MACDEPLOYQT_LIB_DIR} -libpath=${BIN_OUTPUT_DIRECTORY} ${NO_STRIP} diff --git a/src/gui/ConflictDelegate.qml b/src/gui/ConflictDelegate.qml index 09318d46cea0d..250f0b753b3ca 100644 --- a/src/gui/ConflictDelegate.qml +++ b/src/gui/ConflictDelegate.qml @@ -8,7 +8,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls import Style -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import "./tray" Item { diff --git a/src/gui/EmojiPicker.qml b/src/gui/EmojiPicker.qml index f49410d6ea399..b71f84578fea5 100644 --- a/src/gui/EmojiPicker.qml +++ b/src/gui/EmojiPicker.qml @@ -8,7 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style -import com.nextcloud.desktopclient 1.0 as NC +import com.strato.hidrivenext.desktopclient as NC import "./tray" ColumnLayout { diff --git a/src/gui/ResolveConflictsDialog.qml b/src/gui/ResolveConflictsDialog.qml index dd55598d86d30..604ed781d9bbe 100644 --- a/src/gui/ResolveConflictsDialog.qml +++ b/src/gui/ResolveConflictsDialog.qml @@ -10,7 +10,7 @@ import QtQuick.Layouts import QtQuick.Controls import QtQml.Models import Style -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import "./tray" ApplicationWindow { @@ -20,6 +20,10 @@ ApplicationWindow { flags: Qt.Window | Qt.Dialog visible: true + color: Style.sesBackgroundColor + palette.base: Style.sesBackgroundColor + palette.windowText: Style.sesTrayFontColor + palette.text: Style.sesTrayFontColor LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft LayoutMirroring.childrenInherit: true @@ -131,22 +135,37 @@ ApplicationWindow { delegate: ConflictDelegate { width: conflictListView.contentItem.width - height: 100 + height: 140 } } } } DialogButtonBox { + id: buttonBox Layout.fillWidth: true - Button { + background: Rectangle { + color: Style.sesBackgroundColor + } + + readonly property int pixelSize: Style.sesFontPixelSize + readonly property int fontWeight: Style.sesFontNormalWeight + + PrimaryPillButton { + font.pixelSize: pixelSize + font.weight: fontWeight text: qsTr("Resolve conflicts") - DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + + onClicked: buttonBox.onAccepted() } - Button { + + SecondaryPillButton { + font.pixelSize: pixelSize + font.weight: fontWeight text: qsTr("Cancel") - DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + + onClicked: buttonBox.onRejected() } onAccepted: function() { @@ -159,10 +178,4 @@ ApplicationWindow { } } } - - Rectangle { - color: palette.base - anchors.fill: parent - z: 1 - } } diff --git a/src/gui/SesComponents/SesErrorBox.qml b/src/gui/SesComponents/SesErrorBox.qml new file mode 100644 index 0000000000000..3c57f97a53076 --- /dev/null +++ b/src/gui/SesComponents/SesErrorBox.qml @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2021 by Felix Weilbach + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +import Style 1.0 +import "../tray/" + +Item { + id: errorBox + + property string text: "" + + implicitHeight: errorMessageLayout.implicitHeight + (2 * Style.standardSpacing) + + Rectangle { + anchors.fill: parent + // Same translucent-overlay technique as ErrorBox.qml / MainWindow.qml's + // trayWindowSyncWarning: tint with the already dark-mode-aware border color + // at low alpha instead of a separate, fixed pastel fill color. + color: Style.tintedFill(Style.sesErrorBoxBorder, 0.2) + border.color: Style.sesErrorBoxBorder + border.width: Style.thickBorderWidth + radius: Style.sesCornerRadius + } + + GridLayout { + id: errorMessageLayout + + anchors.fill: parent + anchors.margins: Style.standardSpacing + anchors.leftMargin: Style.standardSpacing + + columns: 2 + + Image { + source: Style.sesErrorBoxIcon + width: 24 + height: 24 + Layout.rightMargin: Style.standardSpacing + } + + EnforcedPlainTextLabel { + Layout.fillWidth: true + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontBoldWeight + + text: qsTr("Error") + color: Style.sesErrorBoxText + } + + EnforcedPlainTextLabel { + id: errorMessage + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.columnSpan: 2 + + wrapMode: Text.WordWrap + text: errorBox.text + + font.pixelSize: Style.sesFontErrortextPixelSize + font.weight: Style.sesFontNormalWeight + } + } +} diff --git a/src/gui/SesComponents/SesTrayHeader.qml b/src/gui/SesComponents/SesTrayHeader.qml new file mode 100644 index 0000000000000..9155bdc70be4f --- /dev/null +++ b/src/gui/SesComponents/SesTrayHeader.qml @@ -0,0 +1,85 @@ +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import Qt.labs.platform 1.1 as NativeDialogs + +import "../" +import "../filedetails/" +import "../tray/" + +// Custom qml modules are in /theme (and included by resources.qrc) +import Style 1.0 + +import com.strato.hidrivenext.desktopclient + + +Rectangle { + id: root + + signal featuredAppButtonClicked + + height: Style.sesTrayHeaderHeight + Style.sesHeaderTopMargin * 2 + color: Style.sesBackgroundColor + radius: 0.0 + clip: true + + RowLayout { + id: trayWindowHeaderLayout + + anchors.fill: parent + anchors.leftMargin: Style.sesTrayHeaderMargin + anchors.rightMargin: Style.sesTrayHeaderMargin + anchors.topMargin: Style.sesHeaderTopMargin + anchors.bottomMargin: Style.sesHeaderTopMargin + + TrayWindowAccountMenu{ + id: currentAccountHeaderButton + Layout.preferredWidth: Style.sesAccountButtonWidth + Layout.preferredHeight: Style.sesAccountButtonHeight + } + + HeaderButton { + id: trayWindowWebsiteButton + + icon.source: Style.sesWebsiteIcon + icon.color: Style.sesIconColor + onClicked: UserModel.openCurrentAccountServer() + + text: qsTr("Website") + + Layout.rightMargin: 2 + + Accessible.role: Accessible.Button + Accessible.name: qsTr("Open Nextcloud in browser") + Accessible.onPressAction: trayWindowWebsiteButton.clicked() + } + + TrayFoldersMenuButton { + id: openLocalFolderButton + + visible: currentUser.hasLocalFolder + currentUser: UserModel.currentUser + + onClicked: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder() + + onFolderEntryTriggered: isGroupFolder ? UserModel.openCurrentAccountFolderFromTrayInfo(fullFolderPath) : UserModel.openCurrentAccountLocalFolder() + + Accessible.role: Accessible.Graphic + Accessible.name: qsTr("Open local or team folders") + Accessible.onPressAction: openLocalFolderButton.userHasGroupFolders ? openLocalFolderButton.toggleMenuOpen() : UserModel.openCurrentAccountLocalFolder() + } + + HeaderButton { + id: trayWindowFeaturedAppButton + + visible: UserModel.currentUser.isAssistantEnabled + icon.source: UserModel.currentUser.featuredAppIcon + "/" + palette.windowText + onClicked: root.featuredAppButtonClicked() + + Accessible.role: Accessible.Button + Accessible.name: UserModel.currentUser.featuredAppAccessibleName + Accessible.onPressAction: trayWindowFeaturedAppButton.clicked() + } + } +} // Rectangle trayWindowHeaderBackground diff --git a/src/gui/SesComponents/syncdirvalidation.cpp b/src/gui/SesComponents/syncdirvalidation.cpp new file mode 100644 index 0000000000000..3357866fb897f --- /dev/null +++ b/src/gui/SesComponents/syncdirvalidation.cpp @@ -0,0 +1,53 @@ +#include "syncdirvalidation.h" +#include +#include +#include "logger.h" + +#ifdef Q_OS_WIN +bool SyncDirValidator::isValidDir() { + QString appDataPath = SyncDirValidator::appDataPath().replace("/", QDir::separator()); + QStringList pathComponents = _path.replace("/", QDir::separator()).split(QDir::separator(), Qt::SkipEmptyParts); + QStringList appDataPathComponents = appDataPath.split(QDir::separator(), Qt::SkipEmptyParts); + /* + If path is shorter than appDataPath and one path component is different, then path cannot be a real subset and is sowith valid + If appDataPath is shorter than path, we need to check, if the last appDataPath component is different from the related path component, then path is valid. + Otherwise path is a subpath from appDataPath and invalid + */ + for(int i = 0; i < qMin(pathComponents.size(), appDataPathComponents.size()); i++) { + if(pathComponents[i] != appDataPathComponents[i]) { + return true; + } + } + return false; + +} + +QString SyncDirValidator::message() { + return QObject::tr("The directory %1 cannot be part of your sync directory. Please choose another folder.").arg(_path.replace("/", QDir::separator())); +} + +QString SyncDirValidator::appDataPath() { + //Path: AppData/Roaming/ + QString appDataRoamingApplicationNamePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir appDataRoamingApplicationNameDir(appDataRoamingApplicationNamePath); + appDataRoamingApplicationNameDir.cdUp(); + appDataRoamingApplicationNameDir.cdUp(); + QString appDataPath = appDataRoamingApplicationNameDir.absolutePath(); + return appDataPath; +} + +#else + +bool SyncDirValidator::isValidDir() { + return true; +} + +QString SyncDirValidator::message() { + return ""; +} + +QString SyncDirValidator::appDataPath() { + return ""; +} + +#endif \ No newline at end of file diff --git a/src/gui/SesComponents/syncdirvalidation.h b/src/gui/SesComponents/syncdirvalidation.h new file mode 100644 index 0000000000000..a91812da85775 --- /dev/null +++ b/src/gui/SesComponents/syncdirvalidation.h @@ -0,0 +1,19 @@ +#ifndef SYNCDIRVALIDATION_H +#define SYNCDIRVALIDATION_H + +#include + +class SyncDirValidator { +public: + SyncDirValidator(const QString &path) : _path(path) {} + + bool isValidDir(); + QString message(); + +private: + QString appDataPath(); + QString _path; +}; + + +#endif // SYNCDIRVALIDATION_H \ No newline at end of file diff --git a/src/gui/UserStatusMessageView.qml b/src/gui/UserStatusMessageView.qml index 965f2fe0e31b3..4379ec00637d7 100644 --- a/src/gui/UserStatusMessageView.qml +++ b/src/gui/UserStatusMessageView.qml @@ -9,7 +9,7 @@ import QtQuick.Layouts import QtQuick.Controls import QtQuick.Window -import com.nextcloud.desktopclient as NC +import com.strato.hidrivenext.desktopclient as NC import Style import "./tray" @@ -185,6 +185,14 @@ ColumnLayout { userStatusSelectorModel.setClearAt(currentValue) } } + + indicator: Image { + x: clearComboBox.width - width - clearComboBox.rightPadding + y: clearComboBox.topPadding + (clearComboBox.availableHeight - height) / 2 + source: "image://svgimage-custom-color/caret-down.svg/" + Style.sesTrayFontColor + sourceSize.width: 14 + sourceSize.height: 14 + } } } } diff --git a/src/gui/UserStatusSelectorPage.qml b/src/gui/UserStatusSelectorPage.qml index 0f297f9796ea0..a03ef5483c3fe 100644 --- a/src/gui/UserStatusSelectorPage.qml +++ b/src/gui/UserStatusSelectorPage.qml @@ -8,7 +8,7 @@ import QtQuick.Controls import QtQuick.Layouts import Style -import com.nextcloud.desktopclient as NC +import com.strato.hidrivenext.desktopclient as NC Page { id: page diff --git a/src/gui/UserStatusSetStatusView.qml b/src/gui/UserStatusSetStatusView.qml index abff60d1e16a5..cc325498dda32 100644 --- a/src/gui/UserStatusSetStatusView.qml +++ b/src/gui/UserStatusSetStatusView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient as NC +import com.strato.hidrivenext.desktopclient as NC import Style import "./tray" diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index a4ae1f0775dc7..b4e9c60baf954 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -4,59 +4,60 @@ * SPDX-License-Identifier: GPL-2.0-or-later */ - #include "accountsettings.h" #include "common/syncjournaldb.h" #include "common/syncjournalfilerecord.h" #include "qmessagebox.h" #include "ui_accountsettings.h" -#include "theme.h" -#include "foldercreationdialog.h" -#include "folderman.h" -#include "folderwizard.h" -#include "folderstatusmodel.h" -#include "folderstatusdelegate.h" -#include "common/utility.h" -#include "guiutility.h" -#include "application.h" -#include "configfile.h" #include "account.h" -#include "accountstate.h" -#include "userinfo.h" #include "accountmanager.h" -#include "owncloudsetupwizard.h" +#include "accountstate.h" +#include "application.h" +#include "buttonstyle.h" +#include "common/utility.h" +#include "configfile.h" #include "creds/abstractcredentials.h" #include "creds/httpcredentialsgui.h" -#include "tooltipupdater.h" -#include "filesystem.h" #include "encryptfolderjob.h" -#include "syncresult.h" +#include "filesystem.h" +#include "foldercreationdialog.h" +#include "folderman.h" +#include "folderstatusdelegate.h" +#include "folderstatusmodel.h" +#include "folderwizard.h" +#include "guiutility.h" #include "ignorelisttablewidget.h" -#include "wizard/owncloudwizard.h" #include "networksettings.h" +#include "owncloudsetupwizard.h" +#include "syncresult.h" +#include "theme.h" +#include "tooltipupdater.h" #include "ui_mnemonicdialog.h" +#include "userinfo.h" +#include "whitelabeltheme.h" +#include "wizard/owncloudwizard.h" #include +#include +#include #include #include #include +#include +#include +#include +#include #include #include -#include -#include +#include #include -#include +#include +#include #include -#include -#include +#include #include -#include -#include -#include -#include -#include using namespace Qt::StringLiterals; @@ -65,14 +66,12 @@ using namespace Qt::StringLiterals; #endif #ifdef Q_OS_MACOS -#include "common/utility_mac_sandbox.h" -#include "common/macsandboxsecurityscopedaccess.h" #include "common/macsandboxpersistentaccess.h" +#include "common/macsandboxsecurityscopedaccess.h" +#include "common/utility_mac_sandbox.h" #endif - -#include "account.h" - -namespace { +namespace +{ constexpr auto propertyFolder = "folder"; constexpr auto propertyPath = "path"; constexpr auto e2eUiActionIdKey = "id"; @@ -82,7 +81,8 @@ constexpr auto e2EeUiActionDisplayMnemonicId = "display_mnemonic"; constexpr auto e2EeUiActionMigrateCertificateId = "migrate_certificate"; } -namespace OCC { +namespace OCC +{ class AccountSettings; @@ -93,11 +93,12 @@ void showEnableE2eeWithVirtualFilesWarningDialog(std::function onAcc const auto messageBox = new QMessageBox; messageBox->setAttribute(Qt::WA_DeleteOnClose); messageBox->setText(AccountSettings::tr("End-to-end Encryption with Virtual Files")); - messageBox->setInformativeText(AccountSettings::tr("You seem to have the Virtual Files feature enabled on this folder. " - "At the moment, it is not possible to implicitly download virtual files that are " - "end-to-end encrypted. To get the best experience with virtual files and " - "end-to-end encryption, make sure the encrypted folder is marked with " - "\"Make always available locally\".")); + messageBox->setInformativeText( + AccountSettings::tr("You seem to have the Virtual Files feature enabled on this folder. " + "At the moment, it is not possible to implicitly download virtual files that are " + "end-to-end encrypted. To get the best experience with virtual files and " + "end-to-end encryption, make sure the encrypted folder is marked with " + "\"Make always available locally\".")); messageBox->setIcon(QMessageBox::Warning); const auto dontEncryptButton = messageBox->addButton(QMessageBox::StandardButton::Cancel); Q_ASSERT(dontEncryptButton); @@ -157,9 +158,9 @@ class MouseCursorChanger : public QObject Qt::CursorShape shape = Qt::ArrowCursor; const auto pos = folderList->mapFromGlobal(QCursor::pos()); const auto index = folderList->indexAt(pos); - if (model->classify(index) == FolderStatusModel::RootFolder && - (FolderStatusDelegate::errorsListRect(folderList->visualRect(index)).contains(pos) || - FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index),folderList->layoutDirection()).contains(pos))) { + if (model->classify(index) == FolderStatusModel::RootFolder + && (FolderStatusDelegate::errorsListRect(folderList->visualRect(index)).contains(pos) + || FolderStatusDelegate::optionsButtonRect(folderList->visualRect(index), folderList->layoutDirection()).contains(pos))) { shape = Qt::PointingHandCursor; } folderList->setCursor(shape); @@ -213,8 +214,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) const auto fpSettingsLayout = new QVBoxLayout(fileProviderPanelContents); const auto fpAccountUserIdAtHost = _accountState->account()->userIdAtHostWithPort(); const auto fpSettingsController = Mac::FileProviderSettingsController::instance(); - const auto fpSettingsWidget = fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderPanelContents, - QQuickWidget::SizeRootObjectToView); + const auto fpSettingsWidget = + fpSettingsController->settingsViewWidget(fpAccountUserIdAtHost, fileProviderPanelContents, QQuickWidget::SizeRootObjectToView); fpSettingsLayout->setContentsMargins(0, 0, 0, 0); fpSettingsLayout->setSpacing(0); @@ -243,7 +244,8 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connectionSettingsLayout->setSpacing(0); connectionSettingsLayout->addWidget(networkSettings, 1); connectionSettingsPanelContents->setLayout(connectionSettingsLayout); - + _ui->connectionSettingsPanel->setVisible(false); + const auto mouseCursorChanger = new MouseCursorChanger(this); mouseCursorChanger->folderList = _ui->_folderList; mouseCursorChanger->model = _model; @@ -251,27 +253,19 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) _ui->_folderList->setAttribute(Qt::WA_Hover, true); _ui->_folderList->installEventFilter(mouseCursorChanger); - connect(this, &AccountSettings::removeAccountFolders, - AccountManager::instance(), &AccountManager::removeAccountFolders); - connect(_ui->_folderList, &QWidget::customContextMenuRequested, - this, &AccountSettings::slotCustomContextMenuRequested); - connect(_ui->_folderList, &QAbstractItemView::clicked, - this, &AccountSettings::slotFolderListClicked); + connect(this, &AccountSettings::removeAccountFolders, AccountManager::instance(), &AccountManager::removeAccountFolders); + connect(_ui->_folderList, &QWidget::customContextMenuRequested, this, &AccountSettings::slotCustomContextMenuRequested); + connect(_ui->_folderList, &QAbstractItemView::clicked, this, &AccountSettings::slotFolderListClicked); connect(_ui->_folderList, &QTreeView::expanded, this, &AccountSettings::refreshSelectiveSyncStatus); connect(_ui->_folderList, &QTreeView::collapsed, this, &AccountSettings::refreshSelectiveSyncStatus); - connect(_ui->selectiveSyncNotification, &QLabel::linkActivated, - this, &AccountSettings::slotLinkActivated); + connect(_ui->selectiveSyncNotification, &QLabel::linkActivated, this, &AccountSettings::slotLinkActivated); connect(_model, &FolderStatusModel::suggestExpand, _ui->_folderList, &QTreeView::expand); connect(_model, &FolderStatusModel::dirtyChanged, this, &AccountSettings::refreshSelectiveSyncStatus); refreshSelectiveSyncStatus(); - connect(_model, &QAbstractItemModel::rowsInserted, - this, &AccountSettings::refreshSelectiveSyncStatus); - connect(_model, &QAbstractItemModel::rowsInserted, - _ui->_folderList, &QWidget::updateGeometry); - connect(_model, &QAbstractItemModel::rowsRemoved, - _ui->_folderList, &QWidget::updateGeometry); - connect(_model, &QAbstractItemModel::modelReset, - _ui->_folderList, &QWidget::updateGeometry); + connect(_model, &QAbstractItemModel::rowsInserted, this, &AccountSettings::refreshSelectiveSyncStatus); + connect(_model, &QAbstractItemModel::rowsInserted, _ui->_folderList, &QWidget::updateGeometry); + connect(_model, &QAbstractItemModel::rowsRemoved, _ui->_folderList, &QWidget::updateGeometry); + connect(_model, &QAbstractItemModel::modelReset, _ui->_folderList, &QWidget::updateGeometry); auto *syncNowAction = new QAction(this); syncNowAction->setShortcut(QKeySequence(Qt::Key_F6)); @@ -283,7 +277,6 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connect(syncNowWithRemoteDiscovery, &QAction::triggered, this, &AccountSettings::slotScheduleCurrentFolderForceRemoteDiscovery); addAction(syncNowWithRemoteDiscovery); - slotHideSelectiveSyncWidget(); _ui->bigFolderUi->setVisible(false); connect(_model, &QAbstractItemModel::dataChanged, this, &AccountSettings::slotSelectiveSyncChanged); @@ -316,19 +309,23 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) connect(_accountState, &AccountState::stateChanged, this, &AccountSettings::slotAccountStateChanged); slotAccountStateChanged(); - connect(&_userInfo, &UserInfo::quotaUpdated, - this, &AccountSettings::slotUpdateQuota); + connect(&_userInfo, &UserInfo::quotaUpdated, this, &AccountSettings::slotUpdateQuota); customizeStyle(); - connect(_accountState->account()->e2e(), &ClientSideEncryption::startingDiscoveryEncryptionUsbToken, - Systray::instance(), &Systray::createEncryptionTokenDiscoveryDialog); - connect(_accountState->account()->e2e(), &ClientSideEncryption::finishedDiscoveryEncryptionUsbToken, - Systray::instance(), &Systray::destroyEncryptionTokenDiscoveryDialog); + connect(_accountState->account()->e2e(), + &ClientSideEncryption::startingDiscoveryEncryptionUsbToken, + Systray::instance(), + &Systray::createEncryptionTokenDiscoveryDialog); + connect(_accountState->account()->e2e(), + &ClientSideEncryption::finishedDiscoveryEncryptionUsbToken, + Systray::instance(), + &Systray::destroyEncryptionTokenDiscoveryDialog); } void AccountSettings::slotE2eEncryptionMnemonicReady() { + return; // E2E encryption message disabled const auto actionDisableEncryption = addActionToEncryptionMessage(tr("Forget encryption setup"), e2EeUiActionForgetEncryptionId); connect(actionDisableEncryption, &QAction::triggered, this, [this] { forgetEncryptionOnDeviceForAccount(_accountState->account()); @@ -375,7 +372,7 @@ void AccountSettings::slotE2eEncryptionInitializationFinished(bool isNewMnemonic void AccountSettings::slotEncryptFolderFinished(int status) { qCInfo(lcAccountSettings) << "Current folder encryption status code:" << status; - auto job = qobject_cast(sender()); + auto job = qobject_cast(sender()); Q_ASSERT(job); if (!job->errorString().isEmpty()) { QMessageBox::warning(nullptr, tr("Warning"), job->errorString()); @@ -439,8 +436,9 @@ bool AccountSettings::canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo if (!_accountState->account()->e2e() || !_accountState->account()->e2e()->isInitialized()) { QMessageBox msgBox; - msgBox.setText(tr("You cannot encrypt this folder because the end-to-end encryption is not set-up yet on this device.\n" - "Would you like to do this now?")); + msgBox.setText( + tr("You cannot encrypt this folder because the end-to-end encryption is not set-up yet on this device.\n" + "Would you like to do this now?")); msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Ok); const auto ret = msgBox.exec(); @@ -459,19 +457,20 @@ bool AccountSettings::canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo // for some reason the actual folder in disk is info->_folder->path + info->_path. QDir folderPath(info->_folder->path() + info->_path); - folderPath.setFilter( QDir::AllEntries | QDir::NoDotAndDotDot ); + folderPath.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); if (folderPath.count() != 0) { QMessageBox msgBox; - msgBox.setText(tr("You cannot encrypt a folder with contents, please remove the files.\n" - "Wait for the new sync, then encrypt it.")); + msgBox.setText( + tr("You cannot encrypt a folder with contents, please remove the files.\n" + "Wait for the new sync, then encrypt it.")); msgBox.exec(); return false; } return true; } -void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInfo* folderInfo) +void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInfo *folderInfo) { if (!canEncryptOrDecrypt(folderInfo)) { return; @@ -577,23 +576,25 @@ void AccountSettings::slotEditCurrentLocalIgnoredFiles() openIgnoredFilesDialog(fileName); } -void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) +void AccountSettings::openIgnoredFilesDialog(const QString &absFolderPath) { Q_ASSERT(QFileInfo(absFolderPath).isAbsolute()); const QString ignoreFile{absFolderPath + ".sync-exclude.lst"}; const auto layout = new QVBoxLayout(); const auto ignoreListWidget = new IgnoreListTableWidget(this); + ignoreListWidget->setFont(WLTheme.settingsFont()); ignoreListWidget->readIgnoreFile(ignoreFile); layout->addWidget(ignoreListWidget); const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + customizeButtonBox(buttonBox); layout->addWidget(buttonBox); const auto dialog = new QDialog(); dialog->setLayout(layout); - connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { + connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) { if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) { ignoreListWidget->slotWriteIgnoreFile(ignoreFile); } @@ -601,10 +602,30 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) }); connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::close); + dialog->setPalette(QPalette(QPalette::Window, WLTheme.dialogBackgroundColor())); + dialog->setWindowFlag(Qt::WindowContextHelpButtonHint, false); + dialog->open(); } -void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) +void AccountSettings::customizeButtonBox(QDialogButtonBox *buttonBox) +{ + buttonBox->layout()->setSpacing(16); + buttonBox->setContentsMargins(0, 0, 11, 10); + + const auto okButton = buttonBox->button(QDialogButtonBox::Ok); + + okButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + okButton->setMinimumSize(80, 40); + + buttonBox->button(QDialogButtonBox::Cancel)->setMinimumSize(80, 40); + +#if defined(Q_OS_MAC) + buttonBox->layout()->setSpacing(32); +#endif +} + +void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex &index, const QPoint &pos) { Q_UNUSED(pos); @@ -629,7 +650,9 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index if (!isEncrypted && !isParentEncrypted && !isExternal && isTopFolder) { ac = menu.addAction(tr("Encrypt")); - connect(ac, &QAction::triggered, [this, info] { slotMarkSubfolderEncrypted(info); }); + connect(ac, &QAction::triggered, [this, info] { + slotMarkSubfolderEncrypted(info); + }); } else { // Ignore decrypting for now since it only works with an empty folder // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); }); @@ -662,15 +685,66 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index const auto path = rec.isValid() ? rec._path : remotePath; ac = availabilityMenu->addAction(Utility::vfsPinActionText()); - connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::AlwaysLocal); }); + connect(ac, &QAction::triggered, this, [this, folder, path] { + slotSetSubFolderAvailability(folder, path, PinState::AlwaysLocal); + }); ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText()); - connect(ac, &QAction::triggered, this, [this, folder, path] { slotSetSubFolderAvailability(folder, path, PinState::OnlineOnly); }); + connect(ac, &QAction::triggered, this, [this, folder, path] { + slotSetSubFolderAvailability(folder, path, PinState::OnlineOnly); + }); + + styleCustomContextMenu(availabilityMenu); } + styleCustomContextMenu(&menu); + menu.exec(QCursor::pos()); } +void AccountSettings::styleCustomContextMenu(QMenu *menu) const +{ + menu->setWindowFlags(menu->windowFlags() | Qt::FramelessWindowHint | Qt::NoDropShadowWindowHint); + + menu->setAttribute(Qt::WA_TranslucentBackground); + + menu->setStyleSheet(QStringLiteral("QMenu {" + "background-color: %1; " + "border: 1px solid %2; " + "padding: 15px; " + "border-radius: %7; " + "font-family: %8; " + "font-size: %9; " + "font-weight: %10; " + "}" + "QMenu::item {" + "background-color: transparent;" + "padding: 16px 18px; " + "color: %3; " + "border-radius: 8px; " + "}" + "QMenu::item:selected {" + "background-color: %5; " + "color: %3; " + "border-radius: 8px; " + "}" + "QMenu::item:pressed {" + "background-color: %6; " + "color: %4; " + "border-radius: 8px; " + "}") + .arg(WLTheme.trayBackgroundColor(), + WLTheme.menuBorderColor(), + WLTheme.menuTextColor(), + WLTheme.menuPressedTextColor(), + WLTheme.menuSelectedItemColor(), + WLTheme.menuPressedItemColor(), + WLTheme.menuBorderRadius(), + WLTheme.contextMenuFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight())); +} + void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) { const auto treeView = _ui->_folderList; @@ -701,6 +775,12 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) const auto menu = new QMenu(treeView); + connect(menu, &QMenu::aboutToHide, [treeView, index]() { + auto *delegate = qobject_cast(treeView->itemDelegate(index)); + delegate->MousePos = QPoint(-1, -1); + treeView->update(); + }); + menu->setAttribute(Qt::WA_DeleteOnClose); auto ac = menu->addAction(tr("Open folder")); @@ -738,23 +818,25 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) auto availabilityMenu = menu->addMenu(tr("Availability")); ac = availabilityMenu->addAction(Utility::vfsPinActionText()); - connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::AlwaysLocal); }); + connect(ac, &QAction::triggered, this, [this]() { + slotSetCurrentFolderAvailability(PinState::AlwaysLocal); + }); ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder()); ac = availabilityMenu->addAction(Utility::vfsFreeSpaceActionText()); - connect(ac, &QAction::triggered, this, [this]() { slotSetCurrentFolderAvailability(PinState::OnlineOnly); }); + connect(ac, &QAction::triggered, this, [this]() { + slotSetCurrentFolderAvailability(PinState::OnlineOnly); + }); ac = menu->addAction(tr("Disable virtual file support …")); connect(ac, &QAction::triggered, this, &AccountSettings::slotDisableVfsCurrentFolder); ac->setDisabled(Theme::instance()->enforceVirtualFilesSyncFolder()); + + styleCustomContextMenu(availabilityMenu); } - if (const auto mode = bestAvailableVfsMode(); - !Theme::instance()->disableVirtualFilesSyncFolder() && - Theme::instance()->showVirtualFilesOption() && - !folder->virtualFilesEnabled() && - mode != Vfs::Off && - Vfs::checkAvailability(folder->path(), mode)) { + if (const auto mode = bestAvailableVfsMode(); !Theme::instance()->disableVirtualFilesSyncFolder() && Theme::instance()->showVirtualFilesOption() + && !folder->virtualFilesEnabled() && mode != Vfs::Off && Vfs::checkAvailability(folder->path(), mode)) { if (mode == Vfs::WindowsCfApi || ConfigFile().showExperimentalOptions()) { ac = menu->addAction(tr("Enable virtual file support %1 …").arg(mode == Vfs::WindowsCfApi ? QString() : tr("(experimental)"))); // TODO: remove when UX decision is made @@ -764,6 +846,7 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) } } + styleCustomContextMenu(menu); menu->popup(treeView->mapToGlobal(pos)); } @@ -772,24 +855,11 @@ void AccountSettings::slotFolderListClicked(const QModelIndex &indx) { if (indx.data(FolderStatusDelegate::AddButton).toBool()) { // "Add Folder Sync Connection" - const auto treeView = _ui->_folderList; - const auto pos = treeView->mapFromGlobal(QCursor::pos()); - QStyleOptionViewItem opt; - opt.initFrom(treeView); - const auto btnRect = treeView->visualRect(indx); - const auto btnSize = treeView->itemDelegateForIndex(indx)->sizeHint(opt, indx); - const auto actual = QStyle::visualRect(opt.direction, btnRect, QRect(btnRect.topLeft(), btnSize)); - if (!actual.contains(pos)) { - return; - } if (indx.flags() & Qt::ItemIsEnabled) { slotAddFolder(); } else { - QToolTip::showText( - QCursor::pos(), - _model->data(indx, Qt::ToolTipRole).toString(), - this); + QToolTip::showText(QCursor::pos(), _model->data(indx, Qt::ToolTipRole).toString(), this); } return; } @@ -835,7 +905,6 @@ void AccountSettings::slotAddFolder() folderWizard->open(); } - void AccountSettings::slotFolderWizardAccepted() { const auto folderWizard = qobject_cast(sender()); @@ -844,10 +913,8 @@ void AccountSettings::slotFolderWizardAccepted() qCInfo(lcAccountSettings) << "Folder wizard completed"; FolderDefinition definition; - definition.localPath = FolderDefinition::prepareLocalPath( - folderWizard->field(QLatin1String("sourceFolder")).toString()); - definition.targetPath = FolderDefinition::prepareTargetPath( - folderWizard->property("targetPath").toString()); + definition.localPath = FolderDefinition::prepareLocalPath(folderWizard->field(QLatin1String("sourceFolder")).toString()); + definition.targetPath = FolderDefinition::prepareTargetPath(folderWizard->property("targetPath").toString()); if (folderWizard->property("useVirtualFiles").toBool()) { definition.virtualFilesMode = bestAvailableVfsMode(); @@ -858,9 +925,10 @@ void AccountSettings::slotFolderWizardAccepted() if (!dir.exists()) { qCInfo(lcAccountSettings) << "Creating folder" << definition.localPath; if (!dir.mkpath(".")) { - QMessageBox::warning(this, tr("Folder creation failed"), - tr("

Could not create local folder %1.

") - .arg(Utility::escape(QDir::toNativeSeparators(definition.localPath)))); + QMessageBox::warning( + this, + tr("Folder creation failed"), + tr("

Could not create local folder %1.

").arg(Utility::escape(QDir::toNativeSeparators(definition.localPath)))); return; } } @@ -893,8 +961,7 @@ void AccountSettings::slotFolderWizardAccepted() folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, selectiveSyncBlackList); // The user already accepted the selective sync dialog. everything is in the white list - folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, - QStringList() << QLatin1String("/")); + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList() << QLatin1String("/")); folderMan->scheduleAllFolders(); emit folderChanged(); } @@ -918,16 +985,23 @@ void AccountSettings::slotRemoveCurrentFolder() const auto shortGuiLocalPath = folder->shortGuiLocalPath(); auto messageBox = new QMessageBox(QMessageBox::Question, - tr("Confirm Folder Sync Connection Removal"), - tr("

Do you really want to stop syncing the folder %1?

" - "

Note: This will not delete any files.

") - .arg(shortGuiLocalPath), - QMessageBox::NoButton, - this); + tr("Confirm Folder Sync Connection Removal"), + tr("

Do you really want to stop syncing the folder %1?

" + "

Note: This will not delete any files.

") + .arg(shortGuiLocalPath), + QMessageBox::NoButton, + this); + + messageBox->setStyleSheet( + QStringLiteral("QMessageBox QLabel { %1; } QDialog { background-color: %2; }") + .arg(WLTheme.fontConfigurationCss(WLTheme.settingsFont(), WLTheme.settingsTextSize(), WLTheme.settingsTextWeight(), WLTheme.titleColor()), + WLTheme.dialogBackgroundColor())); + messageBox->setAttribute(Qt::WA_DeleteOnClose); const auto yesButton = messageBox->addButton(tr("Remove Folder Sync Connection"), QMessageBox::YesRole); + yesButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); messageBox->addButton(tr("Cancel"), QMessageBox::NoRole); - connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this]{ + connect(messageBox, &QMessageBox::finished, this, [messageBox, yesButton, folder, row, this] { if (messageBox->clickedButton() == yesButton) { FolderMan::instance()->removeFolder(folder); _model->removeRow(row); @@ -958,11 +1032,8 @@ void AccountSettings::slotFixSandboxBookmark(Folder *folder) const auto expectedPath = FolderDefinition::prepareLocalPath(folder->path()); // Use URL-based variant to preserve security-scoped bookmark from NSOpenPanel - const auto selectedUrl = QFileDialog::getExistingDirectoryUrl( - this, - tr("Grant access to sync folder"), - QUrl::fromLocalFile(expectedPath), - QFileDialog::ShowDirsOnly); + const auto selectedUrl = + QFileDialog::getExistingDirectoryUrl(this, tr("Grant access to sync folder"), QUrl::fromLocalFile(expectedPath), QFileDialog::ShowDirsOnly); if (selectedUrl.isEmpty()) { return; @@ -971,37 +1042,28 @@ void AccountSettings::slotFixSandboxBookmark(Folder *folder) // Acquire temporary security-scoped access from the dialog-returned URL auto tempAccess = Utility::MacSandboxSecurityScopedAccess::create(selectedUrl); if (!tempAccess || !tempAccess->isValid()) { - QMessageBox::warning(this, - tr("Access Error"), - tr("Could not acquire access to the selected folder. Please try again.")); + QMessageBox::warning(this, tr("Access Error"), tr("Could not acquire access to the selected folder. Please try again.")); return; } // Validate that the selected path matches the folder's configured local path const auto selectedPath = FolderDefinition::prepareLocalPath(selectedUrl.toLocalFile()); if (selectedPath != expectedPath) { - QMessageBox::warning(this, - tr("Wrong Folder"), - tr("Please select the original sync folder: %1") - .arg(QDir::toNativeSeparators(expectedPath))); + QMessageBox::warning(this, tr("Wrong Folder"), tr("Please select the original sync folder: %1").arg(QDir::toNativeSeparators(expectedPath))); return; } // Create the persistent security-scoped bookmark const auto bookmarkData = Utility::createSecurityScopedBookmarkData(selectedPath); if (bookmarkData.isEmpty()) { - QMessageBox::warning(this, - tr("Bookmark Error"), - tr("Could not create a security bookmark for the folder. Please try again.")); + QMessageBox::warning(this, tr("Bookmark Error"), tr("Could not create a security bookmark for the folder. Please try again.")); return; } // Resolve the bookmark to get a persistent access handle auto persistentAccess = Utility::MacSandboxPersistentAccess::createFromBookmarkData(bookmarkData); if (!persistentAccess || !persistentAccess->isValid()) { - QMessageBox::warning(this, - tr("Bookmark Error"), - tr("Could not resolve the security bookmark. Please try again.")); + QMessageBox::warning(this, tr("Bookmark Error"), tr("Could not resolve the security bookmark. Please try again.")); return; } @@ -1100,21 +1162,28 @@ void AccountSettings::slotDisableVfsCurrentFolder() return; } - const auto msgBox = new QMessageBox( - QMessageBox::Question, - tr("Disable virtual file support?"), - tr("This action will disable virtual file support. As a consequence contents of folders that " - "are currently marked as \"available online only\" will be downloaded." - "\n\n" - "The only advantage of disabling virtual file support is that the selective sync feature " - "will become available again." - "\n\n" - "This action will abort any currently running synchronization.")); + const auto msgBox = new QMessageBox(QMessageBox::Question, + tr("Disable virtual file support?"), + tr("This action will disable virtual file support. As a consequence contents of folders that " + "are currently marked as \"available online only\" will be downloaded." + "\n\n" + "The only advantage of disabling virtual file support is that the selective sync feature " + "will become available again." + "\n\n" + "This action will abort any currently running synchronization.")); + + msgBox->setStyleSheet( + QStringLiteral("QMessageBox QLabel { %1 background-color: %2; }") + .arg(WLTheme.fontConfigurationCss(WLTheme.settingsFont(), WLTheme.settingsTextSize(), WLTheme.settingsTextWeight(), WLTheme.titleColor()), + WLTheme.dialogBackgroundColor())); + const auto acceptButton = msgBox->addButton(tr("Disable support"), QMessageBox::AcceptRole); + acceptButton->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + msgBox->addButton(tr("Cancel"), QMessageBox::RejectRole); connect(msgBox, &QMessageBox::finished, msgBox, [this, msgBox, folder, acceptButton] { msgBox->deleteLater(); - if (msgBox->clickedButton() != acceptButton|| !folder) { + if (msgBox->clickedButton() != acceptButton || !folder) { return; } @@ -1226,23 +1295,23 @@ void AccountSettings::forgetEncryptionOnDeviceForAccount(const AccountPtr &accou QMessageBox dialog; dialog.setWindowTitle(tr("Forget the end-to-end encryption on this device")); dialog.setText(tr("Do you want to forget the end-to-end encryption settings for %1 on this device?").arg(account->davUser())); - dialog.setInformativeText(tr("Forgetting end-to-end encryption will remove the sensitive data and all the encrypted files from this device." - "
" - "However, the encrypted files will remain on the server and all your other devices, if configured.")); + dialog.setInformativeText( + tr("Forgetting end-to-end encryption will remove the sensitive data and all the encrypted files from this device." + "
" + "However, the encrypted files will remain on the server and all your other devices, if configured.")); dialog.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); dialog.setDefaultButton(QMessageBox::Ok); dialog.adjustSize(); const auto ret = dialog.exec(); - switch(ret) { + switch (ret) { case QMessageBox::Ok: - connect(account->e2e(), &ClientSideEncryption::sensitiveDataForgotten, - this, &AccountSettings::forgetE2eEncryption); + connect(account->e2e(), &ClientSideEncryption::sensitiveDataForgotten, this, &AccountSettings::forgetE2eEncryption); account->e2e()->forgetSensitiveData(); break; case QMessageBox::Cancel: break; - Q_UNREACHABLE(); + Q_UNREACHABLE(); } } @@ -1260,25 +1329,56 @@ void AccountSettings::migrateCertificateForAccount(const AccountPtr &account) void AccountSettings::showConnectionLabel(const QString &message, QStringList errors) { - const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;" - "border-width: 1px; border-style: solid; border-color: #aaaaaa;" - "border-radius:5px;"); +#ifndef IONOS_BUILD + // SES-578: white-on-red is a fixed, self-contained contrast pair - deliberately not themed, + // matching the tray's own alert colors (e.g. Style.errorBoxBackgroundColor / infoBoxBackgroundColor + // in Style.qml), which stay constant across light/dark rather than getting a dark variant. + const auto errStyle = QLatin1String( + "color:#ffffff; background-color:#bb4d4d;padding:5px;" + "border-width: 1px; border-style: solid; border-color: #aaaaaa;" + "border-radius:5px;"); if (errors.isEmpty()) { auto msg = message; - Theme::replaceLinkColorStringBackgroundAware(msg); + Theme::replaceLinkColorString(msg, QColor(WLTheme.settingsLinkColor())); _ui->connectLabel->setText(msg); _ui->connectLabel->setToolTip({}); - _ui->connectLabel->setStyleSheet({}); + _ui->connectLabel->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); } else { errors.prepend(message); auto userFriendlyMsg = errors.join(QLatin1String("
")); qCDebug(lcAccountSettings) << userFriendlyMsg; - Theme::replaceLinkColorString(userFriendlyMsg, QColor(0xc1c8e6)); + Theme::replaceLinkColorString(userFriendlyMsg, QColor("#c1c8e6")); + _ui->connectLabel->setText(userFriendlyMsg); + _ui->connectLabel->setToolTip({}); + _ui->connectLabel->setStyleSheet(errStyle); + } + _ui->accountStatus->setVisible(false); +#else + + // SES-578: white-on-red is a fixed, self-contained contrast pair - deliberately not themed, + // matching the tray's own alert colors (e.g. Style.errorBoxBackgroundColor / infoBoxBackgroundColor + // in Style.qml), which stay constant across light/dark rather than getting a dark variant. + const auto errStyle = QLatin1String( + "color:#ffffff; background-color:#bb4d4d;padding:5px;" + "border-width: 1px; border-style: solid; border-color: #aaaaaa;" + "border-radius:5px;"); + if (errors.isEmpty()) { + auto msg = message; + Theme::replaceLinkColorString(msg, QColor(WLTheme.settingsLinkColor())); + _ui->connectLabel->setText(msg); + _ui->connectLabel->setToolTip({}); + _ui->connectLabel->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + } else { + errors.prepend(message); + auto userFriendlyMsg = errors.join(QLatin1String("
")); + qCDebug(lcAccountSettings) << userFriendlyMsg; + Theme::replaceLinkColorString(userFriendlyMsg, QColor("#c1c8e6")); _ui->connectLabel->setText(userFriendlyMsg); _ui->connectLabel->setToolTip({}); _ui->connectLabel->setStyleSheet(errStyle); } _ui->accountStatus->setVisible(!message.isEmpty()); +#endif } void AccountSettings::slotEnableCurrentFolder(bool terminate) @@ -1307,14 +1407,32 @@ void AccountSettings::slotEnableCurrentFolder(bool terminate) if (!currentlyPaused && !terminate) { // check if a sync is still running and if so, ask if we should terminate. if (folder->isBusy()) { // its still running - const auto msgbox = new QMessageBox(QMessageBox::Question, tr("Sync Running"), - tr("The syncing operation is running.
Do you want to terminate it?"), - QMessageBox::Yes | QMessageBox::No, this); + const auto msgbox = new QMessageBox(QMessageBox::Question, + tr("Sync Running"), + tr("The syncing operation is running.
Do you want to terminate it?"), + QMessageBox::Yes | QMessageBox::No, + this); msgbox->setAttribute(Qt::WA_DeleteOnClose); msgbox->setDefaultButton(QMessageBox::Yes); - connect(msgbox, &QMessageBox::accepted, this, [this]{ + msgbox->defaultButton()->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + + QHBoxLayout *buttonLayout = msgbox->findChild(); + buttonLayout->setSpacing(8); + +#ifdef Q_OS_MAC + buttonLayout->setSpacing(24); +#endif + + connect(msgbox, &QMessageBox::accepted, this, [this] { slotEnableCurrentFolder(true); }); + + msgbox->setStyleSheet(QStringLiteral("QMessageBox QLabel { %1; }") + .arg(WLTheme.fontConfigurationCss(WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor()))); + msgbox->open(); return; } @@ -1392,6 +1510,9 @@ void AccountSettings::slotAccountStateChanged() const auto state = _accountState ? _accountState->state() : AccountState::Disconnected; if (state != AccountState::Disconnected) { _ui->sslButton->updateAccountState(_accountState); + _ui->sslButton->setMenu(nullptr); + _ui->sslButton->setAttribute(Qt::WA_TransparentForMouseEvents, true); + _ui->sslButton->setFocusPolicy(Qt::NoFocus); const auto account = _accountState->account(); auto safeUrl = account->url(); safeUrl.setPassword({}); // Remove the password from the URL to avoid showing it in the UI @@ -1400,9 +1521,7 @@ void AccountSettings::slotAccountStateChanged() _model->slotUpdateFolderState(folder); } - const auto server = QString::fromLatin1("%2") - .arg(Utility::escape(account->url().toString()), - Utility::escape(safeUrl.toString())); + const auto server = QString::fromLatin1("%2").arg(Utility::escape(account->url().toString()), Utility::escape(safeUrl.toString())); auto serverWithUser = server; if (const auto cred = account->credentials()) { auto user = account->davDisplayName(); @@ -1442,14 +1561,11 @@ void AccountSettings::slotAccountStateChanged() break; } case AccountState::NetworkError: - showConnectionLabel(tr("Unable to connect to %1.") - .arg(Utility::escape(Theme::instance()->appNameGUI())), - _accountState->connectionErrors()); + showConnectionLabel(tr("Unable to connect to %1.").arg(Utility::escape(Theme::instance()->appNameGUI())), _accountState->connectionErrors()); break; case AccountState::ConfigurationError: - showConnectionLabel(tr("Server configuration error: %1 at %2.") - .arg(Utility::escape(Theme::instance()->appNameGUI()), server), - _accountState->connectionErrors()); + showConnectionLabel(tr("Server configuration error: %1 at %2.").arg(Utility::escape(Theme::instance()->appNameGUI()), server), + _accountState->connectionErrors()); break; case AccountState::Disconnected: // we can't end up here as the whole block is ifdeffed @@ -1461,8 +1577,7 @@ void AccountSettings::slotAccountStateChanged() } } else { // ownCloud is not yet configured. - showConnectionLabel(tr("No %1 connection configured.") - .arg(Utility::escape(Theme::instance()->appNameGUI()))); + showConnectionLabel(tr("No %1 connection configured.").arg(Utility::escape(Theme::instance()->appNameGUI()))); } /* Allow to expand the item if the account is connected. */ @@ -1491,12 +1606,14 @@ void AccountSettings::slotAccountStateChanged() void AccountSettings::checkClientSideEncryptionState() { + return; // E2E encryption message disabled + /* TODO: We should probably do something better here. * Verify if the user has a private key already uploaded to the server, * if it has, do not offer to create one. */ - qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName() - << "Client Side Encryption" << accountsState()->account()->capabilities().clientSideEncryptionAvailable(); + qCInfo(lcAccountSettings) << "Account" << accountsState()->account()->displayName() << "Client Side Encryption" + << accountsState()->account()->capabilities().clientSideEncryptionAvailable(); if (_accountState->account()->capabilities().clientSideEncryptionAvailable()) { _ui->encryptionMessage->show(); @@ -1552,9 +1669,16 @@ void AccountSettings::slotHideSelectiveSyncWidget() _ui->selectiveSyncLabel->hide(); } -void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, - const QModelIndex &bottomRight, - const QVector &roles) +void AccountSettings::slotExpandMemoryClicked() +{ +#ifdef STRATO_WL_BUILD + QDesktopServices::openUrl(QUrl(QCoreApplication::translate("OCC::Theme", "ExpandMemory-Link_STRATO"))); +#else + QDesktopServices::openUrl(QUrl(QCoreApplication::translate("OCC::Theme", "ExpandMemory-Link"))); +#endif +} + +void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { Q_UNUSED(bottomRight); if (!roles.contains(Qt::CheckStateRole)) { @@ -1582,6 +1706,7 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, } _ui->selectiveSyncApply->setEnabled(true); + _ui->selectiveSyncApply->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->selectiveSyncButtons->setVisible(true); if (shouldBeVisible != wasVisible) { @@ -1609,7 +1734,10 @@ void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() return; } - disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); + disconnect(_accountState->account()->e2e(), + &ClientSideEncryption::initializationFinished, + this, + &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); for (const auto folder : FolderMan::instance()->map()) { if (folder->accountState() != _accountState) { @@ -1646,7 +1774,9 @@ void AccountSettings::slotE2eEncryptionCertificateNeedMigration() }); } -void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const +void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, + OCC::Folder *folder, + const QStringList &foldersToRemoveFromBlacklist) const { folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); @@ -1663,14 +1793,15 @@ void AccountSettings::folderTerminateSyncAndUpdateBlackList(const QStringList &b return; } // in case sync is already running - terminate it and start a new one - const QMetaObject::Connection syncTerminatedConnection = connect(folder, &Folder::syncFinished, this, [this, blackList, folder, foldersToRemoveFromBlacklist]() { - const auto foundConnectionIt = _folderConnections.find(folder->alias()); - if (foundConnectionIt != _folderConnections.end()) { - disconnect(*foundConnectionIt); - _folderConnections.erase(foundConnectionIt); - } - updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); - }); + const QMetaObject::Connection syncTerminatedConnection = + connect(folder, &Folder::syncFinished, this, [this, blackList, folder, foldersToRemoveFromBlacklist]() { + const auto foundConnectionIt = _folderConnections.find(folder->alias()); + if (foundConnectionIt != _folderConnections.end()) { + disconnect(*foundConnectionIt); + _folderConnections.erase(foundConnectionIt); + } + updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); + }); _folderConnections.insert(folder->alias(), syncTerminatedConnection); folder->slotTerminateSync(); } @@ -1734,9 +1865,9 @@ void AccountSettings::refreshSelectiveSyncStatus() QString infoString; if (!unsyncedFoldersString.isEmpty()) { - infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big: ") - : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages: ") - : tr("There are folders that were not synchronized because they are too big or external storages: "); + infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big:") + " " + : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages:") + " " + : tr("There are folders that were not synchronized because they are too big or external storages:") + " "; infoString += unsyncedFoldersString; } @@ -1779,14 +1910,49 @@ void AccountSettings::slotStyleChanged() void AccountSettings::customizeStyle() { + // Unlike GeneralSettings, this page never painted its own background, so it stayed + // stuck on the ambient (unreliable on Windows 11 dark mode, see below) palette instead + // of the already-themed dialogBackgroundColor(). + setAutoFillBackground(true); + setPalette(QPalette(QPalette::Window, WLTheme.dialogBackgroundColor())); + auto msg = _ui->connectLabel->text(); - Theme::replaceLinkColorStringBackgroundAware(msg); + Theme::replaceLinkColorString(msg, QColor(WLTheme.settingsLinkColor())); _ui->connectLabel->setText(msg); + // Skip if an error is currently shown (fixed white-on-red errStyle from showConnectionLabel(), + // deliberately not theme-dependent) - don't clobber it with the normal-state text color. + if (!_ui->connectLabel->styleSheet().contains(QLatin1String("background-color"))) { + _ui->connectLabel->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + } + + _ui->syncFoldersPanelTitle->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + _ui->fileProviderPanelTitle->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + _ui->connectionSettingsPanelTitle->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + + _ui->encryptionMessageLabel->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + + _ui->selectiveSyncLabel->setStyleSheet(QStringLiteral("color: %1;").arg(WLTheme.titleColor())); + + const auto color = palette().highlight().color(); + const auto toolTipStyle = + QStringLiteral("QToolTip { color: %1; background-color: %2; border: 1px solid %1; }").arg(WLTheme.titleColor(), WLTheme.dialogBackgroundColor()); + + _ui->_folderList->setStyleSheet( + QStringLiteral("background: %1; %2;") + .arg(WLTheme.dialogBackgroundColor(), + WLTheme.fontConfigurationCss(WLTheme.settingsFont(), WLTheme.settingsTextSize(), WLTheme.settingsTextWeight(), WLTheme.titleColor()))); + +#if defined(Q_OS_MAC) + _ui->horizontalLayout->setSpacing(16); +#endif } void AccountSettings::setupE2eEncryption() { - connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); + connect(_accountState->account()->e2e(), + &ClientSideEncryption::initializationFinished, + this, + &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); if (_accountState->account()->e2e()->isInitialized()) { slotE2eEncryptionMnemonicReady(); @@ -1795,9 +1961,10 @@ void AccountSettings::setupE2eEncryption() connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, [this] { if (!_accountState->account()->e2e()->getPublicKey().isNull()) { - _ui->encryptionMessageLabel->setText(tr("End-to-end encryption has been initialized on this account with another device." - "
" - "Enter the unique mnemonic to have the encrypted folders synchronize on this device as well.")); + _ui->encryptionMessageLabel->setText( + tr("End-to-end encryption has been initialized on this account with another device." + "
" + "Enter the unique mnemonic to have the encrypted folders synchronize on this device as well.")); } }); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(false); @@ -1825,9 +1992,10 @@ void AccountSettings::forgetE2eEncryption() void AccountSettings::removeActionFromEncryptionMessage(const QString &actionId) { - const auto foundEnableEncryptionActionIt = std::find_if(std::cbegin(_ui->encryptionMessage->actions()), std::cend(_ui->encryptionMessage->actions()), [&actionId](const QAction *action) { - return action->property(e2eUiActionIdKey).toString() == actionId; - }); + const auto foundEnableEncryptionActionIt = + std::find_if(std::cbegin(_ui->encryptionMessage->actions()), std::cend(_ui->encryptionMessage->actions()), [&actionId](const QAction *action) { + return action->property(e2eUiActionIdKey).toString() == actionId; + }); if (foundEnableEncryptionActionIt != std::cend(_ui->encryptionMessage->actions())) { _ui->encryptionMessage->removeAction(*foundEnableEncryptionActionIt); (*foundEnableEncryptionActionIt)->deleteLater(); @@ -1856,7 +2024,8 @@ QAction *AccountSettings::addActionToEncryptionMessage(const QString &actionTitl void AccountSettings::setupE2eEncryptionMessage() { #ifdef BUILD_FILE_PROVIDER_MODULE - const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first.") + QStringLiteral(" ") + tr("The virtual files integration does not support end-to-end encryption yet."); + const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first.") + QStringLiteral(" ") + + tr("The virtual files integration does not support end-to-end encryption yet."); #else const auto encryptionMessage = tr("This account supports end-to-end encryption, but it needs to be set up first."); #endif diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 1b875845b4891..70e5c5c9cd4bf 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -12,6 +12,7 @@ #include #include #include +#include #include "folder.h" #include "userinfo.h" @@ -137,8 +138,10 @@ private slots: void forgetE2eEncryption(); void checkClientSideEncryptionState(); void removeActionFromEncryptionMessage(const QString &actionId); + void slotExpandMemoryClicked(); private: + void styleCustomContextMenu(QMenu *menu) const; bool event(QEvent *) override; QAction *addActionToEncryptionMessage(const QString &actionTitle, const QString &actionId); @@ -149,6 +152,8 @@ private slots: /// Returns the alias of the selected folder, empty string if none [[nodiscard]] QString selectedFolderAlias() const; + void customizeButtonBox(QDialogButtonBox *buttonBox); + Ui::AccountSettings *_ui; FolderStatusModel *_model; diff --git a/src/gui/addcertificatedialog.cpp b/src/gui/addcertificatedialog.cpp index 36bcd5364ba0b..3b9a21fac10c6 100644 --- a/src/gui/addcertificatedialog.cpp +++ b/src/gui/addcertificatedialog.cpp @@ -20,6 +20,7 @@ AddCertificateDialog::AddCertificateDialog(QWidget *parent) { ui->setupUi(this); ui->labelErrorCertif->setText(""); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); } AddCertificateDialog::~AddCertificateDialog() diff --git a/src/gui/application.cpp b/src/gui/application.cpp index 0a3fa090bb88b..96ea584dd4c7f 100644 --- a/src/gui/application.cpp +++ b/src/gui/application.cpp @@ -23,99 +23,149 @@ #include "socketapi/socketapi.h" #include "theme.h" +#include "ga4/datacollectionwrapper.h" + #if defined(BUILD_UPDATER) #include "updater/ocupdater.h" #endif +#include "common/vfs.h" +#include "csync_exclude.h" #include "owncloudsetupwizard.h" +#include "sesstyle.h" #include "version.h" -#include "csync_exclude.h" -#include "common/vfs.h" + +#include +#include #include "config.h" #if defined(Q_OS_WIN) -#include #include "shellextensionsserver.h" +#include +#include #elif defined(Q_OS_MACOS) #include "macOS/fileprovider.h" #endif +#include +#include +#include +#include +#include #include -#include #include #include -#include -#include +#include +#include +#include #include #include -#include -#include -#include -#include -#include #include class QSocket; -namespace OCC { +namespace OCC +{ Q_LOGGING_CATEGORY(lcApplication, "nextcloud.gui.application", QtInfoMsg) -namespace { - - static const char optionsC[] = - "Options:\n" - " --help, -h : show this help screen.\n" - " --version, -v : show version information.\n" - " -q --quit : quit the running instance\n" - " --logwindow, -l : open a window to show log output.\n" - " --logfile : write log output to file .\n" - " --logdir : write each sync log output in a new file\n" - " in folder .\n" - " --logexpire : removes logs older than hours.\n" - " (to be used with --logdir)\n" - " --logflush : flush the log file after every write.\n" - " --logdebug : also output debug-level messages in the log.\n" - " --confdir : Use the given configuration folder.\n" - " --background : launch the application in the background.\n" - " --overrideserverurl : specify a server URL to use for the force override to be used in the account setup wizard.\n" - " --overridelocaldir : specify a local dir to be used in the account setup wizard.\n" - " --userid : userId (username as on the server) to pass when creating an account via command-line.\n" - " --apppassword : appPassword to pass when creating an account via command-line.\n" - " --set-language : specify a language to use for the client, regardless of the OS language.\n" - " --localdirpath : (optional) path where to create a local sync folder when creating an account via command-line.\n" - " --isvfsenabled : whether to set a VFS or non-VFS folder (1 for 'yes' or 0 for 'no') when creating an account via command-line.\n" - " --remotedirpath : (optional) path to a remote subfolder when creating an account via command-line.\n" - " --serverurl : a server URL to use when creating an account via command-line.\n" +namespace +{ + +static const char optionsC[] = + "Options:\n" + " --help, -h : show this help screen.\n" + " --version, -v : show version information.\n" + " -q --quit : quit the running instance\n" + " --logwindow, -l : open a window to show log output.\n" + " --logfile : write log output to file .\n" + " --logdir : write each sync log output in a new file\n" + " in folder .\n" + " --logexpire : removes logs older than hours.\n" + " (to be used with --logdir)\n" + " --logflush : flush the log file after every write.\n" + " --logdebug : also output debug-level messages in the log.\n" + " --confdir : Use the given configuration folder.\n" + " --background : launch the application in the background.\n" + " --overrideserverurl : specify a server URL to use for the force override to be used in the account setup wizard.\n" + " --overridelocaldir : specify a local dir to be used in the account setup wizard.\n" + " --userid : userId (username as on the server) to pass when creating an account via command-line.\n" + " --apppassword : appPassword to pass when creating an account via command-line.\n" + " --set-language : specify a language to use for the client, regardless of the OS language.\n" + " --localdirpath : (optional) path where to create a local sync folder when creating an account via command-line.\n" + " --isvfsenabled : whether to set a VFS or non-VFS folder (1 for 'yes' or 0 for 'no') when creating an account via command-line.\n" + " --remotedirpath : (optional) path to a remote subfolder when creating an account via command-line.\n" + " --serverurl : a server URL to use when creating an account via command-line.\n" #if !DISABLE_ACCOUNT_MIGRATION - " --forcelegacyconfigimport : forcefully import account configurations from legacy clients (if available).\n" + " --forcelegacyconfigimport : forcefully import account configurations from legacy clients (if available).\n" #endif - " --reverse : use a reverse layout direction.\n"; + " --reverse : use a reverse layout direction.\n"; - QString applicationTrPath() - { - QString devTrPath = qApp->applicationDirPath() + QString::fromLatin1("/../src/gui/"); - if (QDir(devTrPath).exists()) { - // might miss Qt, QtKeyChain, etc. - qCWarning(lcApplication) << "Running from build location! Translations may be incomplete!"; - return devTrPath; - } +QString applicationTrPath() +{ + QString devTrPath = qApp->applicationDirPath() + QString::fromLatin1("/../src/gui/"); + if (QDir(devTrPath).exists()) { + // might miss Qt, QtKeyChain, etc. + qCWarning(lcApplication) << "Running from build location! Translations may be incomplete!"; + return devTrPath; + } #if defined(Q_OS_WIN) - return QApplication::applicationDirPath() + QLatin1String("/i18n/"); + return QApplication::applicationDirPath() + QLatin1String("/i18n/"); #elif defined(Q_OS_MACOS) - return QApplication::applicationDirPath() + QLatin1String("/../Resources/Translations"); // path defaults to app dir. + return QApplication::applicationDirPath() + QLatin1String("/../Resources/Translations"); // path defaults to app dir. #elif defined(Q_OS_UNIX) - if (qEnvironmentVariableIsSet("APPIMAGE")) { - return QApplication::applicationDirPath() + QLatin1String("/../share/" APPLICATION_EXECUTABLE "/i18n/"); - } else { - return QString::fromLatin1(SHAREDIR "/" APPLICATION_EXECUTABLE "/i18n/"); - } + if (qEnvironmentVariableIsSet("APPIMAGE")) { + return QApplication::applicationDirPath() + QLatin1String("/../share/" APPLICATION_EXECUTABLE "/i18n/"); + } else { + return QString::fromLatin1(SHAREDIR "/" APPLICATION_EXECUTABLE "/i18n/"); + } #endif +} + +#if defined(Q_OS_WIN) +// Native window chrome (title bar, borders) is drawn by DWM, outside Qt's own +// palette/stylesheet system - Qt does not switch it automatically. Windows 11's +// own dark title bar only follows an app's declared preference per-HWND. +void applyImmersiveDarkMode(QWidget *widget, bool dark) +{ + if (!widget || !widget->isWindow()) { + return; + } + const auto hwnd = reinterpret_cast(widget->winId()); + if (!hwnd) { + return; + } + const BOOL enabled = dark ? TRUE : FALSE; + // 20 = DWMWA_USE_IMMERSIVE_DARK_MODE on Windows 10 20H1+ and Windows 11; older + // insider builds (10 build 18985-18999) used 19 instead - try both. + if (DwmSetWindowAttribute(hwnd, 20, &enabled, sizeof(enabled)) != S_OK) { + DwmSetWindowAttribute(hwnd, 19, &enabled, sizeof(enabled)); } } +// Qt's "windows11" style (used on Windows 11 for palette/dark-mode-aware native +// primitives, see baseStyleName below) requests rounded window corners from DWM +// on its own as part of matching the native Fluent look. Override that back to +// square corners here if that's not wanted. +void applyWindowCornerPreference(QWidget *widget) +{ + if (!widget || !widget->isWindow()) { + return; + } + const auto hwnd = reinterpret_cast(widget->winId()); + if (!hwnd) { + return; + } + // 33 = DWMWA_WINDOW_CORNER_PREFERENCE, 1 = DWMWCP_DONOTROUND (Windows 11 only; + // no-op and harmless on Windows 10). + const DWORD doNotRound = 1; + DwmSetWindowAttribute(hwnd, 33, &doNotRound, sizeof(doNotRound)); +} +#endif +} + // ---------------------------------------------------------------------------------- bool Application::configVersionMigration() @@ -134,7 +184,7 @@ bool Application::configVersionMigration() FolderMan::backwardMigrationSettingsKeys(&deleteKeys, &ignoreKeys); configFile.setClientPreviousVersionString(configFile.clientVersionString()); - qCDebug(lcApplication) << "Migration is in progress:" << configFile.isMigrationInProgress(); + qCDebug(lcApplication) << "Migration is in progress:" << configFile.isMigrationInProgress(); const auto versionChanged = configFile.hasVersionChanged(); if (versionChanged) { qCInfo(lcApplication) << "Version changed. Removing updater settings from config."; @@ -172,18 +222,17 @@ bool Application::configVersionMigration() // We want to message the user either for destructive changes, // or if we're ignoring something and the client version changed. if (configFile.showConfigBackupWarning() && backupFilesList.count() > 0) { - QMessageBox box( - QMessageBox::Warning, - APPLICATION_SHORTNAME, - tr("Some settings were configured in %1 versions of this client and " - "use features that are not available in this version.
" - "
" - "Continuing will mean %2 these settings.
" - "
" - "The current configuration file was already backed up to %3.") - .arg((configFile.isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), - deleteKeys.isEmpty()? tr("ignoring") : tr("deleting"), - backupFilesList.join("
"))); + QMessageBox box(QMessageBox::Warning, + APPLICATION_SHORTNAME, + tr("Some settings were configured in %1 versions of this client and " + "use features that are not available in this version.
" + "
" + "Continuing will mean %2 these settings.
" + "
" + "The current configuration file was already backed up to %3.") + .arg((configFile.isDowngrade() ? tr("newer", "newer software version") : tr("older", "older software version")), + deleteKeys.isEmpty() ? tr("ignoring") : tr("deleting"), + backupFilesList.join("
"))); box.addButton(tr("Quit"), QMessageBox::AcceptRole); auto continueBtn = box.addButton(tr("Continue"), QMessageBox::DestructiveRole); @@ -255,7 +304,7 @@ Application::Application(int &argc, char **argv) setWindowIcon(_theme->applicationIcon()); parseOptions(arguments()); - //no need to waste time; + // no need to waste time; if (_helpOnly || _versionOnly) { return; } @@ -295,7 +344,7 @@ Application::Application(int &argc, char **argv) const QStringList filesList = QDir(legacyDir).entryList(QDir::Files); qCDebug(lcApplication) << "Will move the individual files" << filesList; for (const auto &name : filesList) { - if (!QFile::rename(legacyDir + "/" + name, confDir + "/" + name)) { + if (!QFile::rename(legacyDir + "/" + name, confDir + "/" + name)) { qCDebug(lcApplication) << "Fallback move of " << name << "also failed"; } } @@ -323,6 +372,13 @@ Application::Application(int &argc, char **argv) setupLogging(); setupTranslations(); +#ifdef IONOS_BUILD +#ifdef STRATO_WL_BUILD + _theme->setOverrideServerUrl(QCoreApplication::translate("OCC::Theme", "Login_URL_STRATO")); +#else + _theme->setOverrideServerUrl(QCoreApplication::translate("OCC::Theme", "Login_URL")); +#endif +#endif // try to migrate legacy accounts and folders from a previous client version // only copy the settings and check what should be skipped @@ -379,10 +435,22 @@ Application::Application(int &argc, char **argv) } _theme->setSystrayUseMonoIcons(ConfigFile().monoIcons()); - connect(this, &Application::systemPaletteChanged, - _theme, &Theme::systemPaletteHasChanged); + connect(this, &Application::systemPaletteChanged, _theme, &Theme::systemPaletteHasChanged); + // Establish the correct (light/dark) WindowText immediately, rather than waiting for + // the first actual system palette change event. + _theme->systemPaletteHasChanged(); #if defined(Q_OS_WIN) + // Give every top-level window its dark/light title bar as soon as it becomes visible, + // and re-apply to all of them if the theme changes while windows are already open. + installEventFilter(this); + connect(_theme, &Theme::darkModeChanged, this, [this] { + const auto dark = _theme->darkMode(); + for (auto *widget : QApplication::topLevelWidgets()) { + applyImmersiveDarkMode(widget, dark); + } + }); + _shellExtensionsServer.reset(new ShellExtensionsServer); #endif @@ -392,6 +460,21 @@ Application::Application(int &argc, char **argv) connect(&_singleApp, &KDSingleApplication::messageReceived, this, &Application::slotParseMessage); #endif +#ifdef IONOS_BUILD +#if defined(Q_OS_WIN) + // Match stable's OS-version-based widget style choice (see main.cpp) so native + // primitives (e.g. QTreeView branch indicators) stay palette/dark-mode aware + // instead of falling back to the legacy UxTheme-based "WindowsVista" style. + const auto baseStyleName = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows11 + ? QStringLiteral("windows11") + : QStringLiteral("Fusion"); + setStyle(new sesStyle(QStyleFactory::create(baseStyleName))); +#else + // QProxyStyle falls back to the platform's current style when given nullptr. + setStyle(new sesStyle(nullptr)); +#endif +#endif + // create accounts and folders from a legacy desktop client or from the current config file setupAccountsAndFolders(); @@ -430,23 +513,18 @@ Application::Application(int &argc, char **argv) _proxy.setupQtProxyFromConfig(); // folders have to be defined first, than we set up the Qt proxy. - connect(AccountManager::instance(), &AccountManager::accountAdded, - this, &Application::slotAccountStateAdded); - connect(AccountManager::instance(), &AccountManager::accountRemoved, - this, &Application::slotAccountStateRemoved); + connect(AccountManager::instance(), &AccountManager::accountAdded, this, &Application::slotAccountStateAdded); + connect(AccountManager::instance(), &AccountManager::accountRemoved, this, &Application::slotAccountStateRemoved); const auto accounts = AccountManager::instance()->accounts(); for (const auto &ai : accounts) { slotAccountStateAdded(ai.data()); } - connect(FolderMan::instance()->socketApi(), &SocketApi::shareCommandReceived, - _gui.data(), &ownCloudGui::slotShowShareDialog); + connect(FolderMan::instance()->socketApi(), &SocketApi::shareCommandReceived, _gui.data(), &ownCloudGui::slotShowShareDialog); - connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived, - _gui.data(), &ownCloudGui::slotShowFileActivityDialog); + connect(FolderMan::instance()->socketApi(), &SocketApi::fileActivityCommandReceived, _gui.data(), &ownCloudGui::slotShowFileActivityDialog); - connect(FolderMan::instance()->socketApi(), &SocketApi::fileActionsCommandReceived, - _gui.data(), &ownCloudGui::slotShowFileActionsDialog); + connect(FolderMan::instance()->socketApi(), &SocketApi::fileActionsCommandReceived, _gui.data(), &ownCloudGui::slotShowFileActionsDialog); // startup procedure. connect(&_checkConnectionTimer, &QTimer::timeout, this, &Application::slotCheckConnection); @@ -456,16 +534,13 @@ Application::Application(int &argc, char **argv) QTimer::singleShot(0, this, &Application::slotCheckConnection); // Can't use onlineStateChanged because it is always true on modern systems because of many interfaces - connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, - this, &Application::slotSystemOnlineConfigurationChanged); + connect(QNetworkInformation::instance(), &QNetworkInformation::reachabilityChanged, this, &Application::slotSystemOnlineConfigurationChanged); #if defined(BUILD_UPDATER) // Update checks auto *updaterScheduler = new UpdaterScheduler(this); - connect(updaterScheduler, &UpdaterScheduler::updaterAnnouncement, - _gui.data(), &ownCloudGui::slotShowTrayUpdateMessage); - connect(updaterScheduler, &UpdaterScheduler::requestRestart, - _folderManager.data(), &FolderMan::slotScheduleAppRestart); + connect(updaterScheduler, &UpdaterScheduler::updaterAnnouncement, _gui.data(), &ownCloudGui::slotShowTrayUpdateMessage); + connect(updaterScheduler, &UpdaterScheduler::requestRestart, _folderManager.data(), &FolderMan::slotScheduleAppRestart); #endif // Cleanup at Quit. @@ -478,6 +553,11 @@ Application::Application(int &argc, char **argv) handleEditLocallyFromOptions(); + if (AccountSetupCommandLineManager::instance()->isCommandLineParsed()) { + AccountSetupCommandLineManager::instance()->setupAccountFromCommandLine(); + } + AccountSetupCommandLineManager::destroy(); + #ifdef Q_OS_MACOS // If any sync folder needs sandbox reapproval after upgrading to v33+, // automatically open the settings dialog on the first affected account @@ -504,11 +584,34 @@ Application::~Application() } // Remove the account from the account manager so it can be deleted. - disconnect(AccountManager::instance(), &AccountManager::accountRemoved, - this, &Application::slotAccountStateRemoved); + disconnect(AccountManager::instance(), &AccountManager::accountRemoved, this, &Application::slotAccountStateRemoved); AccountManager::instance()->shutdown(); } +void Application::startTracking() +{ + DataCollectionWrapper dcw; + dcw.initDataCollection(); + AccountPtr account = AccountManager::instance()->accounts().first()->account(); + QByteArray byteArray = account->credentials()->user().toUtf8(); // Convert the input string to a byte array + QByteArray hash = QCryptographicHash::hash(byteArray, QCryptographicHash::Sha256); // Perform the hash + + ConfigFile cfg; + dcw.setSendData(cfg.sendData()); + dcw.setAccount(account); + + dcw.setClientID(hash.toHex()); + dcw.login(); +} + +void Application::stopTracking() +{ + DataCollectionWrapper dcw; + dcw.accountRemoved(); + dcw.setClientID(QString()); + dcw.setAccount(nullptr); +} + void Application::setupAccountsAndFolders() { _folderManager.reset(new FolderMan); @@ -538,24 +641,17 @@ void Application::setupAccountsAndFolders() const auto accounts = AccountManager::instance()->accounts(); const auto accountsListSize = accounts.size(); - if (accountsRestoreResult == AccountManager::AccountsRestoreSuccessFromLegacyVersion - && Theme::instance()->displayLegacyImportDialog() - && !AccountManager::instance()->forceLegacyImport() - && accountsListSize > 0) { - const auto accountsRestoreMessage = accountsListSize > 1 - ? tr("%1 accounts", "number of accounts imported").arg(QString::number(accountsListSize)) - : tr("1 account"); - const auto foldersRestoreMessage = foldersListSize > 1 - ? tr("%1 folders", "number of folders imported").arg(QString::number(foldersListSize)) - : tr("1 folder"); - const auto messageBox = new QMessageBox(QMessageBox::Information, - tr("Legacy import"), - tr("Imported %1 and %2 from a legacy desktop client.\n%3", - "number of accounts and folders imported. list of users.") - .arg(accountsRestoreMessage, - foldersRestoreMessage, - prettyNamesList(accounts)) - ); + if (accountsRestoreResult == AccountManager::AccountsRestoreSuccessFromLegacyVersion && Theme::instance()->displayLegacyImportDialog() + && !AccountManager::instance()->forceLegacyImport() && accountsListSize > 0) { + const auto accountsRestoreMessage = + accountsListSize > 1 ? tr("%1 accounts", "number of accounts imported").arg(QString::number(accountsListSize)) : tr("1 account"); + const auto foldersRestoreMessage = + foldersListSize > 1 ? tr("%1 folders", "number of folders imported").arg(QString::number(foldersListSize)) : tr("1 folder"); + const auto messageBox = + new QMessageBox(QMessageBox::Information, + tr("Legacy import"), + tr("Imported %1 and %2 from a legacy desktop client.\n%3", "number of accounts and folders imported. list of users.") + .arg(accountsRestoreMessage, foldersRestoreMessage, prettyNamesList(accounts))); messageBox->setWindowModality(Qt::NonModal); messageBox->open(); } @@ -570,7 +666,7 @@ void Application::setupConfigFile() // Migrate from version <= 2.4 setApplicationName(_theme->appNameGUI()); #ifndef QT_WARNING_DISABLE_DEPRECATED // Was added in Qt 5.9 - #define QT_WARNING_DISABLE_DEPRECATED QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") +#define QT_WARNING_DISABLE_DEPRECATED QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") #endif QT_WARNING_PUSH QT_WARNING_DISABLE_DEPRECATED @@ -604,7 +700,7 @@ void Application::setupConfigFile() const QStringList filesList = QDir(oldDir).entryList(QDir::Files); qCDebug(lcApplication) << "Will move the individual files" << filesList; for (const auto &name : filesList) { - if (!QFile::rename(oldDir + "/" + name, confDir + "/" + name)) { + if (!QFile::rename(oldDir + "/" + name, confDir + "/" + name)) { qCDebug(lcApplication) << "Fallback move of " << name << "also failed"; } } @@ -622,23 +718,19 @@ AccountManager::AccountsRestoreResult Application::restoreLegacyAccount() ConfigFile cfg; const auto tryMigrate = cfg.overrideServerUrl().isEmpty(); auto accountsRestoreResult = AccountManager::AccountsRestoreFailure; - if (accountsRestoreResult = AccountManager::instance()->restore(tryMigrate); - accountsRestoreResult == AccountManager::AccountsRestoreFailure) { + if (accountsRestoreResult = AccountManager::instance()->restore(tryMigrate); accountsRestoreResult == AccountManager::AccountsRestoreFailure) { // If there is an error reading the account settings, try again // after a couple of seconds, if that fails, give up. // (non-existence is not an error) Utility::sleep(5); - if (accountsRestoreResult = AccountManager::instance()->restore(tryMigrate); - accountsRestoreResult == AccountManager::AccountsRestoreFailure) { + if (accountsRestoreResult = AccountManager::instance()->restore(tryMigrate); accountsRestoreResult == AccountManager::AccountsRestoreFailure) { qCCritical(lcApplication) << "Could not read the account settings, quitting"; - QMessageBox::critical( - nullptr, - tr("Error accessing the configuration file"), - tr("There was an error while accessing the configuration " - "file at %1. Please make sure the file can be accessed by your system account.") - .arg(ConfigFile().configFile()), - QMessageBox::Ok - ); + QMessageBox::critical(nullptr, + tr("Error accessing the configuration file"), + tr("There was an error while accessing the configuration " + "file at %1. Please make sure the file can be accessed by your system account.") + .arg(ConfigFile().configFile()), + QMessageBox::Ok); QTimer::singleShot(0, qApp, &QCoreApplication::quit); } } @@ -648,16 +740,18 @@ AccountManager::AccountsRestoreResult Application::restoreLegacyAccount() void Application::slotAccountStateRemoved(AccountState *accountState) { if (_gui) { - disconnect(accountState, &AccountState::stateChanged, - _gui.data(), &ownCloudGui::slotComputeOverallSyncStatus); - disconnect(accountState->account().data(), &Account::serverVersionChanged, - _gui.data(), &ownCloudGui::slotTrayMessageIfServerUnsupported); + disconnect(accountState, &AccountState::stateChanged, _gui.data(), &ownCloudGui::slotComputeOverallSyncStatus); + disconnect(accountState->account().data(), &Account::serverVersionChanged, _gui.data(), &ownCloudGui::slotTrayMessageIfServerUnsupported); } if (_folderManager) { - disconnect(accountState, &AccountState::stateChanged, - _folderManager.data(), &FolderMan::slotAccountStateChanged); - disconnect(accountState->account().data(), &Account::serverVersionChanged, - _folderManager.data(), &FolderMan::slotServerVersionChanged); + disconnect(accountState, &AccountState::stateChanged, _folderManager.data(), &FolderMan::slotAccountStateChanged); + disconnect(accountState->account().data(), &Account::serverVersionChanged, _folderManager.data(), &FolderMan::slotServerVersionChanged); + } + + if (AccountManager::instance()->accounts().isEmpty()) { + stopTracking(); + } else { + startTracking(); } // if there is no more account, show the wizard. @@ -670,16 +764,13 @@ void Application::slotAccountStateRemoved(AccountState *accountState) void Application::slotAccountStateAdded(AccountState *accountState) { - connect(accountState, &AccountState::stateChanged, - _gui.data(), &ownCloudGui::slotComputeOverallSyncStatus); - connect(accountState->account().data(), &Account::serverVersionChanged, - _gui.data(), &ownCloudGui::slotTrayMessageIfServerUnsupported); - connect(accountState, &AccountState::termsOfServiceChanged, - _gui.data(), &ownCloudGui::slotNeedToAcceptTermsOfService); - connect(accountState, &AccountState::stateChanged, - _folderManager.data(), &FolderMan::slotAccountStateChanged); - connect(accountState->account().data(), &Account::serverVersionChanged, - _folderManager.data(), &FolderMan::slotServerVersionChanged); + connect(accountState, &AccountState::stateChanged, _gui.data(), &ownCloudGui::slotComputeOverallSyncStatus); + connect(accountState->account().data(), &Account::serverVersionChanged, _gui.data(), &ownCloudGui::slotTrayMessageIfServerUnsupported); + connect(accountState, &AccountState::termsOfServiceChanged, _gui.data(), &ownCloudGui::slotNeedToAcceptTermsOfService); + connect(accountState, &AccountState::stateChanged, _folderManager.data(), &FolderMan::slotAccountStateChanged); + connect(accountState->account().data(), &Account::serverVersionChanged, _folderManager.data(), &FolderMan::slotServerVersionChanged); + + startTracking(); _gui->slotTrayMessageIfServerUnsupported(accountState->account()); } @@ -698,8 +789,8 @@ void Application::slotCleanup() // Maybe we need 2 validators, one triggered by timer, one by network configuration changes? void Application::slotSystemOnlineConfigurationChanged() { - if (QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Site || - QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online) { + if (QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Site + || QNetworkInformation::instance()->reachability() == QNetworkInformation::Reachability::Online) { const auto list = AccountManager::instance()->accounts(); for (const auto &accountState : list) { accountState->systemOnlineConfigurationChanged(); @@ -765,15 +856,11 @@ void Application::setupLogging() logger->setLogDebug(true); #endif - logger->enterNextLogFile(QStringLiteral("nextcloud.log"), OCC::Logger::LogType::Log); + logger->enterNextLogFile(QStringLiteral("hidrivenext.log"), OCC::Logger::LogType::Log); logger->enterNextLogFile(QStringLiteral("permanent_delete.log"), OCC::Logger::LogType::DeleteLog); - qCInfo(lcApplication) << "##################" << _theme->appName() - << "locale:" << QLocale::system().name() - << "ui_lang:" << property("ui_lang") - << "version:" << _theme->version() - << "os:" << Utility::platformName() - << "platform:" << QApplication::platformName(); + qCInfo(lcApplication) << "##################" << _theme->appName() << "locale:" << QLocale::system().name() << "ui_lang:" << property("ui_lang") + << "version:" << _theme->version() << "os:" << Utility::platformName() << "platform:" << QApplication::platformName(); qCInfo(lcApplication) << "Arguments:" << qApp->arguments(); } @@ -827,7 +914,7 @@ void Application::parseOptions(const QStringList &options) bool shouldExit = false; - //parse options; if help or bad option exit + // parse options; if help or bad option exit while (it.hasNext()) { QString option = it.next(); if (option == QLatin1String("--help") || option == QLatin1String("-h")) { @@ -879,7 +966,9 @@ void Application::parseOptions(const QStringList &options) setLayoutDirection(layoutDirection() == Qt::LeftToRight ? Qt::RightToLeft : Qt::LeftToRight); } else if (option.endsWith(QStringLiteral(APPLICATION_DOTVIRTUALFILE_SUFFIX))) { // virtual file, open it after the Folder were created (if the app is not terminated) - QTimer::singleShot(0, this, [this, option] { openVirtualFile(option); }); + QTimer::singleShot(0, this, [this, option] { + openVirtualFile(option); + }); } else if (option.startsWith(QStringLiteral(APPLICATION_URI_HANDLER_SCHEME "://open"))) { // see the section Local file editing of the Architecture page of the user documentation _editFileLocallyUrl = QUrl::fromUserInput(option); @@ -966,18 +1055,12 @@ void Application::showHelp() setHelp(); QString helpText; QTextStream stream(&helpText); - stream << _theme->appName() - << QLatin1String(" version ") - << _theme->version() << Qt::endl; + stream << _theme->appName() << QLatin1String(" version ") << _theme->version() << Qt::endl; - stream << QLatin1String("File synchronisation desktop utility.") << Qt::endl - << Qt::endl - << QLatin1String(optionsC); + stream << QLatin1String("File synchronisation desktop utility.") << Qt::endl << Qt::endl << QLatin1String(optionsC); if (_theme->appName() == QLatin1String("ownCloud")) - stream << Qt::endl - << "For more information, see http://www.owncloud.org" << Qt::endl - << Qt::endl; + stream << Qt::endl << "For more information, see http://www.owncloud.org" << Qt::endl << Qt::endl; displayHelpText(helpText); } @@ -1047,6 +1130,7 @@ void Application::setupTranslations() auto translator = std::make_unique(); auto qtTranslator = std::make_unique(); auto qtkeychainTranslator = std::make_unique(); + auto fallbackTranslator = std::make_unique(); const auto trPath = applicationTrPath(); const auto trFolder = QDir{trPath}; @@ -1058,7 +1142,7 @@ void Application::setupTranslations() qCInfo(lcApplication) << "System UI languages are:" << QLocale::system().uiLanguages(); auto choosenLanguage = enforcedLanguage(); if (choosenLanguage.isEmpty()) { - for(const auto &localeToTest : QLocale::system().uiLanguages(QLocale::TagSeparator::Underscore)){ + for (const auto &localeToTest : QLocale::system().uiLanguages(QLocale::TagSeparator::Underscore)) { const auto trFile = QString{QLatin1String{"client_"} + localeToTest}; qCDebug(lcApplication()) << "trying to load" << localeToTest << "in" << trFile << "from" << trPath; if (translator->load(trFile, trPath)) { @@ -1081,6 +1165,19 @@ void Application::setupTranslations() // have a translation file provided. qCInfo(lcApplication) << "Using" << choosenLanguage << "translation"; setProperty("ui_lang", choosenLanguage); + + // Some translatable "keys" are not real sentences but data lookups (e.g. locale-specific + // links such as "ExpandMemory-Link"). If a language's .ts file has no entry at all for such + // a key, Qt returns the raw source string (the key itself) instead of a sensible value. + // Loading English as a fallback catalog - installed before (and therefore checked after, + // QTranslator lookups are LIFO) the language-specific one - lets every language fall back + // to the English value for keys it doesn't translate, without duplicating English content + // into every translation file. + if (!choosenLanguage.startsWith(QLatin1String("en")) && fallbackTranslator->load(QLatin1String("client_en"), trPath) + && !fallbackTranslator->isEmpty()) { + installTranslator(fallbackTranslator.release()); + } + const QString qtTrPath = QLibraryInfo::path(QLibraryInfo::TranslationsPath); const QString qtTrFile = QLatin1String("qt_") + choosenLanguage; const QString qtBaseTrFile = QLatin1String("qtbase_") + choosenLanguage; @@ -1180,18 +1277,21 @@ bool Application::event(QEvent *event) const auto openEvent = dynamic_cast(event); qCDebug(lcApplication) << "macOS: Received a QFileOpenEvent"; - if(!openEvent->file().isEmpty()) { + if (!openEvent->file().isEmpty()) { qCDebug(lcApplication) << "QFileOpenEvent" << openEvent->file(); // virtual file, open it after the Folder were created (if the app is not terminated) const auto fn = openEvent->file(); - QTimer::singleShot(0, this, [this, fn] { openVirtualFile(fn); }); + QTimer::singleShot(0, this, [this, fn] { + openVirtualFile(fn); + }); } else if (!openEvent->url().isEmpty() && openEvent->url().isValid()) { // On macOS, Qt does not handle receiving a custom URI as it does on other systems (as an application argument). // Instead, it sends out a QFileOpenEvent. We therefore need custom handling for our URI handling on macOS. qCInfo(lcApplication) << "macOS: Opening local file for editing: " << openEvent->url(); EditLocallyManager::instance()->handleRequest(openEvent->url()); } else { - const auto errorParsingLocalFileEditingUrl = QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(openEvent->url().toString()); + const auto errorParsingLocalFileEditingUrl = + QStringLiteral("The supplied url for local file editing '%1' is invalid!").arg(openEvent->url().toString()); qCInfo(lcApplication) << errorParsingLocalFileEditingUrl; showHint(errorParsingLocalFileEditingUrl.toStdString()); } @@ -1202,4 +1302,20 @@ bool Application::event(QEvent *event) return QGuiApplication::event(event); } +bool Application::eventFilter(QObject *watched, QEvent *event) +{ +#if defined(Q_OS_WIN) + if (event->type() == QEvent::Show) { + if (auto *widget = qobject_cast(watched)) { + applyImmersiveDarkMode(widget, _theme->darkMode()); + applyWindowCornerPreference(widget); + } + } +#else + Q_UNUSED(watched) + Q_UNUSED(event) +#endif + return false; +} + } // namespace OCC diff --git a/src/gui/application.h b/src/gui/application.h index be751ebb690fb..e6a378133b1f6 100644 --- a/src/gui/application.h +++ b/src/gui/application.h @@ -66,6 +66,7 @@ class Application : public QApplication [[nodiscard]] ownCloudGui *gui() const; bool event(QEvent *event) override; + bool eventFilter(QObject *watched, QEvent *event) override; public slots: // TODO: this should not be public @@ -102,7 +103,8 @@ protected slots: private: void setHelp(); - + void startTracking(); + void stopTracking(); void handleEditLocallyFromOptions(); AccountManager::AccountsRestoreResult restoreLegacyAccount(); diff --git a/src/gui/authenticationdialog.cpp b/src/gui/authenticationdialog.cpp index de4a1e66cadb1..b371ca5de4a45 100644 --- a/src/gui/authenticationdialog.cpp +++ b/src/gui/authenticationdialog.cpp @@ -35,6 +35,7 @@ AuthenticationDialog::AuthenticationDialog(const QString &realm, const QString & connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); lay->addWidget(box); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); } QString AuthenticationDialog::user() const diff --git a/src/gui/basetheme.h b/src/gui/basetheme.h new file mode 100644 index 0000000000000..4fadf766b52a5 --- /dev/null +++ b/src/gui/basetheme.h @@ -0,0 +1,575 @@ +#ifndef _BASETHEME_H +#define _BASETHEME_H + +#include +#include +#include +#include "theme.h" +#include "common/utility.h" + +namespace OCC { + +class BaseTheme : public QObject{ + Q_OBJECT + Q_PROPERTY(QString dialogBackgroundColor READ dialogBackgroundColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString checkboxCheckmarkColor READ checkboxCheckmarkColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString trayFontColor READ trayFontColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString trayBorderColor READ trayBorderColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString trayInputFieldBorderColor READ trayInputFieldBorderColor CONSTANT) + Q_PROPERTY(QString trayBackgroundColor READ trayBackgroundColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString iconDarkColor READ iconDarkColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString headerBannerColor READ headerBannerColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString buttonIconColor READ buttonIconColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString buttonHoveredColor READ buttonHoveredColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString buttonPressedColor READ buttonPressedColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString toolButtonHoveredColor READ toolButtonHoveredColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString toolButtonPressedColor READ toolButtonPressedColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString pillButtonPrimaryColor READ pillButtonPrimaryColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString pillButtonSecondaryColor READ pillButtonSecondaryColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString pillButtonBorderColor READ pillButtonBorderColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString clipboardBackgroundColor READ clipboardBackgroundColor CONSTANT) + Q_PROPERTY(QString trayErrorBorderColor READ trayErrorBorderColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString trayErrorTextColor READ trayErrorTextColor NOTIFY themeColorsChanged) + Q_PROPERTY(QString sesHeaderLogoIcon READ sesHeaderLogoIcon CONSTANT) + Q_PROPERTY(QString websiteIcon READ websiteIcon CONSTANT) + Q_PROPERTY(QString folderIcon READ folderIcon CONSTANT) + Q_PROPERTY(QString moreIcon READ moreIcon CONSTANT) + Q_PROPERTY(QString moreHoverIcon READ moreHoverIcon CONSTANT) + Q_PROPERTY(QString avatarIcon READ avatarIcon CONSTANT) + Q_PROPERTY(QString plusIcon READ plusIcon CONSTANT) + Q_PROPERTY(QString lightPlusIcon READ lightPlusIcon CONSTANT) + Q_PROPERTY(QString quitIcon READ quitIcon CONSTANT) + Q_PROPERTY(QString resumeIcon READ resumeIcon CONSTANT) + Q_PROPERTY(QString pauseIcon READ pauseIcon CONSTANT) + Q_PROPERTY(QString settingsIcon READ settingsIcon CONSTANT) + Q_PROPERTY(QString logoutIcon READ logoutIcon CONSTANT) + Q_PROPERTY(QString deleteIcon READ deleteIcon CONSTANT) + Q_PROPERTY(QString clipboardIcon READ clipboardIcon CONSTANT) + Q_PROPERTY(QString lightClipboardIcon READ lightClipboardIcon CONSTANT) + Q_PROPERTY(QString chevronIcon READ chevronIcon CONSTANT) + Q_PROPERTY(QString syncSuccessIcon READ syncSuccessIcon CONSTANT) + Q_PROPERTY(QString syncErrorIcon READ syncErrorIcon CONSTANT) + Q_PROPERTY(QString syncOfflineIcon READ syncOfflineIcon CONSTANT) + Q_PROPERTY(QString snackbarErrorIcon READ snackbarErrorIcon CONSTANT) + Q_PROPERTY(QString activityIcon READ activityIcon CONSTANT) + +public: + + BaseTheme() { + connect(Theme::instance(), &Theme::darkModeChanged, this, &BaseTheme::themeColorsChanged); + } + + virtual ~BaseTheme() = default; + + virtual QString themePrefix(QString context = "qml") const { + if (context == "qml") { + return QString("qrc:///client/theme/"); + } + return QString(Theme::themePrefix); + } + + virtual QString additionalThemePrefix() const { return QStringLiteral(""); } + + // Recolors a themed ses-*.svg icon at load time via Qt's svgimage-custom-color image + // provider (see SvgImageProvider), ignoring whatever fill color is baked into the SVG. + // Use this instead of a static icon getter wherever the icon is consumed by QML as an + // "image://" source, so it stays legible when darkMode() flips. + Q_INVOKABLE virtual QString coloredIcon(const QString &fileName, const QString &color) const { + return QStringLiteral("image://svgimage-custom-color/") + _sesFolder + additionalThemePrefix() + fileName + QStringLiteral("/") + color; + } + + virtual QString avatarIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settingsAvatar.svg"); + } + + virtual QString roundAvatarIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settingsAvatarRound.svg"); + } + + virtual QString folderIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-folderIcon.svg"); + } + + virtual QString syncArrows() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncArrows.svg"); + } + + virtual QString questionCircleIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-questionMark.svg"); + } + + virtual QString liveBackupPlusIcon() const { + return QString(Theme::themePrefix) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-addlivebackup.svg"); + } + + virtual QString websiteIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-website.svg"); + } + + virtual QString moreIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-more.svg"); + } + + virtual QString moreHoverIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-more-hover.svg"); + } + + virtual QString plusIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-darkPlus.svg"); + } + + virtual QString lightPlusIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-lightPlus.svg"); + } + + virtual QString quitIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountQuit.svg"); + } + + virtual QString resumeIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountResume.svg"); + } + + virtual QString pauseIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountPause.svg"); + } + + virtual QString settingsIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-settings.svg"); + } + + virtual QString logoutIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountLogout.svg"); + } + + virtual QString sesHeaderLogoIcon() const = 0; + + virtual QString deleteIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-accountDelete.svg"); + } + + virtual QString activityDeleteIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-activityDelete.svg"); + } + + virtual QString refreshIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-refresh.svg"); + } + + virtual QString infoIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-info.svg"); + } + + virtual QString clipboardIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-clipboard.svg"); + } + + virtual QString lightClipboardIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-lightClipboard.svg"); + } + + virtual QString chevronIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-chevron.svg"); + } + + virtual QString syncSuccessIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-success.svg"); + } + + virtual QString syncErrorIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-error.svg"); + } + + virtual QString syncPausedIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-paused.svg"); + } + + virtual QString syncWarningIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-warning.svg"); + } + + virtual QString syncSyncingIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-syncstate-syncing.svg"); + } + + virtual QString syncOfflineIcon(QString context = "qml") const { + return themePrefix(context) + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-state-offline.svg"); + } + + virtual QString snackbarErrorIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-snackbar-error.svg"); + } + + virtual QString activityIcon() const { + return themePrefix() + _sesFolder + additionalThemePrefix() + QStringLiteral("ses-activity.svg"); + } + + virtual int treeViewIconSize() const { + return 32; + } + + //Control Configuration: Sizes + virtual QString toolbarActionBorderRadius() const { + return "8px"; + } + + virtual QString toolbarSideMargin() const { + return "10px"; + } + + virtual int toolbarIconSize() const { + return 24; + } + + virtual QString buttonRadius() const { + return "4px"; + } + + virtual int buttonRadiusInt() const { + return 4; + } + + virtual QString buttonPadding() const { + return "10px"; + } + + virtual QString smallMargin() const { + return "8"; + } + + virtual int minimalSettingsDialogWidth() const { + return 740; + } + + virtual int wizardFixedWidth() const { + return 576; + } + + virtual int wizardFixedHeight() const { + return 704; + } + + virtual int LoginPageSpacer() const { + return 45; + } + + //Font Configuration + virtual QString settingsFont() const { + return Utility::isWindows() ? QStringLiteral("Segoe UI") : QStringLiteral("Open Sans"); + } + + virtual QString contextMenuFont() const { + return settingsFont(); + } + + virtual QString settingsSmallTextSize() const { + return "14px"; + } + + virtual int settingsTextPixel() const { + return 16; + } + + virtual QString settingsTextSize() const { + return QString::number(settingsTextPixel()) + "px"; + } + + virtual int settingsTitlePixel() const { + return 20; + } + + virtual QString settingsTitleSize() const { + return QString::number(settingsTitlePixel()) + "px"; + } + + virtual int settingsBigTitlePixel() const { + return 24; + } + + virtual QString settingsBigTitleSize() const { + return QString::number(settingsBigTitlePixel()) + "px"; + } + + virtual QString onboardingTitle() const { + return "28px"; + } + + virtual QString settingsTextWeight() const { + return "400"; + } + + virtual QString settingsTitleWeight400() const { + return "400"; + } + + virtual QString settingsTitleWeight500() const { + return "500"; + } + + virtual QString settingsTitleWeight600() const { + return "600"; + } + + virtual QFont::Weight settingsTitleWeightDemiBold() const { + return QFont::DemiBold; + } + + virtual QFont::Weight settingsTitleWeightNormal() const { + return QFont::Normal; + } + + virtual QFont settingsFontDefault() const { + QFont defaultFont(settingsFont()); + defaultFont.setPixelSize(settingsTextPixel()); + defaultFont.setWeight(settingsTitleWeightNormal()); + return defaultFont; + } + + virtual QString titleColor() const { + return themedColor("#000000", "#D6E4F5"); + } + + virtual QString folderWizardSubtitleColor() const { + return themedColor("#104996", "#5FA8E0"); + } + + virtual QString folderWizardPathColor() const { + return themedColor("#97A3B4", "#A8B4C6"); + } + + virtual QString loginWizardFontGrey() const { + return themedColor("#616161", "#D6D6D6"); + } + + virtual QString loginWizardFontLightGrey() const { + return themedColor("#BDBDBD", "#8A8A8A"); + } + + virtual QString trayFontColor() const { + return themedColor("#001B41", "#D6E4F5"); + } + + virtual QString trayBorderColor() const { + return themedColor("#D7D7D7", "#3A3D42"); + } + + virtual QString trayInputFieldBorderColor() const { + return "#718095"; + } + + virtual QString fontConfigurationCss(QString font, QString size, QString weight, QString color) const { + return QString("font-family: %1; font-size: %2; font-weight: %3; color: %4; ").arg( + font, + size, + weight, + color); + } + + //Colors + virtual QString settingsLinkColor() const { + return themedColor("#02306A", "#5FA8E0"); + } + + virtual QString quotaProgressColor() const { + return themedColor("#308cc6", "#5FA8E0"); + } + + virtual QString syncProgressColor() const { + return themedColor("#359ada", "#4FB6F0"); + } + + virtual QString buttonPrimaryColor() const { + return themedColor("#0F6CBD", "#3D6BB0"); + } + + virtual QString buttonSecondaryColor() const { + // Dark value matches the tray's pillButtonSecondaryColor() (SecondaryPillButton.qml). + return themedColor("#FFFFFF", "#2A2E35"); + } + + virtual QString buttonSecondaryBorderColor() const { + // Dark value matches the tray's pillButtonBorderColor() (SecondaryPillButton.qml). + return themedColor("#D1D1D1", "#3D6BB0"); + } + + virtual QString buttonDisabledColor() const { + return themedColor("#F0F0F0", "#33363C"); + } + + virtual QString buttonPrimaryHoverColor() const { + return themedColor("#115EA3", "#4B7CC4"); + } + + virtual QString buttonSecondaryHoverColor() const { + return themedColor("#F5F5F5", "#343841"); + } + + virtual QString buttonPrimaryPressedColor() const { + return themedColor("#0C3B5E", "#2A4D82"); + } + + virtual QString buttonSecondaryPressedColor() const { + return themedColor("#E0E0E0", "#3D424D"); + } + + virtual QString buttonPrimaryFocusedBorderColor() const { + return themedColor("#000000", "#FFFFFF"); + } + + virtual QString buttonSecondaryFocusedBorderColor() const { + return themedColor("#000000", "#FFFFFF"); + } + + virtual QString buttonDisabledFontColor() const { + return themedColor("#BDBDBD", "#5A5D63"); + } + + virtual QString pillButtonPrimaryColor() const { + return themedColor("#0B2A63", "#3D6BB0"); + } + + virtual QString pillButtonSecondaryColor() const { + return themedColor("#FFFFFF", "#2A2E35"); + } + + virtual QString pillButtonBorderColor() const { + return themedColor("#0B2A63", "#3D6BB0"); + } + + virtual QString clipboardBackgroundColor() const { + return "#FFFFFF"; + } + + virtual QString white() const { + return "#FFFFFF"; + } + + virtual QString black() const { + return "#000000"; + } + + virtual QString dialogBackgroundColor() const { + return themedColor("#FFFFFF", "#1E2126"); + } + + // Deliberately inverted from the native checked-box contrast (which the checked box's + // accent-color fill already provides) - white checkmark in Light Mode, black in Dark Mode. + virtual QString checkboxCheckmarkColor() const { + return themedColor("#FFFFFF", "#000000"); + } + + virtual QString trayBackgroundColor() const { + return themedColor("#FFFFFF", "#1E2126"); + } + + virtual QString menuBorderColor() const { + return themedColor("#2E4360", "#5B7699"); + } + + virtual QString menuTextColor() const { + return themedColor("#001B41", "#D6E4F5"); + } + + virtual QString menuPressedTextColor() const { + return themedColor("#001B41", "#D6E4F5"); + } + + virtual QString iconDarkColor() const { + return themedColor("#001B41", "#D6E4F5"); + } + + virtual QString menuSelectedItemColor() const { + return themedColor("#F4F7FA", "#333844"); + } + + // Background of the brand logo banner at the top of the tray dropdown (HeaderLogo.qml). + // Kept separate from menuSelectedItemColor so a brand can give its logo banner its own + // look without also affecting selected-menu-item styling elsewhere. + virtual QString headerBannerColor() const { + return themedColor("#F4F7FA", "#333844"); + } + + virtual QString menuPressedItemColor() const { + return themedColor("#F4F7FA", "#333844"); + } + + virtual QString menuBorderRadius() const { + return "16px"; + } + + virtual QString buttonIconColor() const { + return themedColor("#1474C4", "#5FA8E0"); + } + + virtual QString buttonIconHoverColor() const { + return themedColor("#FFFFFF", "#0B2A63"); + } + + virtual QString buttonPressedColor() const { + return themedColor("#0B2A63", "#123B85"); + } + + virtual QString buttonHoveredColor() const { + return themedColor("#1474C4", "#5FA8E0"); + } + + virtual QString toolButtonHoveredColor() const { + return themedColor("#DBEDF8", "#233240"); + } + + virtual QString toolButtonPressedColor() const { + return themedColor("#95CAEB", "#2C4A63"); + } + + virtual QString errorBorderColor() const { + return themedColor("#EEACB2", "#B25C63"); + } + + virtual QString trayErrorBorderColor() const { + return themedColor("#F50C00", "#FF6B61"); + } + + virtual QString trayErrorTextColor() const { + return themedColor("#C80A00", "#FF8A80"); + } + + virtual QString warningBorderColor() const { + return themedColor("#F4BFAB", "#C98F5E"); + } + + virtual QString successBorderColor() const { + return themedColor("#9FD89F", "#5FA86A"); + } + + virtual QString infoBorderColor() const { + return themedColor("#11C7E6", "#4DD9F0"); + } + + virtual QString treeViewHoverColor() const { + return themedColor("#e5f3ff", "#242A33"); + } + +signals: + void themeColorsChanged(); + +protected: + // TODO: first-pass dark variants, derived by hand and not yet design-reviewed. + // Needs a proper visual QA/design pass before being considered final. + static QString themedColor(const QString &light, const QString &dark) { + return Theme::instance()->darkMode() ? dark : light; + } + +public: + // Tints an already theme-aware border color into a translucent fill, instead of + // inventing a separate opaque pastel per dark/light variant. + static QColor tintedFillFromBorder(const QColor &border, float alpha = 0.2f) { + auto fill = border; + fill.setAlphaF(alpha); + return fill; + } + + private: + inline static const QString _sesFolder = QStringLiteral("ses/"); +}; +} +#endif // _BASETHEME_H \ No newline at end of file diff --git a/src/gui/buttonstyle.h b/src/gui/buttonstyle.h new file mode 100644 index 0000000000000..783b88cfd45c3 --- /dev/null +++ b/src/gui/buttonstyle.h @@ -0,0 +1,340 @@ + +#ifndef _BUTTONSTYLE_H +#define _BUTTONSTYLE_H + +#include "whitelabeltheme.h" +#include +#include + +namespace OCC{ + +enum class ButtonStyleName { + Primary, + Secondary, + MoreOptions, +}; +OCSYNC_EXPORT Q_NAMESPACE; +Q_ENUM_NS(ButtonStyleName); +} +Q_DECLARE_METATYPE(OCC::ButtonStyleName); + +namespace OCC{ +class ButtonStyle +{ +protected: + ButtonStyle() + { + qRegisterMetaType("OCC::ButtonStyleName"); + } + ~ButtonStyle() {} + +public: + + // Default + virtual QString buttonDefaultColor() const = 0; + virtual QString buttonDefaultBorderColor() const = 0; + // Hover + virtual QString buttonHoverColor() const = 0; + virtual QString buttonHoverBorderColor() const = 0; + // Pressed + virtual QString buttonPressedColor() const = 0; + virtual QString buttonPressedBorderColor() const = 0; + // Disabled + virtual QString buttonDisabledColor() const = 0; + virtual QString buttonDisabledBorderColor() const = 0; + // Focused + virtual QString buttonFocusedColor() const = 0; + virtual QString buttonFocusedBorderColor() const = 0; + // Font + virtual QString buttonDisabledFontColor() const = 0; + virtual QString buttonFontColor() const = 0; + //Icon + virtual QString buttonIconDefaultColor() const = 0; + virtual QString buttonIconHoverColor() const = 0; +}; + +class PrimaryButtonStyle : public ButtonStyle { +private: + PrimaryButtonStyle() + { + } + ~PrimaryButtonStyle() {} +public: + + PrimaryButtonStyle(PrimaryButtonStyle &other) = delete; + void operator=(const PrimaryButtonStyle &) = delete; + + static PrimaryButtonStyle& GetInstance() { + static PrimaryButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + //Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonPrimaryHoverColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryHoverColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonPrimaryPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryPressedColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.buttonPrimaryColor(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.buttonPrimaryFocusedBorderColor(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.white(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonIconHoverColor() const override + { + return OCC::WLTheme.white(); + } +}; + +class SecondaryButtonStyle : public ButtonStyle { +protected: + SecondaryButtonStyle() + { + } + ~SecondaryButtonStyle() {} +public: + + SecondaryButtonStyle(SecondaryButtonStyle &other) = delete; + void operator=(const SecondaryButtonStyle &) = delete; + + static SecondaryButtonStyle& GetInstance() { + static SecondaryButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + return OCC::WLTheme.buttonSecondaryColor(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonSecondaryHoverColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonSecondaryPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryBorderColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.buttonSecondaryFocusedBorderColor(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.titleColor(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonIconHoverColor() const override + { + return OCC::WLTheme.white(); + } +}; + +class MoreOptionsButtonStyle : public ButtonStyle { +protected: + MoreOptionsButtonStyle() + { + } + ~MoreOptionsButtonStyle() {} +public: + + MoreOptionsButtonStyle(MoreOptionsButtonStyle &other) = delete; + void operator=(const MoreOptionsButtonStyle &) = delete; + + static MoreOptionsButtonStyle& GetInstance() { + static MoreOptionsButtonStyle instance; + return instance; + } + + // Default + QString buttonDefaultColor() const override + { + // Blends into the row background this button is painted on (folderstatusdelegate.cpp). + return OCC::WLTheme.dialogBackgroundColor(); + } + + QString buttonDefaultBorderColor() const override + { + return OCC::WLTheme.dialogBackgroundColor(); + } + + // Hover + QString buttonHoverColor() const override + { + return OCC::WLTheme.buttonHoveredColor(); + } + + QString buttonHoverBorderColor() const override + { + return OCC::WLTheme.buttonHoveredColor(); + } + + // Pressed + QString buttonPressedColor() const override + { + return OCC::WLTheme.buttonPressedColor(); + } + + QString buttonPressedBorderColor() const override + { + return OCC::WLTheme.buttonPressedColor(); + } + + // Disabled + QString buttonDisabledColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + QString buttonDisabledBorderColor() const override + { + return OCC::WLTheme.buttonDisabledColor(); + } + + // Focused + QString buttonFocusedColor() const override + { + return OCC::WLTheme.white(); + } + + QString buttonFocusedBorderColor() const override + { + return OCC::WLTheme.black(); + } + + // Font + QString buttonDisabledFontColor() const override + { + return OCC::WLTheme.buttonDisabledFontColor(); + } + + QString buttonFontColor() const override + { + return OCC::WLTheme.black(); + } + + // Icon (Three Dots) + QString buttonIconDefaultColor() const + { + return OCC::WLTheme.buttonIconColor(); + } + + QString buttonIconHoverColor() const + { + return OCC::WLTheme.buttonIconHoverColor(); + } +}; +} + +#endif // _BUTTONSTYLE_H diff --git a/src/gui/buttonstylestrategy.h b/src/gui/buttonstylestrategy.h new file mode 100644 index 0000000000000..c6a316d25d3de --- /dev/null +++ b/src/gui/buttonstylestrategy.h @@ -0,0 +1,60 @@ +#ifndef BUTTONSTYLESTRATEGY_H +#define BUTTONSTYLESTRATEGY_H + +#include "buttonstyle.h" +#include +#include + + +class ButtonStyleStrategy +{ +public: + virtual ~ButtonStyleStrategy() = default; + + static OCC::ButtonStyle& getButtonStyle(const QWidget *widget, const QStyleOptionButton *option) + { + OCC::ButtonStyleName buttonStyleName; + if(widget != nullptr) + { + buttonStyleName = determineButtonStyleName(widget, option); + } + else + { + buttonStyleName = OCC::ButtonStyleName::Secondary; + } + + switch (buttonStyleName) + { + case OCC::ButtonStyleName::MoreOptions: + return OCC::MoreOptionsButtonStyle::GetInstance(); + case OCC::ButtonStyleName::Primary: + return OCC::PrimaryButtonStyle::GetInstance(); + case OCC::ButtonStyleName::Secondary: + default: + return OCC::SecondaryButtonStyle::GetInstance(); + } + } + + static OCC::ButtonStyleName determineButtonStyleName(const QWidget *widget, const QStyleOptionButton *option) + { + QVariant propertyValue = widget->property("buttonStyle"); + if(propertyValue.isValid()){ + + return propertyValue.value(); + } + + return getButtonStyleNameByObjectName(widget); + } + + static OCC::ButtonStyleName getButtonStyleNameByObjectName(const QWidget *widget) + { + static const QMap buttonStyleMap = { + {"qt_wizard_finish", OCC::ButtonStyleName::Primary} + }; + + QString buttonName = widget->objectName(); + return buttonStyleMap.value(buttonName, OCC::ButtonStyleName::Secondary); + } +}; + +#endif // BUTTONSTYLESTRATEGY_H \ No newline at end of file diff --git a/src/gui/caseclashfilenamedialog.cpp b/src/gui/caseclashfilenamedialog.cpp index 59599330ae80d..790aa6a4a4a8d 100644 --- a/src/gui/caseclashfilenamedialog.cpp +++ b/src/gui/caseclashfilenamedialog.cpp @@ -8,6 +8,7 @@ #include "account.h" #include "folder.h" +#include "buttonstyle.h" #include "common/filesystembase.h" #include "common/utility.h" @@ -74,6 +75,8 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, Q_ASSERT(_account); Q_ASSERT(_folder); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + const auto filePathFileInfo = QFileInfo(_filePath); const auto conflictFileName = filePathFileInfo.fileName(); @@ -89,6 +92,7 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, _ui->setupUi(this); _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Rename file")); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because of a case clash conflict with an existing file on this system.").arg(_originalFileName)); _ui->explanationLabel->setText(tr("%1 does not support equal file names with only letter casing differences.").arg(QSysInfo::prettyProductName())); @@ -140,10 +144,13 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No); if (_conflictSolver.allowedToRename()) { _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + _ui->filenameLineEdit->setEnabled(true); _ui->filenameLineEdit->selectAll(); } else { _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); } }); @@ -157,6 +164,8 @@ CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account, }); checkIfAllowedToRename(); + + customizeStyle(); } CaseClashFilenameDialog::~CaseClashFilenameDialog() = default; @@ -276,5 +285,43 @@ void CaseClashFilenameDialog::onFilenameLineEditTextChanged(const QString &text) _ui->buttonBox->button(QDialogButtonBox::Ok) ->setEnabled(isTextValid); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + } + +void CaseClashFilenameDialog::customizeStyle() +{ + this->setStyleSheet( + QStringLiteral("QDialog {background-color: %1; color: %2;} QLabel{ %3;}").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.titleColor(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + _ui->filenameLineEdit->setStyleSheet( + QStringLiteral( + "color: %1; font-family: %2; font-size: %3; font-weight: %4; border-radius: %5; border: 1px " + "solid %6; padding: 0px 12px; text-align: left; vertical-align: middle; height: 40px; background: %7; ").arg( + WLTheme.folderWizardPathColor(), + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.buttonRadius(), + WLTheme.menuBorderColor(), + WLTheme.dialogBackgroundColor() + ) + ); + + #ifdef Q_OS_MAC + _ui->buttonBox->layout()->setSpacing(24); + _ui->buttonBox->setLayoutDirection(Qt::LeftToRight); + #endif +} + } diff --git a/src/gui/caseclashfilenamedialog.h b/src/gui/caseclashfilenamedialog.h index 295c482f0fc9d..115fcb1052ace 100644 --- a/src/gui/caseclashfilenamedialog.h +++ b/src/gui/caseclashfilenamedialog.h @@ -68,5 +68,7 @@ private slots: QString _relativeFilePath; QString _originalFileName; QString _newFilename; + + void customizeStyle(); }; } diff --git a/src/gui/caseclashfilenamedialog.ui b/src/gui/caseclashfilenamedialog.ui index a5944fc709886..7477525cbe60a 100644 --- a/src/gui/caseclashfilenamedialog.ui +++ b/src/gui/caseclashfilenamedialog.ui @@ -120,6 +120,9 @@ Open existing file + + true + @@ -226,6 +229,9 @@ Open clashing file + + true + diff --git a/src/gui/clickablelabel.h b/src/gui/clickablelabel.h new file mode 100644 index 0000000000000..f0ae1628cb596 --- /dev/null +++ b/src/gui/clickablelabel.h @@ -0,0 +1,28 @@ +#ifndef CLICKABLELABEL_H +#define CLICKABLELABEL_H + +#include +#include +#include + +class ClickableLabel : public QLabel { + Q_OBJECT + +public: + explicit ClickableLabel(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) + : QLabel(parent, f) {} + explicit ClickableLabel(const QString &text, QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()) + : QLabel(text, parent, f) {} +signals: + void clicked(); + +protected: + void mousePressEvent(QMouseEvent* event) override { + if (event->button() == Qt::LeftButton) { + emit clicked(); + } + QLabel::mousePressEvent(event); + } +}; + +#endif // CLICKABLELABEL_H \ No newline at end of file diff --git a/src/gui/cloudproviders/cloudproviderwrapper.cpp b/src/gui/cloudproviders/cloudproviderwrapper.cpp index 5d0dad7ff2feb..9acef0e4690b7 100644 --- a/src/gui/cloudproviders/cloudproviderwrapper.cpp +++ b/src/gui/cloudproviders/cloudproviderwrapper.cpp @@ -130,7 +130,7 @@ void CloudProviderWrapper::slotUpdateProgress(const QString &folder, const Progr qint64 currentFile = progress.currentFile(); qint64 totalFileCount = qMax(progress.totalFiles(), currentFile); if (progress.trustEta()) { - msg = tr("Syncing %1 of %2 (%3 left)") + msg = tr("Syncing %1 of %2 (%3 left)") .arg(currentFile) .arg(totalFileCount) .arg(Utility::durationToDescriptiveString2(progress.totalProgress().estimatedEta)); diff --git a/src/gui/conflictdialog.cpp b/src/gui/conflictdialog.cpp index 5b56c2987e768..47c481e25e3b7 100644 --- a/src/gui/conflictdialog.cpp +++ b/src/gui/conflictdialog.cpp @@ -8,6 +8,7 @@ #include "conflictsolver.h" #include "common/utility.h" +#include "buttonstyle.h" #include #include @@ -42,8 +43,10 @@ ConflictDialog::ConflictDialog(QWidget *parent) { _ui->setupUi(this); forceHeaderFont(_ui->conflictMessage); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Keep selected version")); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); _ui->conflictMessage->setTextFormat(Qt::PlainText); @@ -59,6 +62,8 @@ ConflictDialog::ConflictDialog(QWidget *parent) connect(_solver, &ConflictSolver::localVersionFilenameChanged, this, &ConflictDialog::updateWidgets); connect(_solver, &ConflictSolver::remoteVersionFilenameChanged, this, &ConflictDialog::updateWidgets); + + customizeStyle(); } QString ConflictDialog::baseFilename() const @@ -170,6 +175,37 @@ void ConflictDialog::updateButtonStates() : isRemotePicked ? tr("Keep server version") : tr("Keep selected version"); _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(text); + _ui->buttonBox->button(QDialogButtonBox::Ok)->setProperty("buttonStyle", QVariant::fromValue(ButtonStyleName::Primary)); + +} + +void ConflictDialog::customizeStyle() +{ + this->setStyleSheet( + QStringLiteral("QDialog {background-color: %1; color: %2;} QLabel{ %3;}").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.titleColor(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + #ifdef Q_OS_MAC + _ui->buttonBox->layout()->setSpacing(24); + _ui->buttonBox->setLayoutDirection(Qt::LeftToRight); + + _ui->localVersionRadio->setStyleSheet( + QStringLiteral("QCheckBox {color: %1;}").arg(WLTheme.titleColor()) + ); + + _ui->remoteVersionRadio->setStyleSheet( + QStringLiteral("QCheckBox {color: %1;}").arg(WLTheme.titleColor()) + ); + #endif } } // namespace OCC diff --git a/src/gui/conflictdialog.h b/src/gui/conflictdialog.h index f17bcaa986b57..905e5c2da68f1 100644 --- a/src/gui/conflictdialog.h +++ b/src/gui/conflictdialog.h @@ -37,6 +37,7 @@ public slots: private: void updateWidgets(); void updateButtonStates(); + void customizeStyle(); QString _baseFilename; QScopedPointer _ui; diff --git a/src/gui/conflictsolver.cpp b/src/gui/conflictsolver.cpp index 1bdd584055b9b..c7f56565612c6 100644 --- a/src/gui/conflictsolver.cpp +++ b/src/gui/conflictsolver.cpp @@ -5,11 +5,14 @@ #include "conflictsolver.h" +#include #include #include #include "common/utility.h" #include "filesystem.h" +#include "buttonstyle.h" +#include "whitelabeltheme.h" namespace OCC { @@ -196,8 +199,27 @@ bool ConflictSolver::confirmDeletion() QFileInfo info(_localVersionFilename); const auto message = FileSystem::isDir(_localVersionFilename) ? tr("Do you want to delete the directory %1 and all its contents permanently?").arg(Utility::escape(info.dir().dirName())) - : tr("Do you want to delete the file %1 permanently?").arg(Utility::escape(info.fileName())); - const auto result = QMessageBox::question(_parentWidget, tr("Confirm deletion"), message, buttons); + : tr("Do you want to delete the file %1 permanently?").arg(Utility::escape(info.fileName())); + + QMessageBox msgBox(_parentWidget); + msgBox.setWindowTitle(tr("Confirm deletion")); + msgBox.setText(message); + msgBox.setStandardButtons(buttons); + msgBox.button(QMessageBox::Yes)->setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::Primary)); + + msgBox.setStyleSheet( + QStringLiteral("QMessageBox { background-color: %1; } QLabel { %2 }").arg( + WLTheme.dialogBackgroundColor(), + WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.titleColor() + ) + ) + ); + + const auto result = static_cast(msgBox.exec()); switch (result) { case QMessageBox::YesToAll: diff --git a/src/gui/creds/webflowcredentials.cpp b/src/gui/creds/webflowcredentials.cpp index 2bcb7ac49b1d9..198fcdd26bb0b 100644 --- a/src/gui/creds/webflowcredentials.cpp +++ b/src/gui/creds/webflowcredentials.cpp @@ -169,8 +169,8 @@ void WebFlowCredentials::askFromUser() { _askDialog->setUrl(url); } - QString msg = tr("You have been logged out of your account %1 at %2. Please login again.") - .arg(_account->prettyName(), _account->url().toDisplayString()); + QString msg = tr("You have been logged out of your account %1 at %2. Please login again.") + .arg(_account->eliedName(200), _account->url().toDisplayString()); _askDialog->setInfo(msg); _askDialog->show(); diff --git a/src/gui/creds/webflowcredentialsdialog.cpp b/src/gui/creds/webflowcredentialsdialog.cpp index e37f755e90bbc..01c4c76acd226 100644 --- a/src/gui/creds/webflowcredentialsdialog.cpp +++ b/src/gui/creds/webflowcredentialsdialog.cpp @@ -11,7 +11,7 @@ #include "application.h" #include "owncloudgui.h" #include "wizard/owncloudwizardcommon.h" - +#include "whitelabeltheme.h" #ifdef WITH_WEBENGINE #include "wizard/webview.h" #endif // WITH_WEBENGINE @@ -29,6 +29,11 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setStyleSheet(QStringLiteral("QDialog { background-color: %1; }").arg(WLTheme.dialogBackgroundColor())); + + setFixedWidth(646); + setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); + _layout = new QVBoxLayout(this); int spacing = _layout->spacing(); auto margin = _layout->contentsMargins(); @@ -40,12 +45,24 @@ WebFlowCredentialsDialog::WebFlowCredentialsDialog(Account *account, bool useFlo _containerLayout->setContentsMargins(margin); _infoLabel = new QLabel(); - _infoLabel->setTextFormat(Qt::PlainText); + _infoLabel->setTextFormat(Qt::RichText); _infoLabel->setAlignment(Qt::AlignCenter); + _infoLabel->setWordWrap(true); + _infoLabel->setContentsMargins(0, 32, 0, 0); + _infoLabel->setStyleSheet(WLTheme.fontConfigurationCss( + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTitleWeight600(), + WLTheme.titleColor() + )); _containerLayout->addWidget(_infoLabel); + layout()->setSizeConstraint(QLayout::SetFixedSize); + if (_useFlow2) { _flow2AuthWidget = new Flow2AuthWidget(); + _flow2AuthWidget->shrinkTopMarginForText(); + _containerLayout->addWidget(_flow2AuthWidget); connect(_flow2AuthWidget, &Flow2AuthWidget::authResult, this, &WebFlowCredentialsDialog::slotFlow2AuthResult); @@ -122,7 +139,7 @@ void WebFlowCredentialsDialog::setError(const QString &error) { slotShowSettingsDialog(); if (_useFlow2 && _flow2AuthWidget) { - _flow2AuthWidget->setError(error); + _flow2AuthWidget->setError("Error", error); return; } diff --git a/src/gui/filedetails/FileActivityView.qml b/src/gui/filedetails/FileActivityView.qml index aa866baa9c599..b57a4ced7bba8 100644 --- a/src/gui/filedetails/FileActivityView.qml +++ b/src/gui/filedetails/FileActivityView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" diff --git a/src/gui/filedetails/FileDetailsPage.qml b/src/gui/filedetails/FileDetailsPage.qml index c061eff726883..8223b2e0e95e5 100644 --- a/src/gui/filedetails/FileDetailsPage.qml +++ b/src/gui/filedetails/FileDetailsPage.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" @@ -36,7 +36,12 @@ Page { localPath: root.localPath } - Connections { + font.family: Style.sesOpenSansRegular + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + palette.windowText: Style.sesTrayFontColor + + Connections { target: Systray function onShowFileDetailsPage(fileLocalPath, page) { if (!root.fileDetails.sharingAvailable && page == Systray.FileDetailsPage.Sharing) { @@ -60,12 +65,11 @@ Page { bottomPadding: intendedPadding background: Rectangle { - color: palette.base - visible: root.backgroundsVisible + color: Style.sesBackgroundColor } header: ColumnLayout { - spacing: root.intendedPadding + spacing: Style.sesMediumMargin GridLayout { id: headerGridLayout @@ -99,7 +103,7 @@ Page { Layout.rowSpan: headerGridLayout.rows Layout.preferredWidth: Style.trayListItemIconSize - Layout.leftMargin: root.intendedPadding + Layout.leftMargin: Style.sesMediumMargin Layout.fillHeight: true verticalAlignment: Image.AlignVCenter @@ -114,26 +118,34 @@ Page { id: fileNameLabel Layout.fillWidth: true - Layout.rightMargin: headerGridLayout.textRightMargin + Layout.rightMargin: Style.sesFileDetailsHeaderModifier text: root.fileDetails.name - font.bold: true + + font.pixelSize: Style.sesFontPixelSizeTitle + font.weight: Style.sesFontBoldWeight + wrapMode: Text.Wrap } - Button { + IconButton { id: closeButton + customHoverEnabled: false + Layout.rowSpan: headerGridLayout.rows - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width Layout.rightMargin: headerGridLayout.textRightMargin - icon.source: "image://svgimage-custom-color/clear.svg" + "/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize + iconSource: Style.sesAccountQuit + toolTipText: qsTr("Dismiss") + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + visible: root.showCloseButton + onClicked: root.closeButtonClicked() } @@ -141,9 +153,13 @@ Page { id: fileDetailsLabel Layout.fillWidth: true - Layout.rightMargin: headerGridLayout.textRightMargin + Layout.rightMargin: Style.sesFileDetailsHeaderModifier + + text: `${root.fileDetails.sizeString}, ${root.fileDetails.lastChangedString}` + + font.pixelSize: Style.sesFontHintPixelSize + font.weight: Style.sesFontNormalWeight - text: `${root.fileDetails.sizeString} · ${root.fileDetails.lastChangedString}` wrapMode: Text.Wrap } @@ -154,8 +170,12 @@ Page { Layout.rightMargin: headerGridLayout.textRightMargin text: root.fileDetails.lockExpireString + color: palette.midlight wrapMode: Text.Wrap visible: headerGridLayout.showFileLockedString + + font.pixelSize: Style.sesFontHintPixelSize + font.weight: Style.sesFontNormalWeight } Row { @@ -198,38 +218,24 @@ Page { ToolTip { popupType: Qt.platform.os === "windows" ? Popup.Item : Popup.Native visible: hoverHandler.hovered - text: tagRepeater.fileTagModel.overflowTagsString + + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + background: Rectangle { + color: Style.sesBackgroundColor + border.color: Style.sesBorderColor + border.width: 1 + radius: 4 + } + contentItem: Text { + text: tagRepeater.fileTagModel.overflowTagsString + color: Style.sesTrayFontColor + font.family: Style.sesOpenSansRegular + } } } } } - - TabBar { - id: viewBar - - Layout.leftMargin: root.intendedPadding - Layout.rightMargin: root.intendedPadding - - padding: 0 - background: null - - NCTabButton { - svgCustomColorSource: "image://svgimage-custom-color/activity.svg" - text: qsTr("Activity") - checked: swipeView.currentIndex === fileActivityView.swipeIndex - onClicked: swipeView.currentIndex = fileActivityView.swipeIndex - } - - NCTabButton { - width: visible ? implicitWidth : 0 - height: visible ? implicitHeight : 0 - svgCustomColorSource: "image://svgimage-custom-color/share.svg" - text: qsTr("Sharing") - checked: swipeView.currentIndex === shareViewLoader.swipeIndex - onClicked: swipeView.currentIndex = shareViewLoader.swipeIndex - visible: root.fileDetails.sharingAvailable - } - } } SwipeView { @@ -238,18 +244,6 @@ Page { anchors.fill: parent clip: true - FileActivityView { - id: fileActivityView - - readonly property int swipeIndex: SwipeView.index - - delegateHorizontalPadding: root.intendedPadding - - accountState: root.accountState - localPath: root.localPath - iconSize: root.iconSize - } - Loader { id: shareViewLoader diff --git a/src/gui/filedetails/FileDetailsView.qml b/src/gui/filedetails/FileDetailsView.qml index b3b8fe83bb36b..cf872047934e1 100644 --- a/src/gui/filedetails/FileDetailsView.qml +++ b/src/gui/filedetails/FileDetailsView.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style StackView { @@ -22,7 +22,7 @@ StackView { property bool backgroundsVisible: true background: Rectangle { - color: palette.base + color: palette.window visible: root.backgroundsVisible } diff --git a/src/gui/filedetails/FileDetailsWindow.qml b/src/gui/filedetails/FileDetailsWindow.qml index 915b9871be603..8d4c76e3d959c 100644 --- a/src/gui/filedetails/FileDetailsWindow.qml +++ b/src/gui/filedetails/FileDetailsWindow.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style ApplicationWindow { @@ -17,9 +17,6 @@ ApplicationWindow { property var accountState property string localPath: "" - LayoutMirroring.enabled: Application.layoutDirection === Qt.RightToLeft - LayoutMirroring.childrenInherit: true - width: 400 height: 500 minimumWidth: 300 diff --git a/src/gui/filedetails/NCInputDateField.qml b/src/gui/filedetails/NCInputDateField.qml index e4be486a95a7e..c9c339c7d7c4f 100644 --- a/src/gui/filedetails/NCInputDateField.qml +++ b/src/gui/filedetails/NCInputDateField.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient NCInputTextField { id: root diff --git a/src/gui/filedetails/NCInputTextArea.qml b/src/gui/filedetails/NCInputTextArea.qml index da1eb1ca8a413..dd310e6728b0e 100644 --- a/src/gui/filedetails/NCInputTextArea.qml +++ b/src/gui/filedetails/NCInputTextArea.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style TextArea { @@ -35,6 +35,8 @@ TextArea { width: height height: parent.height + background: null + flat: true icon.source: "image://svgimage-custom-color/confirm.svg" + "/" + root.secondaryColor icon.color: hovered && enabled ? UserModel.currentUser.accentColor : root.secondaryColor diff --git a/src/gui/filedetails/NCInputTextField.qml b/src/gui/filedetails/NCInputTextField.qml index 45a06b6eb064f..f22ee1018e5b9 100644 --- a/src/gui/filedetails/NCInputTextField.qml +++ b/src/gui/filedetails/NCInputTextField.qml @@ -7,7 +7,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style TextField { @@ -23,6 +23,14 @@ TextField { rightPadding: submitButton.width selectByMouse: true + + background: Rectangle { + id: textFieldBorder + radius: Style.slightlyRoundedButtonRadius + border.width: Style.normalBorderWidth + border.color: root.activeFocus ? root.validInput ? root.accentColor : Style.errorBoxBackgroundColor : root.secondaryColor + color: palette.base + } Button { id: submitButton diff --git a/src/gui/filedetails/NCTabButton.qml b/src/gui/filedetails/NCTabButton.qml index c68cb91363ad8..b70d8a9f5c6b9 100644 --- a/src/gui/filedetails/NCTabButton.qml +++ b/src/gui/filedetails/NCTabButton.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" diff --git a/src/gui/filedetails/ShareDelegate.qml b/src/gui/filedetails/ShareDelegate.qml index 16407e5f323a9..901a28f9c07d7 100644 --- a/src/gui/filedetails/ShareDelegate.qml +++ b/src/gui/filedetails/ShareDelegate.qml @@ -9,7 +9,7 @@ import QtQuick.Layouts import QtQuick.Controls import Qt5Compat.GraphicalEffects -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" import "../" @@ -40,7 +40,7 @@ GridLayout { property FileDetails fileDetails: FileDetails {} property StackView rootStackView: StackView {} property bool backgroundsVisible: true - property color accentColor: Style.ncBlue + property color accentColor: Style.sesIconColor property bool canCreateLinkShares: true property bool serverAllowsResharing: true @@ -130,6 +130,7 @@ GridLayout { Layout.column: 1 text: root.detailText + elide: Text.ElideRight visible: text !== "" } @@ -142,17 +143,17 @@ GridLayout { spacing: 0 - Button { + IconButton { id: createLinkButton Layout.alignment: Qt.AlignCenter - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - - icon.source: "image://svgimage-custom-color/add.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: AbstractButton.IconOnly + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width + + iconSource: Style.sesDarkPlus + palette.buttonText + icon.width: Style.smallIconSize + icon.height: Style.smallIconSize + customHoverEnabled: false visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares enabled: visible @@ -160,7 +161,7 @@ GridLayout { onClicked: root.createNewLinkShare() } - Button { + SecondaryPillButton { id: copyLinkButton function copyShareLink() { @@ -175,18 +176,26 @@ GridLayout { property bool shareLinkCopied: false - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: shareLinkCopied ? implicitWidth : Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight + removeBorder: true + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: shareLinkCopied ? implicitWidth : Style.iconButtonWidth + Layout.preferredHeight: Style.iconButtonWidth + Layout.rightMargin: Style.sesSmallMargin text: shareLinkCopied ? qsTr("Copied!") : "" + textColor: Style.sesDarkGreen + backgroundColor: Style.clipboardBackgroundColor + + iconSource: shareLinkCopied ? Style.sesSyncSuccessIcon + Style.positiveColor : + Style.sesClipboard + palette.brightText + + icon.width: Style.smallIconSize + icon.height: Style.smallIconSize - icon.source: "image://svgimage-custom-color/copy.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: shareLinkCopied ? AbstractButton.TextOnly : AbstractButton.IconOnly visible: root.isLinkShare || root.isInternalLinkShare enabled: visible + onClicked: copyShareLink() Behavior on Layout.preferredWidth { @@ -205,19 +214,26 @@ GridLayout { } } - Button { + IconButton { id: moreButton - Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight + property bool isHovered: moreButton.hovered || moreButton.visualFocus + property bool isActive: moreButton.pressed + + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width + + toolTipText: qsTr("Share options") + + iconSource: Style.sesMore + iconSourceHovered: Style.sesMoreHover + width: Style.smallIconSize + height: Style.smallIconSize - icon.source: "image://svgimage-custom-color/more.svg/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize - display: AbstractButton.IconOnly visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare enabled: visible + onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition) Component { diff --git a/src/gui/filedetails/ShareDetailsPage.qml b/src/gui/filedetails/ShareDetailsPage.qml index 39fbae87a22cd..e7d15f9e78f65 100644 --- a/src/gui/filedetails/ShareDetailsPage.qml +++ b/src/gui/filedetails/ShareDetailsPage.qml @@ -9,14 +9,44 @@ import QtQuick.Layouts import QtQuick.Controls import Qt5Compat.GraphicalEffects -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" +import "../SesComponents/" import "../" Page { id: root + component SesCheckBox: CheckBox { + hoverEnabled: false + palette.base: Style.sesBackgroundColor + contentItem: Text { + text: parent.text + color: Style.sesTrayFontColor + font: parent.font + leftPadding: parent.indicator.width + parent.spacing + verticalAlignment: Text.AlignVCenter + } + indicator: Rectangle { + implicitWidth: 18 + implicitHeight: 18 + radius: 3 + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + + Rectangle { + visible: parent.parent.checked + anchors.centerIn: parent + width: 10 + height: 10 + radius: 2 + color: root.accentColor + } + } + } + signal closeShareDetails signal deleteShare signal createNewLinkShare @@ -34,6 +64,12 @@ Page { signal setPassword(string password) signal setNote(string note) + + font.family: Style.sesOpenSansRegular + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + palette.text: Style.sesTrayFontColor + property bool backgroundsVisible: true property color accentColor: Style.ncBlue @@ -71,6 +107,7 @@ Page { readonly property bool isHideDownloadInProgress: shareModelData.isHideDownloadInProgress readonly property int currentPermissionMode: shareModelData.currentPermissionMode + readonly property bool isInternalShare: shareModelData.shareType === ShareModel.ShareTypeUser || shareModelData.shareType === ShareModel.ShareTypeGroup || shareModelData.shareType === ShareModel.ShareTypeCircle readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink readonly property bool isEmailShare: shareModelData.shareType === ShareModel.ShareTypeEmail readonly property bool shareSupportsPassword: isLinkShare || isEmailShare @@ -85,6 +122,16 @@ Page { property bool waitingForPasswordChange: false property bool waitingForNoteChange: false + readonly property int titlePixelSize: Style.sesFontPixelSizeTitle + readonly property int titleFontWeight: Style.sesFontNormalWeight + + readonly property int hintPixelSize: Style.sesFontHintPixelSize + readonly property int hintFontWeight: Style.sesFontNormalWeight + + + readonly property int pixelSize: Style.sesFontPixelSize + readonly property int fontWeight: Style.sesFontNormalWeight + function showPasswordSetError(message) { passwordErrorBoxLoader.message = message !== "" ? message : qsTr("An error occurred setting the share password."); @@ -96,8 +143,8 @@ Page { } function resetLinkShareLabelField() { - linkShareLabelTextField.text = linkShareLabel; - waitingForLinkShareLabelChange = false; + // linkShareLabelTextField.text = linkShareLabel; + // waitingForLinkShareLabelChange = false; } function resetPasswordField() { @@ -165,8 +212,7 @@ Page { padding: Style.standardSpacing * 2 background: Rectangle { - color: palette.base - visible: root.backgroundsVisible + color: Style.sesBackgroundColor } header: ColumnLayout { @@ -201,38 +247,46 @@ Page { } EnforcedPlainTextLabel { - id: headLabel + id: fileNameLabel Layout.fillWidth: true + Layout.rightMargin: headerGridLayout.textRightMargin - text: qsTr("Edit share") - font.bold: true - elide: Text.ElideRight + text: root.fileDetails.name + color: Style.sesTrayFontColor + font.pixelSize: titlePixelSize + font.weight: titleFontWeight + + wrapMode: Text.Wrap } - Button { - id: closeButton + IconButton { + id: placeholder + + customHoverEnabled: false Layout.rowSpan: headerGridLayout.rows - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + Layout.preferredWidth: Style.iconButtonWidth + Layout.preferredHeight: width Layout.rightMargin: root.padding - icon.source: "image://svgimage-custom-color/clear.svg" + "/" + palette.buttonText - icon.width: Style.activityListButtonIconSize - icon.height: Style.activityListButtonIconSize + iconSource: Style.sesAccountQuit + onClicked: root.closeShareDetails() } EnforcedPlainTextLabel { - id: secondaryLabel + id: fileDetailsLabel Layout.fillWidth: true - Layout.rightMargin: root.padding + Layout.rightMargin: headerGridLayout.textRightMargin - text: root.fileDetails.name + text: `${root.fileDetails.sizeString}, ${root.fileDetails.lastChangedString}` wrapMode: Text.Wrap + color: Style.sesTrayFontColor + + font.pixelSize: hintPixelSize + font.weight: hintFontWeight } } } @@ -250,207 +304,14 @@ Page { readonly property int itemPadding: Style.smallSpacing width: parent.width - spacing: Style.smallSpacing - - RowLayout { - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing - - visible: root.isLinkShare - - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/label.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth - } - - NCInputTextField { - id: linkShareLabelTextField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - text: root.linkShareLabel - placeholderText: qsTr("Share label") - - enabled: root.isLinkShare && - !root.waitingForLinkShareLabelChange - - onAccepted: if(text !== root.linkShareLabel) { - root.setLinkShareLabel(text); - root.waitingForLinkShareLabelChange = true; - } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForLinkShareLabelChange - running: visible - z: 1 - } - } - } - - Loader { - Layout.fillWidth: true - active: !root.isFolderItem && !root.isEncryptedItem - visible: active - sourceComponent: CheckBox { - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checkable: true - checked: root.editingAllowed - text: qsTr("Allow upload and editing") - enabled: !root.isSharePermissionChangeInProgress - - onClicked: root.toggleAllowEditing(checked) - - NCBusyIndicator { - anchors.fill: parent - visible: root.isSharePermissionChangeInProgress - running: visible - z: 1 - } - } - } - - Loader { - Layout.fillWidth: true - active: root.isFolderItem && !root.isEncryptedItem - visible: active - sourceComponent: ColumnLayout { - id: permissionRadioButtonsLayout - spacing: Layout.smallSpacing - width: parent.width - - ButtonGroup { - id: permissionModeRadioButtonsGroup - } - - RadioButton { - readonly property int permissionMode: ShareModel.ModeViewOnly - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("View only") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - RadioButton { - readonly property int permissionMode: ShareModel.ModeUploadAndEditing - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("Allow upload and editing") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - - RadioButton { - readonly property int permissionMode: ShareModel.ModeFileDropOnly - Layout.fillWidth: true - ButtonGroup.group: permissionModeRadioButtonsGroup - enabled: !root.isSharePermissionChangeInProgress - checked: root.currentPermissionMode === permissionMode - text: qsTr("File drop (upload only)") - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - onClicked: root.permissionModeChanged(permissionMode) - } - - CheckBox { - id: allowResharingCheckBox - - Layout.fillWidth: true - - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checkable: true - checked: root.resharingAllowed - text: qsTr("Allow resharing") - enabled: !root.isSharePermissionChangeInProgress && root.serverAllowsResharing - visible: root.serverAllowsResharing - onClicked: root.toggleAllowResharing(checked); - - Connections { - target: root - onResharingAllowedChanged: allowResharingCheckBox.checked = root.resharingAllowed - } - } - } - - NCBusyIndicator { - anchors.fill: parent - visible: root.isSharePermissionChangeInProgress - running: visible - z: 1 - } - } - - Loader { - Layout.fillWidth: true - - active: root.isLinkShare - visible: active - sourceComponent: ColumnLayout { - CheckBox { - id: hideDownloadEnabledMenuItem - - anchors.left: parent.left - anchors.right: parent.right - - spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding - indicator.width: scrollContentsColumn.indicatorItemWidth - indicator.height: scrollContentsColumn.indicatorItemWidth - - checked: root.hideDownload - text: qsTr("Hide download") - enabled: !root.isHideDownloadInProgress - onClicked: root.toggleHideDownload(checked); - - NCBusyIndicator { - anchors.fill: parent - visible: root.isHideDownloadInProgress - running: visible - z: 1 - } - } - } - } - - CheckBox { + SesCheckBox { id: passwordProtectEnabledMenuItem Layout.fillWidth: true spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding + padding: scrollContentsColumn.itemPadding indicator.width: scrollContentsColumn.indicatorItemWidth indicator.height: scrollContentsColumn.indicatorItemWidth @@ -466,62 +327,6 @@ Page { root.togglePasswordProtect(checked); root.waitingForPasswordProtectEnabledChange = true; } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForPasswordProtectEnabledChange - running: visible - z: 1 - } - } - - RowLayout { - Layout.fillWidth: true - - height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing - - visible: root.shareSupportsPassword && root.passwordProtectEnabled - - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/lock-https.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth - } - - NCInputTextField { - id: passwordTextField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - text: root.password !== "" ? root.password : root.passwordPlaceholder - enabled: visible && - root.passwordProtectEnabled && - !root.waitingForPasswordChange && - !root.waitingForPasswordProtectEnabledChange - - onAccepted: if(text !== root.password && text !== root.passwordPlaceholder) { - passwordErrorBoxLoader.message = ""; - root.setPassword(text); - root.waitingForPasswordChange = true; - } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForPasswordChange || - root.waitingForPasswordProtectEnabledChange - running: visible - z: 1 - } - } } Loader { @@ -542,7 +347,7 @@ Page { // Artificially add vertical padding implicitHeight: passwordErrorBox.implicitHeight + (Style.smallSpacing * 2) - ErrorBox { + SesErrorBox { id: passwordErrorBox anchors.left: parent.left anchors.right: parent.right @@ -553,14 +358,54 @@ Page { } } - CheckBox { + TextEdit { + id: passwordTextEdit + visible: root.passwordProtectEnabled + Layout.fillWidth: true + Layout.leftMargin: 3 + Layout.rightMargin: 3 + height: visible ? 64 : 0 + wrapMode: TextEdit.Wrap + selectByMouse: true + text: root.password !== "" ? root.password : root.passwordPlaceholder + color: Style.sesTrayFontColor + + font.family: root.font.family + font.pixelSize: pixelSize + font.weight: fontWeight + + padding: scrollContentsColumn.itemPadding + enabled: visible && + root.passwordProtectEnabled && + !root.waitingForPasswordChange && + !root.waitingForPasswordProtectEnabledChange + + onEditingFinished: if(text !== root.password && text !== root.passwordPlaceholder) { + passwordErrorBoxLoader.message = ""; + root.setPassword(text); + root.waitingForPasswordChange = true; + } + + Rectangle { + id: passwordTextBorder + anchors.fill: parent + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 + } + } + + SesCheckBox { id: expireDateEnabledMenuItem Layout.fillWidth: true + font.pixelSize: pixelSize + font.weight: fontWeight spacing: scrollContentsColumn.indicatorSpacing - leftPadding: scrollContentsColumn.itemPadding - rightPadding: scrollContentsColumn.itemPadding + padding: scrollContentsColumn.itemPadding indicator.width: scrollContentsColumn.indicatorItemWidth indicator.height: scrollContentsColumn.indicatorItemWidth @@ -573,72 +418,56 @@ Page { root.toggleExpirationDate(checked); root.waitingForExpireDateEnabledChange = true; } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForExpireDateEnabledChange - running: visible - z: 1 - } } - RowLayout { + NCInputDateField { + id: expireDateField + + font.pixelSize: pixelSize + font.weight: fontWeight + Layout.fillWidth: true + Layout.leftMargin: 3 + Layout.rightMargin: 3 height: visible ? implicitHeight : 0 - spacing: scrollContentsColumn.indicatorSpacing + leftPadding: 15 visible: root.expireDateEnabled - Image { - Layout.preferredWidth: scrollContentsColumn.indicatorItemWidth - Layout.fillHeight: true - - verticalAlignment: Image.AlignVCenter - horizontalAlignment: Image.AlignHCenter - fillMode: Image.Pad - - source: "image://svgimage-custom-color/calendar.svg/" + palette.windowText - sourceSize.width: scrollContentsColumn.rowIconWidth - sourceSize.height: scrollContentsColumn.rowIconWidth + selectByMouse: true + + dateInMs: root.expireDate + maximumDateMs: root.maximumExpireDate + minimumDateMs: { + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth(); + const currentMonthDay = currentDate.getDate(); + // Start of day at 00:00:0000 UTC + return Date.UTC(currentYear, currentMonth, currentMonthDay + 1); } - NCInputDateField { - id: expireDateField - - Layout.fillWidth: true - height: visible ? implicitHeight : 0 - - dateInMs: root.expireDate - maximumDateMs: root.maximumExpireDate - minimumDateMs: { - const currentDate = new Date(); - const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth(); - const currentMonthDay = currentDate.getDate(); - // Start of day at 00:00:0000 UTC - return Date.UTC(currentYear, currentMonth, currentMonthDay + 1); - } - - enabled: root.expireDateEnabled && - !root.waitingForExpireDateChange && - !root.waitingForExpireDateEnabledChange + enabled: root.expireDateEnabled && + !root.waitingForExpireDateChange && + !root.waitingForExpireDateEnabledChange - onUserAcceptedDate: { - root.setExpireDate(dateInMs); - root.waitingForExpireDateChange = true; - } + onUserAcceptedDate: { + root.setExpireDate(dateInMs); + root.waitingForExpireDateChange = true; + } - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForExpireDateEnabledChange || - root.waitingForExpireDateChange - running: visible - z: 1 - } + Rectangle { + id: dateTextBorder + anchors.fill: parent + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 } } - - CheckBox { + + SesCheckBox { id: noteEnabledMenuItem Layout.fillWidth: true @@ -660,13 +489,6 @@ Page { root.waitingForNoteChange = true; } } - - NCBusyIndicator { - anchors.fill: parent - visible: root.waitingForNoteChange && !noteEnabledMenuItem.checked - running: visible - z: 1 - } } RowLayout { @@ -698,6 +520,7 @@ Page { text: root.note placeholderText: qsTr("Enter a note for the recipient") + color: Style.sesTrayFontColor enabled: noteEnabledMenuItem.checked && !root.waitingForNoteChange onEditingFinished: if (text !== "" && text !== root.note) { @@ -705,89 +528,284 @@ Page { root.waitingForNoteChange = true; } - NCBusyIndicator { + background: Rectangle { + id: noteTextBorder anchors.fill: parent - visible: root.waitingForNoteChange && noteEnabledMenuItem.checked - running: visible - z: 1 + radius: Style.slightlyRoundedButtonRadius + border.width: Style.thickBorderWidth + border.color: Style.sesTrayInputField + color: Style.sesBackgroundColor + z: -1 } } + } + + Loader { + Layout.fillWidth: true + active: !root.isFolderItem && !root.isEncryptedItem + visible: active + sourceComponent: SesCheckBox { + + font.pixelSize: pixelSize + font.weight: fontWeight + + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checkable: true + checked: root.editingAllowed + text: qsTr("Allow upload and editing") + enabled: !root.isSharePermissionChangeInProgress + + onClicked: root.toggleAllowEditing(checked) + } } - Button { - height: Style.standardPrimaryButtonHeight - icon.source: "image://svgimage-custom-color/close.svg/" + palette.buttonText - icon.height: Style.extraSmallIconSize - text: qsTr("Unshare") - onClicked: root.deleteShare() + Loader { + Layout.fillWidth: true + active: root.isFolderItem && !root.isEncryptedItem + visible: active + sourceComponent: ColumnLayout { + id: permissionRadioButtonsLayout + spacing: 0 + width: parent.width + + ButtonGroup { + id: permissionModeRadioButtonsGroup + } + + SesCheckBox { + id: customPermissionsCheckBox + Layout.fillWidth: true + enabled: !root.isSharePermissionChangeInProgress + checkable: false + checked: true + text: qsTr("Custom Permissions") + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + onClicked: root.permissionModeChanged(permissionMode) + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeViewOnly + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("View only") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeUploadAndEditing + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("Allow upload and editing") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked + font.pixelSize: pixelSize + font.weight: fontWeight + } + + SesCheckBox { + readonly property int permissionMode: ShareModel.ModeFileDropOnly + Layout.fillWidth: true + Layout.leftMargin: 30 + ButtonGroup.group: permissionModeRadioButtonsGroup + enabled: !root.isSharePermissionChangeInProgress + checked: root.currentPermissionMode === permissionMode + text: qsTr("File drop (upload only)") + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + onClicked: root.permissionModeChanged(permissionMode) + visible: customPermissionsCheckBox.checked && !root.isInternalShare // Removed SES-307 + font.pixelSize: pixelSize + font.weight: fontWeight + } + } } - Button { - height: Style.standardPrimaryButtonHeight - icon.source: "image://svgimage-custom-color/add.svg/" + palette.buttonText - icon.height: Style.extraSmallIconSize - text: qsTr("Add another link") - visible: root.isLinkShare && root.canCreateLinkShares - enabled: visible - onClicked: root.createNewLinkShare() + SesCheckBox { + id: allowResharingCheckBox + + Layout.fillWidth: true + + font.pixelSize: pixelSize + font.weight: fontWeight + + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checkable: true + checked: root.resharingAllowed + text: qsTr("Allow resharing") + enabled: !root.isSharePermissionChangeInProgress && root.serverAllowsResharing + visible: root.serverAllowsResharing + onClicked: root.toggleAllowResharing(checked); + + Connections { + target: root + onResharingAllowedChanged: allowResharingCheckBox.checked = root.resharingAllowed + } } - } - } - footer: DialogButtonBox { - topPadding: 0 - bottomPadding: root.padding - rightPadding: root.padding - leftPadding: root.padding - alignment: Qt.AlignRight | Qt.AlignVCenter - contentWidth: (contentItem as ListView).contentWidth - visible: copyShareLinkButton.visible + Loader { + Layout.fillWidth: true - background: Rectangle { color: "transparent" } + active: root.isLinkShare + visible: active + sourceComponent: ColumnLayout { + SesCheckBox { + id: hideDownloadEnabledMenuItem - Button { - id: copyShareLinkButton + anchors.left: parent.left + anchors.right: parent.right - function copyShareLink() { - clipboardHelper.text = root.link; - clipboardHelper.selectAll(); - clipboardHelper.copy(); - clipboardHelper.clear(); + font.pixelSize: pixelSize + font.weight: fontWeight - shareLinkCopied = true; - shareLinkCopyTimer.start(); + spacing: scrollContentsColumn.indicatorSpacing + padding: scrollContentsColumn.itemPadding + indicator.width: scrollContentsColumn.indicatorItemWidth + indicator.height: scrollContentsColumn.indicatorItemWidth + + checked: root.hideDownload + text: qsTr("Hide download") + enabled: !root.isHideDownloadInProgress + onClicked: root.toggleHideDownload(checked); + } + } } + } + } + + footer: ColumnLayout { + id: buttonGrid - property bool shareLinkCopied: false + spacing: 0 - height: Style.standardPrimaryButtonHeight + PrimaryPillButton { + iconSource: Style.sesLightPlus - Layout.preferredWidth: Style.activityListButtonWidth - Layout.preferredHeight: Style.activityListButtonHeight - Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + font.pixelSize: pixelSize + font.weight: fontWeight + text: qsTr("Add another link") - icon.source: "image://svgimage-custom-color/copy.svg/" + palette.brightText - icon.width: Style.smallIconSize - icon.height: Style.smallIconSize - text: shareLinkCopied ? qsTr("Share link copied!") : qsTr("Copy share link") - visible: root.isLinkShare + visible: root.isLinkShare && root.canCreateLinkShares enabled: visible - onClicked: copyShareLink() + Layout.leftMargin: 16 + Layout.bottomMargin: 16 + Layout.maximumWidth: actionButtonsGrid.width - Behavior on Layout.preferredWidth { - SmoothedAnimation { duration: Style.shortAnimationDuration } + onClicked: root.createNewLinkShare() + } + + GridLayout { + id: actionButtonsGrid + + readonly property bool buttonsOverflow: unshareButton.implicitWidth + copyShareLinkButton.implicitWidth + columnSpacing + 16 + 20 > root.width + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 20 + Layout.bottomMargin: 16 + columns: buttonsOverflow ? 1 : 3 + columnSpacing: 8 + rowSpacing: 8 + + SecondaryPillButton { + id: unshareButton + + Layout.maximumWidth: actionButtonsGrid.width + + font.pixelSize: pixelSize + font.weight: fontWeight + text: qsTr("Unshare") + + onClicked: root.deleteShare() } - TextEdit { - id: clipboardHelper - visible: false + Item { + Layout.fillWidth: true + visible: !actionButtonsGrid.buttonsOverflow } - Timer { - id: shareLinkCopyTimer - interval: Style.veryLongAnimationDuration - onTriggered: copyShareLinkButton.shareLinkCopied = false + PrimaryPillButton { + id: copyShareLinkButton + + function copyShareLink() { + clipboardHelper.text = root.link; + clipboardHelper.selectAll(); + clipboardHelper.copy(); + clipboardHelper.clear(); + + shareLinkCopied = true; + shareLinkCopyTimer.start(); + } + + property bool shareLinkCopied: false + property bool hasBeenClicked: false + + clip: true + iconSource: Style.sesLightClipboard + textWrapMode: widthAnimation.running ? Text.NoWrap : Text.WordWrap + + text: shareLinkCopied ? qsTr("Share link copied!") : qsTr("Copy share link") + + Layout.maximumWidth: actionButtonsGrid.width + + visible: root.isLinkShare + enabled: visible + + onClicked: { + hasBeenClicked = true; + copyShareLink(); + } + + Behavior on implicitWidth { + enabled: copyShareLinkButton.hasBeenClicked + SmoothedAnimation { + id: widthAnimation + duration: Style.shortAnimationDuration + } + } + + TextEdit { + id: clipboardHelper + visible: false + } + + Timer { + id: shareLinkCopyTimer + interval: Style.veryLongAnimationDuration + onTriggered: copyShareLinkButton.shareLinkCopied = false + } } } } diff --git a/src/gui/filedetails/ShareView.qml b/src/gui/filedetails/ShareView.qml index 9d593b9d295b3..a4d595a929aab 100644 --- a/src/gui/filedetails/ShareView.qml +++ b/src/gui/filedetails/ShareView.qml @@ -8,9 +8,10 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" +import "../SesComponents" import "../" ColumnLayout { @@ -78,6 +79,10 @@ ColumnLayout { close(); } + background: Rectangle { + color: Style.sesBackgroundColor + } + anchors.centerIn: parent width: parent.width * 0.8 @@ -114,20 +119,14 @@ ColumnLayout { } } - ErrorBox { + SesErrorBox { id: errorBox Layout.fillWidth: true Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding - showCloseButton: true visible: false - - onCloseButtonClicked: { - text = ""; - visible = false; - } } RowLayout { @@ -164,6 +163,7 @@ ColumnLayout { Layout.topMargin: Style.smallSpacing Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding + Layout.preferredHeight: Style.sesSearchFieldHeight visible: root.userGroupSharingPossible enabled: visible && !root.loading && !root.shareModel.isShareDisabledEncryptedFolder && !shareeSearchField.isShareeFetchOngoing @@ -185,6 +185,7 @@ ColumnLayout { Layout.fillHeight: true Layout.leftMargin: root.horizontalPadding Layout.rightMargin: root.horizontalPadding + Layout.topMargin: Style.sesMediumMargin active: root.sharingPossible @@ -206,7 +207,9 @@ ColumnLayout { sourceModel: root.shareModel } - delegate: ShareDelegate { + delegate: ColumnLayout{ + width: parent.width + ShareDelegate { id: shareDelegate Connections { @@ -236,7 +239,7 @@ ColumnLayout { fileDetails: root.fileDetails rootStackView: root.rootStackView backgroundsVisible: root.backgroundsVisible - accentColor: root.accentColor + accentColor: Style.sesIconColor canCreateLinkShares: root.publicLinkSharingPossible serverAllowsResharing: root.serverAllowsResharing @@ -260,6 +263,14 @@ ColumnLayout { onSetExpireDate: shareModel.setShareExpireDateFromQml(model.share, milliseconds) onSetPassword: shareModel.setSharePasswordFromQml(model.share, password) onSetNote: shareModel.setShareNoteFromQml(model.share, note) + width: parent.width + } + + Rectangle{ + height: Style.sesMediumMargin + color: "transparent" + width: parent.width + } } Loader { @@ -302,6 +313,7 @@ ColumnLayout { id: sharingDisabledLabel width: parent.width text: qsTr("Sharing is disabled") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -309,6 +321,7 @@ ColumnLayout { EnforcedPlainTextLabel { width: parent.width text: qsTr("This item cannot be shared.") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter @@ -317,6 +330,7 @@ ColumnLayout { EnforcedPlainTextLabel { width: parent.width text: qsTr("Sharing is disabled.") + color: palette.midlight wrapMode: Text.Wrap horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/src/gui/filedetails/ShareeDelegate.qml b/src/gui/filedetails/ShareeDelegate.qml index 52cf08ee6d423..36723f6910fd0 100644 --- a/src/gui/filedetails/ShareeDelegate.qml +++ b/src/gui/filedetails/ShareeDelegate.qml @@ -8,7 +8,7 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient +import com.strato.hidrivenext.desktopclient import Style import "../tray" @@ -18,6 +18,10 @@ ItemDelegate { text: model.display + background: Rectangle { + color: root.hovered ? root.pressed ? Style.sesButtonPressed : Style.sesHover : Style.sesWhite + } + contentItem: RowLayout { height: visible ? implicitHeight : 0 @@ -53,6 +57,10 @@ ItemDelegate { Layout.preferredHeight: unifiedSearchResultSkeletonItemDetails.iconWidth Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + color: Style.sesTrayFontColor + font.pixelSize: Style.sesFontPixelSize + font.weight: Style.sesFontNormalWeight + Layout.fillWidth: true horizontalAlignment: Text.AlignLeft diff --git a/src/gui/filedetails/ShareeSearchField.qml b/src/gui/filedetails/ShareeSearchField.qml index 8c82ed5abde50..41c3274a7be9d 100644 --- a/src/gui/filedetails/ShareeSearchField.qml +++ b/src/gui/filedetails/ShareeSearchField.qml @@ -8,8 +8,9 @@ import QtQuick.Window import QtQuick.Layouts import QtQuick.Controls -import com.nextcloud.desktopclient -import com.nextcloud.desktopclient as NC +import com.strato.hidrivenext.desktopclient +// TODO Check SES-459 +import com.strato.hidrivenext.desktopclient as NC import Style import "../tray" @@ -30,6 +31,7 @@ TextField { } readonly property int horizontalPaddingOffset: Style.trayHorizontalMargin + readonly property color placeholderColor: Style.sesSearchFieldContent readonly property double iconsScaleFactor: 0.6 function triggerSuggestionsVisibility() { @@ -37,9 +39,13 @@ TextField { } placeholderText: enabled ? qsTr("Search for users or groups…") : qsTr("Sharing is not available for this folder") + placeholderTextColor: placeholderColor + color: Style.sesTrayFontColor + Component.onCompleted: contentItem.cursorColor = Style.sesTrayFontColor verticalAlignment: Qt.AlignVCenter implicitHeight: Math.max(Style.talkReplyTextFieldPreferredHeight, contentHeight) + onActiveFocusChanged: triggerSuggestionsVisibility() onTextChanged: triggerSuggestionsVisibility() Keys.onPressed: { @@ -79,8 +85,16 @@ TextField { } } - leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset - rightPadding: clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset + leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + horizontalPaddingOffset - 5 + rightPadding: root.text ? clearTextButton.width + clearTextButton.anchors.rightMargin + horizontalPaddingOffset : 5 + + background: Rectangle { + radius: 5 + color: Style.sesBackgroundColor + border.color: Style.sesMenuBorder + border.width: 1 + + } Image { id: searchIcon @@ -99,7 +113,7 @@ TextField { fillMode: Image.PreserveAspectFit horizontalAlignment: Image.AlignLeft - source: "image://svgimage-custom-color/search.svg" + "/" + palette.placeholderText + source: "image://svgimage-custom-color/search.svg" + "/" + Style.sesSearchFieldContent sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor) visible: !root.shareeModel.fetchOngoing @@ -115,7 +129,7 @@ TextField { } width: height - color: palette.placeholderText + color: root.placeholderColor visible: root.shareeModel.fetchOngoing running: visible } @@ -137,7 +151,7 @@ TextField { mipmap: true fillMode: Image.PreserveAspectFit - source: "image://svgimage-custom-color/clear.svg" + "/" + palette.placeholderText + source: "image://svgimage-custom-color/clear.svg" + "/" + root.placeholderColor sourceSize: Qt.size(parent.height * root.iconsScaleFactor, parent.height * root.iconsScaleFactor) visible: root.text @@ -155,6 +169,11 @@ TextField { width: root.width y: root.height + background: Rectangle { + color: Style.sesBackgroundColor + border.color: Style.sesMenuBorder + } + contentItem: ScrollView { id: suggestionsScrollView @@ -175,7 +194,7 @@ TextField { highlight: Rectangle { anchors.fill: shareeListView.currentItem - color: palette.highlight + color: Style.sesHover } highlightFollowsCurrentItem: true highlightMoveDuration: 0 diff --git a/src/gui/filedetails/shareemodel.cpp b/src/gui/filedetails/shareemodel.cpp index 4693da29724d6..368b9f20c567b 100644 --- a/src/gui/filedetails/shareemodel.cpp +++ b/src/gui/filedetails/shareemodel.cpp @@ -14,7 +14,7 @@ namespace OCC { -Q_LOGGING_CATEGORY(lcShareeModel, "com.nextcloud.shareemodel") +Q_LOGGING_CATEGORY(lcShareeModel, "com.hidrivenext.shareemodel") ShareeModel::ShareeModel(QObject *parent) : QAbstractListModel(parent) diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index 06d1cb0246843..afea2c9c9c401 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -28,7 +28,7 @@ static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secur namespace OCC { -Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel") +Q_LOGGING_CATEGORY(lcShareModel, "com.hidrivenext.sharemodel") ShareModel::ShareModel(QObject *parent) : QAbstractListModel(parent) @@ -773,7 +773,7 @@ QString ShareModel::iconUrlForShare(const SharePtr &share) const switch(share->getShareType()) { case Share::TypeInternalLink: - return QString(iconsPath + QStringLiteral("external.svg")); + return QString(iconsPath + QStringLiteral("public.svg")); case Share::TypePlaceholderLink: case Share::TypeSecureFileDropPlaceholderLink: case Share::TypeLink: diff --git a/src/gui/filedetails/sortedsharemodel.cpp b/src/gui/filedetails/sortedsharemodel.cpp index 14a06a837b0ab..5fffc1ca891c6 100644 --- a/src/gui/filedetails/sortedsharemodel.cpp +++ b/src/gui/filedetails/sortedsharemodel.cpp @@ -7,7 +7,7 @@ namespace OCC { -Q_LOGGING_CATEGORY(lcSortedShareModel, "com.nextcloud.sortedsharemodel") +Q_LOGGING_CATEGORY(lcSortedShareModel, "com.hidrivenext.sortedsharemodel") SortedShareModel::SortedShareModel(QObject *parent) : QSortFilterProxyModel(parent) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 55e217a0c47f3..987e30dcd5184 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1467,10 +1467,10 @@ void Folder::slotNewBigFolderDiscovered(const QString &newF, bool isExternal) journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, undecidedList); emit newBigFolderDiscovered(newFolder); } - QString message = !isExternal ? (tr("A new folder larger than %1 MB has been added: %2.\n") + QString message = !isExternal ? QString(tr("A new folder larger than %1 MB has been added: %2.") + "\n") .arg(ConfigFile().newBigFolderSizeLimit().second) - .arg(newF)) - : (tr("A folder from an external storage has been added.\n")); + .arg(newF) + : (tr("A folder from an external storage has been added.") + "\n"); message += tr("Please go in the settings to select it if you wish to download it."); auto logger = Logger::instance(); @@ -1752,8 +1752,8 @@ void Folder::registerFolderWatcher() } connect(_folderWatcher.data(), &FolderWatcher::filesLockImposed, this, &Folder::slotFilesLockImposed, Qt::UniqueConnection); _folderWatcher->init(path()); - _folderWatcher->startNotificatonTest(path() + QLatin1String(".nextcloudsync.log")); - _folderWatcher->performSetPermissionsTest(path() + QLatin1String(".nextcloudpermissions.log")); + _folderWatcher->startNotificatonTest(path() + QLatin1String(".hidrivenextsync.log")); + _folderWatcher->performSetPermissionsTest(path() + QLatin1String(".hidrivenextpermissions.log")); connect(_engine.data(), &SyncEngine::lockFileDetected, _folderWatcher.data(), &FolderWatcher::slotLockFileDetectedExternally); } diff --git a/src/gui/foldercreationdialog.cpp b/src/gui/foldercreationdialog.cpp index 93e243842cf96..f47805a0051e3 100644 --- a/src/gui/foldercreationdialog.cpp +++ b/src/gui/foldercreationdialog.cpp @@ -4,6 +4,9 @@ */ #include "foldercreationdialog.h" + +#include "buttonstyle.h" +#include "whitelabeltheme.h" #include "ui_foldercreationdialog.h" #include @@ -11,6 +14,9 @@ #include #include #include +#include +#include +#include namespace OCC { @@ -22,10 +28,13 @@ FolderCreationDialog::FolderCreationDialog(const QString &destination, QWidget * , _destination(destination) { ui->setupUi(this); + setWindowTitle(tr("%1 Create new folder").arg(Theme::instance()->appNameGUI())); + customizeStyle(); - ui->labelErrorMessage->setVisible(false); + ui->errorSnackbar->setVisible(false); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowFlags(windowFlags() | Qt::Dialog | Qt::WindowMinMaxButtonsHint); connect(ui->newFolderNameEdit, &QLineEdit::textChanged, this, &FolderCreationDialog::slotNewFolderNameEditTextEdited); @@ -61,7 +70,7 @@ void FolderCreationDialog::accept() const auto fullPath = QString(_destination + "/" + ui->newFolderNameEdit->text()); if (QDir(fullPath).exists()) { - ui->labelErrorMessage->setVisible(true); + ui->errorSnackbar->setVisible(true); return; } @@ -77,10 +86,54 @@ void FolderCreationDialog::accept() void FolderCreationDialog::slotNewFolderNameEditTextEdited() { if (!ui->newFolderNameEdit->text().isEmpty() && QDir(_destination + "/" + ui->newFolderNameEdit->text()).exists()) { - ui->labelErrorMessage->setVisible(true); + ui->errorSnackbar->setVisible(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + sizeDialog(); + } else { - ui->labelErrorMessage->setVisible(false); + ui->errorSnackbar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + sizeDialog(); } } +void FolderCreationDialog::sizeDialog(){ + adjustSize(); + setFixedWidth(626); + setFixedHeight(sizeHint().height()); +} + +void FolderCreationDialog::customizeStyle() +{ + ui->buttonBox->setLayoutDirection(Qt::RightToLeft); + + this->setAutoFillBackground(true); + setPalette(QPalette(QPalette::Window, WLTheme.dialogBackgroundColor())); + + QPushButton *okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + okButton->setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::Primary)); + + QHBoxLayout* buttonlayout = qobject_cast(ui->buttonBox->layout()); + buttonlayout->setSpacing(16); + + ui->newFolderNameEdit->setStyleSheet( + QStringLiteral( + "color: %1; font-family: %2; font-size: %3; font-weight: %4; border-radius: %5; border: 1px " + "solid %6; padding: 0px 12px; text-align: left; vertical-align: middle; height: 40px; background: %7; ") + .arg(WLTheme.folderWizardPathColor(), + WLTheme.settingsFont(), + WLTheme.settingsTextSize(), + WLTheme.settingsTextWeight(), + WLTheme.buttonRadius(), + WLTheme.menuBorderColor(), + WLTheme.dialogBackgroundColor() + ) + ); + +#if defined(Q_OS_MAC) + buttonlayout->setSpacing(32); +#endif + + sizeDialog(); } +} \ No newline at end of file diff --git a/src/gui/foldercreationdialog.h b/src/gui/foldercreationdialog.h index d1c7ecb90a2d0..f618ecf14d6a2 100644 --- a/src/gui/foldercreationdialog.h +++ b/src/gui/foldercreationdialog.h @@ -34,6 +34,9 @@ private slots: Ui::FolderCreationDialog *ui; QString _destination; + + void customizeStyle(); + void sizeDialog(); }; } diff --git a/src/gui/foldercreationdialog.ui b/src/gui/foldercreationdialog.ui index 84d7c77e18654..de849b09215cd 100644 --- a/src/gui/foldercreationdialog.ui +++ b/src/gui/foldercreationdialog.ui @@ -1,100 +1,93 @@ - OCC::FolderCreationDialog - - - - 0 - 0 - 355 - 138 - - - - Create new folder - - - - - 0 - 90 - 341 - 32 - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - 20 - 30 - 321 - 22 - - - - Enter folder name - - - - - true - - - - 20 - 60 - 321 - 16 - - - - color: rgb(255, 0, 0) - - - Folder already exists - - - - - - - buttonBox - accepted() - OCC::FolderCreationDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - OCC::FolderCreationDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - - + OCC::FolderCreationDialog + + + Create new folder + + + + + + Enter folder name + + + Qt::NoContextMenu + + + + + + + Folder already exists + + + false + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + + buttonBox + accepted() + OCC::FolderCreationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + OCC::FolderCreationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + \ No newline at end of file diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index ac1d755605876..0628f71d815ef 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -1610,12 +1610,12 @@ QString FolderMan::getBackupName(QString fullPathName) const if (fullPathName.isEmpty()) return QString(); - QString newName = fullPathName + tr(" (backup)"); + QString newName = fullPathName + " " + tr("(backup)"); QFileInfo fi(newName); int cnt = 2; do { if (fi.exists()) { - newName = fullPathName + tr(" (backup %1)").arg(cnt++); + newName = fullPathName + " " + tr("(backup %1)").arg(cnt++); fi.setFile(newName); } } while (fi.exists()); diff --git a/src/gui/folderstatusdelegate.cpp b/src/gui/folderstatusdelegate.cpp index 07a93fbd47b32..3fa22f52c38e5 100644 --- a/src/gui/folderstatusdelegate.cpp +++ b/src/gui/folderstatusdelegate.cpp @@ -9,7 +9,11 @@ #include "folderstatusview.h" #include "folderman.h" #include "accountstate.h" +#include "sesstyle.h" +#include "buttonstyle.h" + #include +#include #include #include @@ -17,14 +21,7 @@ #include #include #include - -inline static QFont makeAliasFont(const QFont &normalFont) -{ - QFont aliasFont = normalFont; - aliasFont.setBold(true); - aliasFont.setPointSize(normalFont.pointSize() + 2); - return aliasFont; -} +#include namespace { #ifdef Q_OS_MACOS @@ -34,6 +31,14 @@ namespace { namespace OCC { +inline static QFont makeAliasFont(const QFont &normalFont) +{ + QFont aliasFont = normalFont; + aliasFont.setWeight(WLTheme.settingsTitleWeightDemiBold()); + aliasFont.setPixelSize(WLTheme.settingsBigTitlePixel()); + return aliasFont; +} + FolderStatusDelegate::FolderStatusDelegate() : QStyledItemDelegate() { @@ -42,15 +47,21 @@ FolderStatusDelegate::FolderStatusDelegate() QString FolderStatusDelegate::addFolderText() { - return tr("Add Folder Sync Connection"); + return tr("Add Folder Sync"); +} + +QString FolderStatusDelegate::addInfoText() +{ + return tr("Synchronize any other local folder with your %1").arg(Theme::instance()->appNameGUI()); } // allocate each item size in listview. QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - QFont aliasFont = makeAliasFont(option.font); - QFont font = option.font; + QFont font = QFont(option.font); + font.setPixelSize(WLTheme.settingsTextPixel()); + QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); @@ -61,7 +72,7 @@ QSize FolderStatusDelegate::sizeHint(const QStyleOptionViewItem &option, QFontMetrics fm(qApp->font("QPushButton")); QStyleOptionButton opt; static_cast(opt) = option; - opt.text = addFolderText(); + opt.text = addInfoText(); return QApplication::style()->sizeFromContents( QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)) + QSize(0, margins); } @@ -123,22 +134,78 @@ int FolderStatusDelegate::rootFolderHeightWithoutErrors(const QFontMetrics &fm, return h; } +void FolderStatusDelegate::drawAddButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QFont titleFont = option.font; + titleFont.setWeight(WLTheme.settingsTitleWeightDemiBold()); + titleFont.setPixelSize(WLTheme.settingsTitlePixel()); + QFontMetrics titleTextFm(titleFont); + const auto baseDistanceForCalculus = titleTextFm.height() / 2; + + QFont subtitleFont = option.font; + + QFontMetrics subtitleTextFm(subtitleFont); + const auto distanceToSubline = subtitleTextFm.height() / 4; + + auto iconBox = option.rect; + iconBox.setTop(iconBox.top() + baseDistanceForCalculus); + iconBox.setBottom(iconBox.top() + WLTheme.treeViewIconSize()); + iconBox.setLeft(iconBox.left() + baseDistanceForCalculus); + iconBox.setWidth(iconBox.height()); + + auto titleBox = option.rect; + titleBox.setTop(iconBox.top()); + titleBox.setBottom(iconBox.bottom() - distanceToSubline); + titleBox.setRight(titleBox.right() - baseDistanceForCalculus); + titleBox.setLeft(iconBox.right() + baseDistanceForCalculus); + + auto subtitleBox = option.rect; + subtitleBox.setTop(titleBox.bottom() + distanceToSubline); + subtitleBox.setBottom(subtitleBox.top() + 4 * distanceToSubline); + subtitleBox.setLeft(iconBox.right() + baseDistanceForCalculus); + subtitleBox.setRight(subtitleBox.right() - baseDistanceForCalculus); + + auto titleText = addFolderText(); + auto subtitleText = addInfoText(); + auto addIcon = QIcon(WLTheme.liveBackupPlusIcon()); + const auto addPixmap = addIcon.pixmap(iconBox.size(), QIcon::Normal); + + painter->save(); + painter->drawPixmap(QStyle::visualRect(option.direction, option.rect, iconBox).left(), iconBox.top(), addPixmap); + + drawElidedText(painter, option, titleTextFm, titleFont, titleText, titleBox); + + drawElidedText(painter, option, subtitleTextFm, subtitleFont, subtitleText, subtitleBox); + + painter->restore(); +} + +void FolderStatusDelegate::drawElidedText(QPainter *painter, QStyleOptionViewItem option, QFontMetrics fontMetric, QFont font, QString text, QRect rect) const{ + const auto elidedText = fontMetric.elidedText(text, Qt::ElideRight, rect.width()); + painter->setFont(font); + painter->drawText(QStyle::visualRect(option.direction, option.rect, rect), Qt::AlignLeft, elidedText); +} + void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + QStyleOptionViewItem opt = option; + QFont font = opt.font; + font.setPixelSize(WLTheme.settingsTextPixel()); + opt.font = font; + + QStyledItemDelegate::paint(painter, opt, index); + if (index.data(AddButton).toBool()) { - const_cast(option).showDecorationSelected = false; + drawAddButton(painter, opt, index); + return; } - QStyledItemDelegate::paint(painter, option, index); - auto textAlign = Qt::AlignLeft; - const auto aliasFont = makeAliasFont(option.font); - const auto subFont = option.font; - const auto errorFont = subFont; - auto progressFont = subFont; + const auto aliasFont = makeAliasFont(opt.font); + const auto subFont = opt.font; - progressFont.setPointSize(subFont.pointSize() - 2); + const auto errorFont = subFont; QFontMetrics subFm(subFont); QFontMetrics aliasFm(aliasFont); @@ -146,23 +213,6 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & const auto aliasMargin = aliasFm.height() / 2; const auto margin = subFm.height() / 4; - if (index.data(AddButton).toBool()) { - QStyleOptionButton opt; - static_cast(opt) = option; - if (opt.state & QStyle::State_Enabled && opt.state & QStyle::State_MouseOver && index == _pressedIndex) { - opt.state |= QStyle::State_Sunken; - } else { - opt.state |= QStyle::State_Raised; - } - opt.text = addFolderText(); - opt.rect = addButtonRect(option.rect, option.direction); - painter->save(); - painter->setFont(qApp->font("QPushButton")); - QApplication::style()->drawControl(QStyle::CE_PushButton, &opt, painter, option.widget); - painter->restore(); - return; - } - if (dynamic_cast(index.model())->classify(index) != FolderStatusModel::RootFolder) { return; } @@ -175,7 +225,6 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & auto errorTexts = qvariant_cast(index.data(FolderErrorMsg)); auto infoTexts = qvariant_cast(index.data(FolderInfoMsg)); - auto overallPercent = qvariant_cast(index.data(SyncProgressOverallPercent)); auto overallString = qvariant_cast(index.data(SyncProgressOverallString)); auto itemString = qvariant_cast(index.data(SyncProgressItemString)); auto syncEnabled = qvariant_cast(index.data(FolderAccountConnected)); @@ -212,7 +261,7 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & localPathRect.setLeft(nextToIcon); remotePathRect.setLeft(nextToIcon); - auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + const auto iconSize = iconRect.width(); statusIcon.paint( painter, @@ -220,6 +269,10 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & Qt::AlignCenter, syncEnabled ? QIcon::Normal : QIcon::Disabled ); + + // TODO SES-459 Check if working + // const auto statusPixmap = statusIcon.pixmap(iconSize, iconSize, syncEnabled ? QIcon::Normal : QIcon::Disabled); + // painter->drawPixmap(QStyle::visualRect(option.direction, option.rect, iconRect).left(), iconRect.top(), statusPixmap); auto palette = option.palette; @@ -234,28 +287,23 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & painter->setPen(palette.color(colourGroup, QPalette::Text)); } - const auto elidedAlias = aliasFm.elidedText(aliasText, Qt::ElideRight, aliasRect.width()); - painter->setFont(aliasFont); - painter->drawText(QStyle::visualRect(option.direction, option.rect, aliasRect), textAlign, elidedAlias); + drawElidedText(painter, option, aliasFm, aliasFont, aliasText, aliasRect); const auto showProgess = !overallString.isEmpty() || !itemString.isEmpty(); if (!showProgess) { - painter->setFont(subFont); - const auto elidedRemotePathText = subFm.elidedText(syncText, Qt::ElideRight, remotePathRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, remotePathRect), textAlign, elidedRemotePathText); + drawElidedText(painter, option, subFm, subFont, syncText, remotePathRect); - const auto elidedPathText = subFm.elidedText(pathText, Qt::ElideMiddle, localPathRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, localPathRect), textAlign, elidedPathText); + drawElidedText(painter, option, subFm, subFont, pathText, localPathRect); } - auto textBoxTop = localPathRect.bottom() + margin; + auto textBoxTop = qMax(localPathRect.bottom(), remotePathRect.bottom()) + margin; // paint an error overlay if there is an error string or conflict string - auto drawTextBox = [&](const QStringList &texts, QColor color) { + auto drawTextBox = [&](const QStringList &texts, QColor borderColor) { auto rect = localPathRect; rect.setLeft(iconRect.left()); rect.setTop(textBoxTop); - rect.setRight(option.rect.right() - margin); + rect.setRight(option.rect.right() - aliasMargin); const int innerWidth = qMax(1, rect.width() - 2 * margin); @@ -275,11 +323,15 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & // save previous state to not mess up colours with the background (fixes issue: https://github.com/nextcloud/desktop/issues/1237) painter->save(); - painter->setBrush(color); - painter->setPen(QColor(0xaa, 0xaa, 0xaa)); + // warning/error/infoColor() are opaque near-white pastels with no dark variant. Instead of + // inventing new dark pastels, tint the (already theme-aware) row background with the border + // hue at low alpha - the same translucent-overlay technique the tray uses for its alert/error + // boxes (ErrorBox.qml, MainWindow.qml's trayWindowSyncWarning) so it adapts automatically. + painter->setBrush(BaseTheme::tintedFillFromBorder(borderColor)); + painter->setPen(borderColor); painter->drawRoundedRect(QStyle::visualRect(option.direction, option.rect, rect), 4, 4); - painter->setPen(Qt::white); + painter->setPen(palette.color(colourGroup, QPalette::Text)); painter->setFont(errorFont); QRect textRect(rect.left() + margin, rect.top() + margin, @@ -300,10 +352,10 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & }; if (!conflictTexts.isEmpty()) { - drawTextBox(conflictTexts, QColor(0xba, 0xba, 0x4d)); + drawTextBox(conflictTexts, QColor(WLTheme.warningBorderColor())); } if (!errorTexts.isEmpty()) { - drawTextBox(errorTexts, QColor(0xbb, 0x4d, 0x4d)); + drawTextBox(errorTexts, QColor(WLTheme.errorBorderColor())); } // Paint "Grant access" button when sandbox re-approval is needed @@ -329,67 +381,118 @@ void FolderStatusDelegate::paint(QPainter *painter, const QStyleOptionViewItem & } if (!infoTexts.isEmpty()) { - drawTextBox(infoTexts, QColor(0x4d, 0x4d, 0xba)); + drawTextBox(infoTexts, QColor(WLTheme.infoBorderColor())); } // Sync File Progress Bar: Show it if syncFile is not empty. if (showProgess) { - const auto fileNameTextHeight = subFm.boundingRect(tr("File")).height(); - constexpr auto barHeight = 7; // same height as quota bar - const auto overallWidth = option.rect.right() - aliasMargin - optionsButtonVisualRect.width() - nextToIcon; + drawSyncProgressBar(painter, opt, index, subFm, aliasMargin, remotePathRect, margin, nextToIcon); + } - painter->save(); + drawMoreOptionsButton(painter, option, index); +} + +void FolderStatusDelegate::drawSyncProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QFontMetrics &subFm, const int aliasMargin, const QRect &remotePathRect, const int margin, const int nextToIcon) const +{ + auto overallPercent = qvariant_cast(index.data(SyncProgressOverallPercent)); + auto overallString = qvariant_cast(index.data(SyncProgressOverallString)); + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + const auto fileNameTextHeight = subFm.boundingRect(tr("File")).height(); + constexpr auto barHeight = 7; // same height as quota bar + const auto overallWidth = option.rect.right() - aliasMargin - optionsButtonVisualRect.width() - nextToIcon; + + QFont progressFont(option.font); + progressFont.setPixelSize(WLTheme.settingsTextPixel()); + progressFont.setWeight(WLTheme.settingsTitleWeightNormal()); + painter->save(); // Overall Progress Bar. - const auto progressBarRect = QRect(nextToIcon, - remotePathRect.top(), - overallWidth - 2 * margin, - barHeight); - - QStyleOptionProgressBar progressBarOpt; - - progressBarOpt.state = option.state | QStyle::State_Horizontal; - progressBarOpt.minimum = 0; - progressBarOpt.maximum = 100; - progressBarOpt.progress = overallPercent; - progressBarOpt.state = QStyle::StateFlag::State_Horizontal; - progressBarOpt.rect = QStyle::visualRect(option.direction, option.rect, progressBarRect); + const auto progressBarRect = QRect(nextToIcon, + remotePathRect.top(), + overallWidth - 2 * margin, + barHeight); + + QStyleOptionProgressBar progressBarOpt; + + progressBarOpt.state = option.state | QStyle::State_Horizontal; + progressBarOpt.minimum = 0; + progressBarOpt.maximum = 100; + progressBarOpt.progress = overallPercent; + progressBarOpt.state = QStyle::StateFlag::State_Horizontal; + progressBarOpt.rect = QStyle::visualRect(option.direction, option.rect, progressBarRect); + QPalette paletteTmp = progressBarOpt.palette; + paletteTmp.setColor(QPalette::Base, WLTheme.dialogBackgroundColor()); + paletteTmp.setColor(QPalette::Highlight, WLTheme.syncProgressColor()); + progressBarOpt.palette = paletteTmp; + #ifdef Q_OS_MACOS - backupStyle->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); + backupStyle->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); #else - QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); + QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOpt, painter, option.widget); #endif - // itemString is e.g. Syncing fileName1, filename2 - // syncText is Synchronizing files in local folders or Synchronizing virtual files in local folder - const auto generalSyncStatus = !itemString.isEmpty() ? itemString : syncText; - QRect generalSyncStatusRect; - generalSyncStatusRect.setTop(progressBarRect.bottom() + margin); - generalSyncStatusRect.setHeight(fileNameTextHeight); - generalSyncStatusRect.setLeft(progressBarRect.left()); - generalSyncStatusRect.setWidth(progressBarRect.width()); - painter->setFont(progressFont); +// Overall Progress Text + QRect overallProgressRect; + overallProgressRect.setTop(progressBarRect.bottom() + margin); + overallProgressRect.setHeight(fileNameTextHeight); + overallProgressRect.setLeft(progressBarRect.left()); + overallProgressRect.setWidth(progressBarRect.width()); - painter->drawText(QStyle::visualRect(option.direction, option.rect, generalSyncStatusRect), Qt::AlignLeft | Qt::AlignVCenter, generalSyncStatus); + painter->setFont(progressFont); - painter->restore(); - } + painter->drawText(QStyle::visualRect(option.direction, option.rect, overallProgressRect), Qt::AlignLeft | Qt::AlignVCenter, overallString); + + // // itemString is e.g. Syncing fileName1, filename2 + // // syncText is Synchronizing files in local folders or Synchronizing virtual files in local folder + // const auto generalSyncStatus = !itemString.isEmpty() ? itemString : syncText; + // QRect generalSyncStatusRect; + // generalSyncStatusRect.setTop(progressBarRect.bottom() + margin); + // generalSyncStatusRect.setHeight(fileNameTextHeight); + // generalSyncStatusRect.setLeft(progressBarRect.left()); + // generalSyncStatusRect.setWidth(progressBarRect.width()); + // // painter->setFont(progressFont); + + // painter->drawText(QStyle::visualRect(option.direction, option.rect, generalSyncStatusRect), Qt::AlignLeft | Qt::AlignVCenter, generalSyncStatus); painter->restore(); +} + +void FolderStatusDelegate::drawMoreOptionsButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + QStyleOptionButton btnOpt; + btnOpt.state = option.state; + btnOpt.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus | QStyle::State_MouseOver); + btnOpt.state |= QStyle::State_Raised; + if(optionsButtonVisualRect.contains(MousePos) ) { - QStyleOptionToolButton btnOpt; - btnOpt.state = option.state; - btnOpt.state &= ~(QStyle::State_Selected | QStyle::State_HasFocus); + btnOpt.state |= QStyle::State_MouseOver; + } + + if (btnOpt.state & QStyle::State_Enabled && btnOpt.state & QStyle::State_MouseOver && index == _pressedIndex) { + btnOpt.state |= QStyle::State_Sunken; + } else { btnOpt.state |= QStyle::State_Raised; - btnOpt.arrowType = Qt::NoArrow; - btnOpt.subControls = QStyle::SC_ToolButton; - btnOpt.rect = optionsButtonVisualRect; - btnOpt.icon = _iconMore; - const auto buttonSize = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - btnOpt.iconSize = QSize(buttonSize, buttonSize); - QApplication::style()->drawComplexControl(QStyle::CC_ToolButton, &btnOpt, painter); } + + btnOpt.rect = optionsButtonVisualRect; + btnOpt.icon = _iconMore; + const auto iconSize = optionsButtonIconSize(); + btnOpt.iconSize = QSize(iconSize, iconSize); + QWidget buttonWidget; + buttonWidget.setProperty("buttonStyle", QVariant::fromValue(OCC::ButtonStyleName::MoreOptions)); + + QApplication::style()-> + drawControl( + static_cast(sesStyle::CE_TreeViewMoreOptions), &btnOpt, painter, &buttonWidget); +} + +int FolderStatusDelegate::optionsButtonIconSize() { + // Using this calculation to use the DPI-Scaled values. The QStyleHelper::dpiScaled is not accessible from here. + return QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize) - QApplication::style()->pixelMetric(QStyle::PM_MenuScrollerHeight); } bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, @@ -400,12 +503,24 @@ bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, case QEvent::MouseMove: if (const auto *view = qobject_cast(option.widget)) { auto *me = dynamic_cast(event); - QModelIndex index; + QModelIndex pressedIndex; if (me->buttons()) { - index = view->indexAt(me->pos()); + pressedIndex = view->indexAt(me->pos()); + } + if (_pressedIndex != pressedIndex) { + _pressedIndex = pressedIndex; + view->viewport()->update(); } - if (_pressedIndex != index) { - _pressedIndex = index; + auto optionsButtonVisualRect = optionsButtonRect(option.rect, option.direction); + + MousePos = me->pos(); + if(optionsButtonVisualRect.contains(MousePos)) + { + _hoveredIndex = index; + view->viewport()->update(); + } else if(_hoveredIndex.isValid()) + { + _hoveredIndex = QModelIndex(); view->viewport()->update(); } } @@ -421,34 +536,26 @@ bool FolderStatusDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, QRect FolderStatusDelegate::optionsButtonRect(QRect within, Qt::LayoutDirection direction) { - QFont font = QFont(); + QFont font = QFont(WLTheme.settingsFont()); QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); within.setHeight(FolderStatusDelegate::rootFolderHeightWithoutErrors(fm, aliasFm)); - QStyleOptionToolButton opt; - int e = QApplication::style()->pixelMetric(QStyle::PM_ButtonIconSize); - opt.rect.setSize(QSize(e,e)); - QSize size = QApplication::style()->sizeFromContents(QStyle::CT_ToolButton, &opt, opt.rect.size()); - - int margin = QApplication::style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing); - QRect r(QPoint(within.right() - size.width() - margin, + QStyleOptionButton opt; + int iconSize = optionsButtonIconSize(); + opt.rect.setSize(QSize(iconSize,iconSize)); + QSize size = QApplication::style()->sizeFromContents( + static_cast(sesStyle::CT_TreeViewMoreOptions), &opt, opt.rect.size()); + + // Using PM_LargeIconSize as margin because it get DPI Scaled, which I canot access from here + int margin = QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize); + QRect r(QPoint(within.right() - size.width() - aliasFm.height() / 2, within.top() + within.height() / 2 - size.height() / 2), size); return QStyle::visualRect(direction, within, r); } -QRect FolderStatusDelegate::addButtonRect(QRect within, Qt::LayoutDirection direction) -{ - QFontMetrics fm(qApp->font("QPushButton")); - QStyleOptionButton opt; - opt.text = addFolderText(); - QSize size = QApplication::style()->sizeFromContents(QStyle::CT_PushButton, &opt, fm.size(Qt::TextSingleLine, opt.text)); - QRect r(QPoint(within.left(), within.top() + within.height() / 2 - size.height() / 2), size); - return QStyle::visualRect(direction, within, r); -} - QRect FolderStatusDelegate::sandboxButtonRect(QRect errorBannerRect) { QFontMetrics fm(qApp->font("QPushButton")); @@ -461,7 +568,7 @@ QRect FolderStatusDelegate::sandboxButtonRect(QRect errorBannerRect) QRect FolderStatusDelegate::errorsListRect(QRect within) { - QFont font = QFont(); + QFont font = QFont(WLTheme.settingsFont()); QFont aliasFont = makeAliasFont(font); QFontMetrics fm(font); QFontMetrics aliasFm(aliasFont); @@ -476,7 +583,7 @@ void FolderStatusDelegate::slotStyleChanged() void FolderStatusDelegate::customizeStyle() { - _iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/theme/more.svg")); + _iconMore = Theme::createColorAwareIcon(QLatin1String(":/client/theme/ses/ses-more.svg"), QSize(128, 128)); } } // namespace OCC diff --git a/src/gui/folderstatusdelegate.h b/src/gui/folderstatusdelegate.h index c8924471e21a7..5a6a516c69ab8 100644 --- a/src/gui/folderstatusdelegate.h +++ b/src/gui/folderstatusdelegate.h @@ -18,6 +18,7 @@ class FolderStatusDelegate : public QStyledItemDelegate Q_OBJECT public: FolderStatusDelegate(); + QPoint MousePos; enum datarole { FolderAliasRole = Qt::UserRole + 100, HeaderRole, @@ -49,8 +50,9 @@ class FolderStatusDelegate : public QStyledItemDelegate /** * return the position of the option button within the item */ + + static QRect optionsButtonRect(QRect within, Qt::LayoutDirection direction); - static QRect addButtonRect(QRect within, Qt::LayoutDirection direction); static QRect errorsListRect(QRect within); static QRect sandboxButtonRect(QRect errorBannerRect); static int rootFolderHeightWithoutErrors(const QFontMetrics &fm, const QFontMetrics &aliasFm); @@ -60,9 +62,17 @@ public slots: private: void customizeStyle(); + void drawAddButton(QPainter *,const QStyleOptionViewItem &, const QModelIndex &) const; + void drawElidedText(QPainter *painter, QStyleOptionViewItem option, QFontMetrics fontMetric, QFont font, QString text, QRect rect) const; + void drawSyncProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index, const QFontMetrics &subFm, const int aliasMargin, const QRect &remotePathRect, const int margin, const int nextToIcon) const; + void drawMoreOptionsButton(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; + + static int optionsButtonIconSize(); static QString addFolderText(); + static QString addInfoText(); QPersistentModelIndex _pressedIndex; + QPersistentModelIndex _hoveredIndex; QIcon _iconMore; }; diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index 5c3504674f7bb..b84debdbb78ec 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -5,6 +5,7 @@ */ #include "folderstatusmodel.h" +#include "whitelabeltheme.h" #include "accountstate.h" #include "common/asserts.h" #include "common/utility.h" @@ -288,6 +289,29 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const return toolTip; } case FolderStatusDelegate::FolderStatusIconRole: { +#ifdef IONOS_BUILD + if (!accountConnected) + return QIcon(WLTheme.syncOfflineIcon("cpp")); + { + const auto result = folder->syncResult(); + const auto status = result.status(); + if (folder->syncPaused()) + return QIcon(WLTheme.syncPausedIcon("cpp")); + switch (status) { + case SyncResult::NotYetStarted: + case SyncResult::SyncPrepare: + case SyncResult::SyncRunning: + case SyncResult::SyncAbortRequested: + case SyncResult::Undefined: + return QIcon(WLTheme.syncSyncingIcon("cpp")); + case SyncResult::Success: + case SyncResult::Problem: + return QIcon(result.hasUnresolvedConflicts() ? WLTheme.syncWarningIcon("cpp") : WLTheme.syncSuccessIcon("cpp")); + default: + return QIcon(WLTheme.syncErrorIcon("cpp")); + } + } +#endif if (!accountConnected) { return Theme::instance()->folderStateIcon(SyncResult::SetupError); } diff --git a/src/gui/folderstatusview.cpp b/src/gui/folderstatusview.cpp index 3e06244acf00f..3c101db40ef38 100644 --- a/src/gui/folderstatusview.cpp +++ b/src/gui/folderstatusview.cpp @@ -5,6 +5,7 @@ #include "folderstatusview.h" #include "folderstatusdelegate.h" +#include "whitelabeltheme.h" #include #include @@ -13,6 +14,9 @@ namespace OCC { FolderStatusView::FolderStatusView(QWidget *parent) : QTreeView(parent) { + #ifdef Q_OS_MAC + setPalette(QPalette(QPalette::ButtonText, WLTheme.white())); + #endif } QModelIndex FolderStatusView::indexAt(const QPoint &point) const @@ -26,11 +30,7 @@ QModelIndex FolderStatusView::indexAt(const QPoint &point) const QRect FolderStatusView::visualRect(const QModelIndex &index) const { - QRect rect = QTreeView::visualRect(index); - if (index.data(FolderStatusDelegate::AddButton).toBool()) { - return FolderStatusDelegate::addButtonRect(rect, layoutDirection()); - } - return rect; + return QTreeView::visualRect(index); } } // namespace OCC diff --git a/src/gui/folderwizard.cpp b/src/gui/folderwizard.cpp index 65f9a9562871a..10878e1717d58 100644 --- a/src/gui/folderwizard.cpp +++ b/src/gui/folderwizard.cpp @@ -5,55 +5,45 @@ */ #include "folderwizard.h" -#include "folderman.h" -#include "configfile.h" -#include "theme.h" -#include "networkjobs.h" +#include "SesComponents/syncdirvalidation.h" #include "account.h" -#include "selectivesyncdialog.h" #include "accountstate.h" +#include "buttonstyle.h" +#include "common/asserts.h" +#include "configfile.h" #include "creds/abstractcredentials.h" +#include "folderman.h" +#include "networkjobs.h" +#include "selectivesyncdialog.h" +#include "theme.h" #include "wizard/owncloudwizard.h" -#include "common/asserts.h" #ifdef Q_OS_MACOS #include "common/utility_mac_sandbox.h" #endif +#include #include +#include #include +#include #include -#include #include +#include +#include #include +#include +#include +#include #include +#include #include #include -#include -#include -#include -#include -#include -#include #include -namespace -{ -constexpr QColor darkWarnYellow(63, 63, 0); -constexpr QColor lightWarnYellow(255, 255, 192); - -QPalette yellowWarnWidgetPalette(const QPalette &existingPalette) +namespace OCC { - const auto warnYellow = OCC::Theme::instance()->darkMode() ? darkWarnYellow : lightWarnYellow; - auto modifiedPalette = existingPalette; - modifiedPalette.setColor(QPalette::Window, warnYellow); - modifiedPalette.setColor(QPalette::Base, warnYellow); - return modifiedPalette; -} -} - -namespace OCC { QString FormatWarningsWizardPage::formatWarnings(const QStringList &warnings) const { @@ -61,7 +51,7 @@ QString FormatWarningsWizardPage::formatWarnings(const QStringList &warnings) co if (warnings.count() == 1) { formattedWarning = Utility::escape(warnings.first()); } else if (warnings.count() > 1) { - formattedWarning = "