├── 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 | 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 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 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 | 9 | 10 | 15 | 18 | 21 | 22 | 40 | 41 | 42 | 43 | 47 | 54 | 55 | 56 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 |