├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
├── stale.yml
└── workflows
│ ├── ci.yml
│ └── codeql-analysis.yml
├── .gitignore
├── Directory.Build.props
├── Image Sort.svg
├── Image-Sort-Screenshot.gif
├── Image-Sort-Screenshot.png
├── Image-Sort.sln
├── LICENSE
├── README.md
├── appveyor.yml
├── docs
├── CNAME
├── English_get-it-from-MS_InvariantCulture_Default.png
├── _config.yml
├── google1896f4f871f455a4.html
├── help.md
├── help
│ └── help.de.md
├── index.md
├── privacy_policy.md
├── ror.xml
├── sitemap.html
└── sitemap.xml
├── src
├── ImageSort.Localization
│ ├── ImageSort.Localization.csproj
│ ├── KeyBindingNames.Designer.cs
│ ├── KeyBindingNames.de-DE.resx
│ ├── KeyBindingNames.resx
│ ├── Text.Designer.cs
│ ├── Text.de-DE.resx
│ └── Text.resx
├── ImageSort.MSIX
│ ├── ImageSort.MSIX.wapproj
│ ├── Images
│ │ ├── LargeTile.scale-100.png
│ │ ├── LargeTile.scale-125.png
│ │ ├── LargeTile.scale-150.png
│ │ ├── LargeTile.scale-200.png
│ │ ├── LargeTile.scale-400.png
│ │ ├── LockScreenLogo.scale-200.png
│ │ ├── SmallTile.scale-100.png
│ │ ├── SmallTile.scale-125.png
│ │ ├── SmallTile.scale-150.png
│ │ ├── SmallTile.scale-200.png
│ │ ├── SmallTile.scale-400.png
│ │ ├── SplashScreen.scale-100.png
│ │ ├── SplashScreen.scale-125.png
│ │ ├── SplashScreen.scale-150.png
│ │ ├── SplashScreen.scale-200.png
│ │ ├── SplashScreen.scale-400.png
│ │ ├── Square150x150Logo.scale-100.png
│ │ ├── Square150x150Logo.scale-125.png
│ │ ├── Square150x150Logo.scale-150.png
│ │ ├── Square150x150Logo.scale-200.png
│ │ ├── Square150x150Logo.scale-400.png
│ │ ├── Square44x44Logo.altform-lightunplated_targetsize-16.png
│ │ ├── Square44x44Logo.altform-lightunplated_targetsize-24.png
│ │ ├── Square44x44Logo.altform-lightunplated_targetsize-256.png
│ │ ├── Square44x44Logo.altform-lightunplated_targetsize-32.png
│ │ ├── Square44x44Logo.altform-lightunplated_targetsize-48.png
│ │ ├── Square44x44Logo.altform-unplated_targetsize-16.png
│ │ ├── Square44x44Logo.altform-unplated_targetsize-256.png
│ │ ├── Square44x44Logo.altform-unplated_targetsize-32.png
│ │ ├── Square44x44Logo.altform-unplated_targetsize-48.png
│ │ ├── Square44x44Logo.scale-100.png
│ │ ├── Square44x44Logo.scale-125.png
│ │ ├── Square44x44Logo.scale-150.png
│ │ ├── Square44x44Logo.scale-200.png
│ │ ├── Square44x44Logo.scale-400.png
│ │ ├── Square44x44Logo.targetsize-16.png
│ │ ├── Square44x44Logo.targetsize-24.png
│ │ ├── Square44x44Logo.targetsize-24_altform-unplated.png
│ │ ├── Square44x44Logo.targetsize-256.png
│ │ ├── Square44x44Logo.targetsize-32.png
│ │ ├── Square44x44Logo.targetsize-48.png
│ │ ├── StoreLogo.backup.png
│ │ ├── StoreLogo.scale-100.png
│ │ ├── StoreLogo.scale-125.png
│ │ ├── StoreLogo.scale-150.png
│ │ ├── StoreLogo.scale-200.png
│ │ ├── StoreLogo.scale-400.png
│ │ ├── Wide310x150Logo.scale-100.png
│ │ ├── Wide310x150Logo.scale-125.png
│ │ ├── Wide310x150Logo.scale-150.png
│ │ ├── Wide310x150Logo.scale-200.png
│ │ └── Wide310x150Logo.scale-400.png
│ └── Package.appxmanifest
├── ImageSort.WPF
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── Converters
│ │ ├── PathToBitmapImageConverter.cs
│ │ └── PathToFilenameConverter.cs
│ ├── FileSystem
│ │ ├── FileOperationApiWrapper.cs
│ │ ├── ImageLoading.cs
│ │ └── RecycleBin.cs
│ ├── FolderIcons
│ │ └── ShellFileLoader.cs
│ ├── Icons
│ │ ├── Image Sort.ico
│ │ └── Image Sort.png
│ ├── ImageSort.WPF.csproj
│ ├── Interop.Shell32.dll
│ ├── MainWindow.xaml
│ ├── MainWindow.xaml.cs
│ ├── SettingsManagement
│ │ ├── GeneralSettingsGroupView.xaml
│ │ ├── GeneralSettingsGroupView.xaml.cs
│ │ ├── GeneralSettingsGroupViewModel.cs
│ │ ├── MetadataPanelSettings.cs
│ │ ├── PinnedFolderSettingsViewModel.cs
│ │ ├── SettingsHelper.cs
│ │ ├── SettingsView.xaml
│ │ ├── SettingsView.xaml.cs
│ │ ├── ShortCutManagement
│ │ │ ├── Hotkey.cs
│ │ │ ├── HotkeyEditor.xaml
│ │ │ ├── HotkeyEditor.xaml.cs
│ │ │ ├── HotkeyEditorControl.xaml
│ │ │ ├── HotkeyEditorControl.xaml.cs
│ │ │ ├── KeyBindingsSettingsGroupView.xaml
│ │ │ ├── KeyBindingsSettingsGroupView.xaml.cs
│ │ │ └── KeybindingsSettingsGroupViewModel.cs
│ │ └── WindowPosition
│ │ │ └── WindowPositionSettingsViewModel.cs
│ ├── Styles
│ │ ├── Controls.xaml
│ │ ├── GroupBox.xaml
│ │ └── HotkeyEditor.xaml
│ └── Views
│ │ ├── ActionsView.xaml
│ │ ├── ActionsView.xaml.cs
│ │ ├── Credits
│ │ ├── CreditsWindow.xaml
│ │ ├── CreditsWindow.xaml.cs
│ │ ├── UsedLibraryView.xaml
│ │ └── UsedLibraryView.xaml.cs
│ │ ├── FolderTreeItemView.xaml
│ │ ├── FolderTreeItemView.xaml.cs
│ │ ├── FoldersView.xaml
│ │ ├── FoldersView.xaml.cs
│ │ ├── ImagesView.xaml
│ │ ├── ImagesView.xaml.cs
│ │ ├── InputBox.xaml
│ │ ├── InputBox.xaml.cs
│ │ ├── Metadata
│ │ ├── MetadataFieldView.xaml
│ │ ├── MetadataFieldView.xaml.cs
│ │ ├── MetadataSectionView.xaml
│ │ ├── MetadataSectionView.xaml.cs
│ │ ├── MetadataView.xaml
│ │ └── MetadataView.xaml.cs
│ │ └── WindowHelper.cs
├── ImageSort.WindowsSetup
│ ├── Image Sort.ico
│ ├── ImageSort.WindowsSetup.wixproj
│ ├── License.rtf
│ ├── Product.wxs
│ ├── delete_explorer_context_menu.bat
│ └── exclude-imagesort.exe.xslt
├── ImageSort.WindowsUpdater
│ ├── GitHubUpdateFetcher.cs
│ ├── ImageSort.WindowsUpdater.csproj
│ └── InstallerRunner.cs
└── ImageSort
│ ├── Actions
│ ├── DeleteAction.cs
│ ├── IReversibleAction.cs
│ ├── MoveAction.cs
│ └── RenameAction.cs
│ ├── DependencyManagement
│ └── DependencyRegistrationHelper.cs
│ ├── FileSystem
│ ├── FileRestorationNotPossibleException.cs
│ ├── FullAccessFileSystem.cs
│ ├── FullAccessFileSystemMetadataExtractor.cs
│ ├── IFileSystem.cs
│ ├── IMetadataExtractor.cs
│ └── IRecycleBin.cs
│ ├── Helpers
│ ├── PathHelper.cs
│ └── StringHelper.cs
│ ├── ImageSort.csproj
│ ├── SettingsManagement
│ ├── SettingsGroupViewModelBase.cs
│ └── SettingsViewModel.cs
│ └── ViewModels
│ ├── ActionsViewModel.cs
│ ├── FolderTreeItemViewModel.cs
│ ├── FoldersViewModel.cs
│ ├── ImagesViewModel.cs
│ ├── MainViewModel.cs
│ └── Metadata
│ ├── MetadataFieldViewModel.cs
│ ├── MetadataFieldViewModelFactory.cs
│ ├── MetadataSectionViewModel.cs
│ ├── MetadataSectionViewModelFactory.cs
│ └── MetadataViewModel.cs
└── tests
├── ImageSort.UnitTests
├── Actions
│ ├── DeleteActionTests.cs
│ ├── MoveActionTests.cs
│ └── RenameActionTests.cs
├── ImageSort.UnitTests.csproj
├── SettingsManagement
│ ├── SettingsGroupViewModelBaseTests.cs
│ └── SettingsViewModelTests.cs
└── ViewModels
│ ├── ActionsViewModelTest.cs
│ ├── FolderTreeItemViewModelTests.cs
│ ├── FoldersViewModelTests.cs
│ ├── ImagesViewModelTests.cs
│ ├── MainViewModelTests.cs
│ └── MetadataViewModelTests.cs
└── ImageSort.WPF.UiTests
├── AppCollection.cs
├── AppFixture.cs
├── ControlHelper.cs
├── FileActionsTests.cs
├── FolderActionsTests.cs
├── ImageSort.WPF.UiTests.csproj
├── MockState
├── Subfolder 1
│ ├── Subsubfolder
│ │ └── mock in subsubfolder.jpg
│ └── mock in subfolder.jpg
├── Subfolder 2
│ └── mock in subfolder 2.jpg
├── mock 1.jpg
├── mock 2.jpg
├── mock 3.jpg
├── mock 4.png
├── mock 5.jpg
├── mock 6.jpg
└── mock 7.jpg
├── SearchTests.cs
├── SetupTeardownHelper.cs
└── WindowHelper.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | # All files
4 | [*]
5 | indent_style = space
6 |
7 | # Xml files
8 | [*.xml]
9 | indent_size = 2
10 |
11 | [*.cs]
12 | csharp_style_namespace_declarations=file_scoped:suggestion
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report unintended and unwanted behavior
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. Windows 7/8/10]
28 | - Architecture: [x86/x64]
29 | - Version: [e.g. 2.0.0, 2.0.0-preview.5]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: nuget
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 | groups:
9 | reactiveui:
10 | patterns:
11 | - "ReactiveUI*"
12 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - pinned
8 | - security
9 | # Label to use when marking an issue as stale
10 | staleLabel: wontfix
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Image Sort CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | release:
7 | types: [ created ]
8 |
9 | jobs:
10 | build:
11 | runs-on: windows-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 | with:
15 | fetch-depth: 0
16 | - name: Setup .NET Core
17 | uses: actions/setup-dotnet@v1
18 | with:
19 | dotnet-version: '8.0.x'
20 | - name: GitVersion
21 | uses: gittools/actions/gitversion/setup@v0.10.2
22 | with:
23 | versionSpec: '5.x'
24 | includePrerelease: true
25 | - name: Determine Version
26 | id: gitversion
27 | uses: gittools/actions/gitversion/execute@v0
28 | - name: Build
29 | run: dotnet build --configuration Release
30 | - name: Unit Tests
31 | run: dotnet test .\tests\ImageSort.UnitTests\ImageSort.UnitTests.csproj --configuration Release --no-build
32 | # UI tests are disabled due to changes of the windows runner that broke them
33 | #- name: UI Tests
34 | # run: dotnet test .\tests\ImageSort.WPF.UiTests\ImageSort.WPF.UiTests.csproj --configuration Release --no-build
35 | - name: Build WIX project
36 | if: ${{ github.event_name == 'release' }}
37 | env:
38 | GITHUB_ACTIONS_CI: true
39 | run: |
40 | cd .\src\ImageSort.WindowsSetup\;
41 | dotnet gitversion /updatewixversionfile | Out-String;
42 | dotnet tool install --global wix
43 | dotnet build -c Release -r win-x86 -p:Platform=x86
44 | dotnet build -c Release -r win-x64 -p:Platform=x64
45 | dotnet build -c Release -r win-arm64 -p:Platform=ARM64
46 | cd ..\..;
47 | - name: Upload x86 MSI file
48 | id: upload-x86-msi-file
49 | uses: actions/upload-release-asset@v1.0.2
50 | if: ${{ github.event_name == 'release' }}
51 | with:
52 | asset_path: .\artifacts\x86\ImageSort.x86.msi
53 | asset_name: ImageSort.x86.msi
54 | asset_content_type: application/octet-stream
55 | upload_url: ${{ github.event.release.upload_url }}
56 | env:
57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58 | TIME: ${{ github.run_number }}
59 | - name: Upload x64 MSI file
60 | id: upload-x64-msi-file
61 | uses: actions/upload-release-asset@v1.0.2
62 | if: ${{ github.event_name == 'release' }}
63 | with:
64 | asset_path: .\artifacts\x64\ImageSort.x64.msi
65 | asset_name: ImageSort.x64.msi
66 | asset_content_type: application/octet-stream
67 | upload_url: ${{ github.event.release.upload_url }}
68 | env:
69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70 | TIME: ${{ github.run_number }}
71 | - name: Upload ARM64 MSI file
72 | id: upload-arm64-msi-file
73 | uses: actions/upload-release-asset@v1.0.2
74 | if: ${{ github.event_name == 'release' }}
75 | with:
76 | asset_path: .\artifacts\ARM64\ImageSort.ARM64.msi
77 | asset_name: ImageSort.ARM64.msi
78 | asset_content_type: application/octet-stream
79 | upload_url: ${{ github.event.release.upload_url }}
80 | env:
81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
82 | TIME: ${{ github.run_number }}
83 | - name: Install winget-create
84 | if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
85 | run: choco install wingetcreate
86 | - name: Install .NET 6
87 | uses: actions/setup-dotnet@v3
88 | if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
89 | with:
90 | dotnet-version: '6.0.x'
91 | - name: Update and submit Winget manifest
92 | if: ${{ github.event_name == 'release' && !github.event.release.prerelease }}
93 | run: |
94 | wingetcreate.exe update --submit --token ${{ secrets.PAT_WINGET }} --urls "${{ steps.upload-x86-msi-file.outputs.browser_download_url }}" "${{ steps.upload-x64-msi-file.outputs.browser_download_url }}" "${{ steps.upload-arm64-msi-file.outputs.browser_download_url }}" --version "${{ steps.gitversion.outputs.assemblySemVer }}" Lolle2000la.ImageSort
95 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '16 2 * * 0'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'csharp' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
--------------------------------------------------------------------------------
/Image-Sort-Screenshot.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/Image-Sort-Screenshot.gif
--------------------------------------------------------------------------------
/Image-Sort-Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/Image-Sort-Screenshot.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Lolle2000la
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | image: Visual Studio 2022
2 | configuration: Release
3 |
4 |
5 | cache:
6 | - 'C:\ProgramData\chocolatey\bin -> appveyor.yml'
7 | - 'C:\ProgramData\chocolatey\lib -> appveyor.yml'
8 |
9 | install:
10 | - choco install gitversion.portable -pre -y
11 |
12 | before_build:
13 | - dotnet restore
14 | - ps: gitversion /l console /output buildserver /updateAssemblyInfo
15 |
16 | build:
17 | project: Image-Sort.sln
18 | parallel: true
19 | verbosity: minimal
20 |
21 | environment:
22 | IGNORE_NORMALISATION_GIT_HEAD_MOVE: "1"
23 |
24 | test_script:
25 | - cmd: dotnet test "C:\projects\image-sort\tests\ImageSort.UnitTests\ImageSort.UnitTests.csproj" --configuration Release --no-build
26 |
27 | after_test:
28 | - ps: >-
29 | if ($env:APPVEYOR_REPO_TAG -eq "true") {
30 | cd .\src\ImageSort.WindowsSetup\;
31 | gitversion /updatewixversionfile | Out-String;
32 | MSBuild /p:Configuration=Release /p:Platform=x64 /p:OutputPath=..\..\artifacts\x64 /p:BuildProjectReferences=false | Out-String;
33 | MSBuild /p:Configuration=Release /p:Platform=x86 /p:OutputPath=..\..\artifacts\x86 /p:BuildProjectReferences=false | Out-String;
34 | cd ..\..;
35 | }
36 |
37 | artifacts:
38 | - path: '.\artifacts\**\*.msi'
39 | name: Image Sort Installer
40 | type: File
41 |
42 | #deploy:
43 | # - provider: GitHub
44 | # release: Image Sort v$(appveyor_build_version)
45 | # tag: $(APPVEYOR_REPO_TAG_NAME)
46 | # auth_token:
47 | # secure: Gka4zhwdCzAkR5hGyS988om/2MSNTP+BZDilT87BtI1jA7NYq9x0BB1cBUO3gPKa
48 | # artifact: Image Sort Installer
49 | # on:
50 | # APPVEYOR_REPO_TAG: true
51 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | imagesort.org
--------------------------------------------------------------------------------
/docs/English_get-it-from-MS_InvariantCulture_Default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/docs/English_get-it-from-MS_InvariantCulture_Default.png
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
2 | title: Image Sort - a free app for sorting your folders
3 | description: Image Sort helps you sort your big and messy folders with speed, not friction.
4 | social_image_path: https://github.com/Lolle2000la/Image-Sort/blob/master/Image%20sort.UI/Image_Sort.png
5 | social_logo_path: https://github.com/Lolle2000la/Image-Sort/blob/master/Image%20sort.UI/Image_Sort.png
6 |
--------------------------------------------------------------------------------
/docs/google1896f4f871f455a4.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google1896f4f871f455a4.html
--------------------------------------------------------------------------------
/docs/privacy_policy.md:
--------------------------------------------------------------------------------
1 | ## Privacy Policy
2 |
3 | Lolle2000la built the Image Sort app as an Open Source app. This SERVICE is provided by Lolle2000la at no cost and is intended for use as is.
4 |
5 | This page is used to inform website visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service.
6 |
7 | If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy.
8 |
9 | The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Image Sort unless otherwise defined in this Privacy Policy.
10 |
11 | **Information Collection and Use**
12 |
13 | For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. The information that I request is retained on your device and is not collected by me in any way
14 |
15 | The app does use third party services that may collect information used to identify you.
16 |
17 | **Log Data**
18 |
19 | I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics.
20 |
21 | **Cookies**
22 |
23 | Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory.
24 |
25 | This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service.
26 |
27 | **Service Providers**
28 |
29 | I may employ third-party companies and individuals due to the following reasons:
30 |
31 | - To facilitate our Service;
32 | - To provide the Service on our behalf;
33 | - To perform Service-related services; or
34 | - To assist us in analyzing how our Service is used.
35 |
36 | I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose.
37 |
38 | **Security**
39 |
40 | I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security.
41 |
42 | **Links to Other Sites**
43 |
44 | This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
45 |
46 | **Children’s Privacy**
47 |
48 | These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions.
49 |
50 | **Changes to This Privacy Policy**
51 |
52 | I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page.
53 |
54 | **Contact Us**
55 |
56 | If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me.
57 |
58 | This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.firebaseapp.com/)
59 |
--------------------------------------------------------------------------------
/docs/ror.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ROR Sitemap for https://imagesort.org/
5 | https://imagesort.org/
6 |
7 | -
8 |
https://imagesort.org/
9 | Image Sort - a free app for sorting your folders | Image Sort helps you sort your big and messy folders. Itâs tiny too, just about 3 megabytes.
10 | Image Sort helps you sort your big and messy folders. Itâs tiny too, just about 3 megabytes.
11 |
12 | 0
13 | sitemap
14 |
15 | -
16 |
https://imagesort.org/help.html
17 | Getting started | Image Sort - a free app for sorting your folders
18 | Image Sort helps you sort your big and messy folders. Itâs tiny too, just about 3 megabytes.
19 |
20 | 1
21 | sitemap
22 |
23 | -
24 |
https://imagesort.org/privacy_policy.html
25 | Privacy Policy | Image Sort - a free app for sorting your folders
26 | Image Sort helps you sort your big and messy folders. Itâs tiny too, just about 3 megabytes.
27 |
28 | 1
29 | sitemap
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/docs/sitemap.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | imagesort.org Site Map - Generated by www.xml-sitemaps.com
6 |
10 |
112 |
113 |
114 |
115 |
116 |
imagesort.org HTML Site Map
117 |
118 | Last updated: 2018, June 10
120 | Total pages: %TOTALURLS%
122 | imagesort.org Homepage
123 |
124 |
125 |
165 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/docs/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 | https://imagesort.org/
12 | 2018-06-10T14:05:50+00:00
13 | 1.00
14 |
15 |
16 | https://imagesort.org/help.html
17 | 2018-06-10T14:05:50+00:00
18 | 0.80
19 |
20 |
21 | https://imagesort.org/privacy_policy.html
22 | 2018-06-10T14:05:50+00:00
23 | 0.80
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/ImageSort.Localization/ImageSort.Localization.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Debug;Release;MSIX
6 | win-x86;win-x64;win-arm64
7 |
8 |
9 |
10 |
11 | True
12 | True
13 | KeyBindingNames.resx
14 |
15 |
16 | Text.resx
17 | True
18 | True
19 |
20 |
21 | True
22 | True
23 | Text.resx
24 |
25 |
26 |
27 |
28 |
29 | PublicResXFileCodeGenerator
30 | KeyBindingNames.Designer.cs
31 |
32 |
33 | Text.Designer.cs
34 | PublicResXFileCodeGenerator
35 |
36 |
37 | PublicResXFileCodeGenerator
38 | Text.Designer.cs
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LargeTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LargeTile.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LargeTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LargeTile.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LargeTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LargeTile.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LargeTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LargeTile.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LargeTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LargeTile.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/LockScreenLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/LockScreenLogo.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SmallTile.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SmallTile.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SmallTile.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SmallTile.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SmallTile.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SmallTile.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SmallTile.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SmallTile.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SmallTile.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SmallTile.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SplashScreen.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SplashScreen.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SplashScreen.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SplashScreen.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SplashScreen.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SplashScreen.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SplashScreen.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SplashScreen.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/SplashScreen.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/SplashScreen.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square150x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square150x150Logo.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square150x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square150x150Logo.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square150x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square150x150Logo.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square150x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square150x150Logo.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square150x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square150x150Logo.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-16.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-24.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-256.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-32.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-lightunplated_targetsize-48.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-16.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-256.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-32.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.altform-unplated_targetsize-48.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-16.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-24.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-24_altform-unplated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-24_altform-unplated.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-256.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-32.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Square44x44Logo.targetsize-48.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.backup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.backup.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/StoreLogo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/StoreLogo.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-100.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-125.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-125.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-150.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-200.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.MSIX/Images/Wide310x150Logo.scale-400.png
--------------------------------------------------------------------------------
/src/ImageSort.MSIX/Package.appxmanifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
15 | Image sort
16 | Lolle2000la
17 | Images\StoreLogo.png
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/App.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Converters/PathToBitmapImageConverter.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.SettingsManagement;
2 | using ImageSort.WPF.FileSystem;
3 | using ImageSort.WPF.SettingsManagement;
4 | using Splat;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Globalization;
9 | using System.IO;
10 | using System.Linq;
11 | using System.Windows.Data;
12 | using System.Windows.Media.Imaging;
13 |
14 | namespace ImageSort.WPF.Converters;
15 |
16 | [ValueConversion(typeof(string), typeof(BitmapImage))]
17 | internal class PathToBitmapImageConverter : IValueConverter
18 | {
19 | public int? LoadWidth { get; set; } = null;
20 | public bool ForGifThumbnails { get; set; } = false;
21 |
22 | private GeneralSettingsGroupViewModel generalSettings = Locator.Current.GetService>()
23 | .Select(s => s as GeneralSettingsGroupViewModel)
24 | .First(s => s != null);
25 |
26 | [SuppressMessage("Design", "CA1031:Do not catch general exception types",
27 | Justification = "The app should not crash just because some exception happened")]
28 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
29 | {
30 | if (value == null) return null;
31 | if (ForGifThumbnails && (!generalSettings.AnimateGifThumbnails || !generalSettings.AnimateGifs)) return null; // prevent gifs from loading when disabled.
32 |
33 | if (value is string path)
34 | {
35 | if (ForGifThumbnails && Path.GetExtension(path).ToUpperInvariant() != ".GIF") return null;
36 | return ImageLoading.GetImageFromPath(path);
37 | }
38 |
39 | return null;
40 | }
41 |
42 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
43 | {
44 | throw new NotImplementedException();
45 | }
46 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Converters/PathToFilenameConverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Globalization;
3 | using System.IO;
4 | using System.Windows.Data;
5 |
6 | namespace ImageSort.WPF.Converters;
7 |
8 | [ValueConversion(typeof(string), typeof(string))]
9 | internal class PathToFilenameConverter : IValueConverter
10 | {
11 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
12 | {
13 | if (value == null) return "";
14 |
15 | if (value is string path) return Path.GetFileName(path);
16 |
17 | return "";
18 | }
19 |
20 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
21 | {
22 | throw new NotImplementedException();
23 | }
24 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/FileSystem/ImageLoading.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.Localization;
2 | using LazyCache;
3 | using LazyCache.Providers;
4 | using Microsoft.Extensions.Caching.Memory;
5 | using System;
6 | using System.Diagnostics;
7 | using System.Globalization;
8 | using System.IO;
9 | using System.Windows;
10 | using System.Windows.Media;
11 | using System.Windows.Media.Imaging;
12 |
13 | namespace ImageSort.WPF.FileSystem;
14 |
15 | internal static class ImageLoading
16 | {
17 | static IAppCache cache = new CachingService(
18 | new MemoryCacheProvider(
19 | new MemoryCache(
20 | new MemoryCacheOptions()
21 | {
22 | SizeLimit = 20, // limit the maximum number of
23 | })));
24 |
25 | static MemoryCacheEntryOptions options = new MemoryCacheEntryOptions()
26 | {
27 | Size = 1, // the same unit must be used, as MemoryCache itself knows no units. Here, 1 equals 1 element.
28 | };
29 |
30 | public static ImageSource GetImageFromPath(string path)
31 | {
32 | if (path == null) return null;
33 |
34 | return cache.GetOrAdd(path, () =>
35 | {
36 | try
37 | {
38 | var bitmapImage = new BitmapImage();
39 | Rotation rotation = GetImageOrientation(path);
40 |
41 | bitmapImage.BeginInit();
42 | bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
43 | bitmapImage.UriSource = new Uri(path);
44 | bitmapImage.Rotation = rotation;
45 | bitmapImage.EndInit();
46 |
47 | if (bitmapImage.Width <= 0 || bitmapImage.Height <= 0)
48 | throw new BadImageFormatException($"Image {Path.GetFileName(path)} has invalid dimensions.", path);
49 |
50 | return bitmapImage;
51 | }
52 | catch (Exception ex)
53 | {
54 | var textDrawing = new GeometryDrawing
55 | {
56 | Geometry = new GeometryGroup
57 | {
58 | Children = new GeometryCollection(new[]
59 | {
60 | new FormattedText(Text.CouldNotLoadImageErrorText
61 | .Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
62 | .Replace("{FileName}", Path.GetFileName(path),
63 | StringComparison.OrdinalIgnoreCase),
64 | CultureInfo.CurrentCulture,
65 | FlowDirection.LeftToRight,
66 | new Typeface("Segoe UI"),
67 | 16,
68 | Brushes.Black, 1)
69 | .BuildGeometry(new Point(8, 8))
70 | })
71 | },
72 | Brush = Brushes.Black,
73 | Pen = new Pen(Brushes.White, 0.5)
74 | };
75 |
76 | return new DrawingImage(textDrawing);
77 | }
78 | }, options);
79 | }
80 |
81 | // Required for some images to be displayed in their correct orientation. See https://github.com/Lolle2000la/Image-Sort/issues/445
82 | // Solution taken from StackOverflow user Lâm Quang Minh (https://stackoverflow.com/a/63627972/7147000)
83 | private static Rotation GetImageOrientation(string path)
84 | {
85 | const string _orientationQuery = "System.Photo.Orientation";
86 | Rotation rotation = Rotation.Rotate0;
87 | using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
88 | {
89 | BitmapFrame bitmapFrame = BitmapFrame.Create(fileStream, BitmapCreateOptions.DelayCreation, BitmapCacheOption.None);
90 | BitmapMetadata bitmapMetadata = bitmapFrame.Metadata as BitmapMetadata;
91 |
92 | if ((bitmapMetadata != null) && (bitmapMetadata.ContainsQuery(_orientationQuery)))
93 | {
94 | object o = bitmapMetadata.GetQuery(_orientationQuery);
95 |
96 | if (o != null)
97 | {
98 | switch ((ushort)o)
99 | {
100 | case 6:
101 | {
102 | rotation = Rotation.Rotate90;
103 | }
104 | break;
105 | case 3:
106 | {
107 | rotation = Rotation.Rotate180;
108 | }
109 | break;
110 | case 8:
111 | {
112 | rotation = Rotation.Rotate270;
113 | }
114 | break;
115 | }
116 | }
117 | }
118 | }
119 |
120 | return rotation;
121 | }
122 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/FileSystem/RecycleBin.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reactive.Disposables;
4 | using ImageSort.FileSystem;
5 | using ImageSort.Helpers;
6 | using Shell32;
7 |
8 | namespace ImageSort.WPF.FileSystem;
9 |
10 | internal class RecycleBin : IRecycleBin
11 | {
12 | private readonly Shell shell = new Shell();
13 |
14 | public IDisposable Send(string path, bool confirmationNeeded = false)
15 | {
16 | var success = false;
17 |
18 | if (confirmationNeeded)
19 | success = FileOperationApiWrapper.Send(path,
20 | FileOperationApiWrapper.FileOperationFlags.FOF_ALLOWUNDO
21 | | FileOperationApiWrapper.FileOperationFlags.FOF_WANTNUKEWARNING);
22 | else
23 | success = FileOperationApiWrapper.Send(path,
24 | FileOperationApiWrapper.FileOperationFlags.FOF_ALLOWUNDO
25 | | FileOperationApiWrapper.FileOperationFlags.FOF_NOCONFIRMATION
26 | | FileOperationApiWrapper.FileOperationFlags.FOF_WANTNUKEWARNING);
27 |
28 | if (!success) throw new IOException($"Could not delete {Path.GetFileName(path)}");
29 |
30 | return Disposable.Create(path, RestoreFileFromRecycleBin);
31 | }
32 |
33 | private void RestoreFileFromRecycleBin(string path)
34 | {
35 | var recycler = shell.NameSpace(10);
36 |
37 | foreach (FolderItem item in recycler.Items())
38 | {
39 | var fileName = recycler.GetDetailsOf(item, 0);
40 |
41 | if (string.IsNullOrEmpty(Path.GetExtension(fileName))) fileName += Path.GetExtension(item.Path);
42 |
43 | var filePath = recycler.GetDetailsOf(item, 1);
44 |
45 | if (path.PathEquals(Path.Combine(filePath, fileName)))
46 | {
47 | Restore(item);
48 | return;
49 | }
50 | }
51 |
52 | throw new FileNotFoundException(null, path);
53 | }
54 |
55 | private void Restore(FolderItem item)
56 | {
57 | var itemVerbs = item.Verbs();
58 |
59 | itemVerbs.Item(0).DoIt();
60 | }
61 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Icons/Image Sort.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.WPF/Icons/Image Sort.ico
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Icons/Image Sort.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.WPF/Icons/Image Sort.png
--------------------------------------------------------------------------------
/src/ImageSort.WPF/ImageSort.WPF.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | WinExe
6 | net8.0-windows
7 | true
8 | Image Sort
9 | Debug;Release;MSIX
10 | AnyCPU;x86;x64;ARM64
11 | win-x86;win-x64;win-arm64
12 | true
13 |
14 |
15 |
16 | true
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | All
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | DO_NOT_INCLUDE_UPDATER
41 |
42 |
43 |
44 |
45 | Interop.Shell32.dll
46 |
47 |
48 |
49 |
50 | false
51 | Icons\Image Sort.ico
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Interop.Shell32.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.WPF/Interop.Shell32.dll
--------------------------------------------------------------------------------
/src/ImageSort.WPF/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/GeneralSettingsGroupView.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
27 |
28 |
29 |
30 |
31 |
33 |
35 |
36 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
47 |
48 |
49 |
50 |
51 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/GeneralSettingsGroupView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Reactive.Disposables;
2 | using System.Reactive.Linq;
3 | using System.Windows;
4 | using System;
5 | using ReactiveUI;
6 |
7 | namespace ImageSort.WPF.SettingsManagement;
8 |
9 | ///
10 | /// Interaction logic for GeneralSettingsGroupView.xaml
11 | ///
12 | public partial class GeneralSettingsGroupView : ReactiveUserControl
13 | {
14 | public GeneralSettingsGroupView()
15 | {
16 | InitializeComponent();
17 |
18 | this.WhenActivated(disposableRegistration =>
19 | {
20 | this.Bind(ViewModel,
21 | vm => vm.DarkMode,
22 | view => view.DarkMode.IsChecked)
23 | .DisposeWith(disposableRegistration);
24 |
25 | this.Bind(ViewModel,
26 | vm => vm.AnimateGifs,
27 | view => view.ActivateAnimatedGifs.IsChecked)
28 | .DisposeWith(disposableRegistration);
29 |
30 | this.Bind(ViewModel,
31 | vm => vm.AnimateGifThumbnails,
32 | view => view.ActivateAnimatedGifsInThumbnails.IsChecked)
33 | .DisposeWith(disposableRegistration);
34 |
35 | // disable the animated gif thumbnail checkbox if animated gifs are disabled
36 | this.Bind(ViewModel,
37 | vm => vm.AnimateGifs,
38 | view => view.ActivateAnimatedGifsInThumbnails.IsEnabled)
39 | .DisposeWith(disposableRegistration);
40 |
41 | // Show the note about changing gif settings
42 | ViewModel.WhenAnyValue(x => x.AnimateGifs, x => x.AnimateGifThumbnails)
43 | .Skip(1) // Skip the startup value
44 | .Subscribe(b => AnimatedGifsSettingsChangeNotice.Visibility = Visibility.Visible)
45 | .DisposeWith(disposableRegistration);
46 |
47 | this.Bind(ViewModel,
48 | vm => vm.CheckForUpdatesOnStartup,
49 | view => view.CheckForUpdates.IsChecked)
50 | .DisposeWith(disposableRegistration);
51 |
52 | this.Bind(ViewModel,
53 | vm => vm.InstallPrereleaseBuilds,
54 | view => view.InstallPrereleaseBuilds.IsChecked)
55 | .DisposeWith(disposableRegistration);
56 |
57 | this.OneWayBind(ViewModel,
58 | vm => vm.CheckForUpdatesOnStartup,
59 | view => view.InstallPrereleaseBuilds.IsEnabled)
60 | .DisposeWith(disposableRegistration);
61 |
62 | this.Bind(ViewModel,
63 | vm => vm.ShowInExplorerContextMenu,
64 | view => view.ShowInExplorerContextMenu.IsChecked)
65 | .DisposeWith(disposableRegistration);
66 | });
67 | }
68 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/GeneralSettingsGroupViewModel.cs:
--------------------------------------------------------------------------------
1 | using AdonisUI;
2 | using ImageSort.Localization;
3 | using ImageSort.SettingsManagement;
4 | using Microsoft.Win32;
5 | using ReactiveUI;
6 | using System;
7 | using System.Windows;
8 |
9 | namespace ImageSort.WPF.SettingsManagement;
10 |
11 | public class GeneralSettingsGroupViewModel : SettingsGroupViewModelBase
12 | {
13 | public override string Name => "General";
14 |
15 | public override string Header => Text.GeneralSettingsHeader;
16 |
17 | private bool _darkMode = false;
18 |
19 | public bool DarkMode
20 | {
21 | get => _darkMode;
22 | set => this.RaiseAndSetIfChanged(ref _darkMode, value);
23 | }
24 |
25 | private bool _checkForUpdatesOnStartup = true;
26 |
27 | public bool CheckForUpdatesOnStartup
28 | {
29 | get => _checkForUpdatesOnStartup;
30 | set => this.RaiseAndSetIfChanged(ref _checkForUpdatesOnStartup, value);
31 | }
32 |
33 | private bool _installPrereleaseBuilds = false;
34 |
35 | public bool InstallPrereleaseBuilds
36 | {
37 | get => _installPrereleaseBuilds;
38 | set => this.RaiseAndSetIfChanged(ref _installPrereleaseBuilds, value);
39 | }
40 |
41 | private bool _animateGifs = true;
42 |
43 | public bool AnimateGifs
44 | {
45 | get => _animateGifs;
46 | set => this.RaiseAndSetIfChanged(ref _animateGifs, value);
47 | }
48 |
49 | private bool _animateGifThumbnails = true;
50 |
51 | public bool AnimateGifThumbnails
52 | {
53 | get => _animateGifThumbnails;
54 | set => this.RaiseAndSetIfChanged(ref _animateGifThumbnails, value);
55 | }
56 |
57 | private bool _showInExplorerContextMenu = CheckForExplorerContextMenu();
58 |
59 | public bool ShowInExplorerContextMenu
60 | {
61 | get => _showInExplorerContextMenu;
62 | set => this.RaiseAndSetIfChanged(ref _showInExplorerContextMenu, value);
63 | }
64 |
65 | public GeneralSettingsGroupViewModel()
66 | {
67 | void SetDarkMode(bool darkMode)
68 | {
69 | ResourceLocator.SetColorScheme(Application.Current.Resources, darkMode ? ResourceLocator.DarkColorScheme : ResourceLocator.LightColorScheme);
70 | }
71 |
72 | this.WhenAnyValue(vm => vm.DarkMode)
73 | .Subscribe(SetDarkMode);
74 |
75 | #if !DEBUG
76 | this.WhenAnyValue(vm => vm.ShowInExplorerContextMenu)
77 | .Subscribe(UpdateExplorerContextMenu);
78 | #endif
79 | }
80 |
81 | private void UpdateExplorerContextMenu(bool show)
82 | {
83 | string[] keys = new[]
84 | {
85 | @"Software\Classes\Directory\shell\ImageSort",
86 | @"Software\Classes\Drive\shell\ImageSort",
87 | @"Software\Classes\Folder\shell\ImageSort"
88 | };
89 |
90 | foreach (var key in keys)
91 | {
92 | if (show)
93 | {
94 | using (var registryKey = Registry.CurrentUser.CreateSubKey(key))
95 | {
96 | registryKey.SetValue("", "Open with Image Sort");
97 | registryKey.CreateSubKey("command").SetValue("", $"\"{AppDomain.CurrentDomain.BaseDirectory}Image Sort.exe\" \"%L\"");
98 | registryKey.SetValue("Icon", $"\"{AppDomain.CurrentDomain.BaseDirectory}Image Sort.exe\"");
99 | }
100 | }
101 | else
102 | {
103 | Registry.CurrentUser.DeleteSubKeyTree(key, false);
104 | }
105 | }
106 | }
107 |
108 | // This is used to grandfather in users who already have the context menu enabled from using it with the installer.
109 | private static bool CheckForExplorerContextMenu()
110 | {
111 | string[] keys = new[]
112 | {
113 | @"Software\Classes\Directory\shell\ImageSort",
114 | @"Software\Classes\Drive\shell\ImageSort",
115 | @"Software\Classes\Folder\shell\ImageSort"
116 | };
117 |
118 | foreach (var key in keys)
119 | {
120 | if (Registry.CurrentUser.OpenSubKey(key) != null)
121 | {
122 | return true;
123 | }
124 | }
125 |
126 | return false;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/MetadataPanelSettings.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.Localization;
2 | using ImageSort.SettingsManagement;
3 | using ReactiveUI;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace ImageSort.WPF.SettingsManagement;
11 | public class MetadataPanelSettings : SettingsGroupViewModelBase
12 | {
13 | public override string Name => "MetadataPanel";
14 |
15 | public override string Header => Text.MetadataPanelHeader;
16 |
17 | public override bool IsVisible => false;
18 |
19 | private bool _isExpanded = false;
20 | public bool IsExpanded
21 | {
22 | get => _isExpanded;
23 | set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
24 | }
25 |
26 | private int _metadataPanelWidth = 300;
27 | public int MetadataPanelWidth
28 | {
29 | get => _metadataPanelWidth;
30 | set => this.RaiseAndSetIfChanged(ref _metadataPanelWidth, value);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/PinnedFolderSettingsViewModel.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.Localization;
2 | using ImageSort.SettingsManagement;
3 | using ReactiveUI;
4 | using System.Collections.Generic;
5 |
6 | namespace ImageSort.WPF.SettingsManagement;
7 |
8 | public class PinnedFolderSettingsViewModel : SettingsGroupViewModelBase
9 | {
10 | public override string Name => "PinnedFolders";
11 |
12 | public override string Header => Text.PinnedFoldersSettingsHeader;
13 |
14 | public override bool IsVisible => false;
15 |
16 | private IEnumerable _pinnedFolders = new List();
17 |
18 | public IEnumerable PinnedFolders
19 | {
20 | get => _pinnedFolders;
21 | set => this.RaiseAndSetIfChanged(ref _pinnedFolders, value);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/SettingsHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text.Json;
6 | using System.Threading.Tasks;
7 | using System.Windows.Input;
8 | using ImageSort.SettingsManagement;
9 | using ImageSort.WPF.SettingsManagement.ShortCutManagement;
10 |
11 | namespace ImageSort.WPF.SettingsManagement;
12 |
13 | internal static class SettingsHelper
14 | {
15 | static SettingsHelper()
16 | {
17 | if (Environment.GetEnvironmentVariable("UI_TEST") is string uiTest)
18 | ConfigFileLocation = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ui_test_config.json");
19 | }
20 |
21 | public static string ConfigFileLocation { get; } = Path.Combine(
22 | Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Image Sort",
23 | #if DEBUG
24 | "debug_config.json"
25 | #else
26 | "config.json"
27 | #endif
28 | );
29 |
30 | public static async Task SaveAsync(this SettingsViewModel settings)
31 | {
32 | var dir = Path.GetDirectoryName(ConfigFileLocation);
33 |
34 | if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
35 |
36 | using var file = File.Create(ConfigFileLocation);
37 |
38 | var serializerOptions = new JsonSerializerOptions
39 | {
40 | WriteIndented = true
41 | };
42 |
43 | await JsonSerializer.SerializeAsync(file, settings.AsDictionary(), serializerOptions).ConfigureAwait(false);
44 | }
45 |
46 | public static void Restore(this SettingsViewModel settings)
47 | {
48 | if (!File.Exists(ConfigFileLocation)) return;
49 |
50 | using var configFile = File.OpenRead(ConfigFileLocation);
51 |
52 | var configContents = JsonSerializer
53 | .DeserializeAsync>>(configFile).Result;
54 |
55 | foreach (var configGroup in new Dictionary>(configContents))
56 | foreach (var config in new Dictionary(configGroup.Value))
57 | {
58 | static object JsonElementToValue(JsonElement element)
59 | {
60 | return element switch
61 | {
62 | { ValueKind: JsonValueKind.False } => false,
63 | { ValueKind: JsonValueKind.True } => true,
64 | { ValueKind: JsonValueKind.String } e => e.GetString(),
65 | { ValueKind: JsonValueKind.Number } e => e.GetInt32(),
66 | { ValueKind: JsonValueKind.Array } e => e.EnumerateArray().Select(JsonElementToValue).ToArray(),
67 | { ValueKind: JsonValueKind.Object } e => new Hotkey(
68 | (Key) Enum.ToObject(typeof(Key),
69 | e.EnumerateObject().First(o => o.Name == "Key").Value.GetInt32()),
70 | (ModifierKeys) Enum.ToObject(typeof(ModifierKeys),
71 | e.EnumerateObject().First(o => o.Name == "Modifiers").Value.GetInt32())),
72 | _ => null
73 | };
74 | }
75 |
76 | configContents[configGroup.Key][config.Key] = JsonElementToValue((JsonElement) config.Value);
77 | }
78 |
79 | settings.RestoreFromDictionary(configContents);
80 | }
81 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/SettingsView.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/SettingsView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Reactive.Linq;
4 | using System.Windows;
5 | using AdonisUI.Controls;
6 | using ImageSort.SettingsManagement;
7 | using ReactiveUI;
8 | using Splat;
9 |
10 | namespace ImageSort.WPF.SettingsManagement;
11 |
12 | ///
13 | /// Interaction logic for SettingsView.xaml
14 | ///
15 | public partial class SettingsView : AdonisWindow, IViewFor
16 | {
17 | public SettingsView()
18 | {
19 | InitializeComponent();
20 |
21 | this.WhenActivated(disposableRegistration =>
22 | {
23 | ViewModel ??= Locator.Current.GetService();
24 |
25 | Closed += async (o, e) => await ViewModel.SaveAsync().ConfigureAwait(false);
26 |
27 | ViewModel.WhenAnyValue(vm => vm.SettingsGroups)
28 | .Where(gs => gs != null)
29 | .Select(gs => gs.Where(g => g.IsVisible))
30 | .Subscribe(gs => Groups.ItemsSource = gs);
31 | });
32 | }
33 |
34 | #region IViewFor implementation
35 |
36 | public static readonly DependencyProperty ViewModelProperty = DependencyProperty
37 | .Register(nameof(ViewModel), typeof(SettingsViewModel), typeof(SettingsView), new PropertyMetadata(null));
38 |
39 | public SettingsViewModel ViewModel
40 | {
41 | get => (SettingsViewModel) GetValue(ViewModelProperty);
42 | set => SetValue(ViewModelProperty, value);
43 | }
44 |
45 | object IViewFor.ViewModel
46 | {
47 | get => ViewModel;
48 | set => ViewModel = (SettingsViewModel) value;
49 | }
50 |
51 | #endregion IViewFor implementation
52 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/ShortCutManagement/Hotkey.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Text;
4 | using System.Windows.Input;
5 |
6 | namespace ImageSort.WPF.SettingsManagement.ShortCutManagement;
7 |
8 | public record Hotkey(Key Key, ModifierKeys Modifiers)
9 | {
10 | public override string ToString()
11 | {
12 | var str = new StringBuilder();
13 |
14 | if (Modifiers.HasFlag(ModifierKeys.Control))
15 | str.Append("Ctrl + ");
16 | if (Modifiers.HasFlag(ModifierKeys.Shift))
17 | str.Append("Shift + ");
18 | if (Modifiers.HasFlag(ModifierKeys.Alt))
19 | str.Append("Alt + ");
20 | if (Modifiers.HasFlag(ModifierKeys.Windows))
21 | str.Append("Win + ");
22 |
23 | str.Append(Key);
24 |
25 | return str.ToString();
26 | }
27 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/ShortCutManagement/HotkeyEditor.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/ShortCutManagement/HotkeyEditor.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 |
5 | namespace ImageSort.WPF.SettingsManagement.ShortCutManagement;
6 |
7 | ///
8 | /// Interaction logic for HotkeyEditor.xaml
9 | ///
10 | public partial class HotkeyEditor : UserControl
11 | {
12 | public HotkeyEditor()
13 | {
14 | InitializeComponent();
15 | DataContext = this;
16 | }
17 |
18 | public string Description
19 | {
20 | get { return (string)GetValue(DescriptionProperty); }
21 | set { SetValue(DescriptionProperty, value); }
22 | }
23 |
24 | // Using a DependencyProperty as the backing store for Description. This enables animation, styling, binding, etc...
25 | public static readonly DependencyProperty DescriptionProperty =
26 | DependencyProperty.Register("Description", typeof(string), typeof(HotkeyEditor), new PropertyMetadata(""));
27 |
28 | public Hotkey Hotkey
29 | {
30 | get { return (Hotkey)GetValue(HotkeyProperty); }
31 | set { SetValue(HotkeyProperty, value); }
32 | }
33 |
34 | // Using a DependencyProperty as the backing store for Hotkey. This enables animation, styling, binding, etc...
35 | public static readonly DependencyProperty HotkeyProperty =
36 | DependencyProperty.Register("Hotkey", typeof(Hotkey), typeof(HotkeyEditor), new PropertyMetadata(null, OnHotkeyChanged));
37 |
38 | private static void OnHotkeyChanged(DependencyObject @object, DependencyPropertyChangedEventArgs args)
39 | {
40 | if (@object is HotkeyEditor editor && args.NewValue is Hotkey hotkey)
41 | {
42 | editor.HotkeyEditorControl.Hotkey = hotkey;
43 | }
44 | }
45 | private void OnHotkeyEditorControlHotkeyChanged(object sender, EventArgs e)
46 | {
47 | Hotkey = (sender as HotkeyEditorControl)?.Hotkey;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/ShortCutManagement/HotkeyEditorControl.xaml:
--------------------------------------------------------------------------------
1 |
8 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/ShortCutManagement/HotkeyEditorControl.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Input;
5 |
6 | namespace ImageSort.WPF.SettingsManagement.ShortCutManagement;
7 |
8 | ///
9 | /// Interaction logic for HotkeyEditorControl.xaml
10 | ///
11 | public partial class HotkeyEditorControl : UserControl
12 | {
13 | public HotkeyEditorControl()
14 | {
15 | InitializeComponent();
16 | DataContext = this;
17 | }
18 |
19 | public static readonly DependencyProperty HotkeyProperty =
20 | DependencyProperty.Register(nameof(Hotkey), typeof(Hotkey),
21 | typeof(HotkeyEditorControl),
22 | new FrameworkPropertyMetadata(default(Hotkey),
23 | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
24 | OnHotkeyChanged));
25 |
26 | public Hotkey Hotkey
27 | {
28 | get => (Hotkey)GetValue(HotkeyProperty);
29 | set => SetValue(HotkeyProperty, value);
30 | }
31 |
32 | private static void OnHotkeyChanged(DependencyObject @object, DependencyPropertyChangedEventArgs args)
33 | {
34 | if (@object is HotkeyEditorControl hotkeyEditorControl)
35 | hotkeyEditorControl.HotkeyChanged?.Invoke(hotkeyEditorControl, new EventArgs());
36 | }
37 |
38 | public event EventHandler HotkeyChanged;
39 |
40 | private void HotkeyTextBox_PreviewKeyDown(object sender, KeyEventArgs e)
41 | {
42 | // Don't let the event pass further
43 | // because we don't want standard textbox shortcuts working
44 | e.Handled = true;
45 |
46 | // Get modifiers and key data
47 | var modifiers = Keyboard.Modifiers;
48 | var key = e.Key;
49 |
50 | // When Alt is pressed, SystemKey is used instead
51 | if (key == Key.System) key = e.SystemKey;
52 |
53 | // Pressing delete, backspace or escape without modifiers clears the current value
54 | if (modifiers == ModifierKeys.None &&
55 | (key == Key.Delete || key == Key.Back || key == Key.Escape))
56 | {
57 | Hotkey = null;
58 | return;
59 | }
60 |
61 | // If no actual key was pressed - return
62 | if (GetIfNotAnActualKey(key))
63 | return;
64 |
65 | // Update the value
66 | Hotkey = new Hotkey(key, modifiers);
67 | }
68 |
69 | private static bool GetIfNotAnActualKey(Key key)
70 | {
71 | return key == Key.LeftCtrl ||
72 | key == Key.RightCtrl ||
73 | key == Key.LeftAlt ||
74 | key == Key.RightAlt ||
75 | key == Key.LeftShift ||
76 | key == Key.RightShift ||
77 | key == Key.LWin ||
78 | key == Key.RWin ||
79 | key == Key.Clear ||
80 | key == Key.OemClear ||
81 | key == Key.Apps;
82 | }
83 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/SettingsManagement/WindowPosition/WindowPositionSettingsViewModel.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using ImageSort.SettingsManagement;
3 | using ReactiveUI;
4 |
5 | namespace ImageSort.WPF.SettingsManagement.WindowPosition;
6 |
7 | public class WindowPositionSettingsViewModel : SettingsGroupViewModelBase
8 | where TWindow : Window
9 | {
10 | // size
11 | private int _height = 600;
12 |
13 | private bool _isMaximized;
14 |
15 | // position
16 | private int _left = 100;
17 |
18 | // used to ensure that when the window count changes the window will still be visible (e.g. when the display count changes everything will be reset)
19 | private int _screenCount;
20 |
21 | private int _top = 100;
22 |
23 | private int _width = 1000;
24 | public override string Name => typeof(TWindow).Name;
25 |
26 | public override string Header => typeof(TWindow).Name;
27 |
28 | public override bool IsVisible => false;
29 |
30 | public bool IsMaximized
31 | {
32 | get => _isMaximized;
33 | set => this.RaiseAndSetIfChanged(ref _isMaximized, value);
34 | }
35 |
36 | public int Left
37 | {
38 | get => _left;
39 | set => this.RaiseAndSetIfChanged(ref _left, value);
40 | }
41 |
42 | public int Top
43 | {
44 | get => _top;
45 | set => this.RaiseAndSetIfChanged(ref _top, value);
46 | }
47 |
48 | public int Height
49 | {
50 | get => _height;
51 | set => this.RaiseAndSetIfChanged(ref _height, value);
52 | }
53 |
54 | public int Width
55 | {
56 | get => _width;
57 | set => this.RaiseAndSetIfChanged(ref _width, value);
58 | }
59 |
60 | public int ScreenCount
61 | {
62 | get => _screenCount;
63 | set => this.RaiseAndSetIfChanged(ref _screenCount, value);
64 | }
65 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Styles/Controls.xaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Styles/GroupBox.xaml:
--------------------------------------------------------------------------------
1 |
4 |
7 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Styles/HotkeyEditor.xaml:
--------------------------------------------------------------------------------
1 |
5 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/ActionsView.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/ActionsView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Reactive;
2 | using System.Reactive.Disposables;
3 | using AdonisUI.Controls;
4 | using ImageSort.Localization;
5 | using ImageSort.ViewModels;
6 | using ReactiveUI;
7 |
8 | namespace ImageSort.WPF.Views;
9 |
10 | ///
11 | /// Interaction logic for ActionsView.xaml
12 | ///
13 | public partial class ActionsView : ReactiveUserControl
14 | {
15 | public ActionsView()
16 | {
17 | InitializeComponent();
18 |
19 | this.WhenActivated(disposableRegistration =>
20 | {
21 | this.BindCommand(ViewModel,
22 | vm => vm.Undo,
23 | view => view.Undo)
24 | .DisposeWith(disposableRegistration);
25 |
26 | this.BindCommand(ViewModel,
27 | vm => vm.Redo,
28 | view => view.Redo)
29 | .DisposeWith(disposableRegistration);
30 |
31 | this.OneWayBind(ViewModel,
32 | vm => vm.LastDone,
33 | view => view.Undo.ToolTip)
34 | .DisposeWith(disposableRegistration);
35 |
36 | this.OneWayBind(ViewModel,
37 | vm => vm.LastUndone,
38 | view => view.Redo.ToolTip)
39 | .DisposeWith(disposableRegistration);
40 |
41 | ViewModel.NotifyUserOfError.RegisterHandler(ic =>
42 | {
43 | var messageBox = new MessageBoxModel
44 | {
45 | Caption = Text.Error,
46 | Text = ic.Input,
47 | Icon = MessageBoxImage.Error,
48 | Buttons = new[] {MessageBoxButtons.Ok(Text.OK)}
49 | };
50 |
51 | MessageBox.Show(messageBox);
52 |
53 | ic.SetOutput(Unit.Default);
54 | });
55 | });
56 | }
57 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Credits/CreditsWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using AdonisUI.Controls;
3 |
4 | namespace ImageSort.WPF.Views.Credits;
5 |
6 | ///
7 | /// Interaction logic for CreditsWindow.xaml
8 | ///
9 | public partial class CreditsWindow : AdonisWindow
10 | {
11 | private static CreditsWindow openWindow;
12 |
13 | private CreditsWindow()
14 | {
15 | InitializeComponent();
16 | }
17 |
18 | public static CreditsWindow Window
19 | {
20 | get
21 | {
22 | if (openWindow == null)
23 | {
24 | openWindow = new CreditsWindow();
25 | openWindow.Closed += OnExistingWindowClosed;
26 | }
27 |
28 | return openWindow;
29 | }
30 | }
31 |
32 | private static void OnExistingWindowClosed(object sender, EventArgs e)
33 | {
34 | openWindow = null;
35 | (sender as CreditsWindow).Closed -= OnExistingWindowClosed;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Credits/UsedLibraryView.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Credits/UsedLibraryView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using System.Windows.Navigation;
6 |
7 | namespace ImageSort.WPF.Views.Credits;
8 |
9 | ///
10 | /// Interaction logic for UsedLibraryView.xaml
11 | ///
12 | public partial class UsedLibraryView : UserControl
13 | {
14 | // Using a DependencyProperty as the backing store for LibraryName. This enables animation, styling, binding, etc...
15 | public static readonly DependencyProperty LibraryNameProperty =
16 | DependencyProperty.Register("LibraryName", typeof(string), typeof(UsedLibraryView),
17 | new PropertyMetadata("Library Name"));
18 |
19 | // Using a DependencyProperty as the backing store for ProjectUrl. This enables animation, styling, binding, etc...
20 | public static readonly DependencyProperty ProjectUrlProperty =
21 | DependencyProperty.Register("ProjectUrl", typeof(string), typeof(UsedLibraryView),
22 | new PropertyMetadata("https://example.com/"));
23 |
24 | // Using a DependencyProperty as the backing store for LicenseUrl. This enables animation, styling, binding, etc...
25 | public static readonly DependencyProperty LicenseUrlProperty =
26 | DependencyProperty.Register("LicenseUrl", typeof(string), typeof(UsedLibraryView),
27 | new PropertyMetadata("https://example.com/"));
28 |
29 | public UsedLibraryView()
30 | {
31 | InitializeComponent();
32 | DataContext = this;
33 | }
34 |
35 | public string LibraryName
36 | {
37 | get => (string) GetValue(LibraryNameProperty);
38 | set => SetValue(LibraryNameProperty, value);
39 | }
40 |
41 | [SuppressMessage("Design", "CA1056:Uri properties should not be strings",
42 | Justification = "Uris are not easily bindable to literal strings in xaml.")]
43 | public string ProjectUrl
44 | {
45 | get => (string) GetValue(ProjectUrlProperty);
46 | set => SetValue(ProjectUrlProperty, value);
47 | }
48 |
49 | [SuppressMessage("Design", "CA1056:Uri properties should not be strings",
50 | Justification = "Uris are not easily bindable to literal strings in xaml.")]
51 | public string LicenseUrl
52 | {
53 | get => (string) GetValue(LicenseUrlProperty);
54 | set => SetValue(LicenseUrlProperty, value);
55 | }
56 |
57 | private void OnRequestNavigate(object sender, RequestNavigateEventArgs e)
58 | {
59 | Process.Start(new ProcessStartInfo
60 | {
61 | FileName = e.Uri.AbsoluteUri,
62 | UseShellExecute = true
63 | });
64 | }
65 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/FolderTreeItemView.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/FolderTreeItemView.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Reactive.Disposables;
5 | using System.Reactive.Linq;
6 | using System.Text;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Data;
10 | using System.Windows.Documents;
11 | using System.Windows.Input;
12 | using System.Windows.Media;
13 | using System.Windows.Media.Imaging;
14 | using System.Windows.Navigation;
15 | using System.Windows.Shapes;
16 | using ImageSort.ViewModels;
17 | using ImageSort.WPF.FolderIcons;
18 | using ReactiveUI;
19 |
20 | namespace ImageSort.WPF.Views;
21 |
22 | ///
23 | /// Interaction logic for FolderTreeItemView.xaml
24 | ///
25 | public partial class FolderTreeItemView : ReactiveUserControl
26 | {
27 | public FolderTreeItemView()
28 | {
29 | InitializeComponent();
30 |
31 | this.WhenActivated(disposableRegistration =>
32 | {
33 | this.OneWayBind(ViewModel,
34 | vm => vm.FolderName,
35 | view => view.FolderName.Text)
36 | .DisposeWith(disposableRegistration);
37 |
38 | this.OneWayBind(ViewModel,
39 | vm => vm.IsCurrentFolder,
40 | view => view.FolderName.FontWeight,
41 | current => current ? FontWeights.Bold : FontWeights.Normal)
42 | .DisposeWith(disposableRegistration);
43 |
44 | this.OneWayBind(ViewModel,
45 | vm => vm.Path,
46 | view => view.FolderIcon.Source,
47 | path => !Directory.Exists(path) ? null : ShellFileLoader.GetThumbnailFromShellForWpf(path))
48 | .DisposeWith(disposableRegistration);
49 |
50 | this.WhenAnyValue(x => x.ActualHeight, x => x.ActualWidth)
51 | .Select(x => x.Item1 > 0 && x.Item2 > 0)
52 | .Where(_ => ViewModel != null)
53 | .Subscribe(v => ViewModel.IsVisible = v);
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/FoldersView.xaml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
28 |
30 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/ImagesView.xaml:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
51 |
52 |
53 |
54 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
80 |
81 |
82 |
83 |
84 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/InputBox.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Question:
19 |
20 |
21 |
22 |
24 | _Ok
25 |
26 | _Cancel
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/InputBox.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using AdonisUI.Controls;
4 |
5 | namespace ImageSort.WPF.Views;
6 |
7 | ///
8 | /// Interaction logic for InputBox.xaml
9 | ///
10 | public partial class InputBox : AdonisWindow
11 | {
12 | public InputBox(string question, string title)
13 | {
14 | InitializeComponent();
15 | Question.Text = question;
16 | Title = title;
17 | }
18 |
19 | public string Answer => AnswerBox.Text;
20 |
21 | private void btnDialogOk_Click(object sender, RoutedEventArgs e)
22 | {
23 | DialogResult = true;
24 | }
25 |
26 | private void Window_ContentRendered(object sender, EventArgs e)
27 | {
28 | AnswerBox.SelectAll();
29 | AnswerBox.Focus();
30 | }
31 | }
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataFieldView.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataFieldView.xaml.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.ViewModels.Metadata;
2 | using ReactiveUI;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Reactive.Disposables;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using System.Windows.Controls;
11 | using System.Windows.Data;
12 | using System.Windows.Documents;
13 | using System.Windows.Input;
14 | using System.Windows.Media;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Navigation;
17 | using System.Windows.Shapes;
18 |
19 | namespace ImageSort.WPF.Views.Metadata;
20 |
21 | ///
22 | /// Interaction logic for MetadataSection.xaml
23 | ///
24 | public partial class MetadataFieldView : ReactiveUserControl
25 | {
26 | public MetadataFieldView()
27 | {
28 | InitializeComponent();
29 |
30 | this.WhenActivated(disposableRegistration =>
31 | {
32 | this.OneWayBind(ViewModel,
33 | vm => vm.Name,
34 | view => view.FieldName.Text)
35 | .DisposeWith(disposableRegistration);
36 |
37 | this.OneWayBind(ViewModel,
38 | vm => vm.Value,
39 | view => view.FieldValue.Text)
40 | .DisposeWith(disposableRegistration);
41 | });
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataSectionView.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataSectionView.xaml.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.ViewModels.Metadata;
2 | using ReactiveUI;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Reactive.Disposables;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using System.Windows;
10 | using System.Windows.Controls;
11 | using System.Windows.Data;
12 | using System.Windows.Documents;
13 | using System.Windows.Input;
14 | using System.Windows.Media;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Navigation;
17 | using System.Windows.Shapes;
18 |
19 | namespace ImageSort.WPF.Views.Metadata;
20 |
21 | ///
22 | /// Interaction logic for MetadataSection.xaml
23 | ///
24 | public partial class MetadataSectionView : ReactiveUserControl
25 | {
26 | public MetadataSectionView()
27 | {
28 | InitializeComponent();
29 |
30 | this.WhenActivated(disposableRegistration =>
31 | {
32 | this.OneWayBind(ViewModel,
33 | vm => vm.Title,
34 | view => view.Titel.Text)
35 | .DisposeWith(disposableRegistration);
36 |
37 | this.OneWayBind(ViewModel,
38 | vm => vm.Fields,
39 | view => view.Fields.ItemsSource)
40 | .DisposeWith(disposableRegistration);
41 | });
42 | }
43 |
44 | private void Fields_PreviewMouseWheel_BubbleUpToParent(object sender, MouseWheelEventArgs e)
45 | {
46 | if (!e.Handled)
47 | {
48 | var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
49 | {
50 | RoutedEvent = UIElement.MouseWheelEvent,
51 | Source = sender
52 | };
53 | var parent = ((Control)sender).Parent as UIElement;
54 | parent.RaiseEvent(eventArg);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataView.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/Metadata/MetadataView.xaml.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.SettingsManagement;
2 | using ImageSort.ViewModels.Metadata;
3 | using ImageSort.WPF.SettingsManagement;
4 | using ReactiveUI;
5 | using Splat;
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Reactive.Disposables;
10 | using System.Reactive.Linq;
11 | using System.Text;
12 | using System.Threading.Tasks;
13 | using System.Windows;
14 | using System.Windows.Controls;
15 | using System.Windows.Data;
16 | using System.Windows.Documents;
17 | using System.Windows.Input;
18 | using System.Windows.Media;
19 | using System.Windows.Media.Imaging;
20 | using System.Windows.Navigation;
21 | using System.Windows.Shapes;
22 |
23 | namespace ImageSort.WPF.Views.Metadata;
24 |
25 | ///
26 | /// Interaction logic for MetadataView.xaml
27 | ///
28 | public partial class MetadataView : ReactiveUserControl
29 | {
30 | public MetadataView()
31 | {
32 | InitializeComponent();
33 |
34 | this.WhenActivated(disposableRegistration =>
35 | {
36 | var metadataSettings = Locator.Current.GetService>()
37 | .Select(s => s as MetadataPanelSettings)
38 | .First(s => s != null);
39 |
40 | ViewModel.IsExpanded = metadataSettings.IsExpanded;
41 |
42 | ViewModel.WhenAnyValue(vm => vm.IsExpanded)
43 | .Subscribe(isExpanded => metadataSettings.IsExpanded = isExpanded)
44 | .DisposeWith(disposableRegistration);
45 |
46 | this.Bind(ViewModel,
47 | vm => vm.IsExpanded,
48 | view => view.ShowMetadataButton.IsChecked)
49 | .DisposeWith(disposableRegistration);
50 |
51 | this.OneWayBind(ViewModel,
52 | vm => vm.IsExpanded,
53 | view => view.MetadataArea.Visibility,
54 | isExpanded => isExpanded ? Visibility.Visible : Visibility.Collapsed)
55 | .DisposeWith(disposableRegistration);
56 |
57 | this.OneWayBind(ViewModel,
58 | vm => vm.Metadata.Type,
59 | view => view.IsEnabled,
60 | type => type is MetadataResultType.Success)
61 | .DisposeWith(disposableRegistration);
62 |
63 | this.OneWayBind(ViewModel,
64 | vm => vm.Metadata.Type,
65 | view => view.Visibility,
66 | type => type is MetadataResultType.Success ? Visibility.Visible : Visibility.Collapsed)
67 | .DisposeWith(disposableRegistration);
68 |
69 | this.OneWayBind(ViewModel,
70 | vm => vm.SectionViewModels,
71 | view => view.Directories.ItemsSource)
72 | .DisposeWith(disposableRegistration);
73 | });
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/ImageSort.WPF/Views/WindowHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Windows;
4 | using System.Windows.Forms;
5 | using ImageSort.SettingsManagement;
6 | using ImageSort.WPF.SettingsManagement.WindowPosition;
7 | using Splat;
8 |
9 | namespace ImageSort.WPF.Views;
10 |
11 | internal static class WindowHelper
12 | {
13 | private static WindowPositionSettingsViewModel GetWindowPostion() where TWindow : Window
14 | {
15 | return Locator.Current.GetService>()
16 | .OfType>()
17 | .FirstOrDefault();
18 | }
19 |
20 | public static void RestoreWindowState(this TWindow window) where TWindow : Window
21 | {
22 | var windowPosition = GetWindowPostion();
23 |
24 | if (windowPosition == null) return;
25 |
26 | var screenCount = Screen.AllScreens.Length;
27 |
28 | // ensure when the number of screen was changed the window will still be visible
29 | if (windowPosition.ScreenCount != screenCount)
30 | {
31 | windowPosition.ScreenCount = screenCount;
32 | }
33 | else
34 | {
35 | window.Left = windowPosition.Left;
36 | window.Top = windowPosition.Top;
37 | }
38 |
39 | window.WindowState = windowPosition.IsMaximized ? WindowState.Maximized : WindowState.Normal;
40 | window.Height = windowPosition.Height;
41 | window.Width = windowPosition.Width;
42 | }
43 |
44 | public static void SaveWindowState(this TWindow window) where TWindow : Window
45 | {
46 | var windowPosition = GetWindowPostion();
47 |
48 | if (windowPosition == null) return;
49 |
50 | windowPosition.IsMaximized = window.WindowState == WindowState.Maximized;
51 |
52 | if (window.WindowState == WindowState.Maximized) window.WindowState = WindowState.Normal;
53 |
54 | windowPosition.Left = (int) window.Left;
55 | windowPosition.Top = (int) window.Top;
56 | windowPosition.Height = (int) window.Height;
57 | windowPosition.Width = (int) window.Width;
58 |
59 | // record the screen count at the time.
60 | windowPosition.ScreenCount = Screen.AllScreens.Length;
61 | }
62 | }
--------------------------------------------------------------------------------
/src/ImageSort.WindowsSetup/Image Sort.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/src/ImageSort.WindowsSetup/Image Sort.ico
--------------------------------------------------------------------------------
/src/ImageSort.WindowsSetup/ImageSort.WindowsSetup.wixproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 |
6 |
7 |
8 |
9 | ImageSort.$(Platform)
10 | $(Platform)
11 | win-arm64
12 | win-$(Platform)
13 | RuntimeIdentifier=$(RuntimeIdentifier);
14 |
15 |
16 | ..\..\artifacts\$(Platform)
17 |
18 |
19 | Debug
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ImageSort.WPF
40 | {ff41f5a7-55c5-47bd-8a6d-d8eb86e9c967}
41 | True
42 | True
43 | Binaries;Content;Satellites
44 | INSTALLFOLDER
45 | True
46 | net8.0-windows
47 | $(RuntimeIdentifier)
48 |
49 |
50 |
51 | True
52 |
53 |
54 |
55 | true
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/ImageSort.WindowsSetup/delete_explorer_context_menu.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | setlocal enabledelayedexpansion
3 |
4 | for /f "tokens=*" %%G in ('reg query HKEY_USERS') do (
5 | set "userSID=%%G"
6 | if not "!userSID!"=="HKEY_USERS" (
7 | reg delete "!userSID!\Software\Classes\Directory\shell\ImageSort" /f
8 | reg delete "!userSID!\Software\Classes\Drive\shell\ImageSort" /f
9 | reg delete "!userSID!\Software\Classes\Folder\shell\ImageSort" /f
10 | )
11 | )
12 |
13 | endlocal
14 |
--------------------------------------------------------------------------------
/src/ImageSort.WindowsSetup/exclude-imagesort.exe.xslt:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/ImageSort.WindowsUpdater/GitHubUpdateFetcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Reflection;
6 | using System.Threading.Tasks;
7 | using Octokit;
8 | using Semver;
9 |
10 | namespace ImageSort.WindowsUpdater;
11 |
12 | public class GitHubUpdateFetcher
13 | {
14 | private readonly GitHubClient client;
15 |
16 | public GitHubUpdateFetcher(GitHubClient client)
17 | {
18 | this.client = client;
19 | }
20 |
21 | public async Task<(bool, Release)> TryGetLatestReleaseAsync(bool allowPrerelease = false)
22 | {
23 | var assembly = Assembly.GetAssembly(typeof(GitHubUpdateFetcher));
24 | var gitVersionInformationType = assembly.GetType("GitVersionInformation");
25 | var versionTag =
26 | (string) gitVersionInformationType?.GetFields().First(f => f.Name == "SemVer").GetValue(null);
27 | var version = SemVersion.Parse(versionTag);
28 |
29 | Release latestFitting;
30 |
31 | try
32 | {
33 | var releases = await client.Repository.Release.GetAll("Lolle2000la", "Image-Sort");
34 |
35 | latestFitting = releases
36 | .FirstOrDefault(release =>
37 | {
38 | var prereleaseCondition = allowPrerelease || !release.Prerelease;
39 |
40 | var firstIndexOfV = release.TagName.IndexOf('v', StringComparison.OrdinalIgnoreCase);
41 |
42 | var releaseVersion = SemVersion.Parse(release.TagName.Substring(firstIndexOfV + 1));
43 |
44 | var isNewVersion = version.CompareByPrecedence(releaseVersion) < 0;
45 |
46 | return prereleaseCondition && isNewVersion;
47 | });
48 | }
49 | catch
50 | {
51 | latestFitting = null;
52 | }
53 |
54 | return (latestFitting != null, latestFitting);
55 | }
56 |
57 | public bool TryGetInstallerFromRelease(Release release, out ReleaseAsset installer)
58 | {
59 | if (release == null) throw new ArgumentNullException(nameof(release));
60 |
61 | var is64bit = Environment.Is64BitProcess;
62 |
63 | installer = release.Assets
64 | .FirstOrDefault(asset => asset.Name
65 | .Equals($"ImageSort.{(is64bit ? "x64" : "x86")}.msi", StringComparison.OrdinalIgnoreCase));
66 |
67 | return installer != null;
68 | }
69 |
70 | public async Task GetStreamFromAssetAsync(ReleaseAsset asset)
71 | {
72 | using var httpClient = new HttpClient();
73 |
74 | httpClient.DefaultRequestHeaders.Add("User-Agent", "Image-Sort");
75 |
76 | try
77 | {
78 | return await httpClient.GetStreamAsync(asset.BrowserDownloadUrl);
79 | }
80 | catch (HttpRequestException)
81 | {
82 | return null;
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/src/ImageSort.WindowsUpdater/ImageSort.WindowsUpdater.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Debug;Release;MSIX
6 | AnyCPU;x86;x64;ARM64
7 | win-x86;win-x64;win-arm64
8 |
9 |
10 |
11 |
12 | All
13 |
14 |
15 |
16 |
17 |
18 |
19 | false
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/ImageSort.WindowsUpdater/InstallerRunner.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 |
6 | namespace ImageSort.WindowsUpdater;
7 |
8 | public static class InstallerRunner
9 | {
10 | public static async Task RunAsync(Stream installer)
11 | {
12 | if (installer == null) throw new ArgumentNullException(nameof(installer));
13 |
14 | var tempFolder = Path.Combine(Path.GetTempPath(), "Image Sort");
15 | var setupPath = Path.Combine(tempFolder, $"ImageSort.{(Environment.Is64BitProcess ? "x64" : "x86")}.msi");
16 |
17 | if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder);
18 |
19 | var fs = File.Create(Path.Combine(tempFolder, setupPath));
20 |
21 | await installer.CopyToAsync(fs);
22 |
23 | await fs.DisposeAsync();
24 |
25 | RunSetup(setupPath);
26 |
27 | Environment.Exit(0);
28 | }
29 |
30 | private static void RunSetup(string path)
31 | {
32 | var processStartInfo = new ProcessStartInfo("msiexec",
33 | $"/i \"{path}\" TARGETDIR=\"{AppDomain.CurrentDomain.BaseDirectory}\" /passive AUTOSTART=1")
34 | {
35 | Verb = "runas",
36 | UseShellExecute = true
37 | };
38 |
39 | Process.Start(processStartInfo);
40 | }
41 |
42 | public static void CleanUpInstaller()
43 | {
44 | var setupPath = Path.Combine(Path.GetTempPath(), "Image Sort",
45 | $"ImageSort.{(Environment.Is64BitProcess ? "x64" : "x86")}.msi");
46 |
47 | if (File.Exists(setupPath)) File.Delete(setupPath);
48 | }
49 | }
--------------------------------------------------------------------------------
/src/ImageSort/Actions/DeleteAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using ImageSort.FileSystem;
4 | using ImageSort.Localization;
5 |
6 | namespace ImageSort.Actions;
7 |
8 | ///
9 | /// Deletes a file. Only a file, not a directory.
10 | ///
11 | public class DeleteAction : IReversibleAction
12 | {
13 | private readonly Action notifyAct;
14 | private readonly Action notifyRevert;
15 | private readonly string oldPath;
16 | private readonly IRecycleBin recycleBin;
17 | private IDisposable deletedFile;
18 |
19 | public DeleteAction(string path, IFileSystem fileSystem, IRecycleBin recycleBin,
20 | Action notifyAct = null, Action notifyRevert = null)
21 | {
22 | if (path == null) throw new ArgumentNullException(nameof(path));
23 | if (fileSystem == null) throw new ArgumentNullException(nameof(fileSystem));
24 | if (recycleBin == null) throw new ArgumentNullException(nameof(recycleBin));
25 | if (!fileSystem.FileExists(path)) throw new FileNotFoundException(null, path);
26 |
27 | this.notifyAct = notifyAct;
28 | this.notifyRevert = notifyRevert;
29 |
30 | oldPath = path;
31 | this.recycleBin = recycleBin;
32 | }
33 |
34 | public string DisplayName => Text.DeleteActionMessage
35 | .Replace("{FileName}", Path.GetFileName(oldPath), StringComparison.OrdinalIgnoreCase);
36 |
37 | public void Act()
38 | {
39 | if (deletedFile == null) deletedFile = recycleBin.Send(oldPath);
40 |
41 | notifyAct?.Invoke(oldPath);
42 | }
43 |
44 | public void Revert()
45 | {
46 | deletedFile?.Dispose();
47 |
48 | deletedFile = null;
49 |
50 | notifyRevert?.Invoke(oldPath);
51 | }
52 | }
--------------------------------------------------------------------------------
/src/ImageSort/Actions/IReversibleAction.cs:
--------------------------------------------------------------------------------
1 | namespace ImageSort.Actions;
2 |
3 | public interface IReversibleAction
4 | {
5 | string DisplayName { get; }
6 | void Act();
7 |
8 | void Revert();
9 | }
--------------------------------------------------------------------------------
/src/ImageSort/Actions/MoveAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using ImageSort.FileSystem;
4 | using ImageSort.Localization;
5 |
6 | namespace ImageSort.Actions;
7 |
8 | public class MoveAction : IReversibleAction
9 | {
10 | private readonly IFileSystem fileSystem;
11 | private readonly string oldDestination;
12 | private readonly string newDestination;
13 | private readonly Action notifyAct;
14 | private readonly Action notifyRevert;
15 |
16 | public MoveAction(string file, string toFolder, IFileSystem fileSystem,
17 | Action notifyAct = null, Action notifyRevert = null)
18 | {
19 | if (file == null) throw new ArgumentNullException(nameof(file));
20 | if (toFolder == null) throw new ArgumentNullException(nameof(toFolder));
21 | if (fileSystem == null) throw new ArgumentNullException(nameof(fileSystem));
22 | if (!fileSystem.FileExists(file)) throw new FileNotFoundException(null, file);
23 | if (!fileSystem.DirectoryExists(toFolder))
24 | throw new DirectoryNotFoundException(
25 | Text.DirectoryNotFoundExceptionMessage.Replace("{Directory}", toFolder,
26 | StringComparison.OrdinalIgnoreCase));
27 |
28 | this.fileSystem = fileSystem;
29 |
30 | this.notifyAct = notifyAct;
31 | this.notifyRevert = notifyRevert;
32 |
33 | // ensure absolute paths, there are weird windows path limit bugs
34 | file = Path.GetFullPath(file);
35 | toFolder = Path.GetFullPath(toFolder);
36 |
37 | oldDestination = file;
38 | newDestination = Path.Combine(toFolder, Path.GetFileName(file));
39 | }
40 |
41 | public string DisplayName => Text.MoveActionMessage
42 | .Replace("{FileName}", Path.GetFileName(oldDestination), StringComparison.OrdinalIgnoreCase)
43 | .Replace("{Directory}", Path.GetDirectoryName(newDestination), StringComparison.OrdinalIgnoreCase);
44 |
45 | public void Act()
46 | {
47 | fileSystem.Move(oldDestination, newDestination);
48 |
49 | notifyAct?.Invoke(oldDestination, newDestination);
50 | }
51 |
52 | public void Revert()
53 | {
54 | fileSystem.Move(newDestination, oldDestination);
55 |
56 | notifyRevert?.Invoke(newDestination, oldDestination);
57 | }
58 | }
--------------------------------------------------------------------------------
/src/ImageSort/Actions/RenameAction.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using ImageSort.FileSystem;
4 | using ImageSort.Localization;
5 |
6 | namespace ImageSort.Actions;
7 |
8 | public class RenameAction : IReversibleAction
9 | {
10 | private readonly IFileSystem fileSystem;
11 | private readonly string newPath;
12 | private readonly string oldPath;
13 | private readonly Action notifyAct;
14 | private readonly Action notifyRevert;
15 |
16 | public RenameAction(string path, string newName, IFileSystem fileSystem,
17 | Action notifyAct = null, Action notifyRevert = null)
18 | {
19 | if (path == null) throw new ArgumentNullException(nameof(path));
20 | if (newName == null) throw new ArgumentNullException(nameof(newName));
21 | if (fileSystem == null) throw new ArgumentNullException(nameof(fileSystem));
22 | if (!fileSystem.FileExists(path)) throw new FileNotFoundException(null, path);
23 |
24 | oldPath = path = Path.GetFullPath(path);
25 | newPath = Path.Combine(Path.GetDirectoryName(path), newName + Path.GetExtension(path));
26 |
27 | if (fileSystem.FileExists(newPath))
28 | throw new IOException(
29 | Text.FileAlreadyExistsExceptionMessage.Replace("{FileName}", newName,
30 | StringComparison.OrdinalIgnoreCase));
31 |
32 | this.fileSystem = fileSystem;
33 |
34 | this.notifyAct = notifyAct;
35 | this.notifyRevert = notifyRevert;
36 | }
37 |
38 | public string DisplayName => Text.RenameActionMessage
39 | .Replace("{OldFileName}", Path.GetFileName(oldPath), StringComparison.OrdinalIgnoreCase)
40 | .Replace("{NewFileName}", Path.GetFileName(newPath), StringComparison.OrdinalIgnoreCase);
41 |
42 | public void Act()
43 | {
44 | fileSystem.Move(oldPath, newPath);
45 |
46 | notifyAct?.Invoke(oldPath, newPath);
47 | }
48 |
49 | public void Revert()
50 | {
51 | fileSystem.Move(newPath, oldPath);
52 |
53 | notifyRevert?.Invoke(newPath, oldPath);
54 | }
55 | }
--------------------------------------------------------------------------------
/src/ImageSort/DependencyManagement/DependencyRegistrationHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using ImageSort.FileSystem;
5 | using ImageSort.SettingsManagement;
6 | using ImageSort.ViewModels.Metadata;
7 | using Splat;
8 |
9 | namespace ImageSort.DependencyManagement;
10 |
11 | public static class DependencyRegistrationHelper
12 | {
13 | public static void RegisterManditoryDependencies(this IMutableDependencyResolver dependencyResolver)
14 | {
15 | dependencyResolver.Register(() => new FullAccessFileSystem());
16 | dependencyResolver.Register(() => new FileSystemWatcher());
17 | dependencyResolver.Register(() => new FullAccessFileSystemMetadataExtractor());
18 | dependencyResolver.Register(() => new MetadataSectionViewModelFactory(new MetadataFieldViewModelFactory()));
19 | }
20 |
21 | ///
22 | /// Registers settings and gives the possibility to registrate custom ones.
23 | ///
24 | ///
25 | /// Allows for registration of custom settings.
26 | /// Simply add them to the and hold onto the added instances.
27 | ///
28 | public static void RegisterSettings(this IMutableDependencyResolver dependencyResolver,
29 | Action> registration = null)
30 | {
31 | var settings = new List();
32 |
33 | var userSettings = new List();
34 | registration?.Invoke(userSettings);
35 |
36 | if (userSettings.Count > 0) settings.AddRange(userSettings);
37 |
38 | dependencyResolver.RegisterConstant>(settings);
39 | }
40 | }
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/FileRestorationNotPossibleException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ImageSort.FileSystem;
4 |
5 | public class FileRestorationNotPossibleException : Exception
6 | {
7 | public FileRestorationNotPossibleException()
8 | {
9 | }
10 |
11 | public FileRestorationNotPossibleException(string message) : base(message)
12 | {
13 | }
14 |
15 | public FileRestorationNotPossibleException(string message, Exception innerException)
16 | : base(message, innerException)
17 | {
18 | }
19 | }
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/FullAccessFileSystem.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Linq;
4 |
5 | namespace ImageSort.FileSystem;
6 |
7 | public class FullAccessFileSystem : IFileSystem
8 | {
9 | public bool IsFolderEmpty(string path) => !Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly).Any();
10 |
11 | public IEnumerable GetSubFolders(string path) => Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly);
12 |
13 | public IEnumerable GetFiles(string folder) => Directory.EnumerateFiles(folder, "*", SearchOption.TopDirectoryOnly);
14 | }
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/FullAccessFileSystemMetadataExtractor.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using MetadataExtractor;
7 |
8 | namespace ImageSort.FileSystem;
9 |
10 | public class FullAccessFileSystemMetadataExtractor : IMetadataExtractor
11 | {
12 | public Dictionary> Extract(string x)
13 | {
14 | var dict = new Dictionary>();
15 | var directories = ImageMetadataReader.ReadMetadata(x);
16 | foreach (var directory in directories)
17 | {
18 | var subDict = new Dictionary();
19 | foreach (var tag in directory.Tags)
20 | {
21 | subDict.Add(tag.Name, tag.Description);
22 | }
23 | dict.Add(directory.Name, subDict);
24 | }
25 | return dict;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/IFileSystem.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 |
4 | namespace ImageSort.FileSystem;
5 |
6 | public interface IFileSystem
7 | {
8 | IEnumerable GetSubFolders(string path);
9 |
10 | IEnumerable GetFiles(string folder);
11 |
12 | bool IsFolderEmpty(string path);
13 |
14 | bool FileExists(string path) => File.Exists(path);
15 |
16 | bool DirectoryExists(string path) => Directory.Exists(path);
17 |
18 | void Move(string source, string destination) => File.Move(source, destination);
19 |
20 | void CreateFolder(string path) => Directory.CreateDirectory(path);
21 | }
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/IMetadataExtractor.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace ImageSort.FileSystem;
4 |
5 | public interface IMetadataExtractor
6 | {
7 | Dictionary> Extract(string x);
8 | }
--------------------------------------------------------------------------------
/src/ImageSort/FileSystem/IRecycleBin.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace ImageSort.FileSystem;
4 |
5 | public interface IRecycleBin
6 | {
7 | ///
8 | /// Sends the file or folder at the given path to the recycle bin.
9 | ///
10 | /// The path to the file or folder that should be send to the recycle bin.
11 | /// Should ask for user confirmation before sending to recycle bin.
12 | /// The will, when disposed, restore the file.
13 | ///
14 | /// The handle to the deleted file will throw a
15 | ///
16 | /// when it cannot restore the file.
17 | ///
18 | IDisposable Send(string path, bool confirmationNeeded = false);
19 | }
--------------------------------------------------------------------------------
/src/ImageSort/Helpers/PathHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace ImageSort.Helpers;
5 |
6 | public static class PathHelper
7 | {
8 | public static bool PathEquals(this string path1, string path2)
9 | {
10 | return Path.GetFullPath(path1).Equals(Path.GetFullPath(path2), StringComparison.OrdinalIgnoreCase);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/ImageSort/Helpers/StringHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 |
4 | namespace ImageSort.Helpers;
5 |
6 | internal static class StringHelper
7 | {
8 | public static bool EndsWithAny(
9 | this string @string,
10 | StringComparison comparisonType,
11 | params string[] atEnd)
12 | {
13 | return atEnd.Any(end => @string.EndsWith(end, comparisonType));
14 | }
15 | }
--------------------------------------------------------------------------------
/src/ImageSort/ImageSort.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | Debug;Release;MSIX
6 | win-x86;win-x64;win-arm64
7 |
8 |
9 |
10 |
11 |
12 |
13 | All
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | false
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/ImageSort/SettingsManagement/SettingsGroupViewModelBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using ReactiveUI;
5 |
6 | namespace ImageSort.SettingsManagement;
7 |
8 | public abstract class SettingsGroupViewModelBase : ReactiveObject
9 | {
10 | protected SettingsGroupViewModelBase()
11 | {
12 | Changed.Subscribe(args =>
13 | {
14 | SettingsStore[args.PropertyName] =
15 | args.Sender?.GetType().GetProperty(args.PropertyName)?.GetValue(args.Sender);
16 | });
17 | }
18 |
19 | ///
20 | /// Used for storage. Should not be changed EVER once set. It must also be unique.
21 | ///
22 | public abstract string Name { get; }
23 |
24 | public abstract string Header { get; }
25 | public virtual bool IsVisible => true;
26 | public Dictionary SettingsStore { get; } = new Dictionary();
27 |
28 | public void UpdatePropertiesFromStore()
29 | {
30 | var properties = GetType().GetProperties();
31 |
32 | foreach (var property in properties)
33 | {
34 | if (!SettingsStore.TryGetValue(property.Name, out var setting)) continue;
35 |
36 | if (setting is object[] objects)
37 | property.SetValue(this, objects.OfType());
38 | else
39 | property.SetValue(this,
40 | typeof(Enum).IsAssignableFrom(property.PropertyType)
41 | ? Enum.ToObject(property.PropertyType, setting)
42 | : setting);
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/src/ImageSort/SettingsManagement/SettingsViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using ReactiveUI;
5 | using Splat;
6 |
7 | namespace ImageSort.SettingsManagement;
8 |
9 | public class SettingsViewModel : ReactiveObject
10 | {
11 | public IEnumerable SettingsGroups { get; }
12 |
13 | public SettingsViewModel(IEnumerable settingsGroups = null)
14 | {
15 | SettingsGroups = settingsGroups ?? Locator.Current.GetService>();
16 | }
17 |
18 | public TGroup GetGroup() where TGroup : SettingsGroupViewModelBase
19 | {
20 | return SettingsGroups.OfType()
21 | .FirstOrDefault();
22 | }
23 |
24 | public Dictionary> AsDictionary()
25 | {
26 | return SettingsGroups.ToDictionary(@group => @group.Name, @group => @group.SettingsStore);
27 | }
28 |
29 | public void RestoreFromDictionary(Dictionary> dictionary)
30 | {
31 | if (dictionary == null) throw new ArgumentNullException(nameof(dictionary));
32 |
33 | foreach (var (group, store) in dictionary)
34 | {
35 | var settingsGroup = SettingsGroups.FirstOrDefault(g => g.Name == group);
36 |
37 | if (settingsGroup == null) continue;
38 |
39 | foreach (var (storeKey, storeValue) in store) settingsGroup.SettingsStore[storeKey] = storeValue;
40 |
41 | settingsGroup.UpdatePropertiesFromStore();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/ActionsViewModel.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.Actions;
2 | using ImageSort.Localization;
3 | using ReactiveUI;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Reactive;
7 | using System.Reactive.Linq;
8 |
9 | namespace ImageSort.ViewModels;
10 |
11 | public class ActionsViewModel : ReactiveObject
12 | {
13 | private readonly Stack done = new Stack();
14 | private readonly Stack undone = new Stack();
15 |
16 | private readonly ObservableAsPropertyHelper lastDone;
17 | public string LastDone => lastDone.Value;
18 |
19 | private readonly ObservableAsPropertyHelper lastUndone;
20 | public string LastUndone => lastUndone.Value;
21 |
22 | public Interaction NotifyUserOfError { get; } = new Interaction();
23 |
24 | public ReactiveCommand Execute { get; }
25 | public ReactiveCommand Undo { get; }
26 | public ReactiveCommand Redo { get; }
27 | public ReactiveCommand Clear { get; }
28 |
29 | public ActionsViewModel()
30 | {
31 | Execute = ReactiveCommand.CreateFromTask(async action =>
32 | {
33 | try
34 | {
35 | action.Act();
36 | }
37 | catch (Exception ex)
38 | {
39 | await NotifyUserOfError.Handle(Text.CouldNotActErrorText
40 | .Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
41 | .Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
42 |
43 | return;
44 | }
45 |
46 | done.Push(action);
47 |
48 | undone.Clear();
49 | });
50 |
51 | Undo = ReactiveCommand.CreateFromTask(async () =>
52 | {
53 | if (done.TryPop(out var action))
54 | {
55 | try
56 | {
57 | action.Revert();
58 | }
59 | catch (Exception ex)
60 | {
61 | await NotifyUserOfError.Handle(Text.CouldNotUndoErrorText
62 | .Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
63 | .Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
64 |
65 | return;
66 | }
67 |
68 | undone.Push(action);
69 | }
70 | });
71 |
72 | Redo = ReactiveCommand.CreateFromTask(async () =>
73 | {
74 | if (undone.TryPop(out var action))
75 | {
76 | try
77 | {
78 | action.Act();
79 | }
80 | catch (Exception ex)
81 | {
82 | await NotifyUserOfError.Handle(Text.CouldNotRedoErrorText
83 | .Replace("{ErrorMessage}", ex.Message, StringComparison.OrdinalIgnoreCase)
84 | .Replace("{ActMessage}", action.DisplayName, StringComparison.OrdinalIgnoreCase));
85 |
86 | return;
87 | }
88 |
89 | done.Push(action);
90 | }
91 | });
92 |
93 | Clear = ReactiveCommand.Create(() =>
94 | {
95 | done.Clear();
96 | undone.Clear();
97 | });
98 |
99 | var historyChanges = Execute.Merge(Undo).Merge(Redo).Merge(Clear);
100 |
101 | lastDone = historyChanges
102 | .Select(_ =>
103 | {
104 | if (done.TryPeek(out var action)) return action.DisplayName;
105 |
106 | return null;
107 | })
108 | .ToProperty(this, vm => vm.LastDone);
109 |
110 | lastUndone = historyChanges
111 | .Select(_ =>
112 | {
113 | if (undone.TryPeek(out var action)) return action.DisplayName;
114 |
115 | return null;
116 | })
117 | .ToProperty(this, vm => vm.LastUndone);
118 | }
119 | }
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/Metadata/MetadataFieldViewModel.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace ImageSort.ViewModels.Metadata;
9 | public class MetadataFieldViewModel : ReactiveObject
10 | {
11 | private string _name;
12 | public string Name
13 | {
14 | get => _name;
15 | set => this.RaiseAndSetIfChanged(ref _name, value);
16 | }
17 |
18 | private string _value;
19 | public string Value
20 | {
21 | get => _value;
22 | set => this.RaiseAndSetIfChanged(ref _value, value);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/Metadata/MetadataFieldViewModelFactory.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 ImageSort.ViewModels.Metadata;
8 | public class MetadataFieldViewModelFactory
9 | {
10 | public MetadataFieldViewModel Create(string name, string value)
11 | {
12 | return new()
13 | {
14 | Name = name,
15 | Value = value
16 | };
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/Metadata/MetadataSectionViewModel.cs:
--------------------------------------------------------------------------------
1 | using ReactiveUI;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Reactive.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace ImageSort.ViewModels.Metadata;
10 | public class MetadataSectionViewModel : ReactiveObject
11 | {
12 | private string _title;
13 | public string Title
14 | {
15 | get => _title;
16 | set => this.RaiseAndSetIfChanged(ref _title, value);
17 | }
18 |
19 | private Dictionary _fields;
20 | public Dictionary Fields
21 | {
22 | get => _fields;
23 | set => this.RaiseAndSetIfChanged(ref _fields, value);
24 | }
25 |
26 | private ObservableAsPropertyHelper> _fieldViewModels;
27 | public IEnumerable FieldViewModels => _fieldViewModels.Value;
28 |
29 | public MetadataSectionViewModel(MetadataFieldViewModelFactory fieldViewModelFactory)
30 | {
31 | _fieldViewModels = this.WhenAnyValue(x => x.Fields)
32 | .Select(f => f?.Select(x => fieldViewModelFactory.Create(x.Key, x.Value)))
33 | .Where(f => f != null)
34 | .ToProperty(this, x => x.FieldViewModels);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/Metadata/MetadataSectionViewModelFactory.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 ImageSort.ViewModels.Metadata;
8 | public class MetadataSectionViewModelFactory
9 | {
10 | private readonly MetadataFieldViewModelFactory fieldViewModelFactory;
11 |
12 | public MetadataSectionViewModelFactory(MetadataFieldViewModelFactory fieldViewModelFactory)
13 | {
14 | this.fieldViewModelFactory = fieldViewModelFactory;
15 | }
16 |
17 | public MetadataSectionViewModel Create(string title, Dictionary fields)
18 | {
19 | return new(fieldViewModelFactory)
20 | {
21 | Title = title,
22 | Fields = fields
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ImageSort/ViewModels/Metadata/MetadataViewModel.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.FileSystem;
2 | using ReactiveUI;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Reactive;
7 | using System.Reactive.Linq;
8 | using System.Runtime.InteropServices;
9 | using System.Text;
10 | using System.Threading.Tasks;
11 |
12 | namespace ImageSort.ViewModels.Metadata;
13 |
14 | public class MetadataViewModel : ReactiveObject
15 | {
16 | private readonly IMetadataExtractor extractor;
17 | private readonly IFileSystem fileSystem;
18 |
19 | private string _imagePath;
20 | public string ImagePath
21 | {
22 | get => _imagePath;
23 | set => this.RaiseAndSetIfChanged(ref _imagePath, value);
24 | }
25 |
26 | private ObservableAsPropertyHelper _metadata;
27 | public MetadataResult Metadata => _metadata.Value;
28 |
29 | private ObservableAsPropertyHelper> _sectionViewModels;
30 | public IEnumerable SectionViewModels => _sectionViewModels.Value;
31 |
32 | private bool _isExpanded = true;
33 | public bool IsExpanded
34 | {
35 | get => _isExpanded;
36 | set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
37 | }
38 |
39 | public ReactiveCommand ToggleIsExpanded { get; }
40 |
41 | public MetadataViewModel(IMetadataExtractor extractor, IFileSystem fileSystem, MetadataSectionViewModelFactory metadataSectionFactory)
42 | {
43 | this.extractor = extractor;
44 | this.fileSystem = fileSystem;
45 |
46 | _metadata = this.WhenAnyValue(x => x.ImagePath, x => x.IsExpanded)
47 | .Where(x => x.Item2) // only load metadata if the panel is expanded
48 | .Select(x => x.Item1)
49 | .Select(ExtractSafely)
50 | .ToProperty(this, x => x.Metadata);
51 |
52 | _sectionViewModels = this.WhenAnyValue(x => x.Metadata)
53 | .Where(x => x.Type == MetadataResultType.Success)
54 | .Select(m => m.Metadata.OrderBy(x => x.Key))
55 | .Select(m => m.Select(d => metadataSectionFactory.Create(d.Key, d.Value)))
56 | .ToProperty(this, x => x.SectionViewModels);
57 |
58 | ToggleIsExpanded = ReactiveCommand.Create(() =>
59 | {
60 | IsExpanded = !IsExpanded;
61 | });
62 | }
63 |
64 | private MetadataResult ExtractSafely(string path)
65 | {
66 | try
67 | {
68 | if (fileSystem.FileExists(path))
69 | {
70 | return new()
71 | {
72 | Type = MetadataResultType.Success,
73 | Metadata = extractor.Extract(path)
74 | };
75 | }
76 | else
77 | {
78 | return new MetadataResult()
79 | {
80 | Type = MetadataResultType.FileDoesNotExist
81 | };
82 | }
83 | }
84 | #pragma warning disable CA1031 // Do not catch general exception types
85 | // since we don't want an exception to take down the application and instead pass it on, we catch all of them here
86 | catch (Exception ex)
87 | {
88 | return new MetadataResult()
89 | {
90 | Type = MetadataResultType.UnexpectedError,
91 | Exception = ex
92 | };
93 | }
94 | #pragma warning restore CA1031 // Do not catch general exception types
95 | }
96 | }
97 |
98 | public record MetadataResult
99 | {
100 | public MetadataResultType Type { get; init; }
101 | public Dictionary> Metadata { get; init; }
102 | public Exception Exception { get; init; }
103 | }
104 |
105 | public enum MetadataResultType
106 | {
107 | Success,
108 | FileDoesNotExist,
109 | UnexpectedError
110 | }
111 |
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/Actions/DeleteActionTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using ImageSort.Actions;
4 | using ImageSort.FileSystem;
5 | using NSubstitute;
6 | using Xunit;
7 |
8 | namespace ImageSort.UnitTests.Actions;
9 |
10 | public class DeleteActionTests
11 | {
12 | [Fact(DisplayName = "Deletes and Restores the file correctly.")]
13 | public void DeletesAndRestoresTheFileCorrectly()
14 | {
15 | const string fileToDelete = @"C:\Some File.png";
16 |
17 | var fsMock = Substitute.For();
18 | var recycleBinMock = Substitute.For();
19 | var fileRestorerMock = Substitute.For();
20 |
21 | fsMock.FileExists(fileToDelete).Returns(true);
22 |
23 | recycleBinMock.Send(fileToDelete, false).Returns(fileRestorerMock);
24 |
25 | var deleteAction = new DeleteAction(fileToDelete, fsMock, recycleBinMock);
26 |
27 | fsMock.Received().FileExists(fileToDelete);
28 |
29 | deleteAction.Act();
30 |
31 | recycleBinMock.Received().Send(fileToDelete, false);
32 |
33 | deleteAction.Revert();
34 |
35 | fileRestorerMock.Received().Dispose();
36 | }
37 |
38 | [Fact(DisplayName = "Throws when the file to delete does not exist")]
39 | public void ThrowsWhenTheFileDoesNotExist()
40 | {
41 | const string fileThatDoesntExist = @"C:\Fictional File.fake";
42 |
43 | var fsMock = Substitute.For();
44 | var recycleBinMock = Substitute.For();
45 |
46 | fsMock.FileExists(fileThatDoesntExist).Returns(false);
47 |
48 | Assert.Throws(() =>
49 | new DeleteAction(fileThatDoesntExist, fsMock, recycleBinMock));
50 |
51 | fsMock.Received().FileExists(fileThatDoesntExist);
52 | }
53 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/Actions/MoveActionTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using ImageSort.Actions;
3 | using ImageSort.FileSystem;
4 | using NSubstitute;
5 | using Xunit;
6 |
7 | namespace ImageSort.UnitTests.Actions;
8 |
9 | public class MoveActionTests
10 | {
11 | [Fact(DisplayName = "File gets moved correctly and caller gets notified of change.")]
12 | public void FileGetsMovedCorrectly()
13 | {
14 | const string oldPath = @"C:\SomeFile.png";
15 | const string newFolder = @"C:\SomeOtherFolder\SomeOtherFolderAsWell\";
16 | const string newPath = newFolder + "SomeFile.png";
17 |
18 | var notifedOfAction = false;
19 | var notifiedOfReversion = false;
20 |
21 | var fsMock = Substitute.For();
22 |
23 | fsMock.FileExists(oldPath).Returns(true);
24 | fsMock.DirectoryExists(newFolder).Returns(true);
25 |
26 | var moveAction = new MoveAction(oldPath, newFolder, fsMock,
27 | (f, t) => notifedOfAction = true,
28 | (f, t) => notifiedOfReversion = true);
29 |
30 | fsMock.Received().FileExists(oldPath);
31 | fsMock.Received().DirectoryExists(newFolder);
32 |
33 | moveAction.Act();
34 |
35 | fsMock.Received().Move(oldPath, newPath);
36 |
37 | moveAction.Revert();
38 |
39 | fsMock.Received().Move(newPath, oldPath);
40 |
41 | Assert.True(notifedOfAction, "The caller should be notified when an action acts.");
42 | Assert.True(notifiedOfReversion, "The caller should be notified when an action is reverted.");
43 | }
44 |
45 | [Fact(DisplayName = "Handles file or directory not existing correctly.")]
46 | public void HandlesFileOrDirectoryNotExisting()
47 | {
48 | const string existingDirectory = @"C:\SomeRealDirectory";
49 | const string existingFile = @"C:\SomeRealFile.tif";
50 | const string fakeDirectory = @"C:\DirectoryThatDoesntExist";
51 | const string fakeFile = @"C:\SomeFakeFile.gif";
52 |
53 | var fsMock = Substitute.For();
54 |
55 | fsMock.DirectoryExists(existingDirectory).Returns(true);
56 | fsMock.FileExists(existingFile).Returns(true);
57 | fsMock.DirectoryExists(fakeDirectory).Returns(false);
58 | fsMock.FileExists(fakeFile).Returns(false);
59 |
60 | Assert.Throws(() => new MoveAction(fakeFile, existingDirectory, fsMock));
61 |
62 | Assert.Throws(() => new MoveAction(existingFile, fakeDirectory, fsMock));
63 | }
64 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/Actions/RenameActionTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using ImageSort.Actions;
3 | using ImageSort.FileSystem;
4 | using NSubstitute;
5 | using Xunit;
6 |
7 | namespace ImageSort.UnitTests.Actions;
8 |
9 | public class RenameActionTests
10 | {
11 | [Fact(DisplayName = "Can rename files and undo")]
12 | public void CanRenameFilesAndUndo()
13 | {
14 | const string oldPath = @"C:\my-image.png";
15 | const string newFileName = "my-renamed-image";
16 | const string newPath = @"C:\my-renamed-image.png";
17 |
18 | var canAct = false;
19 | var canRevert = false;
20 |
21 | var fsMock = Substitute.For();
22 |
23 | fsMock.FileExists(oldPath).Returns(true);
24 | fsMock.FileExists(newPath).Returns(false);
25 |
26 | var renameAction = new RenameAction(oldPath, newFileName, fsMock,
27 | (o, n) => canAct = true, (n, o) => canRevert = true);
28 |
29 | renameAction.Act();
30 |
31 | fsMock.Received().Move(oldPath, newPath);
32 |
33 | renameAction.Revert();
34 |
35 | fsMock.Received().Move(newPath, oldPath);
36 |
37 | Assert.True(canAct);
38 | Assert.True(canRevert);
39 | }
40 |
41 | [Fact(DisplayName = "Throws when the file doesn't exist or the renamed path is already used.")]
42 | public void ThrowsWhenFileDoesNotExistOrNewPathIsAlreadyUsed()
43 | {
44 | const string oldPath = @"C:\my-image.png";
45 | const string invalidOldPath = @"C:\invalid.gif";
46 | const string newFileName = "my-renamed-image";
47 | const string newPath = @"C:\my-renamed-image.png";
48 | const string alreadyExistingName = @"already-exists";
49 | const string alreadyExistingPath = @"C:\already-exists.png";
50 |
51 | var fsMock = Substitute.For();
52 |
53 | fsMock.FileExists(oldPath).Returns(true);
54 | fsMock.FileExists(newPath).Returns(false);
55 | fsMock.FileExists(invalidOldPath).Returns(false);
56 | fsMock.FileExists(alreadyExistingPath).Returns(true);
57 |
58 | Assert.Throws(() => new RenameAction(invalidOldPath, newFileName, fsMock));
59 |
60 | Assert.Throws(() => new RenameAction(oldPath, alreadyExistingName, fsMock));
61 |
62 | fsMock.Received().FileExists(invalidOldPath);
63 | fsMock.Received().FileExists(alreadyExistingPath);
64 | }
65 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/ImageSort.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 |
6 | false
7 |
8 | Debug;Release;MSIX
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 | all
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 |
25 |
26 | all
27 | runtime; build; native; contentfiles; analyzers; buildtransitive
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/SettingsManagement/SettingsGroupViewModelBaseTests.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.SettingsManagement;
2 | using ReactiveUI;
3 | using Xunit;
4 |
5 | namespace ImageSort.UnitTests.SettingsManagement;
6 |
7 | public class SettingsGroupViewModelBaseTests
8 | {
9 | [Fact(DisplayName = "Saves changed properties in settings storage")]
10 | public void SavesChangedProperties()
11 | {
12 | var testSettingsGroup = new TestSettingsGroup();
13 |
14 | Assert.False(testSettingsGroup.SettingsStore.TryGetValue("TestProperty", out var _));
15 | Assert.False(testSettingsGroup.SettingsStore.TryGetValue("TestString", out var _));
16 |
17 | testSettingsGroup.TestProperty = true;
18 | testSettingsGroup.TestString = "first test value";
19 |
20 | Assert.True((bool) testSettingsGroup.SettingsStore["TestProperty"]);
21 | Assert.Equal("first test value", (string) testSettingsGroup.SettingsStore["TestString"]);
22 |
23 | testSettingsGroup.TestProperty = false;
24 | testSettingsGroup.TestString = "second test value";
25 |
26 | Assert.False((bool) testSettingsGroup.SettingsStore["TestProperty"]);
27 | Assert.Equal("second test value", (string) testSettingsGroup.SettingsStore["TestString"]);
28 |
29 | testSettingsGroup.TestString = null;
30 |
31 | Assert.Null(testSettingsGroup.SettingsStore["TestString"]);
32 | }
33 |
34 | [Fact(DisplayName = "Updates properties based on the what is in store")]
35 | public void UpdatesPropertiesBasedOnWhatIsStored()
36 | {
37 | var testSettingsGroup = new TestSettingsGroup
38 | {
39 | TestProperty = false,
40 | TestString = "test value"
41 | };
42 |
43 | testSettingsGroup.SettingsStore["TestProperty"] = true;
44 | testSettingsGroup.SettingsStore["TestString"] = "new test value";
45 |
46 | testSettingsGroup.UpdatePropertiesFromStore();
47 |
48 | Assert.True(testSettingsGroup.TestProperty);
49 | Assert.Equal("new test value", testSettingsGroup.TestString);
50 | }
51 |
52 | private class TestSettingsGroup : SettingsGroupViewModelBase
53 | {
54 | private bool _testProperty;
55 |
56 | private string _testString;
57 |
58 | public bool TestProperty
59 | {
60 | get => _testProperty;
61 | set => this.RaiseAndSetIfChanged(ref _testProperty, value);
62 | }
63 |
64 | public string TestString
65 | {
66 | get => _testString;
67 | set => this.RaiseAndSetIfChanged(ref _testString, value);
68 | }
69 |
70 | public override string Name => "TestGroup";
71 |
72 | public override string Header => "Test Group";
73 | }
74 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/SettingsManagement/SettingsViewModelTests.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using ImageSort.SettingsManagement;
3 | using Xunit;
4 |
5 | namespace ImageSort.UnitTests.SettingsManagement;
6 |
7 | public class SettingsViewModelTests
8 | {
9 | [Fact(DisplayName = "Can retrieve a specific settings group")]
10 | public void CanRetrieveSpecificSettingsGroup()
11 | {
12 | var firstGroup = new FirstGroupMock();
13 | var secondGroup = new SecondGroupMock();
14 |
15 | var settingsGroups = new SettingsGroupViewModelBase[]
16 | {
17 | firstGroup,
18 | secondGroup
19 | };
20 |
21 | var settingsVM = new SettingsViewModel(settingsGroups);
22 |
23 | Assert.Equal(firstGroup, settingsVM.GetGroup());
24 | Assert.Equal(secondGroup, settingsVM.GetGroup());
25 | }
26 |
27 | [Fact(DisplayName = "Correctly converts settings into a dictionary")]
28 | public void CorrectlyConvertsToDictionary()
29 | {
30 | var firstGroup = new FirstGroupMock();
31 | var secondGroup = new SecondGroupMock();
32 |
33 | var settingsGroups = new SettingsGroupViewModelBase[]
34 | {
35 | firstGroup,
36 | secondGroup
37 | };
38 |
39 | var settingsVM = new SettingsViewModel(settingsGroups);
40 |
41 | var settingsDict = settingsVM.AsDictionary();
42 |
43 | Assert.Equal("fake value 2", settingsDict[secondGroup.Name]["some_setting"]);
44 |
45 | Assert.Equal("fake value 1", settingsDict[firstGroup.Name]["some_setting"]);
46 | }
47 |
48 | [Fact(DisplayName = "Correctly restores settings from a dictionary")]
49 | public void CorrectlyRestoresFromDictionary()
50 | {
51 | var firstGroup = new FirstGroupMock();
52 | var secondGroup = new SecondGroupMock();
53 |
54 | var settingsGroups = new SettingsGroupViewModelBase[]
55 | {
56 | firstGroup,
57 | secondGroup
58 | };
59 |
60 | var settingsVM = new SettingsViewModel(settingsGroups);
61 |
62 | const string fakeValue1 = "some other fake value 1";
63 | const string fakeValue2 = "some other fake value 2";
64 |
65 | settingsVM.RestoreFromDictionary(new Dictionary>
66 | {
67 | {
68 | firstGroup.Name, new Dictionary
69 | {
70 | {"some_setting", fakeValue1}
71 | }
72 | },
73 | {
74 | secondGroup.Name, new Dictionary
75 | {
76 | {"some_setting", fakeValue2}
77 | }
78 | }
79 | });
80 |
81 | Assert.Equal(fakeValue1, firstGroup.SettingsStore["some_setting"]);
82 |
83 | Assert.Equal(fakeValue2, secondGroup.SettingsStore["some_setting"]);
84 | }
85 |
86 | private class FirstGroupMock : SettingsGroupViewModelBase
87 | {
88 | public FirstGroupMock()
89 | {
90 | SettingsStore["some_setting"] = "fake value 1";
91 | }
92 |
93 | public override string Name => "FirstGroup";
94 | public override string Header => "First group";
95 | }
96 |
97 | private class SecondGroupMock : SettingsGroupViewModelBase
98 | {
99 | public SecondGroupMock()
100 | {
101 | SettingsStore["some_setting"] = "fake value 2";
102 | }
103 |
104 | public override string Name => "SecondGroup";
105 | public override string Header => "Second group";
106 | }
107 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/ViewModels/ActionsViewModelTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Reactive;
4 | using System.Reactive.Linq;
5 | using System.Threading.Tasks;
6 | using ImageSort.Actions;
7 | using ImageSort.ViewModels;
8 | using NSubstitute;
9 | using Xunit;
10 |
11 | namespace ImageSort.UnitTests.ViewModels;
12 |
13 | public class ActionsViewModelTest
14 | {
15 | [Fact(DisplayName =
16 | "Executes an action, adds it to the history, allows it to be undone and allows it to be redone and makes the last un-/done visible, also checking if clearing works.")]
17 | public async Task WorksCorrectly()
18 | {
19 | const string actionDisplayName = "Test action display name";
20 |
21 | var actionsVM = new ActionsViewModel();
22 |
23 | var actionMock = Substitute.For();
24 |
25 | actionMock.DisplayName.Returns(actionDisplayName);
26 |
27 | await actionsVM.Execute.Execute(actionMock);
28 |
29 | Assert.Equal(actionDisplayName, actionsVM.LastDone);
30 |
31 | await actionsVM.Undo.Execute();
32 |
33 | await Task.Delay(1); // without this the variables do not get updated for some reason.
34 |
35 | Assert.Equal(actionDisplayName, actionsVM.LastUndone);
36 | Assert.NotEqual(actionDisplayName, actionsVM.LastDone);
37 |
38 | await actionsVM.Redo.Execute();
39 |
40 | await Task.Delay(1); // without this the variables do not get updated for some reason.
41 |
42 | Assert.Equal(actionDisplayName, actionsVM.LastDone);
43 | Assert.NotEqual(actionDisplayName, actionsVM.LastUndone);
44 |
45 | actionMock.Received(2).Act();
46 | actionMock.Received(1).Revert();
47 |
48 | // make sure clearing works
49 | await actionsVM.Clear.Execute();
50 |
51 | Assert.Null(actionsVM.LastDone);
52 | Assert.Null(actionsVM.LastUndone);
53 | }
54 |
55 | [Fact(DisplayName = "Notifies user of errors during acting, undo and redo")]
56 | [SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters",
57 | Justification = "Unit tests do not require localization for exception messages.")]
58 | public async Task NotifiesUserOfErrors()
59 | {
60 | // configure an action that fails when executed
61 | var failingActMock = Substitute.For();
62 |
63 | failingActMock.When(a => a.Act()).Do(x => { throw new Exception("Act doesn't work"); });
64 |
65 | // configure an action that fails on reversion (on undo)
66 | var failingRevertMock = Substitute.For();
67 |
68 | failingRevertMock.When(a => a.Revert()).Do(x => { throw new Exception("Revert doesn't work"); });
69 |
70 | // configure an action that fails on the second time being executed (on redo)
71 | var failingActOnUndoMock = Substitute.For();
72 |
73 | var timesCalled = 0;
74 |
75 | failingActOnUndoMock.When(a => a.Act()).Do(x =>
76 | {
77 | timesCalled = timesCalled switch
78 | {
79 | 0 => 1,
80 | _ => throw new Exception("Act doesn't work")
81 | };
82 | });
83 |
84 | var actionsVM = new ActionsViewModel();
85 |
86 | var timesFailureWasReported = 0;
87 |
88 | actionsVM.NotifyUserOfError.RegisterHandler(ic =>
89 | {
90 | timesFailureWasReported++;
91 |
92 | ic.SetOutput(Unit.Default);
93 | });
94 |
95 | // fails on execute
96 | await actionsVM.Execute.Execute(failingActMock);
97 |
98 | // fails on undo
99 | await actionsVM.Execute.Execute(failingRevertMock);
100 | await actionsVM.Undo.Execute();
101 |
102 | // fails on redo
103 | await actionsVM.Execute.Execute(failingActOnUndoMock);
104 | await actionsVM.Undo.Execute();
105 | await actionsVM.Redo.Execute();
106 |
107 | Assert.Equal(3, timesFailureWasReported);
108 | }
109 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/ViewModels/FolderTreeItemViewModelTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Reactive.Linq;
6 | using System.Threading.Tasks;
7 | using ImageSort.FileSystem;
8 | using ImageSort.ViewModels;
9 | using NSubstitute;
10 | using ReactiveUI;
11 | using Xunit;
12 |
13 | namespace ImageSort.UnitTests.ViewModels;
14 |
15 | public class FolderTreeItemViewModelTests
16 | {
17 | [Fact(DisplayName = "Obtains the child folders of the current folder correctly")]
18 | public void ObtainsChildrenCorrectly()
19 | {
20 | const string path = @"C:\current folder";
21 |
22 | var resultingPaths =
23 | new[]
24 | {
25 | @"\folder 1",
26 | @"\folder 2",
27 | @"\folder 3"
28 | }
29 | .Select(sub => path + sub); // make the (mock) subfolders absolute paths.
30 |
31 | var fsMock = Substitute.For();
32 |
33 | fsMock.GetSubFolders(path).Returns(resultingPaths);
34 |
35 | var folderTreeItem = new FolderTreeItemViewModel(fsMock, backgroundScheduler: RxApp.MainThreadScheduler)
36 | {
37 | Path = path,
38 | IsVisible = true
39 | };
40 |
41 | fsMock.Received().GetSubFolders(path);
42 |
43 | while (folderTreeItem.Children.Count == 0) {}
44 |
45 | Assert.Equal(resultingPaths, folderTreeItem.Children.Select(vm => vm.Path).ToArray());
46 | }
47 |
48 | [Fact(DisplayName =
49 | "Handles an tried access to an unauthorized file (UnauthorizedAccessException) gracefully.")]
50 | public void HandlesUnauthorizedAccessExceptionGracefully()
51 | {
52 | const string pathToUnauthorisedFolder = @"C:\UnauthorizedFolder";
53 |
54 | var fsMock = Substitute.For();
55 |
56 | fsMock.GetSubFolders(pathToUnauthorisedFolder).Returns(x => throw new UnauthorizedAccessException());
57 |
58 | var folderTreeItem = new FolderTreeItemViewModel(fsMock)
59 | {
60 | Path = pathToUnauthorisedFolder
61 | };
62 | }
63 |
64 | [Fact(DisplayName = "Can create folders and adds them to the children.")]
65 | public async Task CanCreateFolders()
66 | {
67 | const string currentFolder = @"C:\current_folder";
68 | var subfolders = new[]
69 | {
70 | "sub1", "sub2", "sub3"
71 | }.Select(s => Path.Combine(currentFolder, s));
72 | const string addedFolder = currentFolder + @"\new_ sub";
73 | var result = new List();
74 | result.AddRange(subfolders);
75 | result.Add(addedFolder);
76 |
77 | var fsMock = Substitute.For();
78 |
79 | fsMock.GetSubFolders(currentFolder).Returns(subfolders);
80 | fsMock.CreateFolder(addedFolder);
81 |
82 | var folderTreeItem = new FolderTreeItemViewModel(fsMock, backgroundScheduler: RxApp.MainThreadScheduler)
83 | {
84 | Path = currentFolder,
85 | IsVisible = true
86 | };
87 |
88 | await folderTreeItem.CreateFolder.Execute(addedFolder);
89 | // verify that no second folder is created when a folder already exists
90 | await folderTreeItem.CreateFolder.Execute(addedFolder);
91 |
92 | fsMock.Received().CreateFolder(addedFolder);
93 |
94 | Assert.Equal(result.OrderBy(p => p), folderTreeItem.Children.Select(f => f.Path).OrderBy(p => p));
95 | }
96 |
97 | [Fact(DisplayName = "Do not load subfolders when not visible")]
98 | public void DoNotLoadSubfoldersWhenNotVisible()
99 | {
100 | const string path = @"C:\current folder";
101 |
102 | var resultingPaths =
103 | new[]
104 | {
105 | @"\folder 1",
106 | @"\folder 2",
107 | @"\folder 3"
108 | }
109 | .Select(sub => path + sub); // make the (mock) subfolders absolute paths.
110 |
111 | var fsMock = Substitute.For();
112 |
113 | fsMock.GetSubFolders(path).Returns(resultingPaths);
114 |
115 | var folderTreeItem = new FolderTreeItemViewModel(fsMock, backgroundScheduler: RxApp.MainThreadScheduler)
116 | {
117 | Path = path,
118 | IsVisible = false
119 | };
120 |
121 | fsMock.DidNotReceive().GetSubFolders(path);
122 |
123 | Assert.Empty(folderTreeItem.Children.Select(vm => vm.Path).ToArray());
124 | }
125 | }
--------------------------------------------------------------------------------
/tests/ImageSort.UnitTests/ViewModels/MetadataViewModelTests.cs:
--------------------------------------------------------------------------------
1 | using ImageSort.FileSystem;
2 | using ImageSort.ViewModels.Metadata;
3 | using NSubstitute;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using Xunit;
10 |
11 | namespace ImageSort.UnitTests.ViewModels;
12 |
13 | public class MetadataViewModelTests
14 | {
15 | private readonly MetadataViewModel metadataViewModel;
16 |
17 | private readonly IFileSystem fileSystem = Substitute.For();
18 | private readonly IMetadataExtractor metadataExtractor = Substitute.For();
19 |
20 | public MetadataViewModelTests()
21 | {
22 | metadataViewModel = new(metadataExtractor, fileSystem, new MetadataSectionViewModelFactory(new MetadataFieldViewModelFactory()));
23 | }
24 |
25 | [Fact(DisplayName = "MetadataViewModel should extract metadata from image")]
26 | public void ExtractsMetadataWhenPathIsSet()
27 | {
28 | // setup mocks and view model
29 | string thisFileExists = "C:\\test.jpg";
30 | var extractableMetadata = new Dictionary>(){
31 | { "test", new Dictionary(){
32 | { "test", "test" },
33 | }
34 | }
35 | };
36 |
37 | fileSystem.FileExists(thisFileExists).Returns(true);
38 |
39 | metadataExtractor.Extract(thisFileExists).Returns(extractableMetadata);
40 |
41 | // this should cause the extraction of metadata
42 | metadataViewModel.ImagePath = thisFileExists;
43 |
44 | Assert.Equal(MetadataResultType.Success, metadataViewModel.Metadata.Type);
45 | Assert.Equal(extractableMetadata, metadataViewModel.Metadata.Metadata);
46 |
47 | fileSystem.Received().FileExists(thisFileExists);
48 | metadataExtractor.Received().Extract(thisFileExists);
49 | }
50 |
51 | [Fact(DisplayName = "MetadataViewModel should not extract metadata from image when file does not exist")]
52 | public void DoesNotExtractMetadataWhenPathIsSetAndFileDoesNotExist()
53 | {
54 | // setup mocks and view model
55 | string thisFileDoesNotExist = "C:\\test2.jpg";
56 |
57 | fileSystem.FileExists(thisFileDoesNotExist).Returns(false);
58 |
59 | metadataViewModel.ImagePath = thisFileDoesNotExist;
60 |
61 | Assert.Equal(MetadataResultType.FileDoesNotExist, metadataViewModel.Metadata.Type);
62 | Assert.Null(metadataViewModel.Metadata.Metadata);
63 |
64 | fileSystem.Received().FileExists(thisFileDoesNotExist);
65 | metadataExtractor.DidNotReceive().Extract(thisFileDoesNotExist);
66 | }
67 |
68 | [Fact(DisplayName = "Correctly reports unhandled exceptions that occur when trying to extract metadata")]
69 | public void CorrectlyReportsIssuesWithTheExtractionOfMetadata()
70 | {
71 | // setup mocks and view model
72 | string thisFileHasInvalidMetadata = "C:\\test3.jpg";
73 | Exception invalidMetadata = new("Invalid metadata could not be loaded");
74 |
75 | fileSystem.FileExists(thisFileHasInvalidMetadata).Returns(true);
76 |
77 | metadataExtractor.Extract(thisFileHasInvalidMetadata).Returns(x => throw invalidMetadata);
78 |
79 | metadataViewModel.ImagePath = thisFileHasInvalidMetadata;
80 |
81 | Assert.Equal(MetadataResultType.UnexpectedError, metadataViewModel.Metadata.Type);
82 | Assert.Null(metadataViewModel.Metadata.Metadata);
83 | Assert.Equal(invalidMetadata, metadataViewModel.Metadata.Exception);
84 |
85 | fileSystem.Received().FileExists(thisFileHasInvalidMetadata);
86 | metadataExtractor.Received().Extract(thisFileHasInvalidMetadata);
87 | }
88 |
89 | [Fact(DisplayName = "Correctly creates metadata sections from extracted metadata")]
90 | public void CorrectlyCreatesMetadataSectionsFromExtractedMetadata()
91 | {
92 | // setup mocks and view model
93 | string thisFileHasMetadata = "C:\\test4.jpg";
94 | var extractableMetadata = new Dictionary>(){
95 | { "test", new Dictionary(){
96 | { "test", "test" },
97 | }
98 | }
99 | };
100 |
101 | fileSystem.FileExists(thisFileHasMetadata).Returns(true);
102 |
103 | metadataExtractor.Extract(thisFileHasMetadata).Returns(extractableMetadata);
104 |
105 | metadataViewModel.ImagePath = thisFileHasMetadata;
106 |
107 | Assert.Equal(1, metadataViewModel.SectionViewModels.Count());
108 | Assert.Equal("test", metadataViewModel.SectionViewModels.First().Title);
109 | Assert.Equal(extractableMetadata["test"], metadataViewModel.SectionViewModels.First().Fields);
110 |
111 | fileSystem.Received().FileExists(thisFileHasMetadata);
112 | metadataExtractor.Received().Extract(thisFileHasMetadata);
113 | }
114 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/AppCollection.cs:
--------------------------------------------------------------------------------
1 | using Xunit;
2 |
3 | namespace ImageSort.WPF.UiTests;
4 |
5 | [CollectionDefinition("App collection", DisableParallelization = true)]
6 | public class AppCollection : ICollectionFixture
7 | {
8 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/AppFixture.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FlaUI.Core;
3 | using FlaUI.Core.AutomationElements;
4 | using FlaUI.UIA3;
5 | using Debug = System.Diagnostics.Debug;
6 |
7 | namespace ImageSort.WPF.UiTests;
8 |
9 | public class AppFixture : IDisposable
10 | {
11 | public AppFixture()
12 | {
13 | (CurrentPath, App, Automation, MainWindow) = SetupTeardownHelper.Setup();
14 |
15 | if (MainWindow == null || App == null || Automation == null || CurrentPath == null)
16 | Debug.WriteLine("Could not setup app fixture: One of the instances returned by setup is null");
17 | }
18 |
19 | private string CurrentPath { get; }
20 | public Application App { get; }
21 | public UIA3Automation Automation { get; }
22 | public Window MainWindow { get; }
23 |
24 | public void Dispose()
25 | {
26 | SetupTeardownHelper.TearDown(CurrentPath, App, Automation);
27 | }
28 |
29 | internal void Deconstruct(out string currentPath, out Application app, out UIA3Automation automation,
30 | out Window mainWindow)
31 | {
32 | currentPath = CurrentPath;
33 | app = App;
34 | automation = Automation;
35 | mainWindow = MainWindow;
36 | }
37 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/ControlHelper.cs:
--------------------------------------------------------------------------------
1 | using FlaUI.Core;
2 | using FlaUI.Core.AutomationElements;
3 |
4 | namespace ImageSort.WPF.UiTests;
5 |
6 | internal static class ControlHelper
7 | {
8 | public static Application App { get; set; }
9 | public static Window MainWindow { get; set; }
10 |
11 | public static void ClickButton(this AutomationElement element, string automationId)
12 | {
13 | element.FindFirstDescendant(cf => cf.ByAutomationId(automationId))?.AsButton().Click();
14 |
15 | App.WaitWhileBusy();
16 | MainWindow.WaitUntilClickable();
17 | }
18 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/FileActionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using FlaUI.Core;
4 | using FlaUI.Core.AutomationElements;
5 | using FlaUI.Core.Input;
6 | using FlaUI.Core.WindowsAPI;
7 | using Xunit;
8 |
9 | [assembly: CollectionBehavior(DisableTestParallelization = true)]
10 |
11 | namespace ImageSort.WPF.UiTests;
12 |
13 | [Collection("App collection")]
14 | public class FileActionsTests
15 | {
16 | private readonly Application app;
17 | private readonly string currentPath;
18 | private readonly Window mainWindow;
19 |
20 | public FileActionsTests(AppFixture appFixture)
21 | {
22 | (currentPath, app, _, mainWindow) = appFixture;
23 | }
24 |
25 | [Fact(DisplayName = "Can move image, undo and redo")]
26 | public void CanMoveImages()
27 | {
28 | var oldLocation = mainWindow.GetSelectedImage();
29 | var newLocation = Path.Combine(Directory.GetDirectories(currentPath)[0], Path.GetFileName(oldLocation));
30 |
31 | Assert.True(File.Exists(oldLocation));
32 | Assert.False(File.Exists(newLocation));
33 |
34 | // select folder
35 | Keyboard.Press(VirtualKeyShort.KEY_D);
36 |
37 | Keyboard.Press(VirtualKeyShort.KEY_S);
38 |
39 | app.WaitWhileBusy();
40 |
41 | var selectedImage = mainWindow.GetSelectedImage();
42 |
43 | // move image
44 | mainWindow.ClickButton("Move");
45 |
46 | app.WaitWhileBusy();
47 |
48 | Assert.False(File.Exists(oldLocation));
49 | Assert.True(File.Exists(newLocation));
50 |
51 | // undo
52 | mainWindow.ClickButton("Undo");
53 |
54 | // make sure the image is not added back twice, for example by the FileSystemWatcher in addition to the code itself
55 | Assert.Single(mainWindow.GetImages().Where(i => i == selectedImage));
56 |
57 | Assert.True(File.Exists(oldLocation));
58 | Assert.False(File.Exists(newLocation));
59 |
60 | // redo
61 | mainWindow.ClickButton("Redo");
62 |
63 | Assert.False(File.Exists(oldLocation));
64 | Assert.True(File.Exists(newLocation));
65 |
66 | // clean-up
67 | mainWindow.ClickButton("Undo");
68 |
69 | // unselect folder
70 | Keyboard.Press(VirtualKeyShort.KEY_A);
71 | Keyboard.Press(VirtualKeyShort.KEY_A);
72 | }
73 |
74 | [Fact(DisplayName = "Can delete images")]
75 | public void CanDeleteImages()
76 | {
77 | var file = mainWindow.GetSelectedImage();
78 |
79 | Assert.True(File.Exists(file));
80 |
81 | // delete image
82 | mainWindow.ClickButton("Delete");
83 |
84 | Assert.False(File.Exists(file));
85 |
86 | // clean-up
87 | mainWindow.ClickButton("Undo");
88 |
89 | // make sure the image is not added back twice, for example by the FileSystemWatcher in addition to the code itself
90 | Assert.Single(mainWindow.GetImages().Where(i => i == file));
91 |
92 | Assert.True(File.Exists(file));
93 | }
94 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/FolderActionsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using FlaUI.Core;
4 | using FlaUI.Core.AutomationElements;
5 | using FlaUI.Core.Input;
6 | using FlaUI.Core.Tools;
7 | using FlaUI.Core.WindowsAPI;
8 | using Xunit;
9 |
10 | namespace ImageSort.WPF.UiTests;
11 |
12 | [Collection("App collection")]
13 | public class FolderActionsTests
14 | {
15 | private readonly Application app;
16 | private readonly string currentPath;
17 | private readonly Window mainWindow;
18 |
19 | public FolderActionsTests(AppFixture appFixture)
20 | {
21 | (currentPath, app, _, mainWindow) = appFixture;
22 | }
23 |
24 | [Fact(DisplayName = "Can create folders and reacts to its deletion")]
25 | public void CanCreateFolders()
26 | {
27 | const string newFolderName = "new folder";
28 | var newFolderPath = Path.Combine(currentPath, newFolderName);
29 |
30 | mainWindow.ClickButton("CreateFolder");
31 |
32 | Keyboard.Type(newFolderName);
33 | Keyboard.Press(VirtualKeyShort.ENTER);
34 |
35 | app.WaitWhileBusy();
36 | mainWindow.WaitUntilClickable();
37 |
38 | Assert.True(Retry.WhileFalse(() => Directory.Exists(newFolderPath), timeout: TimeSpan.FromSeconds(5), interval: TimeSpan.FromMilliseconds(50)).Result);
39 |
40 | Assert.True(Directory.Exists(newFolderPath));
41 |
42 | // clean-up
43 | Directory.Delete(newFolderPath);
44 | }
45 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/ImageSort.WPF.UiTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0-windows
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | <_CopyItems Include="$(ProjectDir)\..\..\src\ImageSort.WPF\bin\$(Configuration)\net8.0-windows\*.*" />
30 |
31 |
32 |
33 |
34 |
35 |
36 | PreserveNewest
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/Subfolder 1/Subsubfolder/mock in subsubfolder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/Subfolder 1/Subsubfolder/mock in subsubfolder.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/Subfolder 1/mock in subfolder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/Subfolder 1/mock in subfolder.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/Subfolder 2/mock in subfolder 2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/Subfolder 2/mock in subfolder 2.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 1.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 2.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 3.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 4.png
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 5.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 6.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/MockState/mock 7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lolle2000la/Image-Sort/27156aa0069ad74af8ea5a83b95d0d4958fc2e82/tests/ImageSort.WPF.UiTests/MockState/mock 7.jpg
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/SearchTests.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Linq;
3 | using FlaUI.Core.AutomationElements;
4 | using Xunit;
5 |
6 | namespace ImageSort.WPF.UiTests;
7 |
8 | [Collection("App collection")]
9 | public class SearchTests
10 | {
11 | private readonly Window mainWindow;
12 |
13 | public SearchTests(AppFixture appFixture)
14 | {
15 | (_, _, _, mainWindow) = appFixture;
16 | }
17 |
18 | [Fact(DisplayName = "Filters out images correctly")]
19 | public void FiltersOutImagesCorrectly()
20 | {
21 | var search = mainWindow.FindFirstDescendant(cf => cf.ByAutomationId("SearchTerm")).AsTextBox();
22 | search.Focus();
23 | search.Text = ".jpg";
24 |
25 | var images = mainWindow.GetImages()
26 | .Select(n => Path.GetFileName(n));
27 |
28 | Assert.DoesNotContain("mock 4.png", images);
29 | }
30 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/SetupTeardownHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using FlaUI.Core;
5 | using FlaUI.Core.AutomationElements;
6 | using FlaUI.Core.Tools;
7 | using FlaUI.UIA3;
8 |
9 | namespace ImageSort.WPF.UiTests;
10 |
11 | internal static class SetupTeardownHelper
12 | {
13 | public static (string, Application, UIA3Automation, Window) Setup()
14 | {
15 | var currentPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Temporary State",
16 | Guid.NewGuid().ToString());
17 |
18 | CopyFolder(Path.GetFullPath("MockState"), currentPath);
19 |
20 | // ensure previous config file is removed
21 | if (File.Exists(@".\ui_test_config.json")) File.Delete(@".\ui_test_config.json");
22 |
23 | var procInfo = new ProcessStartInfo("Image Sort.exe", $"\"{currentPath}\"");
24 |
25 | // ensures the app is not affected by and does not affect global config file
26 | procInfo.EnvironmentVariables.Add("UI_TEST", "true");
27 |
28 | var app = Application.Launch(procInfo);
29 | var automation = new UIA3Automation();
30 |
31 | app.WaitWhileBusy();
32 |
33 | var mainWindow = Retry.WhileNull(() =>
34 | {
35 | var allWindows = app.GetAllTopLevelWindows(automation);
36 |
37 | if (allWindows.Length > 0) return allWindows[0];
38 |
39 | return null;
40 | }, TimeSpan.FromSeconds(30), null, true).Result;
41 |
42 | app.WaitWhileBusy();
43 | app.WaitWhileMainHandleIsMissing();
44 | mainWindow.WaitUntilClickable();
45 |
46 | mainWindow.Focus();
47 |
48 | while (currentPath == null || app == null || automation == null || mainWindow == null)
49 | {
50 | }
51 |
52 | ControlHelper.App = app;
53 | ControlHelper.MainWindow = mainWindow;
54 |
55 | return (currentPath, app, automation, mainWindow);
56 | }
57 |
58 | public static void TearDown(string currentPath, Application app, UIA3Automation automation)
59 | {
60 | app.Close();
61 | automation.Dispose();
62 | app.Dispose();
63 |
64 | Directory.Delete(currentPath, true);
65 | }
66 |
67 | private static void CopyFolder(string sourceFolder, string destFolder)
68 | {
69 | if (!Directory.Exists(destFolder)) Directory.CreateDirectory(destFolder);
70 |
71 | var files = Directory.GetFiles(sourceFolder);
72 |
73 | foreach (var file in files)
74 | {
75 | var name = Path.GetFileName(file);
76 | var dest = Path.Combine(destFolder, name);
77 | File.Copy(file, dest);
78 | }
79 |
80 | var folders = Directory.GetDirectories(sourceFolder);
81 |
82 | foreach (var folder in folders)
83 | {
84 | var name = Path.GetFileName(folder);
85 | var dest = Path.Combine(destFolder, name);
86 | CopyFolder(folder, dest);
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/tests/ImageSort.WPF.UiTests/WindowHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using FlaUI.Core.AutomationElements;
4 |
5 | namespace ImageSort.WPF.UiTests;
6 |
7 | internal static class WindowHelper
8 | {
9 | private static ListBox GetImagesBox(this Window mainWindow)
10 | {
11 | return mainWindow
12 | .FindFirstDescendant(cf => cf.ByAutomationId("Images"))
13 | .FindFirstDescendant(cf => cf.ByAutomationId("Images")).AsListBox();
14 | }
15 |
16 | public static IEnumerable GetImages(this Window mainWindow)
17 | {
18 | return mainWindow
19 | .GetImagesBox()
20 | .FindAllChildren()
21 | .Select(e => e.AsListBoxItem())
22 | .Select(e => e.Name);
23 | }
24 |
25 | public static string GetSelectedImage(this Window mainWindow)
26 | {
27 | return mainWindow
28 | .GetImagesBox()
29 | .SelectedItem.Name;
30 | }
31 | }
--------------------------------------------------------------------------------