├── 7z_x64.dll
├── 7z_x86.dll
├── .editorconfig
├── Resources
├── ZipImageViewer.ico
└── Localization.zh.xaml
├── Properties
├── Settings.settings
├── Settings.Designer.cs
└── AssemblyInfo.cs
├── App.config
├── packages.config
├── UserControls
├── BubbleMessage.xaml.cs
├── BubbleMessage.xaml
├── BorderlessWindow.cs
├── PaddedGrid.cs
├── BorderlessWindow.xaml
├── DpiImage.cs
├── AppleStyleScrollBar.xaml
├── StylesAndAnimations.xaml
├── Thumbnail.xaml.cs
├── RoundedWindow.xaml
└── Thumbnail.xaml
├── azure-pipelines.yml
├── InputWindow.xaml
├── ZipImageViewer.sln
├── README.md
├── .gitattributes
├── Helpers
├── TableHelper.cs
├── RegistryHelpers.cs
├── CacheHelper.cs
├── NativeHelpers.cs
├── ObjectInfo.cs
└── SQLiteHelper.cs
├── App.xaml
├── BlockWindow.xaml
├── BlockWindow.xaml.cs
├── app.manifest
├── ViewWindow.xaml.notworking
├── ContextMenuWindow.xaml.cs
├── .gitignore
├── SettingsWindow.xaml.cs
├── InputWindow.xaml.cs
├── SlideshowWindow.xaml
├── SlideshowWindow.xaml.cs
├── ViewWindow.xaml.cs.notworking
└── MainWindow.xaml
/7z_x64.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/changbowen/ZipImageViewer/HEAD/7z_x64.dll
--------------------------------------------------------------------------------
/7z_x86.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/changbowen/ZipImageViewer/HEAD/7z_x86.dll
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # IDE1006: Naming Styles
4 | dotnet_diagnostic.IDE1006.severity = none
5 |
--------------------------------------------------------------------------------
/Resources/ZipImageViewer.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/changbowen/ZipImageViewer/HEAD/Resources/ZipImageViewer.ico
--------------------------------------------------------------------------------
/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/UserControls/BubbleMessage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Threading;
2 | using System.Threading.Tasks;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using System.Windows.Media.Animation;
6 | using System.Windows.Media.Imaging;
7 | using System.Windows.Navigation;
8 | using System.Windows.Shapes;
9 |
10 | namespace ZipImageViewer
11 | {
12 | public partial class BubbleMessage : UserControl
13 | {
14 | public string Message
15 | {
16 | get { return (string)GetValue(MessageProperty); }
17 | set { SetValue(MessageProperty, value); }
18 | }
19 | public static readonly DependencyProperty MessageProperty =
20 | DependencyProperty.Register("Message", typeof(string), typeof(BubbleMessage), new PropertyMetadata(""));
21 |
22 |
23 | public BubbleMessage() {
24 | Opacity = 0d;
25 | InitializeComponent();
26 | }
27 |
28 | public void Show(string message) {
29 | Message = message;
30 | BeginStoryboard((Storyboard)FindResource("SB_FadeInThenOut"));
31 | }
32 |
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace ZipImageViewer.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/UserControls/BubbleMessage.xaml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # .NET Desktop
2 | # Build and run tests for .NET Desktop or Windows classic desktop solutions.
3 | # Add steps that publish symbols, save build artifacts, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net
5 |
6 | trigger:
7 | branches:
8 | include:
9 | - master
10 | paths:
11 | exclude:
12 | - README.md
13 | - azure-pipelines.yml
14 |
15 | pool:
16 | vmImage: 'windows-latest'
17 |
18 | variables:
19 | solution: '**/*.sln'
20 | buildPlatform: 'x64'
21 | buildConfiguration: 'Release'
22 |
23 | steps:
24 | - task: AssembyInfoReader@2
25 | inputs:
26 | searchPattern: '**\AssemblyInfo.cs'
27 |
28 | - task: NuGetToolInstaller@1
29 |
30 | - task: NuGetCommand@2
31 | inputs:
32 | restoreSolution: '$(solution)'
33 |
34 | - task: VSBuild@1
35 | inputs:
36 | solution: '$(solution)'
37 | platform: '$(buildPlatform)'
38 | configuration: '$(buildConfiguration)'
39 | clean: true
40 |
41 | - task: ArchiveFiles@2
42 | inputs:
43 | rootFolderOrFile: '$(System.DefaultWorkingDirectory)/bin/x64/Release'
44 | includeRootFolder: false
45 | archiveType: 'zip'
46 | archiveFile: '$(System.DefaultWorkingDirectory)/$(Build.Repository.Name).zip'
47 | replaceExistingArchive: true
48 |
49 | - task: GitHubRelease@1
50 | inputs:
51 | gitHubConnection: 'github connection azure devops'
52 | repositoryName: '$(Build.Repository.Name)'
53 | action: 'create'
54 | target: '$(Build.SourceVersion)'
55 | tagSource: 'userSpecifiedTag'
56 | tag: 'v$(AssemblyInfo.AssemblyVersion.Major).$(AssemblyInfo.AssemblyVersion.Minor).$(AssemblyInfo.AssemblyVersion.Build)'
57 | assets: '$(System.DefaultWorkingDirectory)/$(Build.Repository.Name).zip'
58 | changeLogCompareToRelease: 'lastFullRelease'
59 | changeLogType: 'commitBased'
--------------------------------------------------------------------------------
/InputWindow.xaml:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/ZipImageViewer.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.3.32929.385
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZipImageViewer", "ZipImageViewer.csproj", "{132432D4-022B-4A90-9DF8-EC3A4E94D79E}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{81E1F9F5-361A-41B5-A9BD-BB4295C798D9}"
9 | ProjectSection(SolutionItems) = preProject
10 | .editorconfig = .editorconfig
11 | EndProjectSection
12 | EndProject
13 | Global
14 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
15 | Debug|Any CPU = Debug|Any CPU
16 | Debug|x64 = Debug|x64
17 | Debug|x86 = Debug|x86
18 | Release|Any CPU = Release|Any CPU
19 | Release|x64 = Release|x64
20 | Release|x86 = Release|x86
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|x64.ActiveCfg = Debug|x64
26 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|x64.Build.0 = Debug|x64
27 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|x86.ActiveCfg = Debug|x86
28 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Debug|x86.Build.0 = Debug|x86
29 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|Any CPU.Build.0 = Release|Any CPU
31 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|x64.ActiveCfg = Release|x64
32 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|x64.Build.0 = Release|x64
33 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|x86.ActiveCfg = Release|x86
34 | {132432D4-022B-4A90-9DF8-EC3A4E94D79E}.Release|x86.Build.0 = Release|x86
35 | EndGlobalSection
36 | GlobalSection(SolutionProperties) = preSolution
37 | HideSolutionNode = FALSE
38 | EndGlobalSection
39 | GlobalSection(ExtensibilityGlobals) = postSolution
40 | SolutionGuid = {AAE22DE7-C7EF-40DF-8B06-68913CE8894E}
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("ZipImageViewer")]
11 | [assembly: AssemblyDescription("")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("Carl Chang")]
14 | [assembly: AssemblyProduct("ZipImageViewer")]
15 | [assembly: AssemblyCopyright("Copyright © 2019")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("1.2.10.0")]
55 | [assembly: AssemblyFileVersion("1.2.10.0")]
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ZipImageViewer
2 | Minimalistic image viewer that introduces no "DPI blurriness", browses password-protected archives on the fly and has a slideshow feature much like [Ken-Burns-Slideshow](https://github.com/changbowen/Ken-Burns-Slideshow).
3 |
4 | Need 64bit Windows and .Net Framework 4.7.2 (included in Windows 10 April 2018 Update)
5 |
6 | [](https://changb0wen.visualstudio.com/ZipImageViewer/_build/latest?definitionId=4&branchName=master)
7 |
8 | 
9 |
10 | ## Features
11 | - Portable and no installation needed.
12 | - Works with all archives supported by 7z.
13 | - Tries a configured list of fallback passwords on any new encrypted archives.
14 | - Keeps a map of the password used for each archive to reduce unnecessary trials and errors.
15 | - Support EXIF orientation metadata.
16 | - DpiImage control.
17 | - 1:1 rendering of images by overriding WPF's device-independent auto-scaling on Image control.
18 | - No blurriness caused by incorrect position (X / Y translation).
19 | - PerMonitor DPI awareness.
20 | - Thumbnail cache for instant loading.
21 | - "Picture Wall"
22 | - A slideshow feature with 3 transition effects.
23 | - English and Simplified Chinese UI language.
24 |
25 | ### Using Fallback Passwords
26 | 
27 |
28 | ### Command Line Arguments
29 | To open folders or files at startup, pass the path as an argument to the executable. E.g. `ZipImageViewer.exe "C:\somefolderorfile"`
30 |
31 | To run slideshow at startup, include `-slideshow` as an argument. E.g. `ZipImageViewer.exe -slideshow "C:\somefolder"`
32 |
33 | ## What's Wrong with Photos
34 | **Be sure to scale the page properly to see the difference.**
35 | **For example if your display is set to 125% scaling, you need to scale the webpage to 80%.**
36 | **And this is actually a chroma test image so you also want to use RGB instead of YCbCr color format on your monitor.**
37 |
38 | Microsoft Photos 2019.19071.17920.0 unable to handle proper rendering after zooming and panning.
39 | (Come on Microsoft... Is that the best you can do? And please let me scale to 100% by double clicking!)
40 |
41 |
42 | ZipImageViewer is always true to the image at 100% scaling.
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/Helpers/TableHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace ZipImageViewer
10 | {
11 | public static class TableHelper
12 | {
13 | public enum Table
14 | { Thumbs, MappedPasswords, FallbackPasswords }
15 |
16 | public enum Column
17 | {
18 | BasePath, SubPath, DecodeWidth, DecodeHeight, ThumbData,//Thumbs table
19 | Path, Password, PasswordHash,//MappedPasswords and FallbackPasswords table
20 | }
21 |
22 | public class TableInfo
23 | {
24 | public readonly string Name;
25 | public string FileName => Name + @".db";
26 | public string FullPath => Path.Combine(Setting.DatabaseDir, FileName);
27 | public readonly object Lock = new object();
28 | public TableInfo(Table table) {
29 | Name = table.ToString();
30 | }
31 | }
32 |
33 | ///
34 | /// When primKeyData is not found in the first primary key column, add to the table with new values.
35 | /// Otherwise update existing one.
36 | ///
37 | public static void UpdateDataTable(this DataTable dt, object primKeyData, string columnName, object newData) {
38 | var row = dt.Rows.Find(primKeyData);
39 | if (row == null) {
40 | var newRow = dt.NewRow();
41 | newRow[dt.PrimaryKey[0]] = primKeyData;
42 | newRow[columnName] = newData;
43 | dt.Rows.Add(newRow);
44 | }
45 | else {
46 | row[columnName] = newData;
47 | }
48 | }
49 |
50 | ///
51 | /// Encrypt password values if not already.
52 | ///
53 | public static void UpdatePasswordHash(object sender, DataColumnChangeEventArgs e) {
54 | //if (!(DataRowAction.Add | DataRowAction.Change | DataRowAction.ChangeCurrentAndOriginal | DataRowAction.ChangeOriginal).HasFlag(e.Action)) return;
55 | if (e.Column.ColumnName != nameof(Column.Password) || !e.Row.Table.Columns.Contains(nameof(Column.PasswordHash)) || !(e.ProposedValue is string rawPwd)) return;
56 |
57 | var pwdHash = EncryptionHelper.GetHash(EncryptionHelper.TryDecrypt(rawPwd).Output);
58 | if (pwdHash == null) return;
59 | //if (!pwd.WasEncrypted) //assume input is encrypted already
60 | // e.ProposedValue = pwd.Encrypted;
61 | if (e.Row[nameof(Column.PasswordHash)].ToString() != pwdHash) //update hash when password changes
62 | e.Row[nameof(Column.PasswordHash)] = pwdHash;
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/App.xaml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | #FF333333
14 | #FF1F1F1F
15 | #FF444444
16 | #FF6E6E6E
17 | LightGray
18 | #FFA6A6A6
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/BlockWindow.xaml:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
35 |
37 |
39 |
40 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/BlockWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using static ZipImageViewer.Helpers;
8 |
9 | namespace ZipImageViewer
10 | {
11 | public partial class BlockWindow : RoundedWindow
12 | {
13 | public int Percentage {
14 | get { return (int)GetValue(PercentageProperty); }
15 | set { SetValue(PercentageProperty, value); }
16 | }
17 | public static readonly DependencyProperty PercentageProperty =
18 | DependencyProperty.Register("Percentage", typeof(int), typeof(BlockWindow), new PropertyMetadata(-1));
19 |
20 | public string MessageTitle {
21 | get { return (string)GetValue(MessageTitleProperty); }
22 | set { SetValue(MessageTitleProperty, value); }
23 | }
24 | public static readonly DependencyProperty MessageTitleProperty =
25 | DependencyProperty.Register("MessageTitle", typeof(string), typeof(BlockWindow), new PropertyMetadata(""));
26 |
27 | public string MessageBody {
28 | get { return (string)GetValue(MessageBodyProperty); }
29 | set { SetValue(MessageBodyProperty, value); }
30 | }
31 | public static readonly DependencyProperty MessageBodyProperty =
32 | DependencyProperty.Register("MessageBody", typeof(string), typeof(BlockWindow), new PropertyMetadata(GetRes("msg_PleaseWait")));
33 |
34 |
35 | ///
36 | /// Need to set the CancellationTokenSource to null in Work for window to close properly.
37 | ///
38 | public Action Work { get; set; }
39 | public bool AutoClose { get; private set; } = false;
40 | internal CancellationTokenSource tknSrc_Work;
41 | internal readonly object lock_Work = new object();
42 |
43 | ///
44 | /// If set, and its owned windows will be disabled until BlockWindow is closed.
45 | ///
46 | public BlockWindow(Window owner = null, bool autoClose = false) {
47 | InitializeComponent();
48 |
49 | Owner = owner;
50 | AutoClose = autoClose;
51 | ButtonCloseVisible = !autoClose;
52 | }
53 |
54 | private void BlockWin_Loaded(object sender, RoutedEventArgs e) {
55 | setOwnerState(false);
56 |
57 | var threadStart = new ThreadStart(Work);
58 | if (AutoClose) threadStart += () => Dispatcher.Invoke(Close);
59 | var thrd = new Thread(threadStart) { IsBackground = true };
60 | thrd.Start();
61 | }
62 |
63 | private async void BlockWin_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
64 | tknSrc_Work?.Cancel();
65 | while (tknSrc_Work != null) {
66 | await Task.Delay(200);
67 | }
68 |
69 | setOwnerState(true);
70 | }
71 |
72 | private void setOwnerState(bool enable) {
73 | if (Owner == null) return;
74 | foreach (Window win in Owner.OwnedWindows) {
75 | if (win == this) continue;
76 | win.IsEnabled = enable;
77 | win.IsHitTestVisible = enable;
78 | }
79 | Owner.IsEnabled = enable;
80 | Owner.IsHitTestVisible = enable;
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 |
54 | PerMonitor
55 | true
56 |
57 |
58 |
59 |
60 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/UserControls/BorderlessWindow.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Input;
5 |
6 | namespace ZipImageViewer
7 | {
8 | public class BorderlessWindow : Window
9 | {
10 | public FrameworkElement RightTitle
11 | {
12 | get { return (FrameworkElement)GetValue(RightTitleProperty); }
13 | set { SetValue(RightTitleProperty, value); }
14 | }
15 | public static readonly DependencyProperty RightTitleProperty =
16 | DependencyProperty.Register("RightTitle", typeof(FrameworkElement), typeof(BorderlessWindow), new PropertyMetadata(null));
17 |
18 |
19 | public Visibility ButtonCloseVisibility {
20 | get { return (Visibility)GetValue(ButtonCloseVisibilityProperty); }
21 | set { SetValue(ButtonCloseVisibilityProperty, value); }
22 | }
23 | public static readonly DependencyProperty ButtonCloseVisibilityProperty =
24 | DependencyProperty.Register("ButtonCloseVisibility", typeof(Visibility), typeof(BorderlessWindow), new PropertyMetadata(Visibility.Visible));
25 |
26 |
27 | public Visibility ButtonMinVisibility {
28 | get { return (Visibility)GetValue(ButtonMinVisibilityProperty); }
29 | set { SetValue(ButtonMinVisibilityProperty, value); }
30 | }
31 | public static readonly DependencyProperty ButtonMinVisibilityProperty =
32 | DependencyProperty.Register("ButtonMinVisibility", typeof(Visibility), typeof(BorderlessWindow), new PropertyMetadata(Visibility.Visible));
33 |
34 |
35 | public Visibility ButtonMaxVisibility {
36 | get { return (Visibility)GetValue(ButtonMaxVisibilityProperty); }
37 | set { SetValue(ButtonMaxVisibilityProperty, value); }
38 | }
39 | public static readonly DependencyProperty ButtonMaxVisibilityProperty =
40 | DependencyProperty.Register("ButtonMaxVisibility", typeof(Visibility), typeof(BorderlessWindow), new PropertyMetadata(Visibility.Visible));
41 |
42 |
43 | public Visibility TitleVisibility {
44 | get { return (Visibility)GetValue(TitleVisibilityProperty); }
45 | set { SetValue(TitleVisibilityProperty, value); }
46 | }
47 | public static readonly DependencyProperty TitleVisibilityProperty =
48 | DependencyProperty.Register("TitleVisibility", typeof(Visibility), typeof(BorderlessWindow), new PropertyMetadata(Visibility.Visible));
49 |
50 |
51 | public BorderlessWindow() {
52 | //need below line for styles to apply to derived window classes.
53 | //otherwise need to move Generic.xaml to Themes folder and add this in static BorderlessWindow() { }:
54 | //DefaultStyleKeyProperty.OverrideMetadata(typeof(BorderlessWindow), new FrameworkPropertyMetadata(typeof(BorderlessWindow)));
55 |
56 | SetResourceReference(StyleProperty, typeof(BorderlessWindow));
57 | }
58 |
59 | public override void OnApplyTemplate() {
60 | base.OnApplyTemplate();
61 |
62 | //system button click handlers
63 | if (GetTemplateChild("minimizeButton") is Button minimizeButton) minimizeButton.Click += MinimizeClick;
64 | if (GetTemplateChild("restoreButton") is Button restoreButton) restoreButton.Click += RestoreClick;
65 | if (GetTemplateChild("closeButton") is Button closeButton) closeButton.Click += CloseClick;
66 | if (GetTemplateChild("titleBar") is DockPanel titleBar) titleBar.PreviewMouseDown += TitleBar_PreviewMouseDown;
67 | }
68 |
69 | private void TitleBar_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
70 | if (e.ClickCount != 2) return;
71 | RestoreClick(null, null);
72 | e.Handled = true;
73 | }
74 |
75 | protected override void OnMouseDown(MouseButtonEventArgs e) {
76 | base.OnMouseDown(e);
77 | if (e.Source is Window &&
78 | e.ChangedButton == MouseButton.Left &&
79 | e.ButtonState == MouseButtonState.Pressed) {
80 | DragMove();
81 | }
82 | }
83 |
84 | protected void MinimizeClick(object sender, RoutedEventArgs e) {
85 | WindowState = WindowState.Minimized;
86 | }
87 |
88 | protected void RestoreClick(object sender, RoutedEventArgs e) {
89 | WindowState = (WindowState == WindowState.Normal) ? WindowState.Maximized : WindowState.Normal;
90 | }
91 |
92 | protected void CloseClick(object sender, RoutedEventArgs e) {
93 | Close();
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/UserControls/PaddedGrid.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows.Controls;
3 | using System.Windows;
4 | using System.Windows.Data;
5 | using System.ComponentModel;
6 |
7 | namespace ZipImageViewer
8 | {
9 | ///
10 | /// The PaddedGrid control is a Grid that supports padding.
11 | ///
12 | public class PaddedGrid : Grid
13 | {
14 | ///
15 | /// Initializes a new instance of the class.
16 | ///
17 | public PaddedGrid()
18 | {
19 | // Add a loded event handler.
20 | Loaded += new RoutedEventHandler(PaddedGrid_Loaded);
21 | }
22 |
23 | ///
24 | /// Handles the Loaded event of the PaddedGrid control.
25 | ///
26 | /// The source of the event.
27 | /// The instance containing the event data.
28 | void PaddedGrid_Loaded(object sender, RoutedEventArgs e)
29 | {
30 | foreach (UIElement child in this.Children)
31 | {
32 | // FrameworkElement introduces the MarginProperty
33 | if (child is FrameworkElement)
34 | {
35 | // Bind the child's margin to the grid's padding.
36 | BindingOperations.SetBinding(child, FrameworkElement.MarginProperty, new Binding("Padding") { Source = this });
37 |
38 | // Bind the child's alignments to the grid's ChildrenAlignments if it is not set.
39 | if (child.ReadLocalValue(HorizontalAlignmentProperty) == DependencyProperty.UnsetValue)
40 | BindingOperations.SetBinding(child, HorizontalAlignmentProperty, new Binding("HorizontalChildrenAlignment") { Source = this });
41 | if (child.ReadLocalValue(VerticalAlignmentProperty) == DependencyProperty.UnsetValue)
42 | BindingOperations.SetBinding(child, VerticalAlignmentProperty, new Binding("VerticalChildrenAlignment") { Source = this });
43 | }
44 | }
45 | }
46 |
47 | ///
48 | /// Called when the padding changes.
49 | ///
50 | /// The dependency object.
51 | /// The instance containing the event data.
52 | private static void OnPaddingChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
53 | {
54 | // Get the padded grid that has had its padding changed.
55 | PaddedGrid paddedGrid = dependencyObject as PaddedGrid;
56 |
57 | // Force the layout to be updated.
58 | paddedGrid.UpdateLayout();
59 | }
60 |
61 | ///
62 | /// The internal dependency property object for the 'Padding' property.
63 | ///
64 | private static readonly DependencyProperty PaddingProperty =
65 | DependencyProperty.Register("Padding", typeof(Thickness), typeof(PaddedGrid),
66 | new UIPropertyMetadata(new Thickness(0.0), new PropertyChangedCallback(OnPaddingChanged)));
67 |
68 | ///
69 | /// Gets or sets the padding.
70 | ///
71 | /// The padding.
72 | [Description("The padding property."), Category("Common Properties")]
73 | public Thickness Padding
74 | {
75 | get { return (Thickness)GetValue(PaddingProperty); }
76 | set { SetValue(PaddingProperty, value); }
77 | }
78 |
79 |
80 |
81 | public HorizontalAlignment HorizontalChildrenAlignment
82 | {
83 | get { return (HorizontalAlignment)GetValue(HorizontalChildrenAlignmentProperty); }
84 | set { SetValue(HorizontalChildrenAlignmentProperty, value); }
85 | }
86 |
87 | public static readonly DependencyProperty HorizontalChildrenAlignmentProperty =
88 | DependencyProperty.Register("HorizontalChildrenAlignment", typeof(HorizontalAlignment), typeof(PaddedGrid), new PropertyMetadata(HorizontalAlignment.Stretch));
89 |
90 |
91 |
92 | public VerticalAlignment VerticalChildrenAlignment
93 | {
94 | get { return (VerticalAlignment)GetValue(VerticalChildrenAlignmentProperty); }
95 | set { SetValue(VerticalChildrenAlignmentProperty, value); }
96 | }
97 |
98 | public static readonly DependencyProperty VerticalChildrenAlignmentProperty =
99 | DependencyProperty.Register("VerticalChildrenAlignment", typeof(VerticalAlignment), typeof(PaddedGrid), new PropertyMetadata(VerticalAlignment.Center));
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/ViewWindow.xaml.notworking:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
36 |
54 |
55 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/UserControls/BorderlessWindow.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
79 |
80 |
--------------------------------------------------------------------------------
/Helpers/RegistryHelpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using Microsoft.Win32;
6 |
7 | namespace ZipImageViewer
8 | {
9 | public static class RegistryHelpers
10 | {
11 | #region Supported WIC Decoders
12 | ///
13 | /// GUID of the component registration group for WIC decoders
14 | ///
15 | private const string WICDecoderCategory = @"{7ED96837-96F0-4812-B211-F13C24117ED3}";
16 |
17 | public static List GetWICDecoders() {
18 | var result = new List(new string[] { ".BMP", ".GIF", ".ICO", ".JPEG", ".PNG", ".TIFF", ".DDS", ".JPG", ".JXR", ".HDP", ".WDP" });
19 | string baseKeyPath;
20 |
21 | if (Environment.Is64BitOperatingSystem && !Environment.Is64BitProcess)
22 | baseKeyPath = "Wow6432Node\\CLSID";
23 | else
24 | baseKeyPath = "CLSID";
25 |
26 | using (var baseKey = Registry.ClassesRoot.OpenSubKey(baseKeyPath)) {
27 | if (baseKey == null) return null;
28 | using (var categoryKey = baseKey.OpenSubKey(WICDecoderCategory + @"\instance", false)) {
29 | if (categoryKey == null) return null;
30 | // Read the guids of the registered decoders
31 | var codecGuids = categoryKey.GetSubKeyNames();
32 |
33 | foreach (var codecGuid in codecGuids) {
34 | // Read the properties of the single registered decoder
35 | using (var codecKey = baseKey.OpenSubKey(codecGuid)) {
36 | if (codecKey == null) continue;
37 | var split = codecKey.GetValue("FileExtensions", "").ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
38 | result.AddRange(split);
39 | }
40 | }
41 | return result;
42 | }
43 |
44 | }
45 | }
46 | #endregion
47 |
48 | #region Windows Explorer Context Menu
49 | public static bool CheckExplorerMenuItem(params string[] clsSubDirs) {
50 | return clsSubDirs.All(clsDir => {
51 | using (var itmKey = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{clsDir}\shell\{nameof(ZipImageViewer)}")) {
52 | return itmKey != null;
53 | }
54 | });
55 | }
56 |
57 | ///
58 | /// Enable menu for certain types of files.
59 | ///
60 | /// The types (keys in Software\Classes) to add menu for.
61 | public static void SetExplorerMenuItem(params string[] clsSubDirs) {
62 | // create submenu
63 | using (var itmKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{nameof(ZipImageViewer)}\shell\OpenWith")) {
64 | itmKey.SetValue(@"MUIVerb", Helpers.GetRes(@"ttl_OpenWithZIV"), RegistryValueKind.String);
65 | using (var cmdKey = itmKey.CreateSubKey(@"command")) {
66 | cmdKey.SetValue(null, $@"""{App.ExePath}"" ""%1""", RegistryValueKind.String);
67 | }
68 | }
69 | using (var itmKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{nameof(ZipImageViewer)}\shell\PlaySlideshow")) {
70 | itmKey.SetValue(@"MUIVerb", Helpers.GetRes(@"ttl_PlaySlideshowWithZIV"), RegistryValueKind.String);
71 | using (var cmdKey = itmKey.CreateSubKey(@"command")) {
72 | cmdKey.SetValue(null, $@"""{App.ExePath}"" -slideshow ""%1""", RegistryValueKind.String);
73 | }
74 | }
75 |
76 | foreach (var clsDir in clsSubDirs) {
77 | // create ref to submenu
78 | using (var zivKey = Registry.CurrentUser.CreateSubKey($@"Software\Classes\{clsDir}\shell\{nameof(ZipImageViewer)}")) {
79 | zivKey.SetValue(@"Icon", $@"""{App.ExePath}""", RegistryValueKind.String);
80 | zivKey.SetValue(@"ExtendedSubCommandsKey", nameof(ZipImageViewer), RegistryValueKind.String);
81 | }
82 | }
83 | }
84 |
85 | public static void ClearExplorerMenuItem(params string[] clsSubDirs) {
86 | // delete ref to submenu
87 | foreach (var clsDir in clsSubDirs) {
88 | using (var dirKey = Registry.CurrentUser.OpenSubKey($@"Software\Classes\{clsDir}", true)) {
89 | if (dirKey == null) continue;
90 | dirKey.DeleteSubKeyTree($@"shell\{nameof(ZipImageViewer)}", false);
91 | //shell key might be created by this program. Delete it when nothing is underneath.
92 | using (var shlKey = dirKey.OpenSubKey(@"shell", true)) {
93 | if (shlKey != null && shlKey.SubKeyCount == 0 && shlKey.ValueCount == 0) dirKey.DeleteSubKeyTree("shell");
94 | }
95 | }
96 | }
97 |
98 | // delete submenu
99 | Registry.CurrentUser.DeleteSubKeyTree($@"Software\Classes\{nameof(ZipImageViewer)}", false);
100 | }
101 | #endregion
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/UserControls/DpiImage.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Media;
5 | using System.Windows.Media.Imaging;
6 |
7 | namespace ZipImageViewer
8 | {
9 | ///
10 | /// DpiImage derives from Image and is not affected by DPI-scaled UI and renders 1-to-1 on the display.
11 | /// Size is converted back based on the UI scale of the parent window or the image itself.
12 | /// Be sure to configure Stretch and StretchDirection correctly for the 1-to-1 size to work.
13 | ///
14 | public class DpiImage : Image
15 | {
16 | ///
17 | /// The device-dependent size to use in WPF to avoid DPI scaling. Updated in Measure pass.
18 | /// This is the "WPF" size if you will.
19 | ///
20 | public Size RealSize { get; set; }
21 |
22 | public bool IsRealSize => RealSize.Width.Equals(Math.Round(ActualWidth, 3)) && RealSize.Height.Equals(Math.Round(ActualHeight, 3));
23 |
24 | public double Scale => Width / RealSize.Width;
25 |
26 |
27 | ///
28 | /// Indicate whether animations are being played on the image.
29 | ///
30 | public bool Transforming {
31 | get { return (bool)GetValue(TransformingProperty); }
32 | set { SetValue(TransformingProperty, value); }
33 | }
34 | public static readonly DependencyProperty TransformingProperty =
35 | DependencyProperty.Register("Transforming", typeof(bool), typeof(DpiImage), new PropertyMetadata(false));
36 |
37 |
38 | ///
39 | /// Same as the last CompositionTarget.TransformFromDevice value.
40 | ///
41 | public Point DpiMultiplier { get; set; }
42 |
43 | private void UpdateRealSize() {
44 | Size size = default;
45 | if (Source is BitmapSource sb)
46 | size = new Size(sb.PixelWidth, sb.PixelHeight);
47 | else if (Source is ImageSource si) //to handle when Source is not a BitmapImage
48 | size = new Size(si.Width, si.Height);
49 | if (size == default) return;
50 |
51 | //old .Net implementation (v4.6.1)
52 | //var parentWindow = Window.GetWindow(this);
53 | //var target = parentWindow == null ? PresentationSource.FromVisual(this)?.CompositionTarget :
54 | //PresentationSource.FromVisual(parentWindow)?.CompositionTarget;
55 | //DpiMultiplier = target.TransformFromDevice;
56 | //RealSize = new Size(Math.Round(size.Width * DpiMultiplier.M11, 3),
57 | // Math.Round(size.Height * DpiMultiplier.M22, 3));
58 |
59 | //new .Net implementation (v4.6.2+)
60 | var dpiScale = VisualTreeHelper.GetDpi(this);
61 | DpiMultiplier = new Point(1d / dpiScale.DpiScaleX, 1d / dpiScale.DpiScaleY);
62 | RealSize = new Size(Math.Round(size.Width * DpiMultiplier.X, 3), Math.Round(size.Height * DpiMultiplier.Y, 3));
63 |
64 | //#if DEBUG
65 | // Console.WriteLine($"{nameof(RealSize)}: {RealSize}; Scale: {DpiMultiplier.X};");
66 | //#endif
67 | }
68 |
69 | protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) {
70 | UpdateRealSize();
71 | base.OnDpiChanged(oldDpi, newDpi);
72 | }
73 |
74 | protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {
75 | if (e.Property == SourceProperty) {
76 | if (e.NewValue == null)
77 | RealSize = new Size();
78 | else
79 | UpdateRealSize();
80 | }
81 | base.OnPropertyChanged(e);
82 | }
83 |
84 | protected override Size MeasureOverride(Size constraint) {
85 | if (RealSize == default) UpdateRealSize();
86 | return MeasureArrangeHelper(constraint);
87 | }
88 |
89 | protected override Size ArrangeOverride(Size finalSize) {
90 | return MeasureArrangeHelper(finalSize);
91 | }
92 |
93 | private Size MeasureArrangeHelper(Size constraint) {
94 | if (Source == null) return new Size();
95 |
96 | //get computed scale factor (source from Reference Source)
97 | Size scaleFactor = Helpers.ComputeScaleFactor(constraint, RealSize, Stretch, StretchDirection);
98 |
99 | // Returns our minimum size & sets DesiredSize.
100 | return new Size(RealSize.Width * scaleFactor.Width, RealSize.Height * scaleFactor.Height);
101 |
102 | //old implementation without support for StretchDirection
103 | //Size size = default;
104 | //switch (Stretch) {
105 | // case Stretch.Fill:
106 | // size.Width = double.IsInfinity(constraint.Width) ? RealSize.Width : constraint.Width;
107 | // size.Height = double.IsInfinity(constraint.Height) ? RealSize.Height : constraint.Height;
108 | // break;
109 | // case Stretch.Uniform:
110 | // size = Helpers.UniformScale(RealSize, constraint);
111 | // break;
112 | // case Stretch.UniformToFill:
113 | // size = Helpers.UniformScaleToFill(RealSize, constraint);
114 | // break;
115 | // case Stretch.None:
116 | // default:
117 | // size = RealSize;
118 | // break;
119 | //}
120 | //return size;
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/ContextMenuWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 | using System.Windows.Controls;
9 | using System.Windows.Input;
10 | using static ZipImageViewer.LoadHelper;
11 | using static ZipImageViewer.Helpers;
12 |
13 | namespace ZipImageViewer
14 | {
15 | public partial class ContextMenuWindow : RoundedWindow, INotifyPropertyChanged
16 | {
17 | public event PropertyChangedEventHandler PropertyChanged;
18 | public MainWindow MainWin { get; set; }
19 |
20 | public ContextMenuWindow() {
21 | InitializeComponent();
22 | }
23 |
24 | private ObjectInfo objectInfo;
25 | public ObjectInfo ObjectInfo {
26 | get => objectInfo;
27 | set {
28 | if (objectInfo == value) return;
29 | objectInfo = value;
30 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ObjectInfo)));
31 | Task.Run(() => {
32 | var imgInfo = getImageInfo(ObjectInfo);
33 | Dispatcher.Invoke(() => ImageInfo = imgInfo);
34 | });
35 | }
36 | }
37 |
38 |
39 | public ImageInfo ImageInfo {
40 | get { return (ImageInfo)GetValue(ImageInfoProperty); }
41 | set { SetValue(ImageInfoProperty, value); }
42 | }
43 | public static readonly DependencyProperty ImageInfoProperty =
44 | DependencyProperty.Register("ImageInfo", typeof(ImageInfo), typeof(ContextMenuWindow), new PropertyMetadata(null));
45 |
46 |
47 | private static ImageInfo getImageInfo(ObjectInfo objInfo) {
48 | if (objInfo == null) return null;
49 | var imgInfo = new ImageInfo();
50 | try {
51 | if (objInfo.Flags == (FileFlags.Archive | FileFlags.Image)) {
52 | ExtractFile(objInfo.FileSystemPath, objInfo.FileName, (fileInfo, stream) => {
53 | imgInfo.Created = fileInfo.CreationTime;
54 | imgInfo.Modified = fileInfo.LastWriteTime;
55 | imgInfo.FileSize = (long)fileInfo.Size;
56 | UpdateImageInfo(stream, imgInfo);
57 | });
58 | }
59 | else {
60 | var fileInfo = new FileInfo(objInfo.FileSystemPath);
61 | imgInfo.Created = fileInfo.CreationTime;
62 | imgInfo.Modified = fileInfo.LastWriteTime;
63 | if (!fileInfo.Attributes.HasFlag(FileAttributes.Directory)) {
64 | imgInfo.FileSize = fileInfo.Length;
65 | if (objInfo.Flags.HasFlag(FileFlags.Image)) {
66 | using (var stream = new FileStream(objInfo.FileSystemPath, FileMode.Open, FileAccess.Read)) {
67 | UpdateImageInfo(stream, imgInfo);
68 | }
69 | }
70 | }
71 | }
72 | }
73 | catch { }
74 | return imgInfo;
75 | }
76 |
77 | private void Menu_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
78 | if (ObjectInfo == null) return;
79 | if (sender is Border border) {
80 | switch (border.Name) {
81 | case nameof(B_OpenWithDefaultApp):
82 | Cmd_OpenWithDefaultApp(ObjectInfo.FileSystemPath);
83 | break;
84 | case nameof(B_OpenInExplorer):
85 | Cmd_OpenInExplorer(ObjectInfo.FileSystemPath);
86 | break;
87 | case nameof(B_OpenInNewWindow):
88 | Cmd_OpenInNewWindow(ObjectInfo, MainWin);
89 | break;
90 | case nameof(B_CacheFirst):
91 | CacheHelper.CachePath(ObjectInfo.ContainerPath, true);
92 | break;
93 | case nameof(B_CacheAll):
94 | CacheHelper.CachePath(ObjectInfo.ContainerPath, false);
95 | break;
96 | case nameof(B_Slideshow):
97 | new SlideshowWindow(ObjectInfo.ContainerPath).Show();
98 | break;
99 | }
100 | }
101 | else if (sender is ContentPresenter cp && cp.Content is ObservableObj obsObj) {
102 | Run(obsObj.Str2, CustomCmdArgsReplace(obsObj.Str3, ObjectInfo));
103 | }
104 |
105 | Close();
106 | e.Handled = true;
107 | }
108 |
109 | private void CTMWin_FadedOut(object sender, RoutedEventArgs e) {
110 | ObjectInfo = null;
111 | ImageInfo = null;
112 | MainWin = null;
113 | }
114 |
115 | #region Context Menu Commands
116 | internal static void Cmd_OpenWithDefaultApp(string fsPath) {
117 | Run("explorer", fsPath);
118 | }
119 |
120 | internal static void Cmd_OpenInExplorer(string fsPath) {
121 | Run("explorer", $"/select, \"{fsPath}\"");
122 | }
123 |
124 | internal static void Cmd_OpenInNewWindow(ObjectInfo objInfo, MainWindow mainWin = null) {
125 | if (objInfo.Flags.HasFlag(FileFlags.Image)) {
126 | mainWin?.LoadPath(objInfo);
127 | }
128 | else if (objInfo.Flags.HasFlag(FileFlags.Directory) ||
129 | objInfo.Flags.HasFlag(FileFlags.Archive)) {
130 | var win = new MainWindow {
131 | InitialPath = objInfo.FileSystemPath
132 | };
133 | win.Show();
134 | }
135 | }
136 |
137 | #endregion
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/UserControls/AppleStyleScrollBar.xaml:
--------------------------------------------------------------------------------
1 |
4 |
24 |
106 |
--------------------------------------------------------------------------------
/UserControls/StylesAndAnimations.xaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
62 |
63 |
103 |
104 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Dd]ebugPublic/
19 | [Rr]elease/
20 | [Rr]eleases/
21 | x64/
22 | x86/
23 | [Aa][Rr][Mm]/
24 | [Aa][Rr][Mm]64/
25 | bld/
26 | [Bb]in/
27 | [Oo]bj/
28 | [Ll]og/
29 |
30 | # Visual Studio 2015/2017 cache/options directory
31 | .vs/
32 | # Uncomment if you have tasks that create the project's static files in wwwroot
33 | #wwwroot/
34 |
35 | # Visual Studio 2017 auto generated files
36 | Generated\ Files/
37 |
38 | # MSTest test Results
39 | [Tt]est[Rr]esult*/
40 | [Bb]uild[Ll]og.*
41 |
42 | # NUNIT
43 | *.VisualState.xml
44 | TestResult.xml
45 |
46 | # Build Results of an ATL Project
47 | [Dd]ebugPS/
48 | [Rr]eleasePS/
49 | dlldata.c
50 |
51 | # Benchmark Results
52 | BenchmarkDotNet.Artifacts/
53 |
54 | # .NET Core
55 | project.lock.json
56 | project.fragment.lock.json
57 | artifacts/
58 |
59 | # StyleCop
60 | StyleCopReport.xml
61 |
62 | # Files built by Visual Studio
63 | *_i.c
64 | *_p.c
65 | *_h.h
66 | *.ilk
67 | *.meta
68 | *.obj
69 | *.iobj
70 | *.pch
71 | *.pdb
72 | *.ipdb
73 | *.pgc
74 | *.pgd
75 | *.rsp
76 | *.sbr
77 | *.tlb
78 | *.tli
79 | *.tlh
80 | *.tmp
81 | *.tmp_proj
82 | *_wpftmp.csproj
83 | *.log
84 | *.vspscc
85 | *.vssscc
86 | .builds
87 | *.pidb
88 | *.svclog
89 | *.scc
90 |
91 | # Chutzpah Test files
92 | _Chutzpah*
93 |
94 | # Visual C++ cache files
95 | ipch/
96 | *.aps
97 | *.ncb
98 | *.opendb
99 | *.opensdf
100 | *.sdf
101 | *.cachefile
102 | *.VC.db
103 | *.VC.VC.opendb
104 |
105 | # Visual Studio profiler
106 | *.psess
107 | *.vsp
108 | *.vspx
109 | *.sap
110 |
111 | # Visual Studio Trace Files
112 | *.e2e
113 |
114 | # TFS 2012 Local Workspace
115 | $tf/
116 |
117 | # Guidance Automation Toolkit
118 | *.gpState
119 |
120 | # ReSharper is a .NET coding add-in
121 | _ReSharper*/
122 | *.[Rr]e[Ss]harper
123 | *.DotSettings.user
124 |
125 | # JustCode is a .NET coding add-in
126 | .JustCode
127 |
128 | # TeamCity is a build add-in
129 | _TeamCity*
130 |
131 | # DotCover is a Code Coverage Tool
132 | *.dotCover
133 |
134 | # AxoCover is a Code Coverage Tool
135 | .axoCover/*
136 | !.axoCover/settings.json
137 |
138 | # Visual Studio code coverage results
139 | *.coverage
140 | *.coveragexml
141 |
142 | # NCrunch
143 | _NCrunch_*
144 | .*crunch*.local.xml
145 | nCrunchTemp_*
146 |
147 | # MightyMoose
148 | *.mm.*
149 | AutoTest.Net/
150 |
151 | # Web workbench (sass)
152 | .sass-cache/
153 |
154 | # Installshield output folder
155 | [Ee]xpress/
156 |
157 | # DocProject is a documentation generator add-in
158 | DocProject/buildhelp/
159 | DocProject/Help/*.HxT
160 | DocProject/Help/*.HxC
161 | DocProject/Help/*.hhc
162 | DocProject/Help/*.hhk
163 | DocProject/Help/*.hhp
164 | DocProject/Help/Html2
165 | DocProject/Help/html
166 |
167 | # Click-Once directory
168 | publish/
169 |
170 | # Publish Web Output
171 | *.[Pp]ublish.xml
172 | *.azurePubxml
173 | # Note: Comment the next line if you want to checkin your web deploy settings,
174 | # but database connection strings (with potential passwords) will be unencrypted
175 | *.pubxml
176 | *.publishproj
177 |
178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
179 | # checkin your Azure Web App publish settings, but sensitive information contained
180 | # in these scripts will be unencrypted
181 | PublishScripts/
182 |
183 | # NuGet Packages
184 | *.nupkg
185 | # The packages folder can be ignored because of Package Restore
186 | **/[Pp]ackages/*
187 | # except build/, which is used as an MSBuild target.
188 | !**/[Pp]ackages/build/
189 | # Uncomment if necessary however generally it will be regenerated when needed
190 | #!**/[Pp]ackages/repositories.config
191 | # NuGet v3's project.json files produces more ignorable files
192 | *.nuget.props
193 | *.nuget.targets
194 |
195 | # Microsoft Azure Build Output
196 | csx/
197 | *.build.csdef
198 |
199 | # Microsoft Azure Emulator
200 | ecf/
201 | rcf/
202 |
203 | # Windows Store app package directories and files
204 | AppPackages/
205 | BundleArtifacts/
206 | Package.StoreAssociation.xml
207 | _pkginfo.txt
208 | *.appx
209 |
210 | # Visual Studio cache files
211 | # files ending in .cache can be ignored
212 | *.[Cc]ache
213 | # but keep track of directories ending in .cache
214 | !?*.[Cc]ache/
215 |
216 | # Others
217 | ClientBin/
218 | ~$*
219 | *~
220 | *.dbmdl
221 | *.dbproj.schemaview
222 | *.jfm
223 | *.pfx
224 | *.publishsettings
225 | orleans.codegen.cs
226 |
227 | # Including strong name files can present a security risk
228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
229 | #*.snk
230 |
231 | # Since there are multiple workflows, uncomment next line to ignore bower_components
232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
233 | #bower_components/
234 |
235 | # RIA/Silverlight projects
236 | Generated_Code/
237 |
238 | # Backup & report files from converting an old project file
239 | # to a newer Visual Studio version. Backup files are not needed,
240 | # because we have git ;-)
241 | _UpgradeReport_Files/
242 | Backup*/
243 | UpgradeLog*.XML
244 | UpgradeLog*.htm
245 | ServiceFabricBackup/
246 | *.rptproj.bak
247 |
248 | # SQL Server files
249 | *.mdf
250 | *.ldf
251 | *.ndf
252 |
253 | # Business Intelligence projects
254 | *.rdl.data
255 | *.bim.layout
256 | *.bim_*.settings
257 | *.rptproj.rsuser
258 | *- Backup*.rdl
259 |
260 | # Microsoft Fakes
261 | FakesAssemblies/
262 |
263 | # GhostDoc plugin setting file
264 | *.GhostDoc.xml
265 |
266 | # Node.js Tools for Visual Studio
267 | .ntvs_analysis.dat
268 | node_modules/
269 |
270 | # Visual Studio 6 build log
271 | *.plg
272 |
273 | # Visual Studio 6 workspace options file
274 | *.opt
275 |
276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
277 | *.vbw
278 |
279 | # Visual Studio LightSwitch build output
280 | **/*.HTMLClient/GeneratedArtifacts
281 | **/*.DesktopClient/GeneratedArtifacts
282 | **/*.DesktopClient/ModelManifest.xml
283 | **/*.Server/GeneratedArtifacts
284 | **/*.Server/ModelManifest.xml
285 | _Pvt_Extensions
286 |
287 | # Paket dependency manager
288 | .paket/paket.exe
289 | paket-files/
290 |
291 | # FAKE - F# Make
292 | .fake/
293 |
294 | # JetBrains Rider
295 | .idea/
296 | *.sln.iml
297 |
298 | # CodeRush personal settings
299 | .cr/personal
300 |
301 | # Python Tools for Visual Studio (PTVS)
302 | __pycache__/
303 | *.pyc
304 |
305 | # Cake - Uncomment if you are using it
306 | # tools/**
307 | # !tools/packages.config
308 |
309 | # Tabs Studio
310 | *.tss
311 |
312 | # Telerik's JustMock configuration file
313 | *.jmconfig
314 |
315 | # BizTalk build output
316 | *.btp.cs
317 | *.btm.cs
318 | *.odx.cs
319 | *.xsd.cs
320 |
321 | # OpenCover UI analysis results
322 | OpenCover/
323 |
324 | # Azure Stream Analytics local run output
325 | ASALocalRun/
326 |
327 | # MSBuild Binary and Structured Log
328 | *.binlog
329 |
330 | # NVidia Nsight GPU debugger configuration file
331 | *.nvuser
332 |
333 | # MFractors (Xamarin productivity tool) working folder
334 | .mfractor/
335 |
336 | # Local History for Visual Studio
337 | .localhistory/
338 |
339 | # BeatPulse healthcheck temp database
340 | healthchecksdb
--------------------------------------------------------------------------------
/SettingsWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.ObjectModel;
4 | using System.Data.SQLite;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using static ZipImageViewer.Helpers;
11 | using static ZipImageViewer.TableHelper;
12 | using static ZipImageViewer.SQLiteHelper;
13 | using System.Data;
14 |
15 | namespace ZipImageViewer
16 | {
17 | public partial class SettingsWindow : BorderlessWindow
18 | {
19 | public SettingsWindow(Window owner) {
20 | Owner = owner;
21 | InitializeComponent();
22 | }
23 |
24 | public string CurrentThumbDbSize {
25 | get { return (string)GetValue(CurrentThumbDbSizeProperty); }
26 | set { SetValue(CurrentThumbDbSizeProperty, value); }
27 | }
28 | public static readonly DependencyProperty CurrentThumbDbSizeProperty =
29 | DependencyProperty.Register("CurrentThumbDbSize", typeof(string), typeof(SettingsWindow), new PropertyMetadata("N/A"));
30 |
31 |
32 | private void SettingsWin_Loaded(object sender, RoutedEventArgs e) {
33 | CurrentThumbDbSize = BytesToString(new FileInfo(Tables[Table.Thumbs].FullPath).Length);
34 | }
35 |
36 | private void SettingsWin_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
37 | try {
38 | Setting.FallbackPasswords.AcceptChanges();
39 | Setting.MappedPasswords.AcceptChanges();
40 | Setting.SaveConfigs();
41 | }
42 | catch (Exception ex) {
43 | MessageBox.Show(ex.Message);
44 | }
45 | }
46 |
47 | private void Btn_ChgMstPwd_Click(object sender, RoutedEventArgs e) {
48 | bool showIncorrect = false, showMismatch = false;
49 | for (int i = 0; i < 10; i++) {
50 | var (answer, curPwd, newPwd, cfmPwd) = InputWindow.PromptForPasswordChange(true, showIncorrect, showMismatch);
51 | if (!answer) return;
52 | showIncorrect = false;
53 | showMismatch = false;
54 | if (curPwd != Setting.MasterPassword) showIncorrect = true;
55 | else if (newPwd != cfmPwd) showMismatch = true;
56 | else {
57 | Setting.ChangeMasterPassword(newPwd, curPwd);
58 | break;
59 | }
60 | }
61 | }
62 |
63 | private void SettingsWin_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) {
64 | if (e.Key != System.Windows.Input.Key.Escape || !(e.Source is ScrollViewer)) return;
65 | Close();
66 | }
67 |
68 | private async void Btn_Move_Click(object sender, RoutedEventArgs e) {
69 | if (string.IsNullOrWhiteSpace(TB_DatabaseDir.Text)) return;
70 | var sourceDir = Path.GetFullPath(Setting.DatabaseDir).TrimEnd(Path.DirectorySeparatorChar);
71 | var targetDir = TB_DatabaseDir.Text.Trim();
72 |
73 | //try to move file if dir is not same
74 | if (sourceDir != Path.GetFullPath(targetDir).TrimEnd(Path.DirectorySeparatorChar)) {
75 | try {
76 | ((Button)sender).IsEnabled = false;
77 |
78 | Directory.CreateDirectory(targetDir);
79 | await Task.Run(() => {
80 | foreach (var table in Tables.Values) {
81 | if (!File.Exists(table.FullPath)) continue;
82 | lock (table.Lock) {
83 | var targetPath = Path.Combine(targetDir, table.FileName);
84 | if (File.Exists(targetPath)) throw new Exception(GetRes("msg_FileExists_0", targetPath));
85 | File.Move(table.FullPath, targetPath);
86 | }
87 | }
88 | });
89 |
90 | MessageBox.Show(GetRes("msg_DbMovedSucc"), GetRes("ttl_OperationComplete"), MessageBoxButton.OK, MessageBoxImage.Information);
91 | }
92 | catch (Exception ex) {
93 | MessageBox.Show(ex.Message, GetRes("ttl_OperationFailed"), MessageBoxButton.OK, MessageBoxImage.Error);
94 | return;
95 | }
96 | finally {
97 | ((Button)sender).IsEnabled = true;
98 | }
99 | }
100 | else {
101 | MessageBox.Show(GetRes("msg_DbPathUpdated"), GetRes("ttl_OperationComplete"), MessageBoxButton.OK, MessageBoxImage.Information);
102 | }
103 |
104 | Setting.DatabaseDir = targetDir;
105 | }
106 |
107 | private void Btn_Clean_Click(object sender, RoutedEventArgs e) {
108 | //clean database
109 | Execute(Table.Thumbs, (table, con) => {
110 | using (var cmd = new SQLiteCommand(con)) {
111 | cmd.CommandText = $@"delete from {table.Name}";
112 | cmd.ExecuteNonQuery();
113 | cmd.CommandText = @"vacuum";
114 | cmd.ExecuteNonQuery();
115 | }
116 | return 0;
117 | });
118 |
119 | CurrentThumbDbSize = BytesToString(new FileInfo(Tables[Table.Thumbs].FullPath).Length);
120 | }
121 |
122 | private void Btn_Reload_Click(object sender, RoutedEventArgs e) {
123 | if (!(Owner is MainWindow win)) return;
124 | Task.Run(() => win.LoadPath(win.CurrentPath));
125 | }
126 |
127 | private void DataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e) {
128 | var dg = (DataGrid)sender;
129 | if (e.EditAction != DataGridEditAction.Commit) return;
130 |
131 | //due to the UpdateSourceTrigger is LostFocus for Text, without this e.Row.Item wont have the new value
132 | e.Row.BindingGroup.UpdateSources();
133 | switch (e.Row.Item) {
134 | case ObservablePair op:
135 | if (string.IsNullOrWhiteSpace(op.Item1) ||
136 | string.IsNullOrWhiteSpace(op.Item2)) {
137 | //dg.CancelEdit(); requires implementing IEditableObject on ObservablePair
138 | ((Collection>)dg.ItemsSource).Remove(op);
139 | }
140 | return;
141 | case Observable o:
142 | if (string.IsNullOrWhiteSpace(o.Item))
143 | ((Collection>)dg.ItemsSource).Remove(o);
144 | return;
145 |
146 | }
147 | }
148 |
149 | }
150 |
151 | //public class ObservablesValidationRule : ValidationRule
152 | //{
153 | // public bool ValidateItem1 { get; set; } = true;
154 | // public bool ValidateItem2 { get; set; } = true;
155 |
156 | // public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
157 | // var bg = (BindingGroup)value;
158 | // switch (bg.Items[0]) {
159 | // case ObservablePair op:
160 | // if ((ValidateItem1 && string.IsNullOrWhiteSpace(op.Item1)) ||
161 | // (ValidateItem2 && string.IsNullOrWhiteSpace(op.Item2)))
162 | // return new ValidationResult(false, "Empty values are not allowed.");
163 | // break;
164 | // case Observable o:
165 | // if (ValidateItem1 && string.IsNullOrWhiteSpace(o.Item))
166 | // return new ValidationResult(false, "Empty values are not allowed.");
167 | // break;
168 | // }
169 | // return ValidationResult.ValidResult;
170 | // }
171 | //}
172 |
173 | }
174 |
--------------------------------------------------------------------------------
/InputWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Windows;
4 | using System.Windows.Controls;
5 | using System.Windows.Input;
6 | using System.Windows.Media;
7 | using static ZipImageViewer.Helpers;
8 |
9 | namespace ZipImageViewer
10 | {
11 | public partial class InputWindow : RoundedWindow
12 | {
13 | public class Field where T : FrameworkElement
14 | {
15 | public string Name { get; protected set; }
16 | public T Control { get; protected set; }
17 | public Action InitCallback { get; protected set; }
18 | public Field(string name = null, Action init = null) {
19 | Name = name;
20 | InitCallback = init;
21 |
22 | var control = (T)Activator.CreateInstance(typeof(T));
23 | InitCallback?.Invoke(control);
24 | Control = control;
25 | }
26 | }
27 |
28 | public class Field : Field where T : FrameworkElement
29 | {
30 | public Func ValueCallback { get; private set; }
31 | public V Value => ValueCallback == null ? default : ValueCallback(Control);
32 |
33 | public Field(string name = null, Action init = null, Func value = null) : base(name, init) {
34 | ValueCallback = value;
35 | }
36 | }
37 |
38 |
39 | public readonly KeyedCol Fields;
40 | public readonly Action LoadedCallback;
41 |
42 | public InputWindow(KeyedCol fields, bool okOnly = false, Action loaded = null) {
43 | InitializeComponent();
44 | if (okOnly) SP_OkCancel.Children.Remove(Btn_Cancel);
45 |
46 | Fields = fields;
47 | LoadedCallback = loaded;
48 | }
49 |
50 | public override void OnApplyTemplate() {
51 | foreach (var field in Fields) {
52 | if (field == null) continue;
53 | ContentPanel.Children.Add(field.Control);
54 | }
55 |
56 | base.OnApplyTemplate();
57 | }
58 |
59 | private void InputWin_Loaded(object sender, RoutedEventArgs e) {
60 | LoadedCallback?.Invoke(this);
61 | }
62 |
63 | private void Btn_OK_Click(object sender, RoutedEventArgs e) {
64 | DialogResult = new bool?(true);
65 | }
66 |
67 |
68 | public static (bool Answer, string Password, bool AddToFallback) PromptForArchivePassword() {
69 | var win = new InputWindow(new KeyedCol(f => f.Name) {
70 | new Field(init: c => {
71 | c.Text = GetRes("txt_PasswordForArchive");
72 | c.Margin = new Thickness(0,0,0,10d);
73 | c.FontSize = 16d;
74 | }),
75 | new Field("PB_Password", init: c => {
76 | c.Content = new PasswordBox();
77 | c.Margin = new Thickness(0,0,0,10d);
78 | }, value: c => ((PasswordBox)c.Content).Password),
79 | new Field("CB_Fallback", init: c => {
80 | c.Content = GetRes("txt_AddToFallbackPwdLst");
81 | c.HorizontalAlignment = HorizontalAlignment.Right;
82 | c.Margin = new Thickness(0,0,0,10d);
83 | }, value: c => c.IsChecked),
84 | new Field(init: c => c.Text = GetRes("msg_FallbackPwdTip")),
85 | }, loaded: w => FocusManager.SetFocusedElement(w.ContentPanel, w.Fields["PB_Password"].Control.Content));
86 |
87 | var result = (win.ShowDialog() == true, (string)win.Fields["PB_Password"].Value, (bool?)win.Fields["CB_Fallback"].Value == true);
88 | win.Fields.Clear();
89 | win.Close();
90 | return result;
91 | }
92 |
93 | public static (bool Answer, string CurrentPassword, string NewPassword, string ConfirmPassword)
94 | PromptForPasswordChange(bool showOldPassword = true, bool showIncorrectPassword = false, bool showMismatchPassword = false) {
95 | var fields = new KeyedCol(f => f.Name) {
96 | new Field(init: c => c.Text = $"{GetRes("ttl_New_0", GetRes("ttl_Password"))}"),
97 | new Field("NewPassword", init: c => {
98 | c.Content = new PasswordBox();
99 | c.Margin = new Thickness(0,5d,0,5d);
100 | }, value: c => ((PasswordBox)c.Content).Password),
101 | new Field(init: c => c.Text = $"{GetRes("ttl_Confirm_0", GetRes("ttl_Password"))}"),
102 | new Field("ConfirmPassword", init: c => {
103 | c.Content = new PasswordBox();
104 | c.Margin = new Thickness(0,5d,0,5d);
105 | }, value: c => ((PasswordBox)c.Content).Password),
106 | };
107 | if (showOldPassword) {
108 | fields.Insert(0, new Field("CurrentPassword", init: c => {
109 | c.Content = new PasswordBox();
110 | c.Margin = new Thickness(0, 5d, 0, 5d);
111 | }, value: c => ((PasswordBox)c.Content).Password));
112 | fields.Insert(0, new Field(init: c => c.Text = $"{GetRes("ttl_Current_0", GetRes("ttl_Password"))}"));
113 | }
114 | if (showIncorrectPassword) {
115 | fields.Insert(2, new Field(init: c => {
116 | c.Text = GetRes("msg_IncorrectPassword");
117 | c.Foreground = Brushes.Red;
118 | c.Margin = new Thickness(0, 0, 0, 5d);
119 | }));
120 | }
121 | if (showMismatchPassword) {
122 | fields.Add(new Field(init: c => {
123 | c.Text = GetRes("msg_MismatchPassword");
124 | c.Foreground = Brushes.Red;
125 | c.Margin = new Thickness(0, 0, 0, 5d);
126 | }));
127 | }
128 | var win = new InputWindow(fields, loaded: w => FocusManager.SetFocusedElement(w.ContentPanel, w.Fields[showOldPassword ? "CurrentPassword" : "NewPassword"].Control.Content)) {
129 | Title = $"{GetRes("ttl_Change_0", GetRes("ttl_MasterPassword"))}"
130 | };
131 |
132 | var result = (win.ShowDialog() == true,
133 | showOldPassword ? (string)win.Fields["CurrentPassword"].Value : null,
134 | (string)win.Fields["NewPassword"].Value,
135 | (string)win.Fields["ConfirmPassword"].Value);
136 | win.Fields.Clear();
137 | win.Close();
138 | return result;
139 | }
140 |
141 | public static (bool Answer, string MasterPassword) PromptForMasterPassword(bool showIncorrectPassword = false) {
142 | var win = new InputWindow(fields: new KeyedCol(f => f.Name) {
143 | new Field(init: c => {
144 | c.Text = GetRes("ttl_MasterPassword");
145 | c.Margin = new Thickness(0,0,0,10d);
146 | }),
147 | new Field("MasterPassword", init: c => {
148 | c.Content = new PasswordBox();
149 | }, value: c => ((PasswordBox)c.Content).Password),
150 | }, loaded: w => FocusManager.SetFocusedElement(w.ContentPanel, w.Fields["MasterPassword"].Control.Content));
151 | if (showIncorrectPassword) {
152 | win.Fields.Add(new Field(init: c => {
153 | c.Text = GetRes("msg_IncorrectPassword");
154 | c.Foreground = Brushes.Red;
155 | c.Margin = new Thickness(0, 10d, 0, 0);
156 | }));
157 | }
158 |
159 | var result = (win.ShowDialog() == true, (string)win.Fields["MasterPassword"].Value);
160 | win.Fields.Clear();
161 | win.Close();
162 | return result;
163 | }
164 |
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/Helpers/CacheHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using SizeInt = System.Drawing.Size;
7 | using static ZipImageViewer.Helpers;
8 | using static ZipImageViewer.LoadHelper;
9 | using static ZipImageViewer.SQLiteHelper;
10 | using System.Windows.Media;
11 | using System.IO;
12 | using System.Windows;
13 |
14 | namespace ZipImageViewer
15 | {
16 | public static class CacheHelper
17 | {
18 | /// If not null, MainWindow will be halted before caching is finished.
19 | public static void CachePath(string cachePath, bool firstOnly, Window owner = null, MainWindow mainWin = null) {
20 | var bw = new BlockWindow(owner) {
21 | MessageTitle = GetRes("msg_Processing")
22 | };
23 | //callback used to update progress
24 | Action cb = (path, i, count) => {
25 | var p = (int)Math.Floor((double)i / count * 100);
26 | Application.Current.Dispatcher.Invoke(() => {
27 | bw.Percentage = p;
28 | bw.MessageBody = path;
29 | if (bw.Percentage == 100) bw.MessageTitle = GetRes("ttl_OperationComplete");
30 | });
31 | };
32 |
33 | //work thread
34 | bw.Work = () => {
35 | if (mainWin != null) {
36 | mainWin.tknSrc_LoadThumb?.Cancel();
37 | while (mainWin.tknSrc_LoadThumb != null) {
38 | Thread.Sleep(200);
39 | }
40 | mainWin.preRefreshActions();
41 | }
42 |
43 | //because cache always needs to cache all images under current view, we need to get containers plus images under root
44 | IEnumerable infos = null;
45 | var dirInfo = new DirectoryInfo(cachePath);
46 | switch (GetPathType(dirInfo)) {
47 | case FileFlags.Directory:
48 | infos = dirInfo.EnumerateFiles()
49 | .Where(fi => GetPathType(fi) == FileFlags.Image)
50 | .Select(fi => new ObjectInfo(fi.FullName, FileFlags.Image, fi.Name))
51 | .Concatenate(EnumerateContainers(cachePath, inclRoot: false));
52 | break;
53 | case FileFlags.Archive:
54 | infos = new[] { new ObjectInfo(cachePath, FileFlags.Archive) };
55 | break;
56 | }
57 |
58 | CacheObjInfos(infos, ref bw.tknSrc_Work, bw.lock_Work, firstOnly, cb);
59 |
60 | if (mainWin != null)
61 | Task.Run(() => mainWin.LoadPath(mainWin.CurrentPath));
62 | };
63 | bw.FadeIn();
64 | }
65 |
66 |
67 | public static void CacheObjInfos(IEnumerable infos, ref CancellationTokenSource tknSrc, object tknLock, bool firstOnly,
68 | Action callback = null, int maxThreads = 0) {
69 |
70 | tknSrc?.Cancel();
71 | Monitor.Enter(tknLock);
72 | tknSrc = new CancellationTokenSource();
73 | var tknSrcLocal = tknSrc; //for use in lambda
74 | var count = 0;
75 | var decodeSize = (SizeInt)Setting.ThumbnailSize;
76 |
77 | var total = infos.Count();
78 |
79 | void cacheObjInfo(ObjectInfo objInfo) {
80 | try {
81 | switch (objInfo.Flags) {
82 | case FileFlags.Directory:
83 | case FileFlags.Image:
84 | objInfo.SourcePaths = GetSourcePaths(objInfo);
85 | if (objInfo.SourcePaths == null || objInfo.SourcePaths.Length == 0) break;
86 | if (objInfo.Flags == FileFlags.Directory && firstOnly)
87 | objInfo.SourcePaths = new[] { objInfo.SourcePaths[0] };
88 | foreach (var srcPath in objInfo.SourcePaths) {
89 | if (!ThumbExistInDB(objInfo.ContainerPath, srcPath, decodeSize)) {
90 | GetImageSource(objInfo, srcPath, decodeSize, false);
91 | }
92 | }
93 | break;
94 | case FileFlags.Archive:
95 | ExtractZip(new LoadOptions(objInfo.FileSystemPath) {
96 | Flags = FileFlags.Archive,
97 | LoadImage = false,
98 | DecodeSize = decodeSize,
99 | ExtractorCallback = (ext, fileName, options) => {
100 | try {
101 | if (ThumbExistInDB(ext.FileName, fileName, decodeSize)) {
102 | if (firstOnly) options.Continue = false;
103 | return null;
104 | }
105 | ImageSource source = null;
106 | using (var ms = new MemoryStream()) {
107 | ext.ExtractFile(fileName, ms);
108 | if (ms.Length > 0)
109 | source = GetImageSource(ms, decodeSize);
110 | }
111 | if (source != null) {
112 | AddToThumbDB(source, objInfo.FileSystemPath, fileName, decodeSize);
113 | if (firstOnly) options.Continue = false;
114 | }
115 | }
116 | catch { }
117 | finally {
118 | callback?.Invoke(fileName, count, total);
119 | }
120 | return null;
121 | },
122 | }, tknSrcLocal);
123 | break;
124 | }
125 | }
126 | catch { }
127 | finally {
128 | callback?.Invoke(objInfo.FileSystemPath, Interlocked.Increment(ref count), total);
129 | }
130 | }
131 |
132 | //calculate max thread count
133 | var threadCount = maxThreads > 0 ? maxThreads : MaxLoadThreads / 2;
134 | if (threadCount < 1) threadCount = 1;
135 | else if (threadCount > MaxLoadThreads) threadCount = MaxLoadThreads;
136 |
137 | //loop
138 | try {
139 | //avoid sleep
140 | NativeHelpers.SetPowerState(1);
141 |
142 | if (threadCount == 1) {
143 | foreach (var objInfo in infos) {
144 | if (tknSrc?.IsCancellationRequested == true) break;
145 | cacheObjInfo(objInfo);
146 | }
147 | }
148 | else {
149 | var paraOptions = new ParallelOptions() {
150 | CancellationToken = tknSrc.Token,
151 | MaxDegreeOfParallelism = threadCount,
152 | };
153 | Parallel.ForEach(infos, paraOptions, (objInfo, state) => {
154 | if (paraOptions.CancellationToken.IsCancellationRequested) state.Break();
155 | cacheObjInfo(objInfo);
156 | if (paraOptions.CancellationToken.IsCancellationRequested) state.Break();
157 | });
158 | }
159 | }
160 | catch { }
161 | finally {
162 | tknSrc.Dispose();
163 | tknSrcLocal = null;
164 | tknSrc = null;
165 | Monitor.Exit(tknLock);
166 | NativeHelpers.SetPowerState(0);
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/UserControls/Thumbnail.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 | using System.Windows.Controls;
4 | using System.Windows.Media.Animation;
5 | using System.Windows.Media;
6 | using System.Threading.Tasks;
7 | using System.ComponentModel;
8 | using System.Linq;
9 | using System.Windows.Data;
10 | using SizeInt = System.Drawing.Size;
11 | using System.Windows.Threading;
12 | using System.Threading;
13 | using System.Windows.Media.Imaging;
14 | using static ZipImageViewer.LoadHelper;
15 |
16 | namespace ZipImageViewer
17 | {
18 | public partial class Thumbnail : UserControl, INotifyPropertyChanged {
19 | public ObjectInfo ObjectInfo {
20 | get { return (ObjectInfo)GetValue(ObjectInfoProperty); }
21 | set { SetValue(ObjectInfoProperty, value); }
22 | }
23 | public static readonly DependencyProperty ObjectInfoProperty =
24 | DependencyProperty.Register("ObjectInfo", typeof(ObjectInfo), typeof(Thumbnail), new PropertyMetadata(null));
25 |
26 |
27 | public event PropertyChangedEventHandler PropertyChanged;
28 |
29 | private int sourcePathIdx;
30 | private string sourcePathName;
31 |
32 | private int thumbTransAnimCount;
33 | private string thumbTransAnimName;
34 | private Storyboard thumbTransAnimOut => (Storyboard)FindResource($"{thumbTransAnimName}_Out");
35 | private Storyboard thumbTransAnimIn => (Storyboard)FindResource($"{thumbTransAnimName}_In");
36 |
37 | private ImageSource nextSource;
38 |
39 | private ImageSource thumbImageSource = App.fa_spinner;
40 | public ImageSource ThumbImageSource {
41 | get => thumbImageSource;
42 | set {
43 | if (thumbImageSource == value) return;
44 | nextSource = value;
45 | if (thumbImageSource == App.fa_spinner)
46 | //use simpler animation for initial animation to reduce performance hit
47 | thumbTransAnimName = @"SB_ThumbTransInit";
48 | else
49 | thumbTransAnimName = $@"SB_ThumbTrans_{App.Random.Next(0, thumbTransAnimCount)}";
50 |
51 | Dispatcher.Invoke(() => {
52 | if (IsLoaded) GR1.BeginStoryboard(thumbTransAnimOut);
53 | });
54 | }
55 | }
56 |
57 | private MainWindow mainWin;
58 | private DispatcherTimer cycleTimer;
59 |
60 | public Thumbnail() {
61 | InitializeComponent();
62 | #if DEBUG
63 | IM1.SetBinding(ToolTipProperty, new Binding() {
64 | ElementName = @"TN",
65 | Path = new PropertyPath($@"{nameof(ObjectInfo)}.{nameof(ObjectInfo.DebugInfo)}"),
66 | Mode = BindingMode.OneWay,
67 | });
68 | ToolTipService.SetShowDuration(IM1, 20000);
69 | #endif
70 | thumbTransAnimCount = Resources.Keys.Cast().Count(k => k.StartsWith(@"SB_ThumbTrans_", StringComparison.OrdinalIgnoreCase)) / 2;
71 |
72 | }
73 |
74 | private void ThumbTransAnimOut_Completed(object sender, EventArgs e) {
75 | thumbImageSource = nextSource;
76 | nextSource = null;
77 |
78 | if (thumbImageSource is BitmapSource) {
79 | //fill frame when it's an actual image
80 | IM1.Stretch = Stretch.UniformToFill;
81 | IM1.Width = double.NaN;
82 | IM1.Height = double.NaN;
83 | }
84 | else {
85 | //half size when it's not an image
86 | var uniLength = Math.Min(ActualWidth, ActualHeight) * 0.5;
87 | IM1.Stretch = Stretch.Uniform;
88 | IM1.Width = uniLength;
89 | IM1.Height = uniLength;
90 | }
91 |
92 | //tell binding to update image
93 | if (PropertyChanged != null) {
94 | PropertyChanged.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbImageSource)));
95 | }
96 |
97 | //continue transition animation
98 | GR1.BeginStoryboard(thumbTransAnimIn);
99 | }
100 |
101 | private void TN_Loaded(object sender, RoutedEventArgs e) {
102 | var uniLength = Math.Min(ActualWidth, ActualHeight) * 0.5;
103 | IM1.Stretch = Stretch.Uniform;
104 | IM1.Width = uniLength;
105 | IM1.Height = uniLength;
106 |
107 | cycleTimer = new DispatcherTimer(DispatcherPriority.Normal, Application.Current.Dispatcher);
108 | cycleTimer.Tick += cycleImageSource;
109 |
110 | mainWin = (MainWindow)Window.GetWindow(this);
111 |
112 | sourcePathIdx = -2;
113 | cycleImageSource(null, null);
114 | }
115 |
116 | private void TN_Unloaded(object sender, RoutedEventArgs e) {
117 | ThumbImageSource = null;
118 | nextSource = null;
119 | cycleTimer.Stop();
120 | cycleTimer.Tick -= cycleImageSource;
121 | }
122 |
123 | private static int workingThreads = 0;
124 |
125 | private async void cycleImageSource(object sender, EventArgs e) {
126 | var tn = this;
127 | tn.cycleTimer.Stop();
128 | //wait if main window is minimized
129 | if (mainWin.WindowState == WindowState.Minimized) {
130 | tn.cycleTimer.Interval = TimeSpan.FromMilliseconds(5000);
131 | tn.cycleTimer.Start();
132 | return;
133 | }
134 |
135 | //wait to get image
136 | if (tn.ObjectInfo.ImageSource == null && !Setting.ImmersionMode &&
137 | (mainWin.tknSrc_LoadThumb != null || Interlocked.CompareExchange(ref workingThreads, 0, 0) >= MaxLoadThreads)) {
138 | tn.cycleTimer.Interval = TimeSpan.FromMilliseconds(100);
139 | tn.cycleTimer.Start();
140 | return;
141 | }
142 |
143 | //dont do anything before or after the lifecycle, or if not loaded in virtualizing panel
144 | if (!tn.IsLoaded) return;
145 | if (tn.ObjectInfo == null) return;
146 |
147 | //load from db if exists (only the first time)
148 | var thumbSize = (SizeInt)Setting.ThumbnailSize;
149 | if ((tn.ObjectInfo.Flags == FileFlags.Directory || tn.ObjectInfo.Flags == FileFlags.Archive) && tn.sourcePathIdx == -2) {
150 | tn.sourcePathIdx = -1;
151 | var cached = await SQLiteHelper.GetFromThumbDBAsync(tn.ObjectInfo.ContainerPath, thumbSize);
152 | if (cached != null) {
153 | tn.ThumbImageSource = cached.Item1;
154 | tn.sourcePathName = cached.Item2;
155 | tn.cycleTimer.Interval = TimeSpan.FromMilliseconds(mainWin.ThumbChangeDelay);
156 | tn.cycleTimer.Start();
157 | return;
158 | }
159 | }
160 | if (tn.sourcePathIdx == -2) tn.sourcePathIdx = -1;
161 |
162 | //actual read files
163 | var cycle = false;
164 | Interlocked.Increment(ref workingThreads);
165 | try {
166 | //update source paths if needed
167 | if (tn.ObjectInfo.SourcePaths == null) {
168 | var objInfo = tn.ObjectInfo;
169 | objInfo.SourcePaths = await GetSourcePathsAsync(objInfo);
170 | }
171 | //get the next path index to use
172 | if (tn.ObjectInfo.SourcePaths?.Length > 1) {
173 | tn.sourcePathIdx = tn.sourcePathIdx == tn.ObjectInfo.SourcePaths.Length - 1 ? 0 : tn.sourcePathIdx + 1;
174 | //make sure the next image is not the same as the cache
175 | if (tn.sourcePathName != null && tn.ObjectInfo.SourcePaths[tn.sourcePathIdx] == tn.sourcePathName) {
176 | tn.sourcePathIdx = tn.sourcePathIdx == tn.ObjectInfo.SourcePaths.Length - 1 ? 0 : tn.sourcePathIdx + 1;
177 | tn.sourcePathName = null;//avoid skipping in the 2nd time
178 | }
179 | cycle = true;
180 | }
181 | else
182 | tn.sourcePathIdx = 0;
183 | tn.ThumbImageSource = await GetImageSourceAsync(tn.ObjectInfo, sourcePathIdx: tn.sourcePathIdx, decodeSize: thumbSize);
184 | }
185 | catch { }
186 | finally {
187 | Interlocked.Decrement(ref workingThreads);
188 | }
189 |
190 | //dont do anything before or after the lifecycle
191 | if (!tn.IsLoaded || !mainWin.IsLoaded || !cycle) return;
192 |
193 | //plan for the next run
194 | tn.cycleTimer.Interval = TimeSpan.FromMilliseconds(mainWin.ThumbChangeDelay);
195 | tn.cycleTimer.Start();
196 | }
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/UserControls/RoundedWindow.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
118 |
--------------------------------------------------------------------------------
/Helpers/NativeHelpers.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.WindowsAPICodePack.ApplicationServices;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Runtime.InteropServices;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 |
10 | namespace ZipImageViewer
11 | {
12 | public static class NativeHelpers
13 | {
14 | #region Monitor Info
15 | ////hide taskbar
16 | //[DllImport("user32.dll")]
17 | //private static extern int FindWindow(string className, string windowText);
18 | //[DllImport("user32.dll")]
19 | //private static extern int ShowWindow(int hwnd, int command);
20 | //private const int SW_HIDE = 0;
21 | //private const int SW_SHOW = 1;
22 |
23 | //public static void HideTaskbar() {
24 | // ShowWindow(FindWindow("Shell_TrayWnd", ""), SW_HIDE);
25 | //}
26 | //public static void ShowTaskbar() {
27 | // ShowWindow(FindWindow("Shell_TrayWnd", ""), SW_SHOW);
28 | //}
29 |
30 |
31 | //get monitor
32 | [StructLayout(LayoutKind.Sequential)]
33 | private struct MonitorInfo
34 | {
35 | public uint cbSize;
36 | public Rect2 rcMonitor;
37 | public Rect2 rcWork;
38 | public uint dwFlags;
39 | }
40 |
41 | [StructLayout(LayoutKind.Sequential)]
42 | private struct Rect2
43 | {
44 | public int left;
45 | public int top;
46 | public int right;
47 | public int bottom;
48 | }
49 |
50 | private const int MONITOR_DEFAULTTONULL = 0;
51 | private const int MONITOR_DEFAULTTOPRIMARY = 1;
52 | private const int MONITOR_DEFAULTTONEAREST = 2;
53 | [DllImport("user32.dll")]
54 | private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int flags);
55 | [DllImport("user32.dll")]
56 | private static extern bool GetMonitorInfo(IntPtr hwnd, ref MonitorInfo mInfo);
57 |
58 | public static Rect GetMonitorFromWindow(Window win) {
59 | var mi = new MonitorInfo();
60 | mi.cbSize = (uint)Marshal.SizeOf(mi);
61 | var hwmon = MonitorFromWindow(new System.Windows.Interop.WindowInteropHelper(win).EnsureHandle(), MONITOR_DEFAULTTONULL);
62 | if (hwmon != null && GetMonitorInfo(hwmon, ref mi)) {
63 | //convert to device-independent vaues
64 | var mon = mi.rcMonitor;
65 | Point realp1;
66 | Point realp2;
67 | var trans = PresentationSource.FromVisual(win).CompositionTarget.TransformFromDevice;
68 | realp1 = trans.Transform(new Point(mon.left, mon.top));
69 | realp2 = trans.Transform(new Point(mon.right, mon.bottom));
70 | return new Rect(realp1, realp2);
71 | }
72 | else
73 | throw new Exception("Failed to get monitor info.");
74 | }
75 | #endregion
76 |
77 | #region Natual Sort
78 | [DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
79 | public static extern int StrCmpLogicalW(string psz1, string psz2);
80 |
81 | public class NaturalStringComparer : IComparer
82 | {
83 | private readonly int modifier = 1;
84 |
85 | public NaturalStringComparer() : this(false) { }
86 | public NaturalStringComparer(bool descending) {
87 | if (descending) modifier = -1;
88 | }
89 |
90 | public int Compare(string a, string b) {
91 | return StrCmpLogicalW(a ?? "", b ?? "") * modifier;
92 | }
93 |
94 | public static int Compare(string a, string b, bool descending = false) {
95 | return StrCmpLogicalW(a ?? "", b ?? "") * (descending ? -1 : 1);
96 | }
97 | }
98 | #endregion
99 |
100 | #region Get File Icon
101 | public static System.Windows.Media.ImageSource GetIcon(string path, bool smallIcon, bool isDirectory) {
102 | // SHGFI_USEFILEATTRIBUTES takes the file name and attributes into account if it doesn't exist
103 | uint flags = SHGFI_ICON | SHGFI_USEFILEATTRIBUTES;
104 | if (smallIcon) flags |= SHGFI_SMALLICON;
105 |
106 | uint attributes = FILE_ATTRIBUTE_NORMAL;
107 | if (isDirectory) attributes |= FILE_ATTRIBUTE_DIRECTORY;
108 |
109 | if (0 != SHGetFileInfo(path, attributes, out SHFILEINFO shfi, (uint)Marshal.SizeOf(typeof(SHFILEINFO)), flags)) {
110 | var source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHIcon(shfi.hIcon, Int32Rect.Empty,
111 | System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
112 | DestroyIcon(shfi.hIcon);
113 | return source;
114 | }
115 | return null;
116 | }
117 |
118 | [StructLayout(LayoutKind.Sequential)]
119 | private struct SHFILEINFO
120 | {
121 | public IntPtr hIcon;
122 | public int iIcon;
123 | public uint dwAttributes;
124 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
125 | public string szDisplayName;
126 | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
127 | public string szTypeName;
128 | }
129 |
130 | [DllImport("shell32")]
131 | private static extern int SHGetFileInfo(string pszPath, uint dwFileAttributes, out SHFILEINFO psfi, uint cbFileInfo, uint flags);
132 |
133 | [DllImport("user32.dll", CharSet = CharSet.Auto)]
134 | private static extern bool DestroyIcon(IntPtr handle);
135 |
136 | private const uint FILE_ATTRIBUTE_READONLY = 0x00000001;
137 | private const uint FILE_ATTRIBUTE_HIDDEN = 0x00000002;
138 | private const uint FILE_ATTRIBUTE_SYSTEM = 0x00000004;
139 | private const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010;
140 | private const uint FILE_ATTRIBUTE_ARCHIVE = 0x00000020;
141 | private const uint FILE_ATTRIBUTE_DEVICE = 0x00000040;
142 | private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;
143 | private const uint FILE_ATTRIBUTE_TEMPORARY = 0x00000100;
144 | private const uint FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200;
145 | private const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400;
146 | private const uint FILE_ATTRIBUTE_COMPRESSED = 0x00000800;
147 | private const uint FILE_ATTRIBUTE_OFFLINE = 0x00001000;
148 | private const uint FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000;
149 | private const uint FILE_ATTRIBUTE_ENCRYPTED = 0x00004000;
150 | private const uint FILE_ATTRIBUTE_VIRTUAL = 0x00010000;
151 |
152 | private const uint SHGFI_ICON = 0x000000100; // get icon
153 | private const uint SHGFI_DISPLAYNAME = 0x000000200; // get display name
154 | private const uint SHGFI_TYPENAME = 0x000000400; // get type name
155 | private const uint SHGFI_ATTRIBUTES = 0x000000800; // get attributes
156 | private const uint SHGFI_ICONLOCATION = 0x000001000; // get icon location
157 | private const uint SHGFI_EXETYPE = 0x000002000; // return exe type
158 | private const uint SHGFI_SYSICONINDEX = 0x000004000; // get system icon index
159 | private const uint SHGFI_LINKOVERLAY = 0x000008000; // put a link overlay on icon
160 | private const uint SHGFI_SELECTED = 0x000010000; // show icon in selected state
161 | private const uint SHGFI_ATTR_SPECIFIED = 0x000020000; // get only specified attributes
162 | private const uint SHGFI_LARGEICON = 0x000000000; // get large icon
163 | private const uint SHGFI_SMALLICON = 0x000000001; // get small icon
164 | private const uint SHGFI_OPENICON = 0x000000002; // get open icon
165 | private const uint SHGFI_SHELLICONSIZE = 0x000000004; // get shell size icon
166 | private const uint SHGFI_PIDL = 0x000000008; // pszPath is a pidl
167 | private const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; // use passed dwFileAttribute
168 | #endregion
169 |
170 | #region Prevent Sleep
171 | [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
172 | private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
173 |
174 | [Flags]
175 | private enum EXECUTION_STATE : uint
176 | {
177 | ES_AWAYMODE_REQUIRED = 0x00000040,
178 | ES_CONTINUOUS = 0x80000000,
179 | ES_DISPLAY_REQUIRED = 0x00000002,
180 | ES_SYSTEM_REQUIRED = 0x00000001
181 | }
182 |
183 | ///
184 | /// Prevent turning screen off and / or sleeping.
185 | ///
186 | /// Set to 2 to configure both ES_DISPLAY_REQUIRED and ES_SYSTEM_REQUIRED flags.
187 | /// Set to 1 to configure ES_SYSTEM_REQUIRED flag only.
188 | /// Set to 0 to clear previous configs.
189 | public static void SetPowerState(int level = 0)
190 | {
191 | if (level > 1)
192 | SetThreadExecutionState(EXECUTION_STATE.ES_DISPLAY_REQUIRED | EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
193 | else if (level == 1)
194 | SetThreadExecutionState(EXECUTION_STATE.ES_SYSTEM_REQUIRED | EXECUTION_STATE.ES_CONTINUOUS);
195 | else
196 | SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
197 | }
198 | #endregion
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/Helpers/ObjectInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Windows.Media;
7 | using SizeInt = System.Drawing.Size;
8 |
9 | namespace ZipImageViewer
10 | {
11 | [System.Diagnostics.DebuggerDisplay("{Flags}: {VirtualPath}")]
12 | public class ObjectInfo : INotifyPropertyChanged
13 | {
14 | public event PropertyChangedEventHandler PropertyChanged;
15 |
16 | private string fileName;
17 | ///
18 | /// For archives, relative path of the file inside the archive. Otherwise name of the file.
19 | ///
20 | public string FileName {
21 | get => fileName;
22 | set {
23 | if (fileName == value) return;
24 | fileName = value;
25 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FileName)));
26 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DisplayName)));
27 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DebugInfo)));
28 | if (Flags.HasFlag(FileFlags.Archive))
29 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(VirtualPath)));
30 | }
31 | }
32 |
33 | ///
34 | /// For archives, full path to the archive file.
35 | /// For directories, full path to the directory.
36 | /// Otherwise full path to the image file.
37 | ///
38 | public string FileSystemPath { get; }
39 |
40 | private FileFlags flags;
41 | ///
42 | /// Indicates the flags of the file. Affects click operations etc.
43 | ///
44 | public FileFlags Flags {
45 | get => flags;
46 | set {
47 | if (flags == value) return;
48 | flags = value;
49 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Flags)));
50 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(VirtualPath)));
51 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContainerPath)));
52 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsContainer)));
53 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DebugInfo)));
54 | }
55 | }
56 |
57 | ///
58 | /// A virtual path used to avoid duplicated paths in a collection for zipped files.
59 | /// For non-zip files, same as FileSystemPath.
60 | ///
61 | public string VirtualPath {
62 | get {
63 | if ((Flags.HasFlag(FileFlags.Archive) && Flags.HasFlag(FileFlags.Image)) ||
64 | (Flags.HasFlag(FileFlags.Directory) && Flags.HasFlag(FileFlags.Image)))
65 | return Path.Combine(FileSystemPath, FileName);
66 | return FileSystemPath;
67 | }
68 | }
69 |
70 | public string DisplayName {
71 | get => FileName;
72 | }
73 |
74 | ///
75 | /// Return the immediate container path. If self is a container, same as FileSystemPath.
76 | ///
77 | public string ContainerPath {
78 | get {
79 | if (Flags.HasFlag(FileFlags.Directory) ||
80 | Flags.HasFlag(FileFlags.Archive))
81 | return FileSystemPath;
82 | else
83 | return Path.GetDirectoryName(FileSystemPath);
84 | }
85 | }
86 |
87 | ///
88 | /// Whether the target is a container (folder or archive).
89 | ///
90 | public bool IsContainer =>
91 | Flags.HasFlag(FileFlags.Directory) ||
92 | (Flags.HasFlag(FileFlags.Archive) && !Flags.HasFlag(FileFlags.Image));
93 |
94 | private string[] sourcePaths;
95 | ///
96 | /// Contains the child items. Null indicates the children are not retrived yet.
97 | /// For containers, the paths relative to the container's path.
98 | /// For images, this should be a single-element array with the image's FileName in it.
99 | ///
100 | public string[] SourcePaths {
101 | get => sourcePaths;
102 | set {
103 | if (sourcePaths == value) return;
104 | sourcePaths = value;
105 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SourcePaths)));
106 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DebugInfo)));
107 | }
108 | }
109 |
110 | private ImageSource imageSource;
111 | ///
112 | /// Be careful when setting this property. Clean up when necessary to avoid memory leaks.
113 | ///
114 | public ImageSource ImageSource {
115 | get => imageSource;
116 | set {
117 | if (imageSource == value) return;
118 | imageSource = value;
119 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageSource)));
120 | }
121 | }
122 |
123 | [System.Diagnostics.DebuggerBrowsable(System.Diagnostics.DebuggerBrowsableState.Never)]
124 | public string DebugInfo {
125 | get {
126 | return $"{nameof(FileName)}: {FileName}\r\n" +
127 | $"{nameof(FileSystemPath)}: {FileSystemPath}\r\n" +
128 | $"{nameof(Flags)}: {Flags.ToString()}\r\n" +
129 | $"{nameof(SourcePaths)}: {SourcePaths?.Length}\r\n" +
130 | $"{nameof(VirtualPath)}: {VirtualPath}\r\n" +
131 | $"{nameof(DisplayName)}: {DisplayName}\r\n" +
132 | $"{nameof(Comments)}:\r\n{Comments}";
133 | }
134 | }
135 |
136 | private string comments;
137 | public string Comments {
138 | get => comments;
139 | set {
140 | if (comments == value) return;
141 | comments = value;
142 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Comments)));
143 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DebugInfo)));
144 | }
145 | }
146 |
147 | ///
148 | /// Setting when you can otherwise it will be set based on FileSystemPath if it's not an archive.
149 | ///
150 | public ObjectInfo(string fsPath, FileFlags flag, string fName = null) {
151 | FileSystemPath = fsPath;
152 | flags = flag;
153 | fileName = fName;
154 | if (fileName == null && !flags.HasFlag(FileFlags.Archive))
155 | fileName = Path.GetFileName(FileSystemPath);
156 | }
157 | }
158 |
159 | public class ImageInfo : INotifyPropertyChanged
160 | {
161 | public event PropertyChangedEventHandler PropertyChanged;
162 |
163 | private long fileSize;
164 | public long FileSize {
165 | get => fileSize;
166 | set {
167 | if (fileSize == value) return;
168 | fileSize = value;
169 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FileSize)));
170 | }
171 | }
172 |
173 | private SizeInt dimensions;
174 | public SizeInt Dimensions {
175 | get => dimensions;
176 | set {
177 | if (dimensions == value) return;
178 | dimensions = value;
179 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Dimensions)));
180 | }
181 | }
182 |
183 | private DateTime created;
184 | public DateTime Created {
185 | get => created;
186 | set {
187 | if (created == value) return;
188 | created = value;
189 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Created)));
190 | }
191 | }
192 |
193 | private DateTime modified;
194 | public DateTime Modified {
195 | get => modified;
196 | set {
197 | if (modified == value) return;
198 | modified = value;
199 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Modified)));
200 | }
201 | }
202 |
203 | private string meta_DateTaken;
204 | public string Meta_DateTaken {
205 | get => meta_DateTaken;
206 | set {
207 | if (meta_DateTaken == value) return;
208 | meta_DateTaken = value;
209 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Meta_DateTaken)));
210 | }
211 | }
212 |
213 | private string meta_Camera;
214 | public string Meta_Camera {
215 | get => meta_Camera;
216 | set {
217 | if (meta_Camera == value) return;
218 | meta_Camera = value;
219 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Meta_Camera)));
220 | }
221 | }
222 |
223 | private string meta_applicationName;
224 | public string Meta_ApplicationName {
225 | get => meta_applicationName;
226 | set {
227 | if (meta_applicationName == value) return;
228 | meta_applicationName = value;
229 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Meta_ApplicationName)));
230 | }
231 | }
232 |
233 |
234 | public ImageInfo() { }
235 |
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/SlideshowWindow.xaml:
--------------------------------------------------------------------------------
1 |
8 |
172 |
173 |
--------------------------------------------------------------------------------
/SlideshowWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Windows;
5 | using System.Windows.Controls;
6 | using System.Windows.Media;
7 | using System.Windows.Media.Animation;
8 | using System.Windows.Threading;
9 | using SizeInt = System.Drawing.Size;
10 | using static ZipImageViewer.Helpers;
11 | using static ZipImageViewer.SlideshowHelper;
12 | using static ZipImageViewer.LoadHelper;
13 | using System.Threading.Tasks;
14 | using System.IO;
15 |
16 | namespace ZipImageViewer
17 | {
18 | public partial class SlideshowWindow : BorderlessWindow
19 | {
20 | private readonly string basePath;
21 | private readonly DispatcherTimer animTimer;
22 | private readonly SlideAnimConfig animConfig;
23 |
24 | private DpiImage currImage;
25 | private Rect lastRect;
26 |
27 | private (int objIdx, int subIdx) index = (0, 0);
28 | private ObjectInfo[] objectList;
29 |
30 |
31 | ///
32 | /// path will be used to get images from.
33 | ///
34 | public SlideshowWindow(string path) {
35 | InitializeComponent();
36 |
37 | basePath = path;
38 | animConfig = Setting.SlideAnimConfig;
39 | animTimer = new DispatcherTimer(DispatcherPriority.Normal, Application.Current.Dispatcher);
40 | animTimer.Tick += AnimTick;
41 |
42 | B_ControlPanel.Loaded += B_ControlPanel_Loaded;
43 | }
44 |
45 | private void B_ControlPanel_Loaded(object sender, RoutedEventArgs e) {
46 | //simple useless animation that cannot be done easily in xaml achieving the same effect
47 | var b = (Border)sender;
48 | var a = new DoubleAnimation(0d, new Duration(new TimeSpan(0, 0, 1))) {
49 | BeginTime = new TimeSpan(0, 0, 3),
50 | FillBehavior = FillBehavior.Stop,
51 | };
52 | a.Completed += (o1, e1) => {
53 | b.Opacity = 0d;
54 | b.Loaded -= B_ControlPanel_Loaded;
55 | a = null;
56 | };
57 | b.BeginAnimation(OpacityProperty, a);
58 | }
59 |
60 | private async void AnimConfig_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) {
61 | if (e.PropertyName != nameof(animConfig.RandomOrder)) return;
62 | if (objectList == null || objectList.Length == 0) return;
63 |
64 | //wait for AnimTick
65 | while (!animTimer.IsEnabled) await Task.Delay(50);
66 |
67 | //restart slideshow
68 | animTimer.Stop();
69 | index.subIdx = 0;
70 | index.objIdx = 0;
71 | objectList = GetContainers(basePath)?.ToArray();
72 | if (objectList == null || objectList.Length == 0) return;
73 |
74 | if (animConfig.RandomOrder && objectList.Length > 1) objectList.Shuffle();
75 | animTimer.Start();
76 | }
77 |
78 | private void SlideWin_Loaded(object sender, RoutedEventArgs e) {
79 | //get image to use
80 | objectList = GetContainers(basePath)?.ToArray();
81 |
82 | if (objectList == null || objectList.Length == 0) {
83 | MessageBox.Show(GetRes("msg_NoImgAndCtnrFound", basePath), string.Empty, MessageBoxButton.OK, MessageBoxImage.Exclamation);
84 | Close();
85 | return;
86 | }
87 |
88 | //subscribing to RandomOrder property change
89 | animConfig.PropertyChanged += AnimConfig_PropertyChanged;
90 |
91 | //all first shuffle. This only shuffles on the first level (images within archives are not shuffled)
92 | if (animConfig.RandomOrder && objectList.Length > 1) objectList.Shuffle();
93 |
94 | //minimize other app windows
95 | foreach (var win in Application.Current.Windows.Cast()) {
96 | if (win is MainWindow || win is ViewWindow) win.WindowState = WindowState.Minimized;
97 | }
98 |
99 | //fullscreen
100 | SwitchFullScreen(this, ref lastRect, true);
101 | Topmost = true;
102 | #if DEBUG
103 | Width = 640d;
104 | Height = 400d;
105 | #endif
106 |
107 | //start
108 | AnimTick(null, null);
109 |
110 | //prevent screen off and sleep
111 | NativeHelpers.SetPowerState(2);
112 | }
113 |
114 | private void SlideWin_Closing(object sender, System.ComponentModel.CancelEventArgs e) {
115 | NativeHelpers.SetPowerState(0);
116 | animConfig.PropertyChanged -= AnimConfig_PropertyChanged;
117 | animTimer.Stop();
118 | Topmost = false;
119 | }
120 |
121 | private void SlideWin_Closed(object sender, EventArgs e) {
122 | //reopen app windows
123 | foreach (var win in Application.Current.Windows.Cast()) {
124 | if ((win is MainWindow || win is ViewWindow) && win.WindowState == WindowState.Minimized) win.WindowState = WindowState.Normal;
125 | }
126 | ShutdownCheck();
127 | }
128 |
129 | private void Btn_Preset_Click(object sender, RoutedEventArgs e) {
130 | switch (animConfig.Transition) {
131 | case SlideTransition.KenBurns:
132 | animConfig.FadeInDuration = new TimeSpan(0, 0, 2);
133 | animConfig.FadeOutDuration = new TimeSpan(0, 0, 2);
134 | animConfig.ImageDuration = new TimeSpan(0, 0, 7);
135 | animConfig.XPanDistanceR = 0.5;
136 | animConfig.YPanDistanceR = 0.5;
137 | animConfig.YPanDownOnly = true;
138 | break;
139 | case SlideTransition.Breath:
140 | animConfig.FadeInDuration = TimeSpan.FromMilliseconds(1500);
141 | animConfig.FadeOutDuration = TimeSpan.FromMilliseconds(1500);
142 | break;
143 | case SlideTransition.Emerge:
144 | animConfig.lastBool1 = ran.Next(2) == 0;
145 | animConfig.FadeInDuration = new TimeSpan(0, 0, 2);
146 | animConfig.FadeOutDuration = TimeSpan.FromMilliseconds(1500);
147 | break;
148 | }
149 | }
150 |
151 |
152 | private void B_ControlPanel_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e) {
153 | var border = (Border)sender;
154 | border.MoveFocus(new System.Windows.Input.TraversalRequest(System.Windows.Input.FocusNavigationDirection.Next));
155 | }
156 |
157 |
158 | private async void AnimTick(object sender, EventArgs e) {
159 | animTimer.Stop();
160 | if (!IsLoaded) return;
161 |
162 | ImageSource nextSrc = null;
163 |
164 | //convert screen size to physical size
165 | var dpi = VisualTreeHelper.GetDpi(this);
166 | var decodeSize = new SizeInt(Convert.ToInt32(canvas.ActualWidth * dpi.DpiScaleX * animConfig.ResolutionScale),
167 | Convert.ToInt32(canvas.ActualHeight * dpi.DpiScaleY * animConfig.ResolutionScale));
168 |
169 | //calculate index and shuffle in the end
170 | var currObj = objectList[index.objIdx];
171 | switch (currObj.Flags) {
172 | case FileFlags.Archive:
173 | case FileFlags.Directory:
174 | if (currObj.SourcePaths == null || currObj.SourcePaths.Length == 0) {
175 | currObj.SourcePaths = await GetSourcePathsAsync(currObj);
176 | if (currObj.SourcePaths == null || currObj.SourcePaths.Length == 0) {
177 | index.objIdx = index.objIdx == objectList.Length - 1 ? 0 : index.objIdx + 1;
178 | break;
179 | }
180 | //container first shuffle
181 | if (animConfig.RandomOrder && currObj.SourcePaths.Length > 1) currObj.SourcePaths.Shuffle();
182 | }
183 | nextSrc = await GetImageSourceAsync(currObj, sourcePathIdx: index.subIdx, decodeSize: decodeSize);
184 | Title = Path.Combine(currObj.ContainerPath, currObj.SourcePaths[index.subIdx]);
185 | index.subIdx++;
186 | if (index.subIdx >= currObj.SourcePaths.Length) {
187 | index.subIdx = 0;
188 | //container end shuffle
189 | if (animConfig.RandomOrder && currObj.SourcePaths.Length > 1) currObj.SourcePaths.Shuffle();
190 | index.objIdx = index.objIdx == objectList.Length - 1 ? 0 : index.objIdx + 1;
191 | }
192 | break;
193 | //case FileFlags.Image:
194 | // nextSrc = await GetImageSourceAsync(currObj.FileSystemPath, decodeSize);
195 | // Title = Path.Combine(currObj.ContainerPath, currObj.SourcePaths[index.subIdx]);
196 | // index.objIdx = index.objIdx == objectList.Length - 1 ? 0 : index.objIdx + 1;
197 | // break;
198 | }
199 | if (animConfig.RandomOrder && index.objIdx == 0 && index.subIdx == 0 && objectList.Length > 1)
200 | objectList.Shuffle();//all end shuffle
201 |
202 | if (nextSrc != null) {
203 | //switch target
204 | if (currImage == IM0) {
205 | Panel.SetZIndex(IM0, 8);
206 | Panel.SetZIndex(IM1, 9);
207 | currImage = IM1;
208 | }
209 | else {
210 | Panel.SetZIndex(IM0, 9);
211 | Panel.SetZIndex(IM1, 8);
212 | currImage = IM0;
213 | }
214 |
215 | currImage.Source = nextSrc;
216 | animTimer.Interval = AnimateImage(currImage, new Size(canvas.ActualWidth, canvas.ActualHeight), animConfig);
217 | }
218 | else {
219 | animTimer.Interval = TimeSpan.FromMilliseconds(50);
220 | }
221 |
222 | animTimer.Start();
223 | }
224 |
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/Resources/Localization.zh.xaml:
--------------------------------------------------------------------------------
1 |
5 | 应用启动中
6 | 初始化
7 | 正在载入
8 | 正在检查
9 | 设置
10 | 缩略图尺寸
11 |
12 | 宽 x 高(像素)。
13 | 更改此值会使所有当前尺寸的缩略图失效。
14 | 使用JPEG缩略图格式可大幅减少数据库文件大小,但质量会有所降低。更改格式不会刷新缓存中已有的缩略图。
15 |
16 | 缩略图格式
17 | 缩略图切换延迟速率
18 | 目录和压缩文件缩略图的切换延迟。数值越高延迟越高。
19 | 自定义图片扩展名
20 |
21 | 定义要作为图像加载的文件扩展名,以空格分隔。
22 | 图片格式是否支持取决于操作系统。
23 | 需要重启程序生效。
24 | 例如:.abc .def .ghi
25 |
26 | 数据库目录
27 | 存放数据库文件(缩略图缓存数据库等)的目录
28 | 缩略图缓存大小 (当前: {0})
29 | 无限制
30 | 缩略图缓存的最大文件尺寸。设置为无限制会解除软件层面的限制。
31 | 添加“播放幻灯片”选项到资源管理器菜单
32 | 在Windows资源管理器右键菜单中添加“播放幻灯片”选项。
33 | 注意如果开启此项之后移动了程序文件,你需要关闭再开启此项,否则菜单项会指向旧的位置。
34 | 用ZipImageViewer播放幻灯片
35 | 用ZipImageViewer打开
36 | 简洁模式
37 | 禁用多数影响界面性能的特效。
38 | 查看器背景
39 | 查看器过渡效果
40 | 过渡效果速度
41 | 查看器中使用的过渡效果。
42 | 缓存视图(第一张)
43 | 缓存视图(所有)
44 | 缓存此项(第一张)
45 | 缓存此项(所有)
46 |
47 |
48 | 缓存当前视图下的图像,还有每个文件夹或压缩文件中的第一个图像。
49 |
50 |
51 |
52 | 缓存当前视图下的图像,还有所有目录和压缩文件内的图像。
53 |
54 | 重新载入视图
55 | 移动
56 | 更改
57 | 目标文件已存在:{0}。
58 | 清空
59 |
60 | 无
61 | 随机
62 | 缩放 + 淡入淡出
63 | 缩放 + 淡入淡出 + 模糊
64 | 淡入淡出
65 | 水平滑动
66 | 快
67 | 中
68 | 慢
69 | 暗色棋盘
70 | 暗色线性渐变
71 | 黑色
72 | 灰色
73 | 白色
74 | Ken Burns
75 | 呼吸
76 | 浮现
77 |
78 | 主密码
79 | 更新{0}
80 | 当前{0}
81 | 新{0}
82 | 确认{0}
83 | 更改{0}
84 | 正在处理{0}...
85 | 密码不正确。
86 | 密码不一致。
87 |
88 | 主密码用于加密保存的密码。
89 | 如果主密码丢失,已保存的密码将无法恢复!
90 |
91 | 保存的密码
92 | 备用密码
93 | 密码字典
94 |
95 | 压缩文件使用的密码。
96 | 备用密码用于读取密码保护的压缩文件。匹配过的密码会保存在密码字典里。
97 |
98 | 自定义命令
99 | 名称
100 | 程序路径
101 | 参数
102 | (输入数据)
103 | 密码
104 | 路径
105 |
106 | 右键菜单中使用的自定义命令,每行一个。
107 | %FileSystemPath% - 当前文件或目录的路径
108 |
109 | 数据库文件移动完成。
110 | 数据库路径已更新。
111 | 操作完成
112 | 操作失败
113 | 请稍后...
114 | 处理中...
115 | 打开文件夹
116 | 选项
117 | 沉浸模式
118 | 关闭窗口
119 | {0:yyyy-MM-dd HH:mm:ss} (创建)
120 | {0:yyyy-MM-dd HH:mm:ss} (修改)
121 | {0} (拍摄)
122 | 用默认程序打开
123 | 在资源管理器中打开
124 | 在新窗口中打开
125 | 开始放映幻灯片
126 | 压缩文件密码
127 | 添加到备用密码
128 | 对陌生加密压缩文件会尝试备用密码用来解压。
129 | 确定
130 | 取消
131 | 随机顺序
132 | 过渡效果
133 | 过渡效果延迟
134 | 分辨率比例
135 | 图像时长
136 | 渐入时长
137 | 渐出时长
138 | 模糊效果
139 | 水平平移缩减
140 | 垂直平移缩减
141 | 垂直平移仅向下
142 | 重置效果默认值
143 | 在{0}下没有找到可用图像、文件夹或压缩文件。
144 | 没有了!
145 | 参数启动错误
146 | 程序启动错误
147 | 打开数据库文件{0}时发生错误。
148 | 载入{0}时发生错误。已跳过加载该项。
149 | 解压失败。密码错误或者图像格式不支持。
150 |
151 | 感谢使用ZipImageViewer!
152 |
153 | 此软件由Carl Chang (changbowen@msn.cn)编写。
154 |
155 | 完整源代码在GitHub [https://github.com/changbowen/ZipImageViewer]。
156 |
157 |
158 | 鼠标操作说明:
159 | 在缩略图上
160 | 左键 - 打开文件夹或图片
161 | 右键 - 显示上下文菜单
162 | 中键 - 在新窗口打开文件夹或图片
163 | 在浏览界面的空白区域
164 | 双击 - 显示打开文件夹对话框 (P.S. 也可以直接拖动文件或文件夹到浏览窗口)
165 | 右键 - 转到父目录
166 | 中键 - 关闭当前窗口
167 | 在查看器里
168 | 双击 - 切换缩放
169 | 右键 - 关闭当前窗口
170 | 中键 - 关闭当前窗口
171 | 滚轮 - 以鼠标位置为中心缩放
172 | 拖动 - 平移
173 |
174 | 有新版本可用。
175 | 当前版本: {0}
176 | 新版本: {1}
177 | 点击确定以打开发布页面。
178 |
--------------------------------------------------------------------------------
/ViewWindow.xaml.cs.notworking:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using System.Windows;
9 | using System.Windows.Controls;
10 | using System.Windows.Data;
11 | using System.Windows.Documents;
12 | using System.Windows.Input;
13 | using System.Windows.Media;
14 | using System.Windows.Media.Animation;
15 | using System.Windows.Media.Imaging;
16 | using System.Windows.Shapes;
17 | using FontAwesome.WPF;
18 | using static ZipImageViewer.ExtentionMethods;
19 |
20 | namespace ZipImageViewer
21 | {
22 | public class CenterConterter : IMultiValueConverter
23 | {
24 | public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
25 | {
26 | if (values == null || values.Length < 2)
27 | throw new ArgumentException("Two double values need to be passed in this order -> totalWidth, width", nameof(values));
28 |
29 | var totalWidth = (double)values[0];
30 | var width = (double)values[1];
31 | return (totalWidth - width) / 2;
32 | }
33 |
34 | public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
35 | {
36 | throw new NotImplementedException();
37 | }
38 | }
39 |
40 | public partial class ViewWindow : Window
41 | {
42 | public ImageInfo ImageInfo
43 | {
44 | get { return (ImageInfo)GetValue(ImageInfoProperty); }
45 | set { SetValue(ImageInfoProperty, value); }
46 | }
47 | public static readonly DependencyProperty ImageInfoProperty =
48 | Thumbnail.ImageInfoProperty.AddOwner(typeof(ViewWindow));
49 |
50 | private double Scale = 1d;
51 |
52 | private bool IsRealSize =>
53 | IM.RealSize.Width.Equals(IM.ActualWidth * Scale) && IM.RealSize.Height.Equals(IM.ActualHeight * Scale);
54 |
55 | public ViewWindow()
56 | {
57 | InitializeComponent();
58 | }
59 |
60 | private void ViewWindow_Loaded(object sender, RoutedEventArgs e) {
61 | // var rect = NativeMethods.GetMonitorFromWindow(this);
62 | // Top = rect.Top;
63 | // Left = rect.Left;
64 | // Width = rect.Width;
65 | // Height = rect.Height;
66 | // if (double.IsNaN(IM.Width)) IM.Width = IM.ActualWidth;
67 | // if (double.IsNaN(IM.Height)) IM.Height = IM.ActualHeight;
68 |
69 | SwitchRealSize();
70 | }
71 |
72 | private void ViewWin_MouseUp(object sender, MouseButtonEventArgs e) {
73 | if (e.ChangedButton == MouseButton.Right) Close();
74 | }
75 |
76 | private void Transform(int ms, Size newSize, Point center, Matrix? fromMatrix = null) {
77 | Transform(ms, newSize.Width / IM.RealSize.Width, center, fromMatrix);
78 | }
79 |
80 |
81 | private void Transform(int ms, double scale, Point center, Matrix? fromMatrix = null) {
82 | var matrix = fromMatrix ?? IM_RT.Matrix;
83 | matrix.ScaleAtPrepend(scale, scale, center.X, center.Y);
84 | var anim = new MatrixAnimation(IM_RT.Value, matrix, new Duration(TimeSpan.FromMilliseconds(ms)));
85 | IM_RT.BeginAnimation(MatrixTransform.MatrixProperty, anim);
86 | Scale *= scale;
87 | BM.Show($"{Scale:P0}");
88 | }
89 |
90 | private void SwitchRealSize(Point? mousePosition = null) {
91 | if (IsRealSize) {
92 | if (IM.ActualHeight * Scale <= CA.ActualHeight && IM.ActualWidth * Scale <= CA.ActualWidth) return;
93 | var newSize = Helpers.UniformScaleUp(IM.ActualWidth, IM.ActualHeight, CA.ActualWidth, CA.ActualHeight);
94 | Transform(400, newSize, new Point(0, 0));
95 | }
96 | else {
97 | if (!mousePosition.HasValue)
98 | mousePosition = new Point(IM.ActualWidth / 2d, IM.ActualHeight / 2d);
99 | Transform(400, 1d, mousePosition.Value);
100 | }
101 | }
102 |
103 | /*Point scrollMousePoint;
104 | double hOff = 1;
105 | double vOff = 1;
106 | private void SV_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
107 | {
108 | scrollMousePoint = e.GetPosition(SV);
109 | hOff = SV.HorizontalOffset;
110 | vOff = SV.VerticalOffset;
111 | SV.CaptureMouse();
112 | }
113 |
114 | private void SV_PreviewMouseMove(object sender, MouseEventArgs e)
115 | {
116 | if(SV.IsMouseCaptured)
117 | {
118 | SV.ScrollToHorizontalOffset(hOff + 3d * (scrollMousePoint.X - e.GetPosition(SV).X));
119 | SV.ScrollToVerticalOffset(vOff + 3d * (scrollMousePoint.Y - e.GetPosition(SV).Y));
120 | }
121 | }
122 |
123 | private void SV_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
124 | {
125 | SV.ReleaseMouseCapture();
126 | }
127 |
128 | private void SV_PreviewMouseDoubleClick(object sender, MouseButtonEventArgs e) {
129 | if (IsActualSize) {
130 | if (IM.ActualHeight <= SV.ViewportHeight && IM.ActualWidth <= SV.ViewportWidth) return;
131 | var newSize = Helpers.UniformScaleUp(IM.ActualWidth, IM.ActualHeight, SV.ViewportWidth, SV.ViewportHeight);
132 |
133 | IM.AnimateDoubleCubicEase(WidthProperty, newSize.Width, 400, EasingMode.EaseOut);
134 | IM.AnimateDoubleCubicEase(HeightProperty, newSize.Height, 400, EasingMode.EaseOut);
135 | IsActualSize = false;
136 | }
137 | else {
138 | IM.AnimateDoubleCubicEase(WidthProperty, IM.RealSize.Width, 400, EasingMode.EaseOut);
139 | IM.AnimateDoubleCubicEase(HeightProperty, IM.RealSize.Height, 400, EasingMode.EaseOut);
140 | // SV.ScrollToHorizontalOffset((SV.ViewportWidth - SV.ExtentWidth) / 2);
141 | // SV.ScrollToVerticalOffset((SV.ViewportHeight - SV.ExtentHeight) / 2);
142 | // SV.UpdateLayout();
143 | IsActualSize = true;
144 | }
145 |
146 | }
147 |
148 | private void SV_PreviewMouseWheel(object sender, MouseWheelEventArgs e) {
149 | IsActualSize = false;
150 | var scale = e.Delta > 0 ? 1.2d : 0.8d;
151 |
152 | //prevent zooming out smaller than viewport
153 | var newSize = Helpers.UniformScaleDown(IM.ActualWidth * scale, IM.ActualHeight * scale,
154 | SV.ViewportWidth, SV.ViewportHeight);
155 |
156 | IM.AnimateDoubleCubicEase(WidthProperty, newSize.Width, 100, EasingMode.EaseOut);
157 | IM.AnimateDoubleCubicEase(HeightProperty, newSize.Height, 100, EasingMode.EaseOut);
158 |
159 | e.Handled = true;
160 | // IM.Width *= scale;
161 | // IM.Height *= scale;
162 | //
163 | // var mousePos = e.GetPosition(SV);
164 | // var scale = e.Delta > 0 ? 0.1d : -0.1d;
165 | //// Console.WriteLine(mousePos.X / SV.ViewportWidth);
166 | //// SV.ScrollToHorizontalOffset(SV.ExtentWidth * mousePos.X / SV.ViewportWidth);
167 | //// SV.ScrollToVerticalOffset(SV.ExtentHeight * mousePos.Y / SV.ViewportHeight);
168 | // var st = (ScaleTransform)IM.LayoutTransform;
169 | // st.CenterX = IM.ActualWidth * mousePos.X / SV.ViewportWidth;
170 | // st.CenterY = IM.ActualHeight * mousePos.Y / SV.ViewportHeight;
171 | // st.ScaleX += scale;
172 | // st.ScaleY += scale;
173 |
174 | // var mousePos = e.GetPosition(SV);
175 | // var scale = e.Delta > 0 ? 0.1d : -0.1d;
176 | // var st = (ScaleTransform)IM.LayoutTransform;
177 | // st.ScaleX += scale;
178 | // st.ScaleY += scale;
179 | }*/
180 | private Point mouseCapturePoint;
181 | private Matrix existingTranslate;
182 |
183 | private void CA_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
184 | if (e.ClickCount == 1) {
185 | mouseCapturePoint = e.GetPosition(CA);
186 | existingTranslate = IM_RT.Matrix;
187 | CA.CaptureMouse();
188 | }
189 | else if (e.ClickCount == 2) {
190 | SwitchRealSize(e.GetPosition(IM));
191 | }
192 | }
193 |
194 | private void CA_PreviewMouseMove(object sender, MouseEventArgs e)
195 | {
196 | if (CA.IsMouseCaptured)
197 | {
198 | // Console.WriteLine(mouseCapturePoint.X - e.GetPosition(CA).X);
199 | IM_RT.BeginAnimation(MatrixTransform.MatrixProperty, null);
200 | IM_RT.Matrix.Translate(
201 | existingTranslate.OffsetX + (e.GetPosition(CA).X - mouseCapturePoint.X) * Scale * 2d,
202 | existingTranslate.OffsetY + (e.GetPosition(CA).Y - mouseCapturePoint.Y) * Scale * 2d);
203 | }
204 | }
205 |
206 | private void CA_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
207 | {
208 | CA.ReleaseMouseCapture();
209 | }
210 |
211 |
212 | private void CA_PreviewMouseWheel(object sender, MouseWheelEventArgs e) {
213 | var scale = e.Delta > 0 ? 1.2d : 1d / 1.2d;
214 |
215 | //prevent zooming out smaller than viewport
216 | var maxSize = new Size(Math.Min(IM.RealSize.Width, CA.ActualWidth), Math.Min(IM.RealSize.Height, CA.ActualHeight));
217 | var newSize = Helpers.UniformScaleDown(IM.ActualWidth * scale, IM.ActualHeight * scale, maxSize.Width, maxSize.Height);
218 | //resize and move according to mouse position
219 | var position = e.GetPosition(IM);
220 | var transform = IM.RenderTransform as MatrixTransform;
221 | var matrix = transform.Matrix;
222 | matrix.ScaleAtPrepend(scale, scale, position.X, position.Y);
223 | transform.Matrix = matrix;
224 | // Transform(100, newSize, new Point(e.GetPosition(CA).X * (scale - 1d), e.GetPosition(CA).Y * (scale - 1d)));
225 | }
226 |
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
40 |
41 |
42 |
44 |
46 |
47 |
48 |
49 |
50 |
51 |
53 |
54 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
--------------------------------------------------------------------------------
/UserControls/Thumbnail.xaml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
26 |
28 |
30 |
31 |
32 |
34 |
36 |
38 |
39 |
40 |
41 |
42 |
44 |
46 |
48 |
50 |
51 |
52 |
54 |
56 |
58 |
60 |
61 |
62 |
63 |
64 |
66 |
68 |
70 |
72 |
73 |
74 |
76 |
78 |
80 |
82 |
83 |
84 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
126 |
129 |
130 |
131 |
132 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
--------------------------------------------------------------------------------
/Helpers/SQLiteHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Data;
4 | using System.Data.SQLite;
5 | using System.IO;
6 | using System.Linq;
7 | using System.Threading;
8 | using System.Windows;
9 | using System.Windows.Media;
10 | using System.Windows.Media.Imaging;
11 | using SizeInt = System.Drawing.Size;
12 | using static ZipImageViewer.Helpers;
13 | using static ZipImageViewer.TableHelper;
14 | using System.Threading.Tasks;
15 |
16 | namespace ZipImageViewer
17 | {
18 | internal static class SQLiteHelper
19 | {
20 | internal static readonly Dictionary Tables =
21 | new Dictionary() {
22 | { Table.Thumbs, new TableInfo(Table.Thumbs) },
23 | { Table.MappedPasswords, new TableInfo(Table.MappedPasswords) },
24 | { Table.FallbackPasswords, new TableInfo(Table.FallbackPasswords) },
25 | };
26 |
27 | ///
28 | /// Returns an array of objects containing the return value from each Func.
29 | /// Errors in callbacks will be ignored and the next callback will be executed;
30 | ///
31 | internal static object[] Execute(Table t, params Func[] callbackFuncs) {
32 | SQLiteConnection con = null;
33 | var affected = new object[callbackFuncs.Length];
34 | var table = Tables[t];
35 |
36 | Monitor.Enter(table.Lock);
37 | try {
38 | con = new SQLiteConnection($"Data Source={table.FullPath};Version=3;");
39 | con.Open();
40 | for (int i = 0; i < callbackFuncs.Length; i++) {
41 | try { affected[i] = callbackFuncs[i].Invoke(table, con); }
42 | catch { }
43 | }
44 | }
45 | catch (Exception ex) {
46 | MessageBox.Show(GetRes("msg_ErrorOpenDbFile", table.FullPath) + $"\r\n{ex.Message}", string.Empty, MessageBoxButton.OK, MessageBoxImage.Error);
47 | }
48 | finally {
49 | if (con != null) {
50 | con.Close();
51 | con.Dispose();
52 | }
53 | Monitor.Exit(table.Lock);
54 | }
55 | return affected;
56 | }
57 |
58 | internal static void CheckThumbsDB() {
59 | var goodColumns = 0;
60 | Execute(Table.Thumbs, (table, con) => {
61 | using (var cmd = new SQLiteCommand(con)) {
62 | cmd.CommandText = $@"pragma table_info({table.Name})";
63 | using (var r = cmd.ExecuteReader()) {
64 | while (r.Read()) {
65 | switch (r["name"]) {
66 | case nameof(Column.BasePath):
67 | if (r["type"].ToString() == "TEXT" &&
68 | r["notnull"].ToString() == "1") goodColumns += 1;
69 | break;
70 | case nameof(Column.SubPath):
71 | if (r["type"].ToString() == "TEXT" &&
72 | r["notnull"].ToString() == "1") goodColumns += 1;
73 | break;
74 | case nameof(Column.DecodeWidth):
75 | case nameof(Column.DecodeHeight):
76 | if ((string)r["type"] == "INTEGER") goodColumns += 1;
77 | break;
78 | case nameof(Column.ThumbData):
79 | if ((string)r["type"] == "BLOB") goodColumns += 1;
80 | break;
81 | }
82 | }
83 | return 0;
84 | }
85 | }
86 | });
87 | if (goodColumns == 5) return;
88 |
89 | //recreate thumbs table
90 | if (File.Exists(Tables[Table.Thumbs].FullPath))
91 | File.Delete(Tables[Table.Thumbs].FullPath);
92 | else
93 | Directory.CreateDirectory(Setting.DatabaseDir);
94 |
95 | Execute(Table.Thumbs, (table, con) => {
96 | using (var cmd = new SQLiteCommand(con)) {
97 | cmd.CommandText =
98 | $@"create table if not exists [{table.Name}] (
99 | [{Column.BasePath}] TEXT NOT NULL,
100 | [{Column.SubPath}] TEXT NOT NULL,
101 | [{Column.DecodeWidth}] INTEGER,
102 | [{Column.DecodeHeight}] INTEGER,
103 | [{Column.ThumbData}] BLOB)";
104 | return cmd.ExecuteNonQuery();
105 | }
106 | });
107 | }
108 |
109 | internal static int AddToThumbDB(ImageSource source, string basePath, string subPath, SizeInt decodeSize) {
110 | if (!(source is BitmapSource bs)) throw new NotSupportedException();
111 |
112 | object[] affected = null;
113 | byte[] bytes;
114 | BitmapEncoder enc;
115 | switch (Setting.ThumbnailFormat) {
116 | case Setting.ThumbnailFormats.Jpeg:
117 | default:
118 | enc = new JpegBitmapEncoder() { QualityLevel = 85 };
119 | break;
120 | case Setting.ThumbnailFormats.Png:
121 | enc = new PngBitmapEncoder();
122 | break;
123 | }
124 | enc.Frames.Add(BitmapFrame.Create(bs));
125 | using (var ms = new MemoryStream()) {
126 | enc.Save(ms);
127 | bytes = ms.ToArray();
128 | }
129 | if (bytes.Length == 0) return 0;
130 |
131 | affected = Execute(Table.Thumbs, (table, con) => {
132 | using (var cmd = new SQLiteCommand(con)) {
133 | //remove existing
134 | cmd.CommandText =
135 | $@"delete from {table.Name} where
136 | {Column.BasePath} = @basePath and
137 | {Column.SubPath} = @subPath";
138 | cmd.Parameters.Add(new SQLiteParameter("@basePath", DbType.String) { Value = basePath });
139 | cmd.Parameters.Add(new SQLiteParameter("@subPath", DbType.String) { Value = subPath });
140 | cmd.ExecuteNonQuery();
141 | //insert new
142 | cmd.CommandText =
143 | $@"insert into {table.Name}
144 | ({Column.BasePath}, {Column.SubPath}, {Column.DecodeWidth}, {Column.DecodeHeight}, {Column.ThumbData}) values
145 | (@basePath, @subPath, {decodeSize.Width}, {decodeSize.Height}, @bytes)";
146 | cmd.Parameters.Add(new SQLiteParameter("@bytes", DbType.Binary) { Value = bytes });
147 | return cmd.ExecuteNonQuery();
148 | }
149 | });
150 |
151 | return (int)affected[0];
152 | }
153 |
154 | ///
155 | /// Async version of
156 | ///
157 | internal static Task> GetFromThumbDBAsync(string basePath, SizeInt decodeSize, string subPath = null) {
158 | return Task.Run(() => GetFromThumbDB(basePath, decodeSize, subPath));
159 | }
160 |
161 | ///
162 | /// Returns null if thumb either does not exist in DB or has different size.
163 | /// If is null, the first match by will be returned.
164 | ///
165 | internal static Tuple GetFromThumbDB(string basePath, SizeInt decodeSize, string subPath = null) {
166 | var png = Execute(Table.Thumbs, (table, con) => {
167 | byte[] pngByte = null;
168 | using (var cmd = new SQLiteCommand(con)) {
169 | cmd.CommandText =
170 | $@"select * from {table.Name} where
171 | {Column.BasePath} = @basePath
172 | {(subPath == null ? "" : $@"and {Column.SubPath} = @subPath")} and
173 | {Column.DecodeWidth} = {decodeSize.Width} and
174 | {Column.DecodeHeight} = {decodeSize.Height} limit 1";
175 | cmd.Parameters.Add(new SQLiteParameter("@basePath", DbType.String) { Value = basePath });
176 | if (subPath != null)
177 | cmd.Parameters.Add(new SQLiteParameter("@subPath", DbType.String) { Value = subPath });
178 | using (var reader = cmd.ExecuteReader()) {
179 | while (reader.Read()) {
180 | pngByte = (byte[])reader[nameof(Column.ThumbData)];
181 | if (subPath == null)
182 | subPath = (string)reader[nameof(Column.SubPath)];
183 | break;
184 | }
185 | }
186 | }
187 | return pngByte;
188 | });
189 |
190 | if (png.Length == 0 || png[0] == null || ((byte[])png[0]).Length == 0) return null;
191 | using (var ms = new MemoryStream((byte[])png[0])) {
192 | var bi = new BitmapImage();
193 | bi.BeginInit();
194 | bi.CacheOption = BitmapCacheOption.OnLoad;
195 | bi.StreamSource = ms;
196 | bi.EndInit();
197 | bi.Freeze();
198 | return new Tuple(bi, subPath);
199 | }
200 | }
201 |
202 | internal static bool ThumbExistInDB(string basePath, string subPath, SizeInt decodeSize) {
203 | return (bool)Execute(Table.Thumbs, (table, con) => {
204 | using (var cmd = new SQLiteCommand(con)) {
205 | cmd.CommandText =
206 | $@"select count({Column.ThumbData}) from {table.Name} where
207 | {Column.BasePath} = @basePath
208 | {(subPath == null ? "" : $@"and {Column.SubPath} = @subPath")} and
209 | {Column.DecodeWidth} = {decodeSize.Width} and
210 | {Column.DecodeHeight} = {decodeSize.Height}";
211 | cmd.Parameters.Add(new SQLiteParameter("@basePath", DbType.String) { Value = basePath });
212 | if (subPath != null)
213 | cmd.Parameters.Add(new SQLiteParameter("@subPath", DbType.String) { Value = subPath });
214 | using (var r = cmd.ExecuteReader()) {
215 | r.Read();
216 | return (long)r[0] > 0;
217 | }
218 | }
219 | })[0];
220 | }
221 | }
222 | }
223 |
--------------------------------------------------------------------------------