65.9K
CodeProject 正在变化。 阅读更多。
Home

Library Commander

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017年6月15日

CPOL

6分钟阅读

viewsIcon

14132

downloadIcon

520

LibraryCommander 是一款个人桌面应用程序,用于管理我的文本(电子书)收藏、对其进行分类以及按类别和标签进行搜索。

引言

LibraryCommander 是一款个人桌面应用程序,用于管理我的文本(电子书)收藏、对其进行分类以及按类别和标签进行搜索。

LibraryCommander 使用 SQLite 数据库来存储文档的元数据(标题、作者、语言等),但实际文件存储在预定义文件夹的磁盘上(在其中创建了额外的文件夹以根据其元数据对文档进行分类)。文件可以轻松地从应用程序、Windows Explorer 或任何其他文件管理器中访问。

Data Model

在讨论存储结构之前,让我们先回顾一下应用程序的数据模型。

DataModel

核心类当然是 Book。一本书属于一个特定的 Category,并且可以有多个 Tags(至少一个)。一本书由一位或多位 Authors 撰写,属于一种 Language。一组书籍可以构成一个系列(Cycle,例如 Terry Pratchett 的“Discworld”),一本书有一个 Volume 属性来存储其在系列中的编号。同一本书可以有不同的 Formats(文件扩展名)。

每个 Category 在存储目录中都有一个单独的文件夹。Category 文件夹包含每个 Language 的子文件夹。书籍存储在语言文件夹中,除非用户在将书籍添加到库时希望为作者或系列创建额外的文件夹。

例如,考虑 Terry Pratchett 的“Discworld”系列。可以将它放在“Fantasy”类别中,并希望有作者文件夹(“Terry Pratchett”)和系列文件夹(“Discworld”)来单独存储 Pratchett 的其他作品。因此,书籍的最终位置将是

"Library\Fantasy\En\Terry Pratchett\Discworld\Terry Pratchett. 16 - Soul Music.epub"

文档通过 BookCard 对话框添加到 Library

Book Card

每个文档应具有标题、类别、作者、标签、语言、格式和要复制的文件位置。如果先选择了文件然后输入标题和格式,则标题和格式将从文件名中获取。系列、卷号和(出版)年份是可选属性。标签和系列与具体类别相关联,不能在选择类别之前选择。

来自同一系列的图书通常具有相同的属性(除了标题和文件位置)。为了简化添加多本图书的任务,LibraryCommander 具有模板功能:输入第一本书的属性,复制模板(Ctrl+C),然后在添加下一本书时粘贴模板中的属性(Ctrl+V)。

LibraryCommander 导航

LibraryCommander 的灵感来自 传统文件管理器。看看主屏幕

Main screen

它显示了一个两面板目录视图(文件和库面板),下方是命令列表。每个面板显示当前文件夹路径以及文件/子文件夹列表。在任何给定时间只有一个面板是活动的。活动面板包含“光标”。活动面板中的文件用作操作的参数。

面板的数据由所谓的 FsNavigator 类(文件系统导航器)提供。给定初始路径,FsNavigator 返回该路径下的文件和文件夹列表,包装在 FsItem 对象中(具有 Name、Size、Extension(针对文件)属性以及 IsDirectory 标志以区分文件/文件夹)。

文件面板的导航器很简单,它使用 DirectoryInfo.EnumerateDirectories()DirectoryInfo.EnumerateFiles() 方法来获取当前文件夹中的所有元素。

库面板的导航器(VirtualFsNavigator 类)基于文档元数据工作。它可以检查一本书是否已添加到库但其文件在存储中丢失。它还会忽略存储文件夹中存在但不在库中的文件。

VirtualFsNavigator 根据存储中的当前级别选择文件和文件夹。

- 存储 Top 级别

  • 无文件
  • 每个 Category 的文件夹

- 存储 Category 级别

  • 无文件
  • 每个 Language 的文件夹

- 存储 Language 级别

  • 当前 Category 和 Language 中未包含作者或系列子文件夹的书籍文件
  • 当前 Category 和 Language 中包含子文件夹的书籍的作者和系列文件夹

- 存储 Author 级别

  • 当前 Category 和 Language 中包含作者子文件夹的书籍文件
  • 当前 Category 和 Language 中当前作者的书籍的系列子文件夹

- 存储 Cycle 级别

  • 当前 Category 和 Language 中当前作者的书籍,包含系列子文件夹的文件
  • 无文件夹

热键

LibraryCommander 中的功能按钮具有关联的热键。WPF 中的热键可以使用 InputBindings 轻松创建。但是,当函数很多时,将所有函数添加到窗口的 InputBindings 会变得很繁琐。为了加快过程并清楚地将热键与特定按钮关联,我为 Button 类创建了一个名为“Hotkey”的字符串附加 DependencyProperty。当分配“Hotkey”(例如“Control+F”或“F1”)时,字符串会在 Cmd 类中的属性更改回调中进行解析,如果按键和修饰键正确,则会将 InputBinding 添加到 Button 窗口。这是 Cmd 代码

    public static class Cmd
    {
        public static readonly DependencyProperty HotkeyProperty = 
            DependencyProperty.RegisterAttached("Hotkey", typeof(string), typeof(Cmd), new PropertyMetadata(null, HotkeyChangedCallback));

        public static string GetHotkey(DependencyObject obj)
        {
            return (string)obj.GetValue(HotkeyProperty);
        }

        public static void SetHotkey(DependencyObject obj, string value)
        {
            obj.SetValue(HotkeyProperty, value);
        }

        private static readonly char _cmdJoinChar = '+';
        private static readonly char _cmdNameChar = '_';

        private static string NormalizeName(string name)
        {
            // + symbol in names is prohibited by NameScope (throws exception)
            return name.Replace(_cmdJoinChar, _cmdNameChar);
        }

        private static void HotkeyChangedCallback(DependencyObject obj, DependencyPropertyChangedEventArgs e)
        {
            var btn = obj as Button;
            if (btn == null)
                return;

            Window parentWindow = Window.GetWindow(btn);
            if (parentWindow == null)
                return;

            KeyBinding kb = null;

            string hkOld = (string) e.OldValue;

            // find and remove key binding with old hotkey
            if (false == String.IsNullOrWhiteSpace(hkOld))
            {
                hkOld = NormalizeName(hkOld);
                kb = parentWindow.InputBindings
                    .OfType<KeyBinding>()
                    .FirstOrDefault(k => hkOld == (string) k.GetValue(FrameworkElement.NameProperty));

                if (kb != null)
                    parentWindow.InputBindings.Remove(kb);
            }

            string hkNew = (string) e.NewValue;

            if (String.IsNullOrWhiteSpace(hkNew))
                return;

            // create key binding with new hotkey

            var keys = hkNew.Split(new [] { _cmdJoinChar }, StringSplitOptions.RemoveEmptyEntries);

            ModifierKeys modifier = ModifierKeys.None;
            ModifierKeys m;

            // parse hotkey string and extract modifiers and main key
            string strKey = null;
            foreach (string k in keys)
            {
                if (Enum.TryParse(k, out m))
                    modifier = modifier | m;
                else
                {
                    // more than one Key is not supported
                    if (strKey != null)
                        return;

                    strKey = k;
                }
            }

            Key key;
            if (false == Enum.TryParse(strKey, out key))
                return;

            // Key + Modifier
            kb = new KeyBinding {Key = key, Modifiers = modifier};
            
            // x:Name
            kb.SetValue(FrameworkElement.NameProperty, NormalizeName(hkNew));

            // Command
            var cmdBinding = new Binding("Command") {Source = btn};
            BindingOperations.SetBinding(kb, InputBinding.CommandProperty, cmdBinding);

            // Command Parameter
            var paramBinding = new Binding("CommandParameter") {Source = btn};
            BindingOperations.SetBinding(kb, InputBinding.CommandParameterProperty, paramBinding);

            // Adding hotkey to Window
            parentWindow.InputBindings.Add(kb);
        }        
    }

LibraryCommander 热键列表

主屏幕

  • Control+{数字}:选择现有分区(数字 >= 1)
  • Tab:切换活动面板
  • 箭头:移动到列表中前一个/后一个文件/文件夹
  • Enter:打开选定的文件/文件夹
  • Escape:从嵌套文件夹返回到父文件夹
  • F2:打开选定的文件/文件夹
  • F3:编辑选定的书籍(仅在 Library 面板上可用)
  • F4:创建新书籍(仅在 Library 面板上可用)
  • F5:将选定的文件复制到 Library(仅在 Files 面板上可用)
  • F6:将选定的文件移动到 Library(仅在 Files 面板上可用)
  • F8:删除选定的书籍(仅在 Library 面板上可用)
  • Control+F:打开 Library 搜索对话框(仅在 Library 面板上可用)

BookCard 窗口

  • Control+C:复制模板
  • Control+V:粘贴模板
  • Control+O:选择书籍文件
  • Control+A:选择作者
  • Control+K:选择类别
  • Control+T:选择标签
  • Control+L:选择语言
  • Control+E:选择文件格式(扩展名)
  • Escape:关闭 BookCard 窗口、选择窗口(用于作者、类别等)和 InputBoxes。

本地化

LibraryCommander 支持两种语言。英语是默认语言,当检测到适当的机器区域设置时,它会切换到俄语。也可以在主屏幕上切换语言。

本地化方法在 CodeProject 文章“Localization for Dummies”中有描述。每个语言(En、Ru)都有一个字符串资源集。该文章建议使用 {x:Static} 扩展从 xaml 访问资源,但这无助于在运行时切换语言。我创建了一个 LocalizationProvider 类,它存储默认文化、当前文化、可以通过键获取资源值并在文化切换时更新值(实现了 INotifyPropertyChanged)。

        private Dictionary<string, string> _cache = new Dictionary<string, string>();

        protected string GetResource([CallerMemberName]string resourceKey = null)
        {
            string resource;
            // trying to get string from Cache            
            if (_cache.TryGetValue(resourceKey, out resource))
                return resource;

            // trying to get string from resources for Current culture
            resource = Resources.ResourceManager.GetString(resourceKey, CurrentCulture);

            if (resource == null && CurrentCulture.Name != DefaultCulture.Name)
                // trying to get string from resources for Default culture
                resource = Resources.ResourceManager.GetString(resourceKey, DefaultCulture);

            // if localized string was not found in Resources, use resourceKey
            // it helps to add less strings to En Resources (property names are in English)
            // for other culture it allows to notice mistake without throwing exception
            if (resource == null)
                resource = resourceKey;

            // add resolved string to cache
            _cache.Add(resourceKey, resource);

            return resource;
        }

获取资源值并非一步到位,因此 LocalizationProvider 使用字符串缓存。LocalizationProvider 是一个基类,不同的本地化应作为派生类实现。此类实现的一个示例是 Commands 类,其中包含主屏幕功能按钮的名称。

    public class Commands: LocalizationProvider
    {
        private static Commands _instance = new Commands();

        private Commands()
        {            
        }

        /// <summary>
        /// Static item accessible from view (via {x:Static} extension)
        /// </summary>
        public static Commands Instance { get { return _instance; } }

        public string Cmd { get { return GetResource(); } }

        public string Pick { get { return GetResource(); } }

        public string Add { get { return GetResource(); } }

        public string Edit { get { return GetResource(); } }

        public string Copy { get { return GetResource(); } }

        public string Move { get { return GetResource(); } }

        public string Del { get { return GetResource(); } }

        public string Quit { get { return GetResource(); } }

        public string Search { get { return GetResource(); } }

        public string Save { get { return GetResource(); } }

        public string Close { get { return GetResource(); } }
    }

CallerMemberName 属性应用于方法参数,可以将实现缩短为一个方法调用(前提是属性名称和资源键匹配)。

视图中的按钮内容通过绑定表达式设置,例如:

{Binding Path=Quit, Source={x:Static localization:Commands.Instance}}

关注点

LibraryCommander 以两种语言(英、俄)进行翻译。本地化值存储在项目的 Resources 中。应用程序文化可以在运行时切换。

LibraryCommander 有多个键盘快捷键。对话框中的快捷键以 Window.KeyBindings 的形式实现。自定义附加属性 Cmd.Hotkey 有助于减少标记长度,并清楚地将热键与特定按钮关联。

LibraryCommander 使用自定义 WPF 样式来模仿老式应用程序。样式集合(代号 RetroUI)是我的创作,包括 Button、CheckBox、RadioButton、TabControl、ListBox、ComboBox、DataGrid、TreeView 控件,可以在我的 GitHub 存储库中找到:https://github.com/AlexanderSharykin/RetroUI

如何使用

使用 LibraryCommander

下载 LibraryCommander.zipGitHub release

更改 LibraryCommander.exe.config 文件中的“library”文件夹

运行 LibraryCommander.exe

应用程序在启动时执行配置验证。LibraryCommander 需要存储文件夹来存放文档和数据库以存储元数据。

存储文件夹路径应在 .config 文件中提供(<appSettings> 部分,键 "library")。如果找不到文件夹,验证将显示一条带有问题描述的错误消息并退出应用程序。

LibraryCommander 发行版默认附带 Books.db 文件和 SQLite 连接。要从 IDE 启动项目,请修改 SQLite 连接字符串中的文件路径。空表的 Books.db 文件可以在源代码的“Db”文件夹中找到。还有一个脚本(“SqlServer Db Schema.sql”)用于在 SQL Server 中创建 LibraryCommander 数据库。.config 文件中提供了 SQL Server 连接设置示例。

© . All rights reserved.