Skip to content

Commit 22d90e9

Browse files
chore(release): merge release/v1.2.0 into main for 1.2.0 (#248)
## Issues addressed - Closes #238: Imports compatible line-format mod lists, reorders existing mods, imports sibling folders, reconciles library entries, identifies missing Nexus mods, and queues Premium downloads into the file order. Adds the in-row import-details correction and version-unknown paths needed to resolve local imports. - Closes #240: Makes a downloaded Nexus update become and remain the latest version after a manual import, including protection from startup cleanup. - Closes #243: Shows complete Nexus thumbnails in responsive 16:9 frames instead of cropping them. - Closes #245: Clones the active profile with its metadata, launch settings, complete mod membership, policies, load order, and locks while keeping the copy independent. ## Additional changes - Isolates the Linux NXM unregister test from the real managed handler directory.
2 parents b3a82d4 + f2c9157 commit 22d90e9

94 files changed

Lines changed: 16492 additions & 736 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 839 additions & 80 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ and it runs unmodified).
2323
profiles, dependency resolution, mod-source integrations). The backend
2424
libraries (Profiles, Mods, Steam, Integrations, Relay-client, General) and
2525
the UI (a left navigation rail with five destinations: Profiles, Mods, Nexus
26-
Integrations, Preferences, Settings; plus profile management and the Launch
26+
Integrations, Preferences, Settings; plus profile management, including
27+
cloning a profile into an independent copy (named "Copy N", with mod files
28+
shared rather than duplicated), and the Launch
2729
flow) are in place. The app is user-usable. The Mods list offers a persisted
2830
Compact and Detailed row density; Detailed rows show a Nexus summary and a
2931
cached thumbnail when one is available, with a neutral placeholder otherwise.

docs/architecture/MODIFICUS-CURATOR.md

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,18 @@ for the full contract (env-var table, logging, the hook-ready handshake).
261261
update of an entry already in the profile is a strict no-op that preserves
262262
the user's current order, enabled state, policy, and lock. Existing profile
263263
entries are not migrated.
264+
- **Cloning:** the focused `IProfileCloner.CloneProfile` capability persists an
265+
independent copy of a profile in one operation (never reconstructed through
266+
repeated `AddMod`, which would replay fresh-add policy such as DMF
267+
placement). The copy gets a new id + creation timestamp, a generated
268+
` (Copy N)` family name (stable data, never localized; `Testing (Copy 1)`
269+
belongs to the `Testing` family), and the source's description, complete mod
270+
membership (enabled state, order, locks, policies incl. pinned version
271+
ids), and launch settings. Mod files stay in the shared repository (the
272+
clone references the same containers; nothing is duplicated), and the
273+
staged tree is not copied: the clone receives an empty `staged/` scaffold
274+
that ordinary launch staging rebuilds. Cloning raises no
275+
`ProfileCreated`, so the DMF offer never fires for a clone.
264276
- Mods are stored **once, in a unified repository** keyed by `(source, identity)`
265277
per UUID container. Profiles reference a mod by `(containerId, policy)` and
266278
store no mod files of their own. See [Mod repository](#mod-repository).
@@ -287,9 +299,12 @@ Each version is a subfolder with an **opaque unique ID**; the raw version tag
287299
lives only in the manifest (for display + pin resolution), never as a folder
288300
name. **`isLatest` is a flag on one version entry**, not a duplicate folder:
289301
the repository re-evaluates the flag on every add/remove with the
290-
**effective-timestamp key** (`remoteUploadedAt` when the remote source
291-
published the file, else the import time, with the import time breaking exact
292-
ties), so importing an older remote file never flips latest. Each version
302+
**arrival rule** (the most recent arrival, i.e. the newest import time,
303+
decides the clock: a manual import with the newest arrival is latest;
304+
otherwise the newest downloaded version by `remoteUploadedAt`, with the
305+
import time breaking exact ties), so importing an older remote file never
306+
flips latest while a download arriving after a manual import takes the
307+
flag. Each version
293308
entry also records the remote file id it was acquired from (`FileId`, the
294309
exact identity the download queue's repository hit check keys on; null for
295310
manual imports, self-healing on re-acquisition). Moving latest is a one-field
@@ -367,7 +382,7 @@ contents; the archive is validated to have a single top-level folder before
367382
extraction). Container dedup: Untracked by name, Nexus by mod id. Version dedup:
368383
re-importing the same tag reuses its folder (refreshed); a new tag creates a new
369384
version, and the repository re-evaluates `isLatest` over all versions with its
370-
effective-timestamp key. The service returns `(containerId, versionId)`,
385+
arrival rule. The service returns `(containerId, versionId)`,
371386
where `versionId` is the imported version's opaque on-disk folder id (a
372387
`ModVersion.Folder` value); the display tag (`ModVersion.VersionString`) is
373388
recorded in the container manifest and is not returned. The caller then adds
@@ -647,10 +662,13 @@ is in [UI reference](../reference/ui.md).
647662
drag-reorder grip, column 1 is the thumbnail/placeholder slot, column 2
648663
holds the name + source badge (row 0) and a two-line summary (row 1), and
649664
row 2 is the action strip. When the card
650-
is wide (greater than 680 DIP) a 112-DIP thumbnail spans all three rows and
651-
the action strip occupies only the content column; when constrained (at or
652-
below 680 DIP) the thumbnail shrinks to 72 DIP spanning name + summary and
653-
the action strip moves to a full-width row beneath all three columns. The action
665+
is wide (greater than 680 DIP) a 192x108 DIP 16:9 thumbnail (stretched
666+
`Uniform`, so the complete source image shows with the frame's neutral
667+
background letterboxing/pillarboxing non-16:9 assets) spans all three rows
668+
and the action strip occupies only the content column; when constrained (at
669+
or below 680 DIP) the thumbnail shrinks to 128x72 DIP spanning name +
670+
summary and the action strip moves to a full-width row beneath all three
671+
columns. The action
654672
strip is a right-aligned `WrapPanel` that wraps at the edge (no horizontal
655673
scrolling); width, height, row span, and the action column/span are driven by
656674
styles so the breakpoint changes them. The summary is plain text with `CharacterEllipsis` trimming and the full

docs/architecture/mod-acquisition.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ and `IHttpClientFactory` (for the raw CDN download) from the container.
166166
the archive structure (single base folder plus matching `<base>.mod`
167167
descriptor; archive detection is content-based via SharpCompress), handles
168168
find-or-create-container (dedup by `NexusSource.ModId`) plus add-version,
169-
re-evaluates `IsLatest` with the repository's effective-timestamp key, records
169+
re-evaluates `IsLatest` with the repository's arrival rule, records
170170
the publish date and the file id on the entry (new or reused), replaces the
171171
container's `DisplayMetadata` in the same
172172
manifest update as the version mutation, and extracts into

docs/architecture/ui-architecture.md

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ a UI-layer singleton that the shell (and other view models) inject:
6868
│ │ strip; mirrors session state
6969
│ │
7070
│ ├── ProfilesViewModel ─── the active profile editor (name + description
71-
│ │ │ + inline launch settings) + banner/picker
71+
│ │ │ + inline launch settings) + banner/picker/clone
7272
│ │ │
7373
│ │ └── LaunchSettingsEditorViewModel the reusable inline launch-settings
7474
│ │ rows (env vars + args + toggles)
@@ -195,6 +195,42 @@ running-state change does not reload the list (the list stays put while the
195195
game runs; edits land on the profile the user will launch next). Active-id
196196
changes rebuild the list from the new profile.
197197

198+
## The Profiles destination (`ProfilesViewModel`)
199+
200+
The Profiles page edits the active profile only (name + description + the
201+
inline launch-settings editor), hosts the persisted-profile banner + picker,
202+
creates new drafts, and carries the Add / Clone / Delete action row for the
203+
active persisted profile. Every voluntary active-profile change routes
204+
through the session's `RequestActive` gate; the page never writes the
205+
persisted profile outside the atomic `UpdateProfile` / `CreateProfile`
206+
boundary.
207+
208+
**Cloning** (the Clone action, between Add and Delete) copies the active
209+
persisted profile through the focused `IProfileCloner` capability:
210+
211+
- **Gating.** Clone is visible only for an active persisted profile and
212+
disabled while a new draft is open, while a Save is in flight, and while
213+
Darktide runs (the clone becomes active, which is a profile switch). All
214+
gates are re-checked in the command body after the dirty-transition await;
215+
the disabled button keeps its tooltip (`ToolTip.ShowOnDisabled`) so the
216+
running-state reason stays available.
217+
- **Dirty guard.** A dirty editor resolves through the same unsaved-changes
218+
transition as navigation/switch/Add: Save persists the edits then clones
219+
the saved profile, Don't save discards the edits then clones the previously
220+
persisted profile, Cancel/ESC/X creates nothing and preserves the edits.
221+
- **Activation.** On success the returned clone is requested active through
222+
the session (with the page's own-reload suppression so the session event
223+
handler does not treat it as an outside displacement), then one
224+
authoritative reload opens the clone in a clean editor. Cloning is
225+
immediate + non-destructive, so it needs no confirmation; the new banner
226+
name is the success feedback.
227+
- **DMF suppression.** `CloneProfile` raises no
228+
`IProfileService.ProfileCreated` (the blank-profile signal behind the DMF
229+
offer), so cloning a profile never queues the DMF prompt.
230+
- **Failure.** An expected clone/read/write failure logs, keeps the source
231+
active + unchanged, and surfaces the localized generic clone error in the
232+
page's fixed error area; raw exception text is never shown.
233+
198234
## The shell (`ShellViewModel` + `MainWindow`)
199235

200236
The shell owns navigation across five hosted destinations (the
@@ -1085,11 +1121,13 @@ subscription re-hydrates from the store when the result lands.
10851121
holds the name + source badge (row 0) and a two-line plain-text summary
10861122
(row 1, `MaxLines=2`, `TextWrapping=Wrap`, `TextTrimming=CharacterEllipsis`,
10871123
full text in the tooltip and the automation name), and row 2 is the action
1088-
strip. When the card is wide (greater than 680 DIP) a 112-DIP
1089-
`UniformToFill` thumbnail spans all three rows and the action strip occupies
1090-
only the right column; when constrained (at or below 680 DIP) the thumbnail
1091-
shrinks to 72 DIP spanning name + summary and the action strip moves to a
1092-
full-width row beneath both columns. Width, height, row span, and action
1124+
strip. When the card is wide (greater than 680 DIP) a 192x108 DIP 16:9
1125+
`Uniform` thumbnail (the complete source image; the frame's neutral
1126+
background letterboxes/pillarboxes non-16:9 assets) spans all three rows and
1127+
the action strip occupies only the right column; when constrained (at or
1128+
below 680 DIP) the thumbnail shrinks to 128x72 DIP spanning name + summary
1129+
and the action strip moves to a full-width row beneath both columns. Width,
1130+
height, row span, and action
10931131
column/span that change at the breakpoint are driven by styles (not local
10941132
values, which would outrank styles); constant row/column positions stay
10951133
local. Both roots bind the exact same per-row state and route to the exact
19.1 KB
Loading

docs/images/main.png

23.5 KB
Loading
108 KB
Loading
175 KB
Loading
106 KB
Loading

0 commit comments

Comments
 (0)