# 040 — Installing and updating the skin from the addon

Status: approved 2026-09-30. Built in F1.

## 1. Kodi facts this relies on (source-verified, 21.3)

- `CAddonMgr::FindAddons()` scans `special://home/addons`; a new folder found
  there is inserted into `Addons33.db` `installed` with **`enabled=0`**
  (`CAddonDatabase::SyncInstalled`) unless it is a system addon. A skin copied
  by us is therefore **disabled** until enabled.
- Builtin `UpdateLocalAddons` triggers that scan. Builtin `EnableAddon(id)`
  asks the user with its own yes/no dialog — **do not use it**; use JSON-RPC
  `Addons.SetAddonEnabled {"addonid":"skin.milionar","enabled":true}` (silent).
- Setting `lookandfeel.skin` (JSON-RPC `Settings.SetSettingValue`) triggers
  `ReloadSkin(confirm)`: Kodi loads the skin, then shows "Keep this skin?"
  (#13111) with a **10 s** timeout; no answer or No reverts to the previous
  skin. If the skin fails to load Kodi resets to the default skin and shows
  error #24102. We keep this safety net.
- `CSkinInfo::OnPostInstall` (the "Switch to this skin?" prompt) runs only for
  installs through the addon installer (repo/zip), not for our copy, so we ask
  ourselves.
- Kodi restarts an addon's service after the addon is updated, so the
  installer runs after every update.

## 2. Flow (`wcs/skin/installer.py`, called from `service.py` before autostart)

```
bundled = resources/skin.milionar/addon.xml version
target  = special://home/addons/skin.milionar
1. target is a symlink                → dev mode: skip copy (log it), go to 4
2. target missing or version differs  → copy bundled → target.tmp, then
                                         rename old → target.old, tmp → target,
                                         delete target.old (atomic swap)
3. UpdateLocalAddons, wait until JSON-RPC Addons.GetAddonDetails(skin.milionar)
   returns (timeout 15 s)
4. Addons.SetAddonEnabled(skin.milionar, true) if disabled
5. active skin == skin.milionar and step 2 copied → ReloadSkin()
6. active skin != skin.milionar and (first install or step 2 copied because of
   an addon update) → offer switch (§4)
```

- Wait for the GUI (`xbmc.getCondVisibility('Window.IsActive(Home)')` or
  `System.HasLoginScreen` false, max 60 s) before any dialog.
- Never run while video plays (`Player.HasVideo`); retry when playback stops.
- Log every step with `[Milionář Skin]`; any exception is logged and the
  service continues (the addon must not break because the skin failed).

## 3. Versioning

- The skin has its own version (`1.0.0`, semver). Every change to
  `resources/skin.milionar/` that should reach users bumps it; `/deploy`
  refuses to build if the skin tree changed since the last deploy without a
  version bump (F1 adds this check to `.agents/commands/deploy.md`).
- Addon settings store `skin_installed_version` and `skin_offer_version` (the
  addon version at which the switch was last offered).

## 4. Offering the switch

Decided: ask on the first install **and after every addon update** while the
skin is not active.

- Dialog: `xbmcgui.Dialog().yesno("Milionář", <#string: Switch to the Milionář
  skin? Everything from the addon on the Home screen. You can switch back in
  Settings → Interface → Skin.>)`.
- Yes → `Settings.SetSettingValue lookandfeel.skin = skin.milionar`; Kodi then
  asks "Keep this skin?" — the user confirms there.
- No → store `skin_offer_version = <addon version>`; ask again after the next
  addon update.
- Addon settings get an action button **"Přepnout na skin Milionář"**
  (`action=skin_switch`) that runs the same switch at any time (cs/sk/en).

## 5. Development mode

- F1 creates `~/Library/Application Support/Kodi/addons/skin.milionar` as a
  symlink to `plugin.video.milionar/resources/skin.milionar`, then
  `UpdateLocalAddons` + enable.
- The installer never overwrites a symlink; `kodi_live.py skin milionar`
  switches without the prompt (100 §3).

## 6. Uninstall and failure

- Uninstalling the addon leaves the skin; the skin hides all Milionář items
  (`System.AddonIsEnabled(plugin.video.milionar)`), so it stays usable as
  Estuary.
- If the copy fails midway the old skin stays (atomic swap); the next service
  start retries.

## 7. Tests (F1 exit, 120 §3)

1. Dev: symlink present → service logs "dev mode", no copy.
2. Fresh install: back up `userdata/Database/Addons33.db` and the addons
   folder entry, remove the symlink, restart Kodi → skin copied, enabled,
   switch offered; answer No → Estuary stays; restart → not asked again.
3. Update: bump the addon version → restart → asked again; Yes → Kodi's keep
   dialog → skin active.
4. Skin active + skin version bump → copied and `ReloadSkin()` without a
   question.
5. Restore the dev symlink and the database backup afterwards.

## 8. Result (F1, 2026-09-30)

All tests of §7 passed in the live Kodi (Kodi restarts, real `Addons33.db`):

1. Dev symlink: log `dev mode ... not copying`, skin enabled, no offer.
2. Fresh install (symlink removed, `skin_offer_version` empty): log
   `installing skin.milionar 1.0.0 (installed: None)`, `enabled`, Yes/No
   dialog shown ~20 s after start; **No** -> `switch declined`, Estuary stays;
   after another restart no question.
3. Update (addon version bumped): asked again; **Yes** -> Kodi's "Keep this
   skin?" -> skin active.
4. Skin active + bundled skin version 1.0.0 -> 1.0.1: log
   `installing ... (installed: 1.0.0)` + `skin updated while active:
   ReloadSkin()` without a question.
5. Afterwards the dev symlink and the original addon version were restored.

Notes: the copy is staged in `special://home/temp/` (not inside `addons/`,
where Kodi would scan it) and swapped with `os.rename`. `kodi_live.py skin`
confirms "Keep this skin?" itself. `/deploy` runs
`scripts/skin_harness/deploy_check.py check` (skin tree changed without a
version bump -> stop) and `record` after the upload.
