diff --git a/CHANGELOG.md b/CHANGELOG.md index afbcd845..0824561d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## Unreleased +## v0.11.0 (2022-08-20) * Added: * Support for multiple full and differential backups per game. diff --git a/Cargo.lock b/Cargo.lock index c3c0052d..1d1edfe8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1932,7 +1932,7 @@ dependencies = [ [[package]] name = "ludusavi" -version = "0.10.0" +version = "0.11.0" dependencies = [ "base64", "byte-unit", diff --git a/Cargo.toml b/Cargo.toml index 68de581f..d644efed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ludusavi" -version = "0.10.0" +version = "0.11.0" authors = ["mtkennerly "] edition = "2021" description = "Game save backup tool" diff --git a/README.md b/README.md index d989b03d..60d309e4 100644 --- a/README.md +++ b/README.md @@ -100,28 +100,33 @@ If you are on Mac: * You can press `preview` to see what the backup will include, without actually performing it. - After you've done one preview or backup, Ludusavi will remember which games - it found and only re-scan those games the next time. If you change your root - configuration, change the "other" settings, or reopen the program, then - it will do another full scan. - + If you do a preview, then Ludusavi will use that list of games for the next + backup, so that the next backup doesn't have to do another full scan. +* The gear icon will reveal some additional options: + + * You can set a number of full and differential backups to keep. + For example, if you set 2 full and 2 differential, then the first backup + for a game will be a full, and then the next two backups will be differential. + That repeats for the next three backups. When a third full backup is made, + the first full backup and its two associated differentials are deleted. + * You can enable compressed zip backups instead of plain files/folders. * You can press `back up` to perform the backup for real. - * If the target folder already exists, it will be deleted first and - recreated, unless you've enabled the merge option. + * If you've disabled the merge option, then the target folder will be + deleted first if it exists, then recreated. * Within the target folder, for every game with data to back up, a subfolder will be created based on the game's name, where some invalid characters are replaced by `_`. In rare cases, if the whole name is invalid characters, then it will be renamed to `ludusavi-renamed-`. * Within each game's subfolder, there will be a `mapping.yaml` file that - Ludusavi needs to identify the game. There will be some drive folders + Ludusavi needs to identify the game. + + When using the simple backup format, there will be some drive folders (e.g., `drive-C` on Windows or `drive-0` on Linux and Mac) containing the backup files, matching the normal file locations on your computer. + When using the zip backup format, there will be zip files instead. * If the game has save data in the registry and you are using Windows, then - the game's subfolder will also contain a `registry.yaml` file. + the game's subfolder will also contain a `registry.yaml` file (or it will + be placed in each backup's zip file). If you are using Steam and Proton instead of Windows, then the Proton `*.reg` files will be backed up along with the other game files instead. * Roots are folders that Ludusavi can check for additional game data. When you @@ -144,6 +149,8 @@ If you are on Mac: * For a Wine prefix root, this should be the folder containing `drive_c`. Currently, Ludusavi does not back up registry-based saves from the prefix, but will back up any file-based saves. + + You may use globs in root paths to identify multiple roots at once. * To select/deselect specific games, you can run a preview, then click the checkboxes by each game. You can also press the `deselect all` button (when all games are selected) or the `select all` button (when at least @@ -153,11 +160,14 @@ If you are on Mac: game entry with the same name, allowing you to override that game's data. See the [custom games](#custom-games) section for more information. + The play icon will trigger an on-demand backup for that specific game. + There is also a globe icon, which will open the game's PCGamingWiki article so that you can quickly double check or update its information if needed. * You can click the search icon and enter some text to just see games with matching names. Note that this only affects which games you see in the list, - but Ludusavi will still back up the full set of games. + but Ludusavi will still back up the full set of games. This also provides + some sorting options. * You may see a "duplicates" badge next to some games. This means that some of the same files were also backed up for another game. That could be intentional (e.g., an HD remaster may reuse the original save locations), but it could @@ -177,13 +187,13 @@ If you are on Mac: * For each subfolder in the source directory, Ludusavi looks for a `mapping.yaml` file in order to identify each game. Subfolders without that file, or with an invalid one, are ignored. - * All files from the drive folders are copied back to their original locations + * All files from the drive folders (or zips) are copied back to their original locations on the respective drive. Any necessary parent directories will be created as well before the copy, but if the directories already exist, then their current files will be left alone (other than overwriting the ones that are being restored from the backup). - * If the game subfolder includes a `registry.yaml` file, then the Windows - registry data will be restored as well. + * If the backup includes a `registry.yaml` file, then the Windows registry + data will be restored as well. * You can use redirects to restore to a different location than the original file. Click `add redirect`, and then enter both the old and new location. For example, if you backed up some saves from `C:/Games`, but then you moved it to `D:/Games`, @@ -229,6 +239,8 @@ If you are on Mac: #### Other settings * Switch to this screen by clicking the `other` button. * This screen contains some additional settings that are less commonly used. + * Backup exclusions let you set paths and registry keys to completely ignore + from all games. They will not be shown at all during backup scans. ### CLI Run `ludusavi --help` for the full usage information. @@ -263,6 +275,7 @@ will have the following structure: * `files` (map): * Each key is a file path, and each value is a map with these fields: * `failed` (optional, boolean): Whether this entry failed to process. + * `ignored` (optional, boolean): Whether this entry was ignored. * `bytes` (number): Size of the file. * `originalPath` (optional, string): If the file was restored to a redirected location, then this is its original path. @@ -271,6 +284,7 @@ will have the following structure: * `registry` (map): * Each key is a registry path, and each value is a map with these fields: * `failed` (optional, boolean): Whether this entry failed to process. + * `ignored` (optional, boolean): Whether this entry was ignored. * `duplicatedBy` (optional, array of strings): Any other games that also have the same registry path. @@ -344,11 +358,16 @@ Here are the available settings (all are required unless otherwise noted): * `url` (string): Where to download the primary manifest. * `etag` (string or null): An identifier for the current version of the manifest. This is generated automatically when the manifest is updated. +* `language` (string, optional): Display language. Valid options: + `en-US` (English, default), `fil-PH` (Filipino), `de-DE` (German), `it-IT` (Italian), `pt-BR` (Brazilian Portuguese), `pl-PL` (Polish), `es-ES` (Spanish). + + Experimental options that currently have graphical display issues: + `ar-SA` (Arabic), `zh-Hans` (Simplified Chinese). * `roots` (list): * Each entry in the list should be a map with these fields: * `path` (string): Where the root is located on your system. * `store` (string): Game store associated with the root. Valid options: - `epic`, `gog`, `gogGalaxy`, `microsoft`, `origin`, + `epic`, `gog`, `gogGalaxy`, `microsoft`, `origin`, `prime`, `steam`, `uplay`, `otherHome`, `otherWine`, `other` * `backup` (map): * `path` (string): Full path to a directory in which to save backups. @@ -356,7 +375,7 @@ Here are the available settings (all are required unless otherwise noted): * `ignoredGames` (optional, array of strings): Names of games to skip when backing up. This can be overridden in the CLI by passing a list of games. * `merge` (optional, boolean): Whether to merge save data into the target - directory rather than deleting the directory first. Default: false. + directory rather than deleting the directory first. Default: true. * `filter` (optional, map): * `excludeOtherOsData` (optional, boolean): If true, then the backup should exclude any files that have only been confirmed for a different operating @@ -364,6 +383,23 @@ Here are the available settings (all are required unless otherwise noted): backed up regardless of this setting. Default: false. * `excludeStoreScreenshots` (optional, boolean): If true, then the backup should exclude screenshots from stores like Steam. Default: false. + * `ignoredPaths` (list of strings): Globally ignored paths. + * `ignoredRegistry` (list of strings): Globally ignored registry keys. + * `toggledPaths` (map): Paths overridden for inclusion/exclusion in the backup. + Each key is a game name, and the value is another map. In the inner map, + each key is a path, and the value is a boolean (true = included). + Settings on child paths override settings on parent paths. + * `toggledRegistry` (map): Same as `toggledPaths`, but for registry entries. + * `sort` (map): + * `key` (string): One of `name`, `size`. + * `reversed` (boolean): If true, sort reverse alphabetical or from the largest size. + * `retention` (map): + * `full` (integer): Full backups to keep. Range: 1-255. + * `differential` (integer): Full backups to keep. Range: 0-255. + * `format` (map): + * `chosen` (string): One of `simple`, `zip`. + * `zip` (map): Settings for the zip format. + * `compression` (string): One of `none`, `deflate`, `bzip2`, `zstd`. * `restore` (map): * `path` (string): Full path to a directory from which to restore data. This can be overridden in the CLI with `--path`. @@ -373,6 +409,9 @@ Here are the available settings (all are required unless otherwise noted): * Each entry in the list should be a map with these fields: * `source` (string): The original location when the backup was performed. * `target` (string): The new location. + * `sort` (map): + * `key` (string): One of `name`, `size`. + * `reversed` (boolean): If true, sort reverse alphabetical or from the largest size. * `customGames` (optional, list): * Each entry in the list should be a map with these fields: * `name` (string): Name of the game. diff --git a/assets/com.github.mtkennerly.ludusavi.metainfo.xml b/assets/com.github.mtkennerly.ludusavi.metainfo.xml index 20d9094d..ad86927a 100644 --- a/assets/com.github.mtkennerly.ludusavi.metainfo.xml +++ b/assets/com.github.mtkennerly.ludusavi.metainfo.xml @@ -34,6 +34,7 @@ com.github.mtkennerly.ludusavi.desktop + diff --git a/docs/demo-cli.gif b/docs/demo-cli.gif index d38503c8..6d3182ab 100644 Binary files a/docs/demo-cli.gif and b/docs/demo-cli.gif differ diff --git a/docs/demo-cli.png b/docs/demo-cli.png deleted file mode 100644 index 1d9b7d48..00000000 Binary files a/docs/demo-cli.png and /dev/null differ diff --git a/docs/demo-gui.gif b/docs/demo-gui.gif index f16e2c85..71a4c9a7 100644 Binary files a/docs/demo-gui.gif and b/docs/demo-gui.gif differ diff --git a/docs/demo-gui.png b/docs/demo-gui.png deleted file mode 100644 index 7e7991f5..00000000 Binary files a/docs/demo-gui.png and /dev/null differ diff --git a/docs/sample-gui-linux.png b/docs/sample-gui-linux.png new file mode 100644 index 00000000..c57744ad Binary files /dev/null and b/docs/sample-gui-linux.png differ diff --git a/src/cli.rs b/src/cli.rs index 6ef72ccf..6a3cfe4d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -239,6 +239,8 @@ struct ApiFile { struct ApiRegistry { #[serde(skip_serializing_if = "crate::serialization::is_false")] failed: bool, + #[serde(skip_serializing_if = "crate::serialization::is_false")] + ignored: bool, #[serde( rename = "duplicatedBy", serialize_with = "crate::serialization::ordered_set", @@ -429,16 +431,17 @@ impl Reporter { api_game.files.insert(readable.render(), api_file); } for entry in itertools::sorted(&scan_info.found_registry_keys) { - let mut api_registry = ApiRegistry::default(); + let mut api_registry = ApiRegistry { + failed: backup_info.failed_registry.contains(&entry.path), + ignored: entry.ignored, + ..Default::default() + }; if duplicate_detector.is_registry_duplicated(&entry.path) { let mut duplicated_by = duplicate_detector.registry(&entry.path); duplicated_by.remove(&scan_info.game_name); api_registry.duplicated_by = duplicated_by; } - if backup_info.failed_registry.contains(&entry.path) { - api_registry.failed = true; - } if api_registry.failed { successful = false; } diff --git a/src/gui/game_list.rs b/src/gui/game_list.rs index bf2510ad..e4059786 100644 --- a/src/gui/game_list.rs +++ b/src/gui/game_list.rs @@ -134,7 +134,7 @@ impl GameListEntry { self.scan_info.backup.as_ref().map(|backup| { Container::new(Text::new(backup.label()).size(18)) .padding([2, 0, 0, 15]) - .width(Length::Units(175)) + .width(Length::Units(185)) .align_x(HorizontalAlignment::Center) }) } else if !self.scan_info.available_backups.is_empty() { @@ -166,7 +166,7 @@ impl GameListEntry { .style(style::PickList::Backup), ) .padding([0, 0, 0, 15]) - .width(Length::Units(175)) + .width(Length::Units(185)) .align_x(HorizontalAlignment::Center); Some(content) } else { diff --git a/src/lang.rs b/src/lang.rs index 5ddf6734..1522b019 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -75,15 +75,15 @@ impl Language { impl ToString for Language { fn to_string(&self) -> String { match self { - Self::Arabic => "العربية", - Self::ChineseSimplified => "中文(简体)", + Self::Arabic => "العربية (41%)", + Self::ChineseSimplified => "中文(简体) (49%)", Self::English => "English", - Self::Filipino => "Filipino", + Self::Filipino => "Filipino (70%)", Self::German => "Deutsch", - Self::Italian => "Italiano", - Self::Polish => "Polski", - Self::PortugueseBrazilian => "Português brasileiro", - Self::Spanish => "Español", + Self::Italian => "Italiano (98%)", + Self::Polish => "Polski (96%)", + Self::PortugueseBrazilian => "Português brasileiro (94%)", + Self::Spanish => "Español (94%)", } .to_string() }