Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 8 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# FromSoft Mod Manager

A native Windows desktop app for managing mods across FromSoftware
games. Install mods from Nexus, configure settings, manage saves,
and launch games with Mod Engine 3 — all from a single app.
Skip the manual mod setup headaches — FromSoft Mod Manager automatically finds your Steam games, installs co-op mods from Nexus with one click, and keeps everything up to date. Connect your Nexus account, pick your game, and you're playing co-op in minutes. Saves, settings, and mod loading through Mod Engine 3 are all handled for you.

> **Note:** This is a **manager tool only**. The Seamless Co-op mods
> are created by [LukeYui](https://github.com/LukeYui). All credit
Expand Down Expand Up @@ -62,9 +60,9 @@ folders across all drives.

### Nexus Mods Integration

- **SSO authentication** — click "Authorize with Nexus Mods",
- **OAuth 2.0 authentication** — click "Authorize with Nexus Mods",
approve in browser, done (no copy-paste needed)
- Manual API key fallback for users who prefer it
- Automatic token refresh — stays connected across sessions
- User profile display in sidebar
- Trending and recommended mods per game
- Direct download with Nexus Premium support
Expand Down Expand Up @@ -176,8 +174,8 @@ The installer:

1. Click **Connect Account** in the sidebar
2. Click **Authorize with Nexus Mods** — your browser opens
3. Click "Authorize" on the Nexus page — the app receives your
API key automatically
3. Click "Authorize" on the Nexus page — the app connects
automatically
4. Your Nexus username appears in the sidebar

### Managing Mods
Expand Down Expand Up @@ -223,13 +221,13 @@ fromsoft_coop_manager/
│ │ └── save_manager.py Save file operations
│ ├── services/
│ │ ├── nexus_service.py Nexus Mods REST API client
│ │ ├── nexus_sso.py Nexus SSO WebSocket auth flow
│ │ ├── nexus_oauth.py Nexus OAuth 2.0 PKCE auth flow
│ │ └── steam_service.py Steam player count and asset APIs
│ └── ui/
│ ├── main_window.py Main window with sidebar + content
│ ├── sidebar.py Game list, player counts, Nexus
│ ├── game_page.py Per-game tab container
│ ├── nexus_widget.py Nexus auth widget (SSO + manual)
│ ├── nexus_widget.py Nexus auth widget (OAuth 2.0)
│ ├── terminal_widget.py Log output panel
│ ├── tabs/
│ │ ├── launch_tab.py Game launcher with cover art
Expand Down Expand Up @@ -265,7 +263,6 @@ fromsoft_coop_manager/
| `PySide6` | Qt 6 UI framework |
| `requests` | HTTP client for API calls |
| `tomlkit` / `tomli-w` | TOML reading/writing for ME3 profiles |
| `websocket-client` | Nexus SSO WebSocket authentication |
| `py7zr` | 7z archive extraction |
| `rarfile` | RAR archive extraction (requires WinRAR or 7-Zip) |
| `pyinstaller` | Build tooling (dev only) |
Expand Down Expand Up @@ -313,7 +310,7 @@ to `ME3_GAME_MAP` in `app/core/me3_service.py`.
| ---------------- | ---------------------------------------------------------- |
| **UI Framework** | PySide6 (Qt 6) with Fusion base style |
| **Mod Loader** | Mod Engine 3 CLI (`me3 launch -g <game>`) |
| **Nexus Auth** | WebSocket SSO via `wss://sso.nexusmods.com` |
| **Nexus Auth** | OAuth 2.0 PKCE with automatic token refresh |
| **Packaging** | PyInstaller (onedir) then Inno Setup installer |
| **Config** | JSON config file (`config.json`) |
| **Theme** | Custom QSS dark theme (#0e0e18 bg, #e94560 accent) |
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.0.3
2.1.0
43 changes: 36 additions & 7 deletions app/config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import sys
import json
import time
from datetime import datetime
from pathlib import Path

Expand All @@ -26,6 +27,7 @@ class ConfigManager:
def __init__(self):
self._migrate_legacy_config()
self._config = self._load()
self._migrate_nexus_api_key()

# ------------------------------------------------------------------
# Migration
Expand Down Expand Up @@ -97,15 +99,42 @@ def get_last_scan(self) -> str | None:
return self._config.get("last_scan")

# ------------------------------------------------------------------
# Nexus
# Nexus OAuth
# ------------------------------------------------------------------
def get_nexus_api_key(self) -> str:
return self._config.get("nexus_api_key", "")

def set_nexus_api_key(self, key: str):
self._config["nexus_api_key"] = key
def _migrate_nexus_api_key(self):
"""Remove legacy API key auth — users must re-authorize via OAuth."""
if "nexus_api_key" in self._config:
self._config.pop("nexus_api_key", None)
self._config.pop("nexus_user", None)
self.save()

def get_nexus_tokens(self) -> dict:
"""Return stored OAuth tokens or empty dict.

Keys: access_token, refresh_token, expires_at
"""
return self._config.get("nexus_tokens", {})

def get_nexus_access_token(self) -> str:
"""Convenience: return the current access token, or empty string."""
return self.get_nexus_tokens().get("access_token", "")

def set_nexus_tokens(self, tokens: dict):
"""Store OAuth tokens (access_token, refresh_token, expires_at)."""
self._config["nexus_tokens"] = {
"access_token": tokens.get("access_token", ""),
"refresh_token": tokens.get("refresh_token", ""),
"expires_at": tokens.get("expires_at", 0),
}
self.save()

def is_nexus_token_expired(self) -> bool:
"""Check if the stored access token has expired."""
tokens = self.get_nexus_tokens()
if not tokens.get("access_token"):
return True
return time.time() >= tokens.get("expires_at", 0)

def get_nexus_user_info(self) -> dict:
return self._config.get("nexus_user", {})

Expand All @@ -114,7 +143,7 @@ def set_nexus_user_info(self, info: dict):
self.save()

def clear_nexus_auth(self):
self._config.pop("nexus_api_key", None)
self._config.pop("nexus_tokens", None)
self._config.pop("nexus_user", None)
self.save()

Expand Down
Loading
Loading