├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── TakeoutExtractor.Cli.Tests ├── CommandLineTests.cs ├── EnumParsinTests.cs ├── TakeoutExtractor.Cli.Tests.csproj └── Usings.cs ├── TakeoutExtractor.Cli ├── CommandLine.cs ├── CommandLineException.cs ├── Program.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml └── TakeoutExtractor.Cli.csproj ├── TakeoutExtractor.Gui ├── AlertsPage.xaml ├── AlertsPage.xaml.cs ├── App.xaml ├── App.xaml.cs ├── AppShell.xaml ├── AppShell.xaml.cs ├── FolderPicker.cs ├── MainPage.xaml ├── MainPage.xaml.cs ├── MauiExt.cs ├── MauiProgram.cs ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ └── values │ │ │ └── colors.xml │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── FolderPicker.cs │ │ ├── Info.plist │ │ ├── Program.cs │ │ └── QuestionDialog.cs │ ├── Tizen │ │ ├── Main.cs │ │ └── tizen-manifest.xml │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── FolderPicker.cs │ │ ├── MauiExt.cs │ │ ├── Package.appxmanifest │ │ ├── QuestionDialog.cs │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs ├── ProgressOverlay.xaml ├── ProgressOverlay.xaml.cs ├── Properties │ ├── PublishProfiles │ │ └── MSIX-win10-x64.pubxml │ └── launchSettings.json ├── QuestionDialog.cs ├── Resources │ ├── AppIcon │ │ └── appicon.svg │ ├── Fonts │ │ ├── OpenSans-Regular.ttf │ │ └── OpenSans-Semibold.ttf │ ├── Images │ │ └── splash_overlay.svg │ ├── Raw │ │ └── AboutAssets.txt │ ├── Splash │ │ └── splash.svg │ └── Styles │ │ ├── Colors.xaml │ │ └── Styles.xaml ├── SplashOverlay.xaml ├── SplashOverlay.xaml.cs └── TakeoutExtractor.Gui.csproj ├── TakeoutExtractor.Lib.Tests ├── DirectoryInfoExtTests.cs ├── FileInfoExtTests.cs ├── Photo │ ├── GeoTests.cs │ ├── PhotoExtractorTests.cs │ ├── PhotoTestsHelper.cs │ └── manifest_template.json ├── TakeoutExtractor.Lib.Tests.csproj └── Usings.cs ├── TakeoutExtractor.Lib ├── DirectoryInfoExt.cs ├── ExceptionExt.cs ├── ExtractorAlert.cs ├── ExtractorAlertEventArgs.cs ├── ExtractorManager.cs ├── FileInfoExt.cs ├── GlobalOptions.cs ├── IExtractorOptions.cs ├── IExtractorResults.cs ├── Photo │ ├── ExifLibraryExt.cs │ ├── GeoLocation.cs │ ├── PhotoExtractor.cs │ ├── PhotoMetadata.cs │ ├── PhotoOptions.cs │ └── PhotoResults.cs ├── ProgressEventArgs.cs ├── StructuredTextWriter.cs └── TakeoutExtractor.Lib.csproj ├── TakeoutExtractor.sln ├── Third party ├── ExifLibrary.dll └── ExifLibrary.pdb ├── screenshot-gui-small.png └── screenshot-gui.png /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Mac-specific 2 | .DS_Store 3 | 4 | ## Ignore Visual Studio temporary files, build results, and 5 | ## files generated by popular Visual Studio add-ons. 6 | ## 7 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | [Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Ww][Ii][Nn]32/ 30 | [Aa][Rr][Mm]/ 31 | [Aa][Rr][Mm]64/ 32 | bld/ 33 | [Bb]in/ 34 | [Oo]bj/ 35 | [Oo]ut/ 36 | [Ll]og/ 37 | [Ll]ogs/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # ASP.NET Scaffolding 70 | ScaffoldingReadMe.txt 71 | 72 | # StyleCop 73 | StyleCopReport.xml 74 | 75 | # Files built by Visual Studio 76 | *_i.c 77 | *_p.c 78 | *_h.h 79 | *.ilk 80 | *.meta 81 | *.obj 82 | *.iobj 83 | *.pch 84 | *.pdb 85 | *.ipdb 86 | *.pgc 87 | *.pgd 88 | *.rsp 89 | *.sbr 90 | *.tlb 91 | *.tli 92 | *.tlh 93 | *.tmp 94 | *.tmp_proj 95 | *_wpftmp.csproj 96 | *.log 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | #*.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio LightSwitch build output 301 | **/*.HTMLClient/GeneratedArtifacts 302 | **/*.DesktopClient/GeneratedArtifacts 303 | **/*.DesktopClient/ModelManifest.xml 304 | **/*.Server/GeneratedArtifacts 305 | **/*.Server/ModelManifest.xml 306 | _Pvt_Extensions 307 | 308 | # Paket dependency manager 309 | .paket/paket.exe 310 | paket-files/ 311 | 312 | # FAKE - F# Make 313 | .fake/ 314 | 315 | # CodeRush personal settings 316 | .cr/personal 317 | 318 | # Python Tools for Visual Studio (PTVS) 319 | __pycache__/ 320 | *.pyc 321 | 322 | # Cake - Uncomment if you are using it 323 | # tools/** 324 | # !tools/packages.config 325 | 326 | # Tabs Studio 327 | *.tss 328 | 329 | # Telerik's JustMock configuration file 330 | *.jmconfig 331 | 332 | # BizTalk build output 333 | *.btp.cs 334 | *.btm.cs 335 | *.odx.cs 336 | *.xsd.cs 337 | 338 | # OpenCover UI analysis results 339 | OpenCover/ 340 | 341 | # Azure Stream Analytics local run output 342 | ASALocalRun/ 343 | 344 | # MSBuild Binary and Structured Log 345 | *.binlog 346 | 347 | # NVidia Nsight GPU debugger configuration file 348 | *.nvuser 349 | 350 | # MFractors (Xamarin productivity tool) working folder 351 | .mfractor/ 352 | 353 | # Local History for Visual Studio 354 | .localhistory/ 355 | 356 | # BeatPulse healthcheck temp database 357 | healthchecksdb 358 | 359 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 360 | MigrationBackup/ 361 | 362 | # Ionide (cross platform F# VS Code tools) working folder 363 | .ionide/ 364 | 365 | # Fody - auto-generated XML schema 366 | FodyWeavers.xsd 367 | 368 | 369 | 370 | # ------------------------------------------------------------------------------------- 371 | # Project specific overrides 372 | 373 | # Don't ignore the third-party directory 374 | !Third party/* 375 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Change Log 2 | 3 | ##2024-01-08 v1.1 4 | 5 | Fixes: 6 | 7 | - Issue #16: Trailing command is lost. 8 | 9 | - Issue #17: Array error accessing manifest title element. 10 | 11 | - Issue #18: Windows GUI is not longer easy installable due to expired publishing certificate. Certificate now expires in 2034. 12 | 13 | - Issue #19: Use sidecar photoTakenTime element for file and directory naming. 14 | 15 | - Issue #20: Ignore json files that are not image sidecars. metadata.json, print-subscriptions.json, shared_album_comments.json, 16 | user-generated-memory-titles.json, etc. are ignored. 17 | 18 | - Issue #20: Deleted photos/videos are extracted. 19 | 20 | Enhancements: 21 | 22 | - Migrated to .net 7 and refreshed gui project structure 23 | 24 | - Added option to ignore (default) or extract deleted photos files in the Bin folder. 25 | 26 | - Improved error detail reporting. Display alert details link only if alert has details to display. 27 | 28 | - Replaced GUI app icon and splash overlay. 29 | 30 | - Added additional readme content and a screenshot. 31 | 32 | - Include PubXml files for this solution since they contain no secrets 33 | 34 | Note: This is a source-only release as publishing the gui app on Windows is broken for unknown reasons. See issue #22. 35 | 36 | 37 | 38 | ##2023-02-01 v1.0 39 | 40 | v1.0 release! 41 | 42 | Enhancements: 43 | - Implemented six different photo version handling strategies instead of the previous single fixed approach. 44 | As part of this, removed configurable handling of directory name and file suffix for original photos. 45 | 46 | Fit and finish: 47 | - Issue #15: Prevent splash screen from being displayed when navigating back to main page from alerts page 48 | - Made alerts page scrollable. Uses theme-dependant link colour for readability. 49 | - Prompt to create output directory if it doesn't exist 50 | 51 | 52 | ##2023-01-18 v0.9 53 | 54 | Multi-platform release: now includes a Mac Catalyst version. 55 | 56 | Principal changes to support multiple platforms: 57 | - Directory and file enumeration ordered specifically by name to ensure consistent results across platforms. 58 | - Implemented DisplayAlert() for Mac platform. 59 | - Removed partial themes support - AppThemeBinding is sufficient. 60 | - Documented edited file matching code. 61 | - Updated tests to use Unix-style paths for cross-platform. 62 | 63 | 64 | ##2022-12-03 v0.8 65 | 66 | Gui: 67 | - Added a simple splash screen for Windows and Mac. 68 | - Improved look of project's app icon and fixed project file so that it is used. 69 | - Conditional gui app title to distinguish development and installed versions. 70 | - Main window (except progress dialog) is disabled during extraction. 71 | - Added some support for styling, but this appers to be broken at present - see https://github.com/dotnet/maui/issues/6596 72 | - Restored Android and iOS targets, as this seems to be the only way to render the project pubishable - see https://github.com/dotnet/maui/issues/11816 73 | 74 | 75 | ##2022-11-18 v0.7 76 | 77 | Added config option for time kind used for photo output file naming. Options are local time or UTC, default is local time to 78 | match EXIF value. 79 | - Added combo to Gui project. 80 | - Added -ft option to Cli project. 81 | 82 | Allow selectable log file format - json or xml. 83 | - Added combo to Gui project. 84 | - Modified -lf option to Cli project to take new values. 85 | 86 | Gui 87 | - Gui starts maximised and flashes its takbar icon on completion (unless manually cancelled). 88 | - Re-organised main page so that start button is immediately visible. 89 | - Restored origonal grey theme and added a little colour. 90 | - Various minor fit and finish changes. 91 | 92 | Added enum parsing tests. 93 | 94 | 95 | ##2022-11-04 v0.6.2 96 | 97 | - Restored previous output package names: tex and tex-gui. 98 | - Misc cleanup including removing unused platform targets. 99 | 100 | 101 | ##2022-11-02 v0.6.1 102 | 103 | Core photo extraction: 104 | - Improved file matching, including for long file names and files distinguished only by "(1)" suffixes. Added tests for these cases. 105 | - Made core test driver more generic and now tests for output edited files 106 | - Extract GPS location and altitude from sidecar and conditionally populate EXIF fields 107 | - Added tests for location/altitude handling. 108 | - Added additional tests for geolocation data. 109 | 110 | GUI: 111 | - Added check for existing files in putput directory. 112 | - Allow alerts to be attached to exceptions. 113 | - Handle exceptions as unrecoverable errors. 114 | - Added details column to Alerts page 115 | - Added View->Alerts menu option to allow access to last extraction results. 116 | - Alerts page displays alerts breakdown count. 117 | - Stop on error global setting defaults to false 118 | 119 | Various refactorings, principally in the TakeoutExtractor.Lib project. 120 | 121 | Migrated to Visual Studio 17.3.6 release version from preview version. 122 | 123 | Reinstated Mac Catalyst platform target. 124 | 125 | 126 | ##2022-08-25 v0.5 127 | 128 | Extractors return results and error/warning/info objects that are presented and logged. Option to stop on first error. 129 | 130 | Various fixed and code cleanup. 131 | 132 | 133 | ##2022-08-12 v0.4 134 | 135 | Added option for extracting photos into directory hierarchy based on creation date (year, year/month, year/month/day) 136 | 137 | Added options to create a JSON log file describing what was extracted. 138 | 139 | General code clean-up 140 | 141 | 142 | ##2022-08-06 v0.3 143 | 144 | Initial, minimal installable GUI 145 | 146 | 147 | ##2022-07-27 v0.2 148 | 149 | Initial release of cli tool only 150 | 151 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Andrew Johnson 4 | 5 | EXCEPT for elements whichare explicitly licenced otherwise, this software is 6 | provided with the follwing licence: 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Takout Extractor 2 | 3 | Extracts the contents of a [Google Takeout](https://takeout.google.com/) archive - re-organising it, adding missing metadata, and 4 | applying a uniform file naming convention. Runs on Windows and MacOS. Requires .NET 7 or later. 5 | 6 | Gui and command-line implementations are provided - see the TakeoutExtractor.Gui and TakeoutExtractor.Cli projects respectively. 7 | Releases include the Gui for Windows and MacOS, and the cli for Windows only. 8 | 9 | ![Takeout Extractor GUI screenshot](screenshot-gui-small.png) 10 | 11 | This software currently extracts only photo and video files. It is planned to add support for other Takeout media types in the future. 12 | 13 | - *Photos and Videos* Image files in a Google takeout dataset have inconsistent naming. They also do not contain exif timestamps - 14 | although, confusingly, they do contain other metadata such as location information and camera settings. This software builds a uniformaly 15 | named copy of the image and video files in a takeout dataset and restores their exif timestamps. 16 | 17 | 18 | ## Getting Started 19 | 20 | 1. Build the solution in Visual Studio 2022 21 | 22 | 2. The command-line extractor, `tex.exe`, will be found in `TakeoutExtractorCli\bin\Release\net6.0\`. 23 | Run `tex /h` for help. 24 | 25 | 26 | ## Built With 27 | 28 | - Visual Studio 2022. Maui-based gui currently requires VS2022 7.3.0 preview 2.0 or later. 29 | - .net 7.0, with nullable reference type checking enabled 30 | - A fork of [ExifLibNet](https://www.nuget.org/packages/ExifLibNet) v2.1.4 with additional fixes, available at https://github.com/andyjohnson0/exiflibrary. 31 | A pre-built dll is included in the ThirdParty directory. 32 | 33 | 34 | ## Author 35 | 36 | Andrew Johnson | [github.com/andyjohnson0](https://github.com/andyjohnson0) | https://andyjohnson.uk 37 | 38 | 39 | ## Licence 40 | 41 | Except for third-party elements that are licened separately, this project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 42 | 43 | The folder picker implementations used in the gui project are based on code from [MauiFolderPickerSample](https://github.com/jfversluis/MauiFolderPickerSample) 44 | and https://blog.verslu.is/maui/folder-picker-with-dotnet-maui/ by Gerald Versluis. 45 | Licenced as [Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://creativecommons.org/licenses/by-sa/4.0/) by the original author. 46 | 47 | The shovel icon used by the GUI project is from [svgrepo.com](https://www.svgrepo.com) and is licenced as 48 | [CC0](https://creativecommons.org/publicdomain/zero/1.0/) / Public Domain by svgrepo. 49 | 50 | 51 | ## Future Enhancements, TODOs, and known bugs 52 | 53 | These are logged as issues at https://github.com/andyjohnson0/TakeoutExtractor/issues/ 54 | 55 | Things to do are flagged with `TODO:` in the code. 56 | 57 | 58 | 59 | ## Implementation Notes 60 | 61 | ### Project Structure 62 | 63 | - **TakeoutExtractor.Gui** GUI front-end using MAUI. 64 | 65 | - **TakeoutExtractor.Cli** Command-line `tex` app that drives the extraction process. 66 | 67 | - **TakeoutExtractor.Lib** Core library for the Takeout Extractor project. Exposes the TakeoutExtractor class which 68 | coordinates the extraction and reassembly of files from an unzipped Google Takeout archive. 69 | 70 | - **TakeoutExtractor.Cli.Tests** Tests for the TakeoutExtractor.Cli project. 71 | 72 | - **TakeoutExtractor.Lib.Tests** Tests for the TakeoutExtractor.Lib project. 73 | 74 | 75 | ### Method 76 | 77 | #### Images and Videos 78 | 79 | 1. Iterate over all .json files containing image or video metadata. 80 | 81 | 2. Extract the title element. This will be the original image file name, including extension. 82 | 83 | 3. Truncate the *name part* of the title to a maximum of 47 characters. This gives the image file name in the archive. 84 | 85 | 4. Search for images with the same name but with a (possibly truncated) "-edited" suffix. If this exists then the 86 | image was edited and the image file in the previous step is the original, un-edited, version. If there is no edited 87 | file then only the original image exists. 88 | 89 | 5. Extract timestamps from the json file and, for images only, update the image's embedded exif metadata. Google seems 90 | to preserve all(?) other exif fields that were populated at the time of image capture. 91 | 92 | 6. Rename the file or files according to the timestamp and place into appropriate directories. 93 | 94 | 95 | 96 | ### Some Resources 97 | 98 | EXIF tag reference: 99 | 100 | 101 | ### Some Notes on Takeout's Image File Naming 102 | 103 | Google takeout appears to provide access to the original captured form of an image, together with the last edited version of 104 | the image, if any. The images are linked together by a photo sidecar file. The easiest way to iterate over the images is to 105 | iterate over the metadata files 106 | 107 | Image file extentions can be ".jpeg", ".jpg", ".png", ".gif" and ".mp4". Sometimes an image may have a different extension in 108 | archives requested at different times. For example, it may be .jpeg in one archive and .jpg in an archive created time time 109 | later. I suspect that this may be caused by the introduction of an attempt to normalise file extensions. 110 | 111 | The maximum length of file names, including the extension but excluding the dot/period, appears to be 50 characters. 112 | So a jpg file will have a name part with a maximum length of 47, and for a json file this will be 48. File names are truncated 113 | to fit these limits, preserving the extension. 114 | 115 | The /title element in the metadata file gives the full file name of the _original_ image. However, as google truncates the name-part 116 | of the image file name, so a title of "a5025662-cb40-45dd-be98-684ee48aa226_IMG_20210818_122959697_HDR.jpg" would refer to an 117 | original image file named "a5025662-cb40-45dd-be98-684ee48aa226_IMG_202108.jpg" 118 | 119 | If the title contains & or ? characters then these are substituted with _ characters in the file names of the corresponding images. 120 | 121 | If an edited version of the image exists then it will have the same name-part as the original, but with a suffix. This suffix 122 | (which I suspect is generated by Google Photos, not Takeout) is usually "-edited"". It can be truncated (e.g. to "-edit" 123 | or "-edi") if necessary by the 47 character name-part limit. 124 | 125 | It is possible to have an "-edited" file with no original file - for example, if the original has been deleted. 126 | In this case the name part of json file name will end with the edited suffix. E.g. IMG_20190329_083618347-edited.jpeg.json 127 | and IMG_20190329_083618347-edited.jpeg 128 | 129 | #### File Name Uniqueness 130 | 131 | To ensure that names are unique, Takeout appends a "uniqueness suffix" in the form of a bracketed integer (e.g. "(1)") to the end 132 | the name part of the filename. Commonly this will be present in the name of the orginal file, because there is another original 133 | file that would otherwise have the same name. The uniqueness suff will also be present in the json manifest filename, but in 134 | a different position. For example: 135 | - `IMG_20180830_123540573.jpg(1).json` 136 | - `IMG_20180830_123540573(1).jpg` 137 | - `IMG_20180830_123540573-edited(1).jpg` 138 | Here the manifest filename is `IMG_20180830_123540573.jpg(1).json`, _not_ `IMG_20180830_123540573(1).jpg.json` as would be expected. 139 | 140 | If the original filename (excluding extension) is 47 characters or more in length then the json manifest will use the first 46 141 | characters in its filename - because the extension is one character longer. If there is an edited file then it will save the *same* 142 | name as the original, but will have a "uniqueness suffix appended to distinguish it from its own original. 143 | 144 | #### EXIF Metadata 145 | 146 | EXIF image metadata is often - _but not always_ - present in the images. This includes timestamps, description (if the user has 147 | provided one), and geolocation data. The data is included in the json manifest. It appears that the original EXIF metadata is 148 | preserved, but if it is edited in the Google Photos website then the edits are only reflected in the json manifest. 149 | 150 | 151 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli.Tests/CommandLineTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | using uk.andyjohnson.TakeoutExtractor.Cli; 4 | 5 | 6 | namespace uk.andyjohnson.TakeoutExtractor.Cli.Tests 7 | { 8 | [TestClass] 9 | public class CommandLineTests 10 | { 11 | [TestMethod] 12 | public void EmptyCommand() 13 | { 14 | var args = new string[0]; 15 | var cl = new CommandLine(args); 16 | Assert.AreEqual(0, cl.Args.Length); 17 | } 18 | 19 | 20 | [TestMethod] 21 | public void GlobalOptions1() 22 | { 23 | var args = new string[] { "-i", "input.dat", "-o", "output.dat" }; 24 | var cl = new CommandLine(args); 25 | Assert.AreEqual(4, cl.Args.Length); 26 | Assert.AreEqual("input.dat", cl.GetArgString("i")); 27 | Assert.AreEqual("output.dat", cl.GetArgString("o")); 28 | Assert.AreEqual(null, cl.GetArgString("x")); 29 | } 30 | 31 | 32 | [TestMethod] 33 | public void GlobalOptions2() 34 | { 35 | var args = new string[] { "-i", "input.dat", "-o", "output.dat" }; 36 | var commands = new string[0]; 37 | var parse = CommandLine.Create(args,commands); 38 | Assert.IsNotNull(parse); 39 | Assert.AreEqual(1, parse.Count); 40 | var cl = parse[""]; // Get root commands 41 | Assert.AreEqual(4, cl.Args.Length); 42 | Assert.AreEqual("input.dat", cl.GetArgString("i")); 43 | Assert.AreEqual("output.dat", cl.GetArgString("o")); 44 | Assert.AreEqual(null, cl.GetArgString("x")); 45 | } 46 | 47 | 48 | [TestMethod] 49 | public void GlobalOptionsAndCommandWithOptions() 50 | { 51 | var args = new string[] { "-i", "input.dat", "-o", "output.dat", "photos", "-os", "original", "-od", "originals" }; 52 | var commands = new string[] { "photos" }; 53 | var parse = CommandLine.Create(args, commands); 54 | Assert.IsNotNull(parse); 55 | Assert.AreEqual(2, parse.Count); 56 | var cl = parse[""]; // get root commands 57 | Assert.AreEqual(4, cl.Args.Length); 58 | cl = parse["photos"]; 59 | Assert.AreEqual(4, cl.Args.Length); 60 | } 61 | 62 | 63 | #region enum mapping 64 | 65 | private enum E 66 | { 67 | None = 0, 68 | Foo = 1, 69 | Bar = 2, 70 | Baz = 3, 71 | Quux = 4 72 | } 73 | 74 | private readonly string?[] V = new string?[] { null, "foo", "bar", "bax", "quux" }; 75 | 76 | [TestMethod] 77 | public void EnumParseMatch() 78 | { 79 | var cl = new CommandLine(new string[] { "-p", "bar" }); 80 | Assert.IsNotNull(cl); 81 | var e = cl.GetArgEnum("p", V); 82 | Assert.AreEqual(E.Bar, e); 83 | } 84 | 85 | 86 | [TestMethod] 87 | [ExpectedException(typeof(CommandLineException))] 88 | public void EnumParseNoMatch() 89 | { 90 | var cl = new CommandLine(new string[] { "-p", "baz" }); // "baz" does not match any of the values in V 91 | Assert.IsNotNull(cl); 92 | var e = cl.GetArgEnum("p", V); 93 | } 94 | 95 | 96 | [TestMethod] 97 | public void EnumParseDefaultOptional() 98 | { 99 | var cl = new CommandLine(new string[] { "-t" }); 100 | Assert.IsNotNull(cl); 101 | var e = cl.GetArgEnum("t", V, required: false); // not required, so return the value (nope) that maps to the default enum value (None / 0) 102 | Assert.AreEqual(E.None, e); 103 | } 104 | 105 | 106 | [TestMethod] 107 | [ExpectedException(typeof(CommandLineException))] 108 | public void EnumParseDefaultRequired() 109 | { 110 | var cl = new CommandLine(new string[] { "-t" }); 111 | Assert.IsNotNull(cl); 112 | var e = cl.GetArgEnum("t", V, required: true); // required overrrides default, so throws an excpetion 113 | } 114 | 115 | #endregion enum mapping 116 | } 117 | } -------------------------------------------------------------------------------- /TakeoutExtractor.Cli.Tests/EnumParsinTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | 3 | using uk.andyjohnson.TakeoutExtractor.Cli; 4 | 5 | 6 | namespace uk.andyjohnson.TakeoutExtractor.Cli.Tests 7 | { 8 | public enum TestEnum 9 | { 10 | None = 0, 11 | Foo, 12 | Bar, 13 | Baz 14 | } 15 | 16 | 17 | [TestClass] 18 | public class EnumParsinTests 19 | { 20 | [TestMethod] 21 | public void ValueFound() 22 | { 23 | var cl = new CommandLine(new string[] { "-f", "foo" }); 24 | var val = cl.GetArgEnum("f", new string?[] { "none", "foo", "bar", "baz" }); 25 | Assert.AreEqual(TestEnum.Foo, val); 26 | } 27 | 28 | 29 | [TestMethod] 30 | [ExpectedException(typeof(CommandLineException))] 31 | public void ValueNotFound() 32 | { 33 | var cl = new CommandLine(new string[] { "-f", "pig" }); 34 | var val = cl.GetArgEnum("f", new string?[] { "none", "foo", "bar", "baz" }); 35 | } 36 | 37 | 38 | [TestMethod] 39 | [ExpectedException(typeof(CommandLineException))] 40 | public void ValueNotAllowed() 41 | { 42 | var cl = new CommandLine(new string[] { "-f", "none" }); 43 | var val = cl.GetArgEnum("f", new string?[] { null, "foo", "bar", "baz" }); // TestEnum.None is not allowed 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli.Tests/TakeoutExtractor.Cli.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | false 8 | uk.andyjohnson.TakeoutExtractor.Cli.Tests 9 | 1.1.0 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Microsoft.VisualStudio.TestTools.UnitTesting; -------------------------------------------------------------------------------- /TakeoutExtractor.Cli/CommandLineException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace uk.andyjohnson.TakeoutExtractor.Cli 8 | { 9 | // Exception related to command-line handling 10 | public class CommandLineException : Exception 11 | { 12 | /// 13 | /// Constructor. Initailise a CommandLineException object. 14 | /// 15 | public CommandLineException() 16 | : base() 17 | { 18 | } 19 | 20 | /// 21 | /// Constructor. Initailise a CommandLineException object. 22 | /// 23 | /// Error description 24 | public CommandLineException(string message) 25 | : base(message) 26 | { 27 | } 28 | 29 | /// 30 | /// Constructor. Initailise a CommandLineException object. 31 | /// 32 | /// Error description 33 | /// Inner exception 34 | public CommandLineException(string message, Exception innerException) 35 | : base(message, innerException) 36 | { 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Reflection; 4 | using System.Linq; 5 | using System.IO; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | using uk.andyjohnson.TakeoutExtractor.Lib; 10 | using uk.andyjohnson.TakeoutExtractor.Lib.Photo; 11 | 12 | namespace uk.andyjohnson.TakeoutExtractor.Cli 13 | { 14 | class Program 15 | { 16 | static async Task Main(string[] args) 17 | { 18 | // Parse command-line 19 | var commands = new string[] { "photo" }; 20 | var cl = CommandLine.Create(args, commands); 21 | if (cl.Count == 0) 22 | { 23 | ShowHelp(); 24 | return 0; 25 | } 26 | 27 | // 28 | var globalOptions = new GlobalOptions(); 29 | var mediaOptions = new List(); 30 | foreach(var kvp in cl) 31 | { 32 | switch (kvp.Key) 33 | { 34 | case "": 35 | if (kvp.Value.GetArgFlag("?", argAltNames: new string[] { "h", "help" })) 36 | { 37 | ShowHelp(); 38 | return 0; 39 | } 40 | globalOptions = new GlobalOptions() 41 | { 42 | InputDir = kvp.Value.GetArgDir("i", required: true), 43 | OutputDir = kvp.Value.GetArgDir("o", required: true), 44 | LogFile = kvp.Value.GetArgEnum("lf", 45 | new string?[] { "none", "json", "xml" }, 46 | defaultValue: GlobalOptions.Defaults.LogFile), 47 | 48 | StopOnError = kvp.Value.GetArgBool("se", defaultValue: GlobalOptions.Defaults.StopOnError) 49 | }; 50 | break; 51 | case "photo": 52 | var opt = new PhotoOptions() 53 | { 54 | OutputFileNameFormat = kvp.Value.GetArgString("fm", defaultValue: PhotoOptions.Defaults.OutputFileNameFormat)!, 55 | OutputFileNameTimeKind = kvp.Value.GetArgEnum("ft", 56 | new string?[] { null, "utc", "local" }, 57 | defaultValue: PhotoOptions.Defaults.OutputFileNameTimeKind), 58 | OutputFileVersionOrganisation = kvp.Value.GetArgEnum("fv", 59 | new string?[] { null, "lv", "lvo", "avs", "avx", "ev", "ov" }, 60 | defaultValue: PhotoOptions.Defaults.OutputFileVersionOrganisation), 61 | UpdateExif = kvp.Value.GetArgBool("ux", defaultValue: PhotoOptions.Defaults.UpdateExif), 62 | OutputDirOrganisation = kvp.Value.GetArgEnum("fd", 63 | new string?[] { null, "y", "ym", "ymd" }, 64 | defaultValue: PhotoOptions.Defaults.OutputDirOrganisation), 65 | ExtractDeletedFiles = kvp.Value.GetArgBool("xd", defaultValue: false) 66 | }; 67 | mediaOptions.Add(opt); 68 | break; 69 | } 70 | } 71 | 72 | if ((globalOptions?.InputDir == null) || (globalOptions?.OutputDir == null)) 73 | throw new CommandLineException("Input and/or output directories not specified"); // This should never happen but it keeps the compiler happy. 74 | 75 | var extractor = new ExtractorManager(globalOptions, mediaOptions); 76 | extractor.Progress += Extractor_Progress; 77 | try 78 | { 79 | // Validate general options 80 | if (!globalOptions.InputDir.Exists) 81 | throw new InvalidOperationException("Input directory does not exist"); 82 | if (globalOptions.OutputDir.IsSubirOf(globalOptions.InputDir) || globalOptions.InputDir.IsSubirOf(globalOptions.OutputDir)) 83 | throw new InvalidOperationException("Input and output directory paths must not overlap"); 84 | 85 | // Validate options. Vaiidators throw expections to be caught below. 86 | mediaOptions.ForEach(o => o.Vaildate()); 87 | 88 | // Perform the extraction. 89 | var results = await extractor.ExtractAsync(CancellationToken.None); 90 | 91 | // Display the results. 92 | var alerts = results.SelectMany(a => a.Alerts); 93 | var errorCount = alerts.Count(a => a.Type == ExtractorAlertType.Error); 94 | var warningCount = alerts.Count(a => a.Type == ExtractorAlertType.Warning); 95 | var infoCount = alerts.Count(a => a.Type == ExtractorAlertType.Information); 96 | Console.WriteLine($"{errorCount} error, {warningCount} warning, {infoCount} information"); 97 | foreach(var alert in alerts) 98 | { 99 | alert.Write(Console.Out); 100 | } 101 | 102 | // All done 103 | return 0; 104 | } 105 | catch(CommandLineException ex) 106 | { 107 | Console.WriteLine("Command error: " + ex.Message); 108 | } 109 | catch(InvalidOperationException ex) 110 | { 111 | Console.WriteLine("Validation error: " + ex.Message); 112 | } 113 | catch(OperationCanceledException) 114 | { 115 | Console.WriteLine("Extraction cancelled"); 116 | } 117 | catch(Exception ex) 118 | { 119 | Console.WriteLine("Error: " + ex.Message); 120 | } 121 | finally 122 | { 123 | extractor.Progress -= Extractor_Progress; 124 | } 125 | 126 | // All done. 127 | return 1; 128 | } 129 | 130 | 131 | 132 | private static void Extractor_Progress(object? sender, ProgressEventArgs e) 133 | { 134 | if ((e?.SourceFile != null) && (e?.DesinationFile != null)) 135 | { 136 | Console.WriteLine($"{e.SourceFile.FullName} => {e.DesinationFile.FullName}"); 137 | } 138 | } 139 | 140 | 141 | private static void ShowHelp() 142 | { 143 | var msg = string.Format("Takeout Extractor v{0} by Andy Johnson. See https://github.com/andyjohnson0/TakeoutExtractor for info.", 144 | Assembly.GetExecutingAssembly().GetName().Version!.ToString()); 145 | Console.WriteLine(msg); 146 | Console.WriteLine(); 147 | Console.WriteLine("Purpose:"); 148 | Console.WriteLine(" Extract and neatly structure the contents of a Google™ Takeout archive"); 149 | Console.WriteLine(); 150 | Console.WriteLine("Usage:"); 151 | Console.WriteLine(" tex global_options command1 command1_options command2 command2_options ..."); 152 | Console.WriteLine(); 153 | Console.WriteLine("Global options:"); 154 | Console.WriteLine(" -i input_dir"); 155 | Console.WriteLine(" -o output_dir"); 156 | Console.WriteLine(" -lf none | json | sml"); 157 | Console.WriteLine(" Create logfile of specified type. Default: none."); 158 | Console.WriteLine(" -se true/false"); 159 | Console.WriteLine(" Stop on error. Default: false."); 160 | Console.WriteLine(" -h"); 161 | Console.WriteLine(" Display help/usage information"); 162 | Console.WriteLine(); 163 | Console.WriteLine("Commands:"); 164 | Console.WriteLine(" photo"); 165 | Console.WriteLine(" Extract photos and videos"); 166 | Console.WriteLine(" Options:"); 167 | Console.WriteLine(" -fm format_str"); 168 | Console.WriteLine(" Time-based format for output file names. Default \"yyyyMMdd_HHmmss.\""); 169 | Console.WriteLine(" -ft time_kind"); 170 | Console.WriteLine(" Kind of time for output file name. Values can be utc or local. Default: local"); 171 | Console.WriteLine(" -fv lv | lvo | avs | avx | ev | ov"); 172 | Console.WriteLine(" Determines whether and how original and edited versions are placed in the output. Values can be:"); 173 | Console.WriteLine(" lv: latest version only"); 174 | Console.WriteLine(" lvo: latest version, with originals in a subdirectory"); 175 | Console.WriteLine(" avs: all versions in same directory, with filename suffixes to disambiguate"); 176 | Console.WriteLine(" avx: all versions in separate original and edited directories"); 177 | Console.WriteLine(" ev: edited versions only"); 178 | Console.WriteLine(" ov: original versions only"); 179 | Console.WriteLine(" -fd y | ym | ymb"); 180 | Console.WriteLine(" Create subdirectories for year, year and month, or year and month and day. Default: none."); 181 | Console.WriteLine(" -ux true | false"); 182 | Console.WriteLine(" Update edited EXIF information in output files. Default: true."); 183 | Console.WriteLine(" -xd true | false"); 184 | Console.WriteLine(" Extract the deleted files in the Bin directory. Default: false."); 185 | Console.WriteLine(); 186 | Console.WriteLine("(end)"); 187 | Console.WriteLine(); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net6.0\publish\0.8 10 | FileSystem 11 | <_TargetId>Folder 12 | 13 | -------------------------------------------------------------------------------- /TakeoutExtractor.Cli/TakeoutExtractor.Cli.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | enable 7 | enable 8 | uk.andyjohnson.TakeoutExtractor.Cli 9 | 1.1.0 10 | tex 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/AlertsPage.xaml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/AlertsPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Windows.Input; 3 | using uk.andyjohnson.TakeoutExtractor.Lib; 4 | 5 | namespace uk.andyjohnson.TakeoutExtractor.Gui 6 | { 7 | /// 8 | /// Display a collection of ExtractorAlert objects. 9 | /// 10 | public partial class AlertsPage : ContentPage 11 | { 12 | /// 13 | /// Constructor. Initialise an AlertsPage object. 14 | /// 15 | /// Collection of ExtractrorAlert objects to display. 16 | public AlertsPage(IEnumerable alerts) 17 | { 18 | this.alerts = alerts != null ? alerts : new ExtractorAlert[0]; 19 | 20 | InitializeComponent(); 21 | } 22 | 23 | private readonly IEnumerable alerts; 24 | 25 | 26 | protected override void OnAppearing() 27 | { 28 | base.OnAppearing(); 29 | 30 | var errorCount = alerts.Count(a => a.Type == ExtractorAlertType.Error); 31 | var warningCount = alerts.Count(a => a.Type == ExtractorAlertType.Warning); 32 | var infoCount = alerts.Count(a => a.Type == ExtractorAlertType.Information); 33 | alertsBreakdownCountsLabel.Text = $"Errors: {errorCount} Warnings: {warningCount} Infos: {infoCount}"; 34 | 35 | alertsCollView.ItemsSource = alerts; 36 | } 37 | 38 | 39 | protected async void OnFileTapped(object sender, EventArgs args) 40 | { 41 | var tea = args as TappedEventArgs; 42 | var fi = tea?.Parameter as FileInfo; 43 | if (fi != null) 44 | { 45 | await Browser.Default.OpenAsync(fi.FullName, BrowserLaunchMode.SystemPreferred); 46 | } 47 | } 48 | 49 | 50 | protected async void OnDetailsTapped(object sender, EventArgs args) 51 | { 52 | var tea = args as TappedEventArgs; 53 | var message = tea?.Parameter as string; 54 | if (tea != null) 55 | { 56 | await DisplayAlert("Details", message, "Ok"); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace uk.andyjohnson.TakeoutExtractor.Gui 2 | { 3 | public partial class App : Application 4 | { 5 | public App() 6 | { 7 | InitializeComponent(); 8 | 9 | MainPage = new AppShell(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/AppShell.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/AppShell.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace uk.andyjohnson.TakeoutExtractor.Gui 2 | { 3 | public partial class AppShell : Shell 4 | { 5 | public AppShell() 6 | { 7 | InitializeComponent(); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/FolderPicker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace uk.andyjohnson.TakeoutExtractor.Gui 8 | { 9 | /// 10 | /// Folder picker. Wraps platform specific implementations. 11 | /// 12 | public static class FolderPicker 13 | { 14 | /// 15 | /// Display a folder picker dialog. 16 | /// 17 | /// Selected directory, or null if no directory selected. 18 | public static async Task PickFolderAsync() 19 | { 20 | #if WINDOWS 21 | return await Platforms.Windows.FolderPicker.PickFolderAsync(); 22 | #elif MACCATALYST 23 | return await Platforms.MacCatalyst.FolderPicker.PickFolderAsync(); 24 | #else 25 | await Task.CompletedTask; // prevents a warning about some code paths not doing an await 26 | throw new PlatformNotSupportedException(); 27 | #endif 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /TakeoutExtractor.Gui/MainPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 9 | 12 | 13 | 15 | 19 | 20 | 22 | 25 | 26 | 27 | 28 | 32 | 34 | 35 | 39 | 40 | 41 | 44 |