From 95051cbd63aa3218819c52266d89fbc026a4c67c Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 3 Sep 2017 22:20:56 -0400 Subject: [PATCH] Initial Commit Rework --- .gitignore | 4 +- .idea/.name | 1 - .idea/Sonarr.iml | 25 - .idea/codeStyleSettings.xml | 59 - .idea/encodings.xml | 6 - .idea/jsLibraryMappings.xml | 6 - .idea/libraries/Sonarr_node_modules.xml | 14 - .idea/misc.xml | 14 - .idea/modules.xml | 8 - .idea/vcs.xml | 6 - .npmrc | 1 + .yarnrc | 1 + build.ps1 | 1 - build.sh | 7 +- frontend/.csscomb.json | 25 + frontend/.esformatter | 335 + frontend/.eslintignore | 1 + frontend/.eslintrc | 288 + frontend/.jsbeautifyrc | 12 + frontend/.stylelintrc | 392 + frontend/.tern-project | 7 + frontend/gulp/build.js | 15 + frontend/gulp/clean.js | 8 + frontend/gulp/copy.js | 45 + frontend/gulp/gulpFile.js | 8 + frontend/gulp/helpers/errorHandler.js | 6 + frontend/gulp/helpers/html-annotate-loader.js | 15 + frontend/gulp/helpers/paths.js | 23 + frontend/gulp/imageMin.js | 15 + frontend/gulp/start.js | 104 + frontend/gulp/stripBom.js | 13 + frontend/gulp/watch.js | 27 + frontend/gulp/webpack.js | 159 + frontend/src/.vscode/settings.json | 4 + frontend/src/Activity/Blacklist/Blacklist.js | 110 + .../Activity/Blacklist/BlacklistConnector.js | 120 + .../Blacklist/BlacklistDetailsModal.js | 89 + .../src/Activity/Blacklist/BlacklistRow.css | 18 + .../src/Activity/Blacklist/BlacklistRow.js | 175 + .../Blacklist/BlacklistRowConnector.js | 17 + .../History/Details/HistoryDetails.js | 237 + .../Details/HistoryDetailsConnector.js | 19 + .../History/Details/HistoryDetailsModal.css | 5 + .../History/Details/HistoryDetailsModal.js | 104 + frontend/src/Activity/History/History.js | 195 + .../src/Activity/History/HistoryConnector.js | 128 + .../Activity/History/HistoryEventTypeCell.css | 3 + .../Activity/History/HistoryEventTypeCell.js | 82 + frontend/src/Activity/History/HistoryRow.css | 23 + frontend/src/Activity/History/HistoryRow.js | 254 + .../Activity/History/HistoryRowConnector.js | 69 + frontend/src/Activity/Queue/ProtocolLabel.css | 13 + frontend/src/Activity/Queue/ProtocolLabel.js | 20 + frontend/src/Activity/Queue/Queue.js | 243 + frontend/src/Activity/Queue/QueueConnector.js | 153 + frontend/src/Activity/Queue/QueueDetails.js | 97 + frontend/src/Activity/Queue/QueueRow.css | 23 + frontend/src/Activity/Queue/QueueRow.js | 348 + .../src/Activity/Queue/QueueRowConnector.js | 76 + .../src/Activity/Queue/QueueStatusCell.css | 5 + .../src/Activity/Queue/QueueStatusCell.js | 132 + .../Activity/Queue/RemoveQueueItemModal.css | 3 + .../Activity/Queue/RemoveQueueItemModal.js | 114 + .../Activity/Queue/RemoveQueueItemsModal.css | 3 + .../Activity/Queue/RemoveQueueItemsModal.js | 114 + .../Queue/Status/QueueStatusConnector.js | 63 + frontend/src/Activity/Queue/TimeleftCell.css | 5 + frontend/src/Activity/Queue/TimeleftCell.js | 82 + frontend/src/Activity/activity.less | 27 + .../AddArtist/AddNewSeries/AddNewSeries.css | 54 + .../AddArtist/AddNewSeries/AddNewSeries.js | 184 + .../AddNewSeries/AddNewSeriesConnector.js | 102 + .../AddNewSeries/AddNewSeriesModal.js | 31 + .../AddNewSeries/AddNewSeriesModalContent.css | 74 + .../AddNewSeries/AddNewSeriesModalContent.js | 265 + .../AddNewSeriesModalContentConnector.js | 105 + .../AddNewSeries/AddNewSeriesSearchResult.css | 40 + .../AddNewSeries/AddNewSeriesSearchResult.js | 169 + .../AddNewSeriesSearchResultConnector.js | 20 + .../ImportSeries/Import/ImportSeries.js | 173 + .../Import/ImportSeriesConnector.js | 121 + .../Import/ImportSeriesFooter.css | 27 + .../ImportSeries/Import/ImportSeriesFooter.js | 263 + .../Import/ImportSeriesFooterConnector.js | 57 + .../Import/ImportSeriesHeader.css | 45 + .../ImportSeries/Import/ImportSeriesHeader.js | 115 + .../ImportSeries/Import/ImportSeriesRow.css | 52 + .../ImportSeries/Import/ImportSeriesRow.js | 121 + .../Import/ImportSeriesRowConnector.js | 91 + .../Import/ImportSeriesSelected.css | 3 + .../ImportSeries/Import/ImportSeriesTable.js | 213 + .../Import/ImportSeriesTableConnector.js | 44 + .../SelectSeries/ImportSeriesSearchResult.css | 8 + .../SelectSeries/ImportSeriesSearchResult.js | 52 + .../ImportSeriesSearchResultConnector.js | 17 + .../SelectSeries/ImportSeriesSelectSeries.css | 72 + .../SelectSeries/ImportSeriesSelectSeries.js | 268 + .../ImportSeriesSelectSeriesConnector.js | 71 + .../Import/SelectSeries/ImportSeriesTitle.css | 17 + .../Import/SelectSeries/ImportSeriesTitle.js | 50 + .../AddArtist/ImportSeries/ImportSeries.js | 30 + .../ImportSeriesRootFolderRow.css | 18 + .../SelectFolder/ImportSeriesRootFolderRow.js | 64 + .../ImportSeriesRootFolderRowConnector.js | 48 + .../SelectFolder/ImportSeriesSelectFolder.css | 32 + .../SelectFolder/ImportSeriesSelectFolder.js | 188 + .../ImportSeriesSelectFolderConnector.js | 87 + .../SeriesMonitoringOptionsPopoverContent.js | 46 + .../src/AddArtist/SeriesTypePopoverContent.js | 26 + frontend/src/App/App.js | 245 + frontend/src/App/AppUpdatedModal.js | 30 + frontend/src/App/AppUpdatedModalConnector.js | 24 + frontend/src/App/AppUpdatedModalContent.css | 15 + frontend/src/App/AppUpdatedModalContent.js | 100 + .../App/AppUpdatedModalContentConnector.js | 69 + frontend/src/App/ConnectionLostModal.css | 3 + frontend/src/App/ConnectionLostModal.js | 55 + .../src/App/ConnectionLostModalConnector.js | 12 + frontend/src/Artist/ArtistNameLink.js | 20 + frontend/src/Artist/ArtistPoster.js | 160 + frontend/src/Artist/Delete/DeleteArtist.less | 39 + .../src/Artist/Delete/DeleteArtistModal.js | 33 + .../Delete/DeleteArtistModalContent.css | 12 + .../Artist/Delete/DeleteArtistModalContent.js | 139 + .../DeleteArtistModalContentConnector.js | 55 + frontend/src/Artist/Details/EpisodeRow.css | 26 + frontend/src/Artist/Details/EpisodeRow.js | 266 + .../src/Artist/Details/EpisodeRowConnector.js | 29 + .../Artist/Details/SeriesAlternateTitles.css | 3 + .../Artist/Details/SeriesAlternateTitles.js | 28 + frontend/src/Artist/Details/SeriesDetails.css | 119 + frontend/src/Artist/Details/SeriesDetails.js | 576 + .../Artist/Details/SeriesDetailsConnector.js | 184 + .../src/Artist/Details/SeriesDetailsLinks.css | 13 + .../src/Artist/Details/SeriesDetailsLinks.js | 84 + .../Details/SeriesDetailsPageConnector.js | 76 + .../Artist/Details/SeriesDetailsSeason.css | 113 + .../src/Artist/Details/SeriesDetailsSeason.js | 393 + .../Details/SeriesDetailsSeasonConnector.js | 118 + frontend/src/Artist/Details/SeriesTags.css | 8 + frontend/src/Artist/Details/SeriesTags.js | 31 + .../src/Artist/Details/SeriesTagsConnector.js | 30 + frontend/src/Artist/Edit/EditArtistModal.js | 25 + .../Artist/Edit/EditArtistModalConnector.js | 39 + .../Artist/Edit/EditArtistModalContent.css | 5 + .../src/Artist/Edit/EditArtistModalContent.js | 165 + .../Edit/EditArtistModalContentConnector.js | 98 + .../Artist/Editor/Delete/DeleteArtistModal.js | 31 + .../Delete/DeleteArtistModalContent.css | 13 + .../Editor/Delete/DeleteArtistModalContent.js | 123 + .../DeleteArtistModalContentConnector.js | 45 + .../Editor/Organize/OrganizeSeriesModal.js | 31 + .../Organize/OrganizeSeriesModalContent.css | 8 + .../Organize/OrganizeSeriesModalContent.js | 74 + .../OrganizeSeriesModalContentConnector.js | 67 + frontend/src/Artist/Editor/SeriesEditor.js | 328 + .../Artist/Editor/SeriesEditorConnector.js | 86 + .../src/Artist/Editor/SeriesEditorFooter.css | 57 + .../src/Artist/Editor/SeriesEditorFooter.js | 314 + .../Artist/Editor/SeriesEditorFooterLabel.css | 8 + .../Artist/Editor/SeriesEditorFooterLabel.js | 40 + .../src/Artist/Editor/SeriesEditorRow.css | 5 + frontend/src/Artist/Editor/SeriesEditorRow.js | 120 + .../Artist/Editor/SeriesEditorRowConnector.js | 34 + frontend/src/Artist/Editor/Tags/TagsModal.js | 31 + .../Artist/Editor/Tags/TagsModalContent.css | 12 + .../Artist/Editor/Tags/TagsModalContent.js | 188 + .../Editor/Tags/TagsModalContentConnector.js | 36 + frontend/src/Artist/Index/ArtistIndex.css | 51 + frontend/src/Artist/Index/ArtistIndex.js | 326 + .../src/Artist/Index/ArtistIndexConnector.js | 160 + .../src/Artist/Index/ArtistIndexFooter.css | 66 + .../src/Artist/Index/ArtistIndexFooter.js | 104 + .../Artist/Index/ArtistIndexItemConnector.js | 77 + frontend/src/Artist/Index/ArtistIndexPage.js | 0 .../Index/Menus/ArtistIndexFilterMenu.js | 76 + .../Artist/Index/Menus/ArtistIndexSortMenu.js | 145 + .../Artist/Index/Menus/ArtistIndexViewMenu.js | 42 + .../Index/Posters/ArtistIndexPoster.css | 88 + .../Artist/Index/Posters/ArtistIndexPoster.js | 230 + .../Index/Posters/ArtistIndexPosterInfo.css | 6 + .../Index/Posters/ArtistIndexPosterInfo.js | 123 + .../Posters/ArtistIndexPosterProgressBar.css | 14 + .../Posters/ArtistIndexPosterProgressBar.js | 45 + .../Index/Posters/ArtistIndexPosters.css | 3 + .../Index/Posters/ArtistIndexPosters.js | 326 + .../Posters/ArtistIndexPostersConnector.js | 33 + .../Options/ArtistIndexPosterOptionsModal.js | 25 + .../ArtistIndexPosterOptionsModalContent.js | 173 + ...IndexPosterOptionsModalContentConnector.js | 23 + .../Index/Table/ArtistIndexActionsCell.js | 102 + .../Artist/Index/Table/ArtistIndexHeader.css | 81 + .../Artist/Index/Table/ArtistIndexHeader.js | 106 + .../Index/Table/ArtistIndexHeaderConnector.js | 13 + .../src/Artist/Index/Table/ArtistIndexRow.css | 90 + .../src/Artist/Index/Table/ArtistIndexRow.js | 389 + .../Artist/Index/Table/ArtistIndexTable.css | 5 + .../Artist/Index/Table/ArtistIndexTable.js | 143 + .../Index/Table/ArtistIndexTableConnector.js | 34 + .../Artist/Index/Table/ArtistStatusCell.css | 9 + .../Artist/Index/Table/ArtistStatusCell.js | 50 + .../Index/Table/artistIndexCellRenderers.js | 138 + frontend/src/Artist/NoArtist.css | 11 + frontend/src/Artist/NoArtist.js | 34 + frontend/src/Calendar/Agenda/Agenda.css | 3 + frontend/src/Calendar/Agenda/Agenda.js | 38 + .../src/Calendar/Agenda/AgendaConnector.js | 14 + frontend/src/Calendar/Agenda/AgendaEvent.css | 113 + frontend/src/Calendar/Agenda/AgendaEvent.js | 160 + .../Calendar/Agenda/AgendaEventConnector.js | 24 + frontend/src/Calendar/Calendar.css | 8 + frontend/src/Calendar/Calendar.js | 64 + frontend/src/Calendar/CalendarConnector.js | 145 + frontend/src/Calendar/CalendarPage.css | 14 + frontend/src/Calendar/CalendarPage.js | 134 + .../src/Calendar/CalendarPageConnector.js | 33 + frontend/src/Calendar/Day/CalendarDay.css | 25 + frontend/src/Calendar/Day/CalendarDay.js | 61 + .../src/Calendar/Day/CalendarDayConnector.js | 55 + frontend/src/Calendar/Day/CalendarDays.css | 14 + frontend/src/Calendar/Day/CalendarDays.js | 163 + .../src/Calendar/Day/CalendarDaysConnector.js | 32 + frontend/src/Calendar/Day/DayOfWeek.css | 13 + frontend/src/Calendar/Day/DayOfWeek.js | 55 + frontend/src/Calendar/Day/DaysOfWeek.css | 4 + frontend/src/Calendar/Day/DaysOfWeek.js | 97 + .../src/Calendar/Day/DaysOfWeekConnector.js | 22 + .../src/Calendar/Events/CalendarEvent.css | 86 + frontend/src/Calendar/Events/CalendarEvent.js | 165 + .../Calendar/Events/CalendarEventConnector.js | 24 + .../Events/CalendarEventQueueDetails.js | 50 + .../src/Calendar/Header/CalendarHeader.css | 53 + .../src/Calendar/Header/CalendarHeader.js | 253 + .../Header/CalendarHeaderConnector.js | 84 + .../Header/CalendarHeaderViewButton.js | 45 + frontend/src/Calendar/Legend/Legend.css | 6 + frontend/src/Calendar/Legend/Legend.js | 68 + frontend/src/Calendar/Legend/LegendItem.css | 41 + frontend/src/Calendar/Legend/LegendItem.js | 36 + frontend/src/Calendar/calendarViews.js | 7 + frontend/src/Calendar/getStatusStyle.js | 33 + .../src/Calendar/iCal/CalendarLinkModal.js | 29 + .../Calendar/iCal/CalendarLinkModalContent.js | 221 + .../iCal/CalendarLinkModalContentConnector.js | 17 + frontend/src/Commands/commandNames.js | 19 + frontend/src/Components/Alert.css | 31 + frontend/src/Components/Alert.js | 32 + frontend/src/Components/Card.css | 8 + frontend/src/Components/Card.js | 39 + .../src/Components/CircularProgressBar.css | 21 + .../src/Components/CircularProgressBar.js | 140 + .../DescriptionList/DescriptionList.css | 4 + .../DescriptionList/DescriptionList.js | 27 + .../DescriptionList/DescriptionListItem.js | 44 + .../DescriptionListItemDescription.css | 13 + .../DescriptionListItemDescription.js | 27 + .../DescriptionListItemTitle.css | 18 + .../DescriptionListItemTitle.js | 27 + frontend/src/Components/DragPreviewLayer.css | 9 + frontend/src/Components/DragPreviewLayer.js | 22 + frontend/src/Components/FieldSet.css | 19 + frontend/src/Components/FieldSet.js | 33 + .../FileBrowser/FileBrowserModal.css | 5 + .../FileBrowser/FileBrowserModal.js | 39 + .../FileBrowser/FileBrowserModalContent.css | 16 + .../FileBrowser/FileBrowserModalContent.js | 213 + .../FileBrowserModalContentConnector.js | 86 + .../Components/FileBrowser/FileBrowserRow.css | 5 + .../Components/FileBrowser/FileBrowserRow.js | 62 + frontend/src/Components/Form/CaptchaInput.css | 23 + frontend/src/Components/Form/CaptchaInput.js | 86 + .../Components/Form/CaptchaInputConnector.js | 98 + frontend/src/Components/Form/CheckInput.css | 105 + frontend/src/Components/Form/CheckInput.js | 187 + .../Components/Form/EnhancedSelectInput.css | 66 + .../Components/Form/EnhancedSelectInput.js | 399 + .../Form/EnhancedSelectInputOption.css | 37 + .../Form/EnhancedSelectInputOption.js | 77 + .../Form/EnhancedSelectInputSelectedValue.css | 3 + .../Form/EnhancedSelectInputSelectedValue.js | 27 + frontend/src/Components/Form/Form.css | 11 + frontend/src/Components/Form/Form.js | 52 + frontend/src/Components/Form/FormGroup.css | 24 + frontend/src/Components/Form/FormGroup.js | 56 + .../src/Components/Form/FormInputButton.css | 12 + .../src/Components/Form/FormInputButton.js | 54 + .../src/Components/Form/FormInputGroup.css | 30 + .../src/Components/Form/FormInputGroup.js | 235 + .../src/Components/Form/FormInputHelpText.css | 39 + .../src/Components/Form/FormInputHelpText.js | 62 + frontend/src/Components/Form/FormLabel.css | 22 + frontend/src/Components/Form/FormLabel.js | 45 + frontend/src/Components/Form/Input.css | 30 + .../LanguageProfileSelectInputConnector.js | 98 + .../Form/MonitorEpisodesSelectInput.js | 59 + frontend/src/Components/Form/NumberInput.js | 52 + frontend/src/Components/Form/OAuthInput.js | 30 + .../Components/Form/OAuthInputConnector.js | 82 + frontend/src/Components/Form/PasswordInput.js | 13 + frontend/src/Components/Form/PathInput.css | 68 + frontend/src/Components/Form/PathInput.js | 206 + .../src/Components/Form/PathInputConnector.js | 67 + .../Components/Form/ProviderFieldFormGroup.js | 126 + .../QualityProfileSelectInputConnector.js | 98 + .../Components/Form/RootFolderSelectInput.js | 105 + .../Form/RootFolderSelectInputConnector.js | 124 + .../Form/RootFolderSelectInputOption.css | 20 + .../Form/RootFolderSelectInputOption.js | 44 + .../RootFolderSelectInputSelectedValue.css | 24 + .../RootFolderSelectInputSelectedValue.js | 44 + frontend/src/Components/Form/SelectInput.css | 18 + frontend/src/Components/Form/SelectInput.js | 88 + .../Components/Form/SeriesTypeSelectInput.js | 53 + frontend/src/Components/Form/TagInput.css | 97 + frontend/src/Components/Form/TagInput.js | 126 + .../src/Components/Form/TagInputConnector.js | 156 + frontend/src/Components/Form/TextInput.css | 19 + frontend/src/Components/Form/TextInput.js | 81 + frontend/src/Components/Form/TextTagInput.js | 68 + .../Components/Form/TextTagInputConnector.js | 81 + frontend/src/Components/HeartRating.css | 4 + frontend/src/Components/HeartRating.js | 30 + frontend/src/Components/Icon.css | 15 + frontend/src/Components/Icon.js | 45 + frontend/src/Components/Label.css | 102 + frontend/src/Components/Label.js | 47 + frontend/src/Components/Link/Button.css | 119 + frontend/src/Components/Link/Button.js | 54 + .../src/Components/Link/ClipboardButton.css | 33 + .../src/Components/Link/ClipboardButton.js | 128 + frontend/src/Components/Link/IconButton.css | 16 + frontend/src/Components/Link/IconButton.js | 44 + frontend/src/Components/Link/Link.css | 25 + frontend/src/Components/Link/Link.js | 101 + .../src/Components/Link/SpinnerButton.css | 37 + frontend/src/Components/Link/SpinnerButton.js | 59 + .../Components/Link/SpinnerErrorButton.css | 23 + .../src/Components/Link/SpinnerErrorButton.js | 162 + .../src/Components/Link/SpinnerIconButton.js | 37 + .../Components/Loading/LoadingIndicator.css | 65 + .../Components/Loading/LoadingIndicator.js | 48 + .../src/Components/Loading/LoadingMessage.css | 6 + .../src/Components/Loading/LoadingMessage.js | 36 + frontend/src/Components/Menu/FilterMenu.css | 9 + frontend/src/Components/Menu/FilterMenu.js | 44 + .../src/Components/Menu/FilterMenuItem.js | 57 + frontend/src/Components/Menu/Menu.css | 7 + frontend/src/Components/Menu/Menu.js | 203 + frontend/src/Components/Menu/MenuButton.css | 15 + frontend/src/Components/Menu/MenuButton.js | 41 + frontend/src/Components/Menu/MenuContent.css | 11 + frontend/src/Components/Menu/MenuContent.js | 43 + frontend/src/Components/Menu/MenuItem.css | 19 + frontend/src/Components/Menu/MenuItem.js | 38 + .../src/Components/Menu/SelectedMenuItem.css | 15 + .../src/Components/Menu/SelectedMenuItem.js | 63 + frontend/src/Components/Menu/SortMenu.js | 39 + frontend/src/Components/Menu/SortMenuItem.js | 37 + .../src/Components/Menu/ToolbarMenuButton.css | 13 + .../src/Components/Menu/ToolbarMenuButton.js | 38 + frontend/src/Components/Menu/ViewMenu.js | 30 + frontend/src/Components/Menu/ViewMenuItem.js | 28 + frontend/src/Components/Modal/ConfirmModal.js | 88 + frontend/src/Components/Modal/Modal.css | 79 + frontend/src/Components/Modal/Modal.js | 196 + frontend/src/Components/Modal/ModalBody.css | 14 + frontend/src/Components/Modal/ModalBody.js | 59 + .../src/Components/Modal/ModalContent.css | 23 + frontend/src/Components/Modal/ModalContent.js | 46 + frontend/src/Components/Modal/ModalFooter.css | 23 + frontend/src/Components/Modal/ModalFooter.js | 32 + frontend/src/Components/Modal/ModalHeader.css | 8 + frontend/src/Components/Modal/ModalHeader.js | 32 + .../src/Components/MonitorToggleButton.css | 13 + .../src/Components/MonitorToggleButton.js | 76 + frontend/src/Components/NotFound.css | 14 + frontend/src/Components/NotFound.js | 31 + frontend/src/Components/Page/ErrorPage.css | 12 + frontend/src/Components/Page/ErrorPage.js | 52 + .../Page/Header/KeyboardShortcutsModal.js | 31 + .../Header/KeyboardShortcutsModalContent.css | 15 + .../Header/KeyboardShortcutsModalContent.js | 90 + .../KeyboardShortcutsModalContentConnector.js | 17 + .../src/Components/Page/Header/PageHeader.css | 60 + .../src/Components/Page/Header/PageHeader.js | 96 + .../Page/Header/PageHeaderActionsMenu.css | 27 + .../Page/Header/PageHeaderActionsMenu.js | 75 + .../Header/PageHeaderActionsMenuConnector.js | 56 + .../Page/Header/SeriesSearchInput.css | 98 + .../Page/Header/SeriesSearchInput.js | 250 + .../Page/Header/SeriesSearchInputConnector.js | 31 + .../Page/Header/SeriesSearchResult.css | 34 + .../Page/Header/SeriesSearchResult.js | 59 + frontend/src/Components/Page/LoadingPage.css | 3 + frontend/src/Components/Page/LoadingPage.js | 15 + frontend/src/Components/Page/Page.css | 18 + frontend/src/Components/Page/Page.js | 130 + frontend/src/Components/Page/PageConnector.js | 179 + frontend/src/Components/Page/PageContent.css | 8 + frontend/src/Components/Page/PageContent.js | 32 + .../src/Components/Page/PageContentBody.css | 19 + .../src/Components/Page/PageContentBody.js | 51 + .../Page/PageContentBodyConnector.js | 17 + .../src/Components/Page/PageContentFooter.css | 26 + .../src/Components/Page/PageContentFooter.js | 33 + frontend/src/Components/Page/PageJumpBar.css | 22 + frontend/src/Components/Page/PageJumpBar.js | 133 + .../src/Components/Page/PageJumpBarItem.css | 14 + .../src/Components/Page/PageJumpBarItem.js | 40 + .../src/Components/Page/PageSectionContent.js | 39 + .../Page/Sidebar/Messages/Message.css | 42 + .../Page/Sidebar/Messages/Message.js | 69 + .../Page/Sidebar/Messages/MessageConnector.js | 67 + .../Page/Sidebar/Messages/Messages.css | 11 + .../Page/Sidebar/Messages/Messages.js | 27 + .../Sidebar/Messages/MessagesConnector.js | 16 + .../Components/Page/Sidebar/PageSidebar.css | 34 + .../Components/Page/Sidebar/PageSidebar.js | 501 + .../Page/Sidebar/PageSidebarItem.css | 48 + .../Page/Sidebar/PageSidebarItem.js | 106 + .../Page/Sidebar/PageSidebarStatus.css | 3 + .../Page/Sidebar/PageSidebarStatus.js | 35 + .../Components/Page/Toolbar/PageToolbar.css | 16 + .../Components/Page/Toolbar/PageToolbar.js | 33 + .../Page/Toolbar/PageToolbarButton.css | 28 + .../Page/Toolbar/PageToolbarButton.js | 56 + .../Page/Toolbar/PageToolbarSection.css | 26 + .../Page/Toolbar/PageToolbarSection.js | 220 + .../Page/Toolbar/PageToolbarSeparator.css | 12 + .../Page/Toolbar/PageToolbarSeparator.js | 17 + frontend/src/Components/ProgressBar.css | 93 + frontend/src/Components/ProgressBar.js | 100 + frontend/src/Components/Router/Switch.js | 44 + .../Components/Scroller/OverlayScroller.css | 14 + .../Components/Scroller/OverlayScroller.js | 115 + frontend/src/Components/Scroller/Scroller.css | 28 + frontend/src/Components/Scroller/Scroller.js | 81 + frontend/src/Components/SignalRConnector.js | 326 + frontend/src/Components/SpinnerIcon.js | 32 + .../Table/Cells/RelativeDateCell.css | 5 + .../Table/Cells/RelativeDateCell.js | 60 + .../Table/Cells/RelativeDateCellConnector.js | 21 + .../Components/Table/Cells/TableRowCell.css | 11 + .../Components/Table/Cells/TableRowCell.js | 37 + .../Table/Cells/TableRowCellButton.css | 4 + .../Table/Cells/TableRowCellButton.js | 25 + .../Table/Cells/TableSelectCell.css | 11 + .../Components/Table/Cells/TableSelectCell.js | 71 + .../Table/Cells/VirtualTableRowCell.css | 14 + .../Table/Cells/VirtualTableRowCell.js | 29 + .../Table/Cells/VirtualTableSelectCell.css | 11 + .../Table/Cells/VirtualTableSelectCell.js | 82 + frontend/src/Components/Table/Table.css | 16 + frontend/src/Components/Table/Table.js | 157 + frontend/src/Components/Table/TableBody.js | 25 + frontend/src/Components/Table/TableHeader.js | 28 + .../src/Components/Table/TableHeaderCell.css | 16 + .../src/Components/Table/TableHeaderCell.js | 94 + .../Table/TableOptions/TableOptionsColumn.css | 48 + .../Table/TableOptions/TableOptionsColumn.js | 68 + .../TableOptionsColumnDragPreview.css | 4 + .../TableOptionsColumnDragPreview.js | 78 + .../TableOptionsColumnDragSource.css | 18 + .../TableOptionsColumnDragSource.js | 164 + .../Table/TableOptions/TableOptionsModal.css | 5 + .../Table/TableOptions/TableOptionsModal.js | 242 + frontend/src/Components/Table/TablePager.css | 70 + frontend/src/Components/Table/TablePager.js | 173 + frontend/src/Components/Table/TableRow.css | 7 + frontend/src/Components/Table/TableRow.js | 31 + .../src/Components/Table/TableRowButton.css | 4 + .../src/Components/Table/TableRowButton.js | 16 + .../Table/TableSelectAllHeaderCell.css | 11 + .../Table/TableSelectAllHeaderCell.js | 47 + .../src/Components/Table/VirtualTable.css | 3 + frontend/src/Components/Table/VirtualTable.js | 173 + .../src/Components/Table/VirtualTableBody.css | 3 + .../src/Components/Table/VirtualTableBody.js | 48 + .../Components/Table/VirtualTableHeader.css | 3 + .../Components/Table/VirtualTableHeader.js | 17 + .../Table/VirtualTableHeaderCell.css | 16 + .../Table/VirtualTableHeaderCell.js | 107 + .../src/Components/Table/VirtualTableRow.css | 14 + .../src/Components/Table/VirtualTableRow.js | 34 + .../Table/VirtualTableSelectAllHeaderCell.css | 11 + .../Table/VirtualTableSelectAllHeaderCell.js | 47 + frontend/src/Components/TagList.css | 3 + frontend/src/Components/TagList.js | 38 + frontend/src/Components/TagListConnector.js | 17 + frontend/src/Components/Tooltip/Popover.css | 104 + frontend/src/Components/Tooltip/Popover.js | 136 + frontend/src/Components/Tooltip/Tooltip.css | 161 + frontend/src/Components/Tooltip/Tooltip.js | 151 + frontend/src/Components/keyboardShortcuts.js | 100 + frontend/src/Components/withScrollPosition.js | 30 + frontend/src/Content/Fonts/FontAwesome.otf | Bin 0 -> 134808 bytes frontend/src/Content/Fonts/Roboto-Light.ttf | Bin 0 -> 162420 bytes frontend/src/Content/Fonts/Roboto-Light.woff | Bin 0 -> 89300 bytes frontend/src/Content/Fonts/Roboto-Light.woff2 | Bin 0 -> 62832 bytes frontend/src/Content/Fonts/Roboto-Regular.ttf | Bin 0 -> 162876 bytes .../src/Content/Fonts/Roboto-Regular.woff | Bin 0 -> 89732 bytes .../src/Content/Fonts/Roboto-Regular.woff2 | Bin 0 -> 63412 bytes .../src/Content/Fonts/UbuntuMono-Regular.eot | Bin .../src/Content/Fonts}/UbuntuMono-Regular.ttf | Bin .../src/Content/Fonts/UbuntuMono-Regular.woff | Bin frontend/src/Content/Fonts/font-awesome.css | 2337 +++ .../src/Content/Fonts/fontawesome-webfont.eot | Bin 0 -> 165742 bytes .../src/Content/Fonts/fontawesome-webfont.svg | 2671 +++ .../src/Content/Fonts/fontawesome-webfont.ttf | Bin 0 -> 165548 bytes .../Content/Fonts/fontawesome-webfont.woff | Bin 0 -> 98024 bytes .../Content/Fonts/fontawesome-webfont.woff2 | Bin 0 -> 77160 bytes frontend/src/Content/Fonts/fonts.css | 27 + .../src}/Content/Images/404.png | Bin .../Images/Icons/android-chrome-192x192.png | Bin 0 -> 4629 bytes .../Images/Icons/android-chrome-512x512.png | Bin 0 -> 93807 bytes .../Content/Images/Icons/apple-touch-icon.png | Bin 0 -> 3817 bytes .../Content/Images/Icons/browserconfig.xml | 9 + .../Content/Images/Icons/favicon-16x16.png | Bin 0 -> 572 bytes .../Content/Images/Icons/favicon-32x32.png | Bin .../Images/Icons/favicon-debug-16x16.png | Bin 0 -> 715 bytes .../Images/Icons/favicon-debug-32x32.png | Bin 0 -> 1385 bytes .../Content/Images/Icons/favicon-debug.ico | Bin 0 -> 15086 bytes .../src/Content/Images/Icons}/favicon.ico | Bin .../src/Content/Images/Icons/manifest.json | 18 + .../Content/Images/Icons/mstile-144x144.png | Bin 0 -> 18020 bytes .../Content/Images/Icons/mstile-150x150.png | Bin 0 -> 3584 bytes .../Content/Images/Icons/mstile-310x150.png | Bin 0 -> 3734 bytes .../Content/Images/Icons/mstile-310x310.png | Bin 0 -> 6632 bytes .../src/Content/Images/Icons/mstile-70x70.png | Bin 0 -> 2439 bytes .../Images/Icons/safari-pinned-tab.svg | 38 + frontend/src/Content/Images/logo.svg | 1 + .../src}/Content/Images/poster-dark.png | Bin frontend/src/Episode/EpisodeDetailsModal.js | 37 + .../Episode/EpisodeDetailsModalContent.css | 44 + .../src/Episode/EpisodeDetailsModalContent.js | 213 + .../EpisodeDetailsModalContentConnector.js | 93 + frontend/src/Episode/EpisodeLanguage.js | 23 + frontend/src/Episode/EpisodeNumber.css | 7 + frontend/src/Episode/EpisodeNumber.js | 106 + frontend/src/Episode/EpisodeQuality.js | 54 + frontend/src/Episode/EpisodeSearchCell.css | 6 + frontend/src/Episode/EpisodeSearchCell.js | 83 + .../src/Episode/EpisodeSearchCellConnector.js | 47 + frontend/src/Episode/EpisodeStatus.css | 4 + frontend/src/Episode/EpisodeStatus.js | 127 + .../src/Episode/EpisodeStatusConnector.js | 53 + frontend/src/Episode/EpisodeTitleLink.css | 8 + frontend/src/Episode/EpisodeTitleLink.js | 68 + .../src/Episode/History/EpisodeHistory.js | 112 + .../History/EpisodeHistoryConnector.js | 63 + .../src/Episode/History/EpisodeHistoryRow.css | 6 + .../src/Episode/History/EpisodeHistoryRow.js | 139 + frontend/src/Episode/SceneInfo.css | 11 + frontend/src/Episode/SceneInfo.js | 83 + frontend/src/Episode/Search/EpisodeSearch.css | 16 + frontend/src/Episode/Search/EpisodeSearch.js | 55 + .../Episode/Search/EpisodeSearchConnector.js | 90 + .../Search/InteractiveEpisodeSearch.js | 130 + .../InteractiveEpisodeSearchConnector.js | 90 + .../Search/InteractiveEpisodeSearchRow.css | 25 + .../Search/InteractiveEpisodeSearchRow.js | 197 + frontend/src/Episode/Search/Peers.js | 57 + frontend/src/Episode/SeasonEpisodeNumber.css | 3 + frontend/src/Episode/SeasonEpisodeNumber.js | 51 + frontend/src/Episode/Summary/EpisodeAiring.js | 86 + .../Episode/Summary/EpisodeAiringConnector.js | 20 + .../src/Episode/Summary/EpisodeSummary.css | 48 + .../src/Episode/Summary/EpisodeSummary.js | 166 + .../Summary/EpisodeSummaryConnector.js | 57 + frontend/src/Episode/episodeEntities.js | 13 + .../Editor/EpisodeFileEditorModal.js | 34 + .../Editor/EpisodeFileEditorModalContent.css | 8 + .../Editor/EpisodeFileEditorModalContent.js | 276 + .../EpisodeFileEditorModalContentConnector.js | 162 + .../Editor/EpisodeFileEditorRow.css | 3 + .../Editor/EpisodeFileEditorRow.js | 83 + .../EpisodeFileLanguageConnector.js | 17 + frontend/src/EpisodeFile/MediaInfo.js | 53 + .../src/EpisodeFile/MediaInfoConnector.js | 21 + frontend/src/EpisodeFile/mediaInfoTypes.js | 2 + .../Props/Shapes/createRouteMatchShape.js | 11 + .../src/Helpers/Props/Shapes/locationShape.js | 11 + .../src/Helpers/Props/Shapes/settingShape.js | 34 + frontend/src/Helpers/Props/align.js | 5 + frontend/src/Helpers/Props/filterTypes.js | 17 + frontend/src/Helpers/Props/icons.js | 88 + frontend/src/Helpers/Props/index.js | 23 + frontend/src/Helpers/Props/inputTypes.js | 33 + frontend/src/Helpers/Props/kinds.js | 19 + frontend/src/Helpers/Props/messageTypes.js | 11 + .../src/Helpers/Props/scrollDirections.js | 5 + frontend/src/Helpers/Props/sizes.js | 5 + frontend/src/Helpers/Props/sortDirections.js | 4 + .../src/Helpers/Props/tooltipPositions.js | 11 + frontend/src/Helpers/dragTypes.js | 3 + frontend/src/Helpers/elementChildren.js | 149 + frontend/src/Helpers/getDisplayName.js | 3 + .../Episode/SelectEpisodeModal.js | 37 + .../Episode/SelectEpisodeModalContent.js | 183 + .../SelectEpisodeModalContentConnector.js | 103 + .../Episode/SelectEpisodeRow.js | 67 + ...eractiveImportSelectFolderModalContent.css | 24 + ...teractiveImportSelectFolderModalContent.js | 161 + ...ImportSelectFolderModalContentConnector.js | 73 + .../Folder/RecentFolderRow.js | 41 + .../InteractiveImportModalContent.css | 45 + .../InteractiveImportModalContent.js | 320 + .../InteractiveImportModalContentConnector.js | 152 + .../Interactive/InteractiveImportRow.css | 11 + .../Interactive/InteractiveImportRow.js | 294 + .../InteractiveImportRowCellPlaceholder.css | 7 + .../InteractiveImportRowCellPlaceholder.js | 10 + .../InteractiveImportModal.js | 78 + .../Quality/SelectQualityModal.js | 37 + .../Quality/SelectQualityModalContent.js | 166 + .../SelectQualityModalContentConnector.js | 94 + .../Season/SelectSeasonModal.js | 37 + .../Season/SelectSeasonModalContent.js | 58 + .../SelectSeasonModalContentConnector.js | 62 + .../Season/SelectSeasonRow.css | 4 + .../Season/SelectSeasonRow.js | 40 + .../Series/SelectSeriesModal.js | 37 + .../Series/SelectSeriesModalContent.css | 18 + .../Series/SelectSeriesModalContent.js | 99 + .../SelectSeriesModalContentConnector.js | 65 + .../Series/SelectSeriesRow.css | 4 + .../Series/SelectSeriesRow.js | 37 + frontend/src/JsLibraries/jquery.js | 9842 ++++++++++ frontend/src/JsLibraries/jquery.signalR.js | 2177 +++ frontend/src/Organize/OrganizePreviewModal.js | 34 + .../Organize/OrganizePreviewModalConnector.js | 39 + .../Organize/OrganizePreviewModalContent.css | 24 + .../Organize/OrganizePreviewModalContent.js | 200 + .../OrganizePreviewModalContentConnector.js | 91 + frontend/src/Organize/OrganizePreviewRow.css | 20 + frontend/src/Organize/OrganizePreviewRow.js | 90 + frontend/src/SeasonPass/SeasonPass.js | 257 + .../src/SeasonPass/SeasonPassConnector.js | 70 + frontend/src/SeasonPass/SeasonPassFooter.css | 14 + frontend/src/SeasonPass/SeasonPassFooter.js | 145 + frontend/src/SeasonPass/SeasonPassRow.css | 20 + frontend/src/SeasonPass/SeasonPassRow.js | 101 + .../src/SeasonPass/SeasonPassRowConnector.js | 77 + frontend/src/SeasonPass/SeasonPassSeason.css | 24 + frontend/src/SeasonPass/SeasonPassSeason.js | 88 + .../DownloadClients/DownloadClientSettings.js | 65 + .../DownloadClients/AddDownloadClientItem.css | 44 + .../DownloadClients/AddDownloadClientItem.js | 110 + .../DownloadClients/AddDownloadClientModal.js | 25 + .../AddDownloadClientModalContent.css | 5 + .../AddDownloadClientModalContent.js | 115 + .../AddDownloadClientModalContentConnector.js | 75 + .../AddDownloadClientPresetMenuItem.js | 49 + .../DownloadClients/DownloadClient.css | 19 + .../DownloadClients/DownloadClient.js | 106 + .../DownloadClients/DownloadClients.css | 20 + .../DownloadClients/DownloadClients.js | 117 + .../DownloadClientsConnector.js | 58 + .../EditDownloadClientModal.js | 25 + .../EditDownloadClientModalConnector.js | 39 + .../EditDownloadClientModalContent.css | 11 + .../EditDownloadClientModalContent.js | 177 + ...EditDownloadClientModalContentConnector.js | 94 + .../Options/DownloadClientOptions.js | 118 + .../Options/DownloadClientOptionsConnector.js | 92 + .../EditRemotePathMappingModal.js | 25 + .../EditRemotePathMappingModalConnector.js | 43 + .../EditRemotePathMappingModalContent.css | 12 + .../EditRemotePathMappingModalContent.js | 149 + ...tRemotePathMappingModalContentConnector.js | 119 + .../RemotePathMappings/RemotePathMapping.css | 23 + .../RemotePathMappings/RemotePathMapping.js | 114 + .../RemotePathMappings/RemotePathMappings.css | 23 + .../RemotePathMappings/RemotePathMappings.js | 102 + .../RemotePathMappingsConnector.js | 59 + .../src/Settings/General/GeneralSettings.js | 656 + .../General/GeneralSettingsConnector.js | 117 + .../src/Settings/Indexers/IndexerSettings.js | 65 + .../Indexers/Indexers/AddIndexerItem.css | 44 + .../Indexers/Indexers/AddIndexerItem.js | 110 + .../Indexers/Indexers/AddIndexerModal.js | 25 + .../Indexers/AddIndexerModalContent.css | 5 + .../Indexers/AddIndexerModalContent.js | 115 + .../AddIndexerModalContentConnector.js | 75 + .../Indexers/AddIndexerPresetMenuItem.js | 49 + .../Indexers/Indexers/EditIndexerModal.js | 25 + .../Indexers/EditIndexerModalConnector.js | 39 + .../Indexers/EditIndexerModalContent.css | 5 + .../Indexers/EditIndexerModalContent.js | 177 + .../EditIndexerModalContentConnector.js | 94 + .../Settings/Indexers/Indexers/Indexer.css | 19 + .../src/Settings/Indexers/Indexers/Indexer.js | 131 + .../Settings/Indexers/Indexers/Indexers.css | 20 + .../Settings/Indexers/Indexers/Indexers.js | 117 + .../Indexers/Indexers/IndexersConnector.js | 58 + .../Indexers/Options/IndexerOptions.js | 93 + .../Options/IndexerOptionsConnector.js | 92 + .../Restrictions/EditRestrictionModal.js | 25 + .../EditRestrictionModalConnector.js | 39 + .../EditRestrictionModalContent.css | 5 + .../EditRestrictionModalContent.js | 126 + .../EditRestrictionModalContentConnector.js | 111 + .../Indexers/Restrictions/Restriction.css | 11 + .../Indexers/Restrictions/Restriction.js | 147 + .../Indexers/Restrictions/Restrictions.css | 20 + .../Indexers/Restrictions/Restrictions.js | 100 + .../Restrictions/RestrictionsConnector.js | 61 + .../MediaManagement/MediaManagement.js | 347 + .../MediaManagementConnector.js | 91 + .../MediaManagement/Naming/Naming.css | 5 + .../Settings/MediaManagement/Naming/Naming.js | 345 + .../MediaManagement/Naming/NamingConnector.js | 102 + .../MediaManagement/Naming/NamingModal.css | 17 + .../MediaManagement/Naming/NamingModal.js | 469 + .../MediaManagement/Naming/NamingOption.css | 66 + .../MediaManagement/Naming/NamingOption.js | 85 + .../Metadata/Metadata/EditMetadataModal.js | 25 + .../Metadata/EditMetadataModalConnector.js | 39 + .../Metadata/EditMetadataModalContent.js | 103 + .../EditMetadataModalContentConnector.js | 93 + .../Settings/Metadata/Metadata/Metadata.css | 17 + .../Settings/Metadata/Metadata/Metadata.js | 103 + .../Settings/Metadata/Metadata/Metadatas.css | 4 + .../Settings/Metadata/Metadata/Metadatas.js | 46 + .../Metadata/Metadata/MetadatasConnector.js | 49 + .../src/Settings/Metadata/MetadataSettings.js | 21 + .../Notifications/NotificationSettings.js | 21 + .../Notifications/AddNotificationItem.css | 44 + .../Notifications/AddNotificationItem.js | 110 + .../Notifications/AddNotificationModal.js | 25 + .../AddNotificationModalContent.css | 5 + .../AddNotificationModalContent.js | 85 + .../AddNotificationModalContentConnector.js | 71 + .../AddNotificationPresetMenuItem.js | 49 + .../Notifications/EditNotificationModal.js | 25 + .../EditNotificationModalConnector.js | 39 + .../EditNotificationModalContent.css | 11 + .../EditNotificationModalContent.js | 235 + .../EditNotificationModalContentConnector.js | 94 + .../Notifications/Notification.css | 19 + .../Notifications/Notification.js | 151 + .../Notifications/Notifications.css | 20 + .../Notifications/Notifications.js | 117 + .../Notifications/NotificationsConnector.js | 58 + frontend/src/Settings/PendingChangesModal.js | 64 + .../Settings/Profiles/Delay/DelayProfile.css | 40 + .../Settings/Profiles/Delay/DelayProfile.js | 174 + .../Delay/DelayProfileDragPreview.css | 3 + .../Profiles/Delay/DelayProfileDragPreview.js | 78 + .../Profiles/Delay/DelayProfileDragSource.css | 17 + .../Profiles/Delay/DelayProfileDragSource.js | 148 + .../Settings/Profiles/Delay/DelayProfiles.css | 27 + .../Settings/Profiles/Delay/DelayProfiles.js | 150 + .../Profiles/Delay/DelayProfilesConnector.js | 105 + .../Profiles/Delay/EditDelayProfileModal.js | 25 + .../Delay/EditDelayProfileModalConnector.js | 43 + .../Delay/EditDelayProfileModalContent.css | 5 + .../Delay/EditDelayProfileModalContent.js | 186 + .../EditDelayProfileModalContentConnector.js | 178 + .../Language/EditLanguageProfileModal.js | 25 + .../EditLanguageProfileModalConnector.js | 43 + .../EditLanguageProfileModalContent.css | 3 + .../EditLanguageProfileModalContent.js | 149 + ...ditLanguageProfileModalContentConnector.js | 194 + .../Profiles/Language/LanguageProfile.css | 19 + .../Profiles/Language/LanguageProfile.js | 124 + .../Profiles/Language/LanguageProfileItem.css | 44 + .../Profiles/Language/LanguageProfileItem.js | 83 + .../LanguageProfileItemDragPreview.css | 4 + .../LanguageProfileItemDragPreview.js | 88 + .../LanguageProfileItemDragSource.css | 18 + .../Language/LanguageProfileItemDragSource.js | 157 + .../Language/LanguageProfileItems.css | 6 + .../Profiles/Language/LanguageProfileItems.js | 103 + .../Language/LanguageProfileNameConnector.js | 31 + .../Profiles/Language/LanguageProfiles.css | 21 + .../Profiles/Language/LanguageProfiles.js | 102 + .../Language/LanguageProfilesConnector.js | 60 + frontend/src/Settings/Profiles/Profiles.js | 36 + .../Quality/EditQualityProfileModal.js | 25 + .../EditQualityProfileModalConnector.js | 43 + .../EditQualityProfileModalContent.css | 3 + .../Quality/EditQualityProfileModalContent.js | 149 + ...EditQualityProfileModalContentConnector.js | 194 + .../Profiles/Quality/QualityProfile.css | 19 + .../Profiles/Quality/QualityProfile.js | 124 + .../Profiles/Quality/QualityProfileItem.css | 44 + .../Profiles/Quality/QualityProfileItem.js | 83 + .../Quality/QualityProfileItemDragPreview.css | 4 + .../Quality/QualityProfileItemDragPreview.js | 88 + .../Quality/QualityProfileItemDragSource.css | 18 + .../Quality/QualityProfileItemDragSource.js | 157 + .../Profiles/Quality/QualityProfileItems.css | 6 + .../Profiles/Quality/QualityProfileItems.js | 103 + .../Quality/QualityProfileNameConnector.js | 31 + .../Profiles/Quality/QualityProfiles.css | 21 + .../Profiles/Quality/QualityProfiles.js | 102 + .../Quality/QualityProfilesConnector.js | 60 + .../Quality/Definition/QualityDefinition.css | 95 + .../Quality/Definition/QualityDefinition.js | 166 + .../Definition/QualityDefinitionConnector.js | 70 + .../Quality/Definition/QualityDefinitions.css | 41 + .../Quality/Definition/QualityDefinitions.js | 65 + .../Definition/QualityDefinitionsConnector.js | 78 + frontend/src/Settings/Quality/Quality.js | 59 + frontend/src/Settings/SettingsToolbar.css | 7 + frontend/src/Settings/SettingsToolbar.js | 104 + .../src/Settings/SettingsToolbarConnector.js | 147 + frontend/src/Shared/piwikCheck.js | 10 + frontend/src/Shims/jquery.js | 8 + frontend/src/Shims/signalR.js | 4 + ...reateBatchToggleEpisodeMonitoredHandler.js | 41 + .../Actions/Creators/createFetchHandler.js | 46 + .../Creators/createFetchSchemaHandler.js | 35 + .../createFetchServerSideCollectionHandler.js | 54 + .../Creators/createRemoveItemHandler.js | 47 + .../Actions/Creators/createSaveHandler.js | 44 + .../Creators/createSaveProviderHandler.js | 53 + .../createServerSideCollectionHandlers.js | 52 + ...ateSetServerSideCollectionFilterHandler.js | 12 + ...reateSetServerSideCollectionPageHandler.js | 37 + ...reateSetServerSideCollectionSortHandler.js | 28 + .../Creators/createTestProviderHandler.js | 41 + .../createToggleEpisodeMonitoredHandler.js | 41 + frontend/src/Store/Actions/actionTypes.js | 394 + .../Store/Actions/addSeriesActionHandlers.js | 98 + .../src/Store/Actions/addSeriesActions.js | 15 + frontend/src/Store/Actions/appActions.js | 27 + .../src/Store/Actions/artistActionHandlers.js | 130 + .../src/Store/Actions/artistIndexActions.js | 8 + frontend/src/Store/Actions/baseActions.js | 13 + .../Store/Actions/blacklistActionHandlers.js | 17 + .../src/Store/Actions/blacklistActions.js | 12 + .../Store/Actions/calendarActionHandlers.js | 264 + frontend/src/Store/Actions/calendarActions.js | 12 + .../Store/Actions/captchaActionHandlers.js | 67 + frontend/src/Store/Actions/captchaActions.js | 8 + .../Store/Actions/commandActionHandlers.js | 141 + frontend/src/Store/Actions/commandActions.js | 14 + .../Store/Actions/episodeActionHandlers.js | 111 + frontend/src/Store/Actions/episodeActions.js | 10 + .../Actions/episodeFileActionHandlers.js | 164 + .../src/Store/Actions/episodeFileActions.js | 9 + .../Actions/episodeHistoryActionHandlers.js | 75 + .../Store/Actions/episodeHistoryActions.js | 7 + .../Store/Actions/historyActionHandlers.js | 60 + frontend/src/Store/Actions/historyActions.js | 16 + .../Actions/importSeriesActionHandlers.js | 172 + .../src/Store/Actions/importSeriesActions.js | 16 + .../interactiveImportActionHandlers.js | 48 + .../Store/Actions/interactiveImportActions.js | 11 + .../src/Store/Actions/oAuthActionHandlers.js | 80 + frontend/src/Store/Actions/oAuthActions.js | 7 + .../Actions/organizePreviewActionHandlers.js | 8 + .../Store/Actions/organizePreviewActions.js | 6 + .../src/Store/Actions/pathActionHandlers.js | 43 + frontend/src/Store/Actions/pathActions.js | 7 + .../src/Store/Actions/queueActionHandlers.js | 230 + frontend/src/Store/Actions/queueActions.js | 24 + .../Store/Actions/releaseActionHandlers.js | 47 + frontend/src/Store/Actions/releaseActions.js | 9 + .../Store/Actions/rootFolderActionHandlers.js | 59 + .../src/Store/Actions/rootFolderActions.js | 6 + .../Store/Actions/seasonPassActionHandlers.js | 83 + .../src/Store/Actions/seasonPassActions.js | 7 + frontend/src/Store/Actions/seriesActions.js | 16 + .../Actions/seriesEditorActionHandlers.js | 86 + .../src/Store/Actions/seriesEditorActions.js | 8 + .../Store/Actions/settingsActionHandlers.js | 238 + frontend/src/Store/Actions/settingsActions.js | 207 + .../src/Store/Actions/systemActionHandlers.js | 48 + frontend/src/Store/Actions/systemActions.js | 28 + .../src/Store/Actions/tagActionHandlers.js | 28 + frontend/src/Store/Actions/tagActions.js | 5 + .../src/Store/Actions/wantedActionHandlers.js | 34 + frontend/src/Store/Actions/wantedActions.js | 35 + frontend/src/Store/Middleware/middlewares.js | 39 + frontend/src/Store/Middleware/persistState.js | 119 + .../Reducers/Creators/createAddItemReducer.js | 23 + .../createClearPendingChangesReducer.js | 21 + .../Reducers/Creators/createClearReducer.js | 12 + .../Store/Reducers/Creators/createReducers.js | 20 + .../Creators/createRemoveItemReducer.js | 20 + ...ateSetClientSideCollectionFilterReducer.js | 17 + ...reateSetClientSideCollectionSortReducer.js | 29 + .../createSetProviderFieldValueReducer.js | 23 + .../Reducers/Creators/createSetReducer.js | 45 + .../Creators/createSetSettingValueReducer.js | 36 + .../Creators/createSetTableOptionReducer.js | 19 + .../Creators/createUpdateItemReducer.js | 36 + .../Reducers/Creators/createUpdateReducer.js | 23 + ...createUpdateServerSideCollectionReducer.js | 24 + .../src/Store/Reducers/addSeriesReducers.js | 68 + frontend/src/Store/Reducers/appReducers.js | 74 + .../src/Store/Reducers/artistIndexReducers.js | 213 + .../src/Store/Reducers/blacklistReducers.js | 80 + .../src/Store/Reducers/calendarReducers.js | 48 + .../src/Store/Reducers/captchaReducers.js | 32 + .../src/Store/Reducers/commandReducers.js | 64 + .../src/Store/Reducers/episodeFileReducers.js | 34 + .../Store/Reducers/episodeHistoryReducers.js | 26 + .../src/Store/Reducers/episodeReducers.js | 106 + .../src/Store/Reducers/historyReducers.js | 113 + .../Store/Reducers/importSeriesReducers.js | 35 + frontend/src/Store/Reducers/index.js | 88 + .../Reducers/interactiveImportReducers.js | 97 + frontend/src/Store/Reducers/oAuthReducers.js | 28 + .../Store/Reducers/organizePreviewReducers.js | 26 + frontend/src/Store/Reducers/pathReducers.js | 45 + frontend/src/Store/Reducers/queueReducers.js | 165 + .../src/Store/Reducers/releaseReducers.js | 65 + .../src/Store/Reducers/rootFolderReducers.js | 28 + .../src/Store/Reducers/seasonPassReducers.js | 39 + .../Store/Reducers/seriesEditorReducers.js | 41 + frontend/src/Store/Reducers/seriesReducers.js | 37 + .../src/Store/Reducers/settingsReducers.js | 337 + frontend/src/Store/Reducers/systemReducers.js | 146 + frontend/src/Store/Reducers/tagReducers.js | 22 + frontend/src/Store/Reducers/wantedReducers.js | 161 + .../Selectors/createAllSeriesSelector.js | 12 + .../Store/Selectors/createArtistSelector.js | 15 + .../createClientSideCollectionSelector.js | 117 + .../createCommandExecutingSelector.js | 14 + .../Store/Selectors/createCommandSelector.js | 14 + .../Store/Selectors/createCommandsSelector.js | 12 + .../Selectors/createDimensionsSelector.js | 12 + .../Selectors/createEpisodeFileSelector.js | 18 + .../Store/Selectors/createEpisodeSelector.js | 15 + .../Selectors/createExistingSeriesSelector.js | 15 + .../createImportSeriesItemSelector.js | 28 + .../createLanguageProfileSelector.js | 14 + .../Selectors/createProfileInUseSelector.js | 19 + .../createProviderSettingsSelector.js | 59 + .../Selectors/createQualityProfileSelector.js | 14 + .../Selectors/createQueueItemSelector.js | 20 + .../createSettingsSectionSelector.js | 32 + .../Selectors/createSystemStatusSelector.js | 12 + .../src/Store/Selectors/createTagsSelector.js | 12 + .../Selectors/createUISettingsSelector.js | 12 + .../src/Store/Selectors/selectSettings.js | 97 + frontend/src/Store/connectSection.js | 57 + frontend/src/Store/createAppStore.js | 15 + frontend/src/Store/scrollPositions.js | 5 + .../src/Styles/Mixins/clickable.css | 0 frontend/src/Styles/Mixins/cover.css | 8 + frontend/src/Styles/Mixins/linkOverlay.css | 11 + frontend/src/Styles/Mixins/scroller.css | 26 + frontend/src/Styles/Mixins/truncate.css | 18 + frontend/src/Styles/Variables/animations.js | 8 + frontend/src/Styles/Variables/colors.js | 170 + frontend/src/Styles/Variables/dimensions.js | 43 + frontend/src/Styles/Variables/fonts.js | 11 + frontend/src/Styles/globals.css | 4 + frontend/src/Styles/scaffolding.css | 42 + frontend/src/System/Backup/Backups.css | 5 + frontend/src/System/Backup/Backups.js | 148 + .../src/System/Backup/BackupsConnector.js | 81 + frontend/src/System/Events/LogsTable.js | 176 + .../src/System/Events/LogsTableConnector.js | 130 + .../System/Events/LogsTableDetailsModal.css | 17 + .../System/Events/LogsTableDetailsModal.js | 74 + frontend/src/System/Events/LogsTableRow.css | 35 + frontend/src/System/Events/LogsTableRow.js | 150 + frontend/src/System/Logs/Files/LogFiles.js | 139 + .../System/Logs/Files/LogFilesConnector.js | 93 + .../System/Logs/Files/LogFilesTableRow.css | 5 + .../src/System/Logs/Files/LogFilesTableRow.js | 50 + frontend/src/System/Logs/Logs.js | 30 + frontend/src/System/Logs/LogsNavMenu.js | 71 + .../Logs/Updates/UpdateLogFilesConnector.js | 93 + frontend/src/System/Status/About/About.js | 71 + .../src/System/Status/About/AboutConnector.js | 48 + .../src/System/Status/DiskSpace/DiskSpace.css | 5 + .../src/System/Status/DiskSpace/DiskSpace.js | 122 + .../Status/DiskSpace/DiskSpaceConnector.js | 54 + frontend/src/System/Status/Health/Health.css | 21 + frontend/src/System/Status/Health/Health.js | 170 + .../System/Status/Health/HealthConnector.js | 56 + .../Status/Health/HealthStatusConnector.js | 79 + .../src/System/Status/MoreInfo/MoreInfo.js | 69 + frontend/src/System/Status/Status.js | 29 + frontend/src/System/Tasks/TaskRow.css | 18 + frontend/src/System/Tasks/TaskRow.js | 94 + frontend/src/System/Tasks/TaskRowConnector.js | 91 + frontend/src/System/Tasks/Tasks.js | 89 + frontend/src/System/Tasks/TasksConnector.js | 46 + frontend/src/System/Updates/UpdateChanges.css | 4 + frontend/src/System/Updates/UpdateChanges.js | 45 + frontend/src/System/Updates/Updates.css | 46 + frontend/src/System/Updates/Updates.js | 149 + .../src/System/Updates/UpdatesConnector.js | 74 + frontend/src/Utilities/Array/sortByName.js | 5 + frontend/src/Utilities/Command/findCommand.js | 10 + frontend/src/Utilities/Command/index.js | 5 + .../Utilities/Command/isCommandComplete.js | 9 + .../Utilities/Command/isCommandExecuting.js | 9 + .../src/Utilities/Command/isCommandFailed.js | 12 + .../src/Utilities/Command/isSameCommand.js | 24 + frontend/src/Utilities/Constants/keyCodes.js | 7 + frontend/src/Utilities/Date/formatDate.js | 11 + frontend/src/Utilities/Date/formatDateTime.js | 39 + frontend/src/Utilities/Date/formatTime.js | 19 + frontend/src/Utilities/Date/formatTimeSpan.js | 24 + .../src/Utilities/Date/getRelativeDate.js | 40 + frontend/src/Utilities/Date/isAfter.js | 17 + frontend/src/Utilities/Date/isBefore.js | 17 + frontend/src/Utilities/Date/isInNextWeek.js | 11 + frontend/src/Utilities/Date/isSameWeek.js | 11 + frontend/src/Utilities/Date/isToday.js | 11 + frontend/src/Utilities/Date/isTomorrow.js | 11 + frontend/src/Utilities/Date/isYesterday.js | 11 + .../src/Utilities/Episode/updateEpisodes.js | 21 + frontend/src/Utilities/Number/formatAge.js | 17 + frontend/src/Utilities/Number/formatBytes.js | 16 + frontend/src/Utilities/Number/padNumber.js | 10 + .../src/Utilities/Object/getErrorMessage.js | 11 + .../src/Utilities/Object/hasDifferentItems.js | 10 + .../src/Utilities/Object/selectUniqueIds.js | 15 + frontend/src/Utilities/ResolutionUtility.js | 26 + .../Utilities/Series/getMonitoringOptions.js | 70 + frontend/src/Utilities/Series/getNewSeries.js | 34 + .../Utilities/Series/getProgressBarKind.js | 15 + .../src/Utilities/State/getProviderState.js | 30 + .../src/Utilities/State/getSectionState.js | 9 + .../Utilities/State/selectProviderSchema.js | 34 + .../src/Utilities/State/updateSectionState.js | 9 + frontend/src/Utilities/String/combinePath.js | 5 + frontend/src/Utilities/String/split.js | 17 + frontend/src/Utilities/String/titleCase.js | 11 + .../src/Utilities/Table/areAllSelected.js | 17 + .../src/Utilities/Table/getSelectedIds.js | 15 + .../src/Utilities/Table/getToggledRange.js | 23 + .../Utilities/Table/removeOldSelectedState.js | 16 + frontend/src/Utilities/Table/selectAll.js | 17 + .../src/Utilities/Table/toggleSelected.js | 26 + frontend/src/Utilities/getPathWithUrlBase.js | 3 + frontend/src/Utilities/getUniqueElementId.js | 7 + frontend/src/Utilities/isMobile.js | 7 + frontend/src/Utilities/pages.js | 9 + frontend/src/Utilities/requestAction.js | 40 + frontend/src/Utilities/sectionTypes.js | 6 + .../Utilities/serverSideCollectionHandlers.js | 12 + .../src/Wanted/CutoffUnmet/CutoffUnmet.js | 285 + .../CutoffUnmet/CutoffUnmetConnector.js | 180 + .../src/Wanted/CutoffUnmet/CutoffUnmetRow.css | 7 + .../src/Wanted/CutoffUnmet/CutoffUnmetRow.js | 169 + frontend/src/Wanted/Missing/Missing.js | 307 + .../src/Wanted/Missing/MissingConnector.js | 168 + frontend/src/Wanted/Missing/MissingRow.css | 6 + frontend/src/Wanted/Missing/MissingRow.js | 155 + frontend/src/index.css | 15 + frontend/src/index.html | 72 + frontend/src/index.js | 18 + frontend/src/jQuery/jquery.ajax.js | 47 + frontend/src/login.html | 220 + {src/UI => frontend/src}/oauth.html | 4 +- frontend/src/polyfills.js | 39 + frontend/src/preload.js | 1 + frontend/src/vendor.js | 5 + gulpFile.js | 2 +- npm-shrinkwrap.json | 5242 ++++++ package.json | 111 +- src/Lidarr.sln | 339 + .../ClientSchemaTests/SchemaBuilderFixture.cs | 6 +- .../NzbDrone.Api.Test.csproj | 4 + .../AlbumStudio/AlbumStudioModule.cs | 4 +- src/NzbDrone.Api/Albums/AlbumModule.cs | 6 +- .../Albums/AlbumModuleWithSignalR.cs | 11 +- src/NzbDrone.Api/Albums/AlbumResource.cs | 4 +- .../Authentication/EnableAuthInNancy.cs | 88 - src/NzbDrone.Api/Blacklist/BlacklistModule.cs | 5 +- .../Blacklist/BlacklistResource.cs | 6 +- src/NzbDrone.Api/Calendar/CalendarModule.cs | 6 +- .../ClientSchema/FieldDefinitionAttribute.cs | 4 - .../ClientSchema/SchemaDeserializer.cs | 7 - src/NzbDrone.Api/Commands/CommandModule.cs | 12 +- src/NzbDrone.Api/Commands/CommandResource.cs | 4 +- .../Config/DownloadClientConfigResource.cs | 2 +- src/NzbDrone.Api/Config/HostConfigModule.cs | 5 +- src/NzbDrone.Api/Config/HostConfigResource.cs | 2 +- .../Config/IndexerConfigModule.cs | 6 +- .../Config/IndexerConfigResource.cs | 2 +- .../Config/MediaManagementConfigResource.cs | 2 +- src/NzbDrone.Api/Config/NamingConfigModule.cs | 8 +- .../Config/NamingConfigResource.cs | 2 +- .../Config/NzbDroneConfigModule.cs | 7 +- src/NzbDrone.Api/Config/UiConfigResource.cs | 2 +- src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs | 5 +- .../DiskSpace/DiskSpaceResource.cs | 2 +- .../EpisodeFiles/EpisodeFileModule.cs | 14 +- .../EpisodeFiles/EpisodeFileResource.cs | 15 +- src/NzbDrone.Api/Episodes/EpisodeModule.cs | 6 +- .../Episodes/EpisodeModuleWithSignalR.cs | 11 +- src/NzbDrone.Api/Episodes/EpisodeResource.cs | 4 +- .../ErrorManagement/NzbDroneErrorPipeline.cs | 78 - .../Pipelines/NzbDroneVersionPipeline.cs | 25 - .../FileSystem/FileSystemModule.cs | 6 +- .../Frontend/Mappers/LoginHtmlMapper.cs | 90 - src/NzbDrone.Api/Health/HealthModule.cs | 7 +- src/NzbDrone.Api/Health/HealthResource.cs | 4 +- src/NzbDrone.Api/History/HistoryModule.cs | 18 +- src/NzbDrone.Api/History/HistoryResource.cs | 6 +- src/NzbDrone.Api/Indexers/ReleaseModule.cs | 6 +- .../Indexers/ReleaseModuleBase.cs | 5 +- .../Indexers/ReleasePushModule.cs | 4 +- src/NzbDrone.Api/Indexers/ReleaseResource.cs | 7 +- src/NzbDrone.Api/Logs/LogFileModuleBase.cs | 7 +- src/NzbDrone.Api/Logs/LogFileResource.cs | 4 +- src/NzbDrone.Api/Logs/LogModule.cs | 8 +- src/NzbDrone.Api/Logs/LogResource.cs | 4 +- .../ManualImport/ManualImportModule.cs | 3 +- .../ManualImport/ManualImportResource.cs | 2 +- .../Music/ArtistBulkImportModule.cs | 7 +- src/NzbDrone.Api/Music/ArtistEditorModule.cs | 4 +- src/NzbDrone.Api/Music/ArtistLookupModule.cs | 7 +- src/NzbDrone.Api/Music/ArtistModule.cs | 17 +- src/NzbDrone.Api/Music/ArtistResource.cs | 6 +- src/NzbDrone.Api/Music/ListImport.cs | 6 +- src/NzbDrone.Api/NancyBootstrapper.cs | 59 - src/NzbDrone.Api/NzbDrone.Api.csproj | 60 +- src/NzbDrone.Api/NzbDroneRestModule.cs | 40 - .../NzbDroneRestModuleWithSignalR.cs | 66 - src/NzbDrone.Api/Parse/ParseModule.cs | 7 +- src/NzbDrone.Api/Parse/ParseResource.cs | 6 +- .../Profiles/Delay/DelayProfileModule.cs | 11 +- .../Profiles/Delay/DelayProfileResource.cs | 4 +- .../Profiles/Languages/LanguageModule.cs | 9 +- .../Profiles/Languages/LanguageResource.cs | 6 +- src/NzbDrone.Api/Profiles/ProfileModule.cs | 11 +- src/NzbDrone.Api/Profiles/ProfileResource.cs | 14 +- .../Profiles/ProfileSchemaModule.cs | 11 +- src/NzbDrone.Api/ProviderModuleBase.cs | 10 +- src/NzbDrone.Api/ProviderResource.cs | 8 +- .../Qualities/QualityDefinitionModule.cs | 8 +- .../Qualities/QualityDefinitionResource.cs | 6 +- src/NzbDrone.Api/Queue/QueueActionModule.cs | 9 +- src/NzbDrone.Api/Queue/QueueModule.cs | 7 +- src/NzbDrone.Api/Queue/QueueResource.cs | 4 +- .../RemotePathMappingModule.cs | 7 +- .../RemotePathMappingResource.cs | 4 +- .../Restrictions/RestrictionModule.cs | 5 +- .../Restrictions/RestrictionResource.cs | 4 +- .../RootFolders/RootFolderModule.cs | 9 +- .../RootFolders/RootFolderResource.cs | 6 +- .../SeasonPass/SeasonPassModule.cs | 4 +- src/NzbDrone.Api/Series/SeriesEditorModule.cs | 5 +- src/NzbDrone.Api/Series/SeriesModule.cs | 15 +- src/NzbDrone.Api/Series/SeriesResource.cs | 9 +- .../System/Backup/BackupModule.cs | 11 +- .../System/Backup/BackupResource.cs | 4 +- src/NzbDrone.Api/System/SystemModule.cs | 4 +- src/NzbDrone.Api/System/Tasks/TaskModule.cs | 5 +- src/NzbDrone.Api/System/Tasks/TaskResource.cs | 4 +- src/NzbDrone.Api/Tags/TagModule.cs | 6 +- src/NzbDrone.Api/Tags/TagResource.cs | 4 +- .../TrackFiles/TrackFileModule.cs | 20 +- .../TrackFiles/TrackFileResource.cs | 11 +- src/NzbDrone.Api/Tracks/RenameTrackModule.cs | 8 +- .../Tracks/RenameTrackResource.cs | 4 +- src/NzbDrone.Api/Tracks/TrackModule.cs | 8 +- .../Tracks/TrackModuleWithSignalR.cs | 20 +- src/NzbDrone.Api/Tracks/TrackResource.cs | 4 +- src/NzbDrone.Api/Update/UpdateModule.cs | 8 +- src/NzbDrone.Api/Update/UpdateResource.cs | 4 +- src/NzbDrone.Api/Wanted/CutoffModule.cs | 5 +- src/NzbDrone.Api/Wanted/MissingModule.cs | 6 +- .../Disk/FileSystemLookupService.cs | 19 +- .../EnvironmentInfo/IRuntimeInfo.cs | 7 +- .../EnvironmentInfo/RuntimeInfo.cs | 46 +- .../EnvironmentInfo/RuntimeMode.cs | 9 + src/NzbDrone.Common/NzbDrone.Common.csproj | 1 + src/NzbDrone.Common/Serializer/Json.cs | 22 +- .../Datastore/DatabaseRelationshipFixture.cs | 4 +- .../Datastore/MarrDataLazyLoadingFixture.cs | 52 +- .../Migration/075_force_lib_updateFixture.cs | 40 +- .../Migration/079_dedupe_tagsFixture.cs | 53 +- ...108_fix_metadata_file_extensionsFixture.cs | 5 +- .../CutoffSpecificationFixture.cs | 206 +- .../HistorySpecificationFixture.cs | 30 +- .../LanguageSpecificationFixture.cs | 29 +- .../PrioritizeDownloadDecisionFixture.cs | 117 +- ...ityAllowedByProfileSpecificationFixture.cs | 6 +- .../QualityUpgradeSpecificationFixture.cs | 78 +- .../QueueSpecificationFixture.cs | 113 +- .../RssSync/DelaySpecificationFixture.cs | 45 +- .../RssSync/ProperSpecificationFixture.cs | 4 +- .../UpgradeDiskSpecificationFixture.cs | 23 +- .../DownloadApprovedFixture.cs | 4 +- .../PendingReleaseServiceTests/AddFixture.cs | 4 +- .../RemoveGrabbedFixture.cs | 4 +- .../RemoveRejectedFixture.cs | 4 +- .../HistoryTests/HistoryServiceFixture.cs | 66 +- .../Languages/LanguageFixture.cs | 100 + .../LanguageProfileRepositoryFixture.cs | 32 + .../LanguageProfileServiceFixture.cs | 75 + .../ImportApprovedEpisodesFixture.cs | 43 +- .../TrackImport/ImportDecisionMakerFixture.cs | 38 +- .../UpgradeSpecificationFixture.cs | 80 +- .../NzbDrone.Core.Test.csproj | 3 + .../ParserTests/LanguageParserFixture.cs | 242 +- .../Profiles/ProfileRepositoryFixture.cs | 6 +- .../Profiles/ProfileServiceFixture.cs | 4 +- .../Qualities/QualityFixture.cs | 4 +- .../Qualities/QualityModelComparerFixture.cs | 4 +- .../EpisodesWhereCutoffUnmetFixture.cs | 97 +- .../SeriesRepositoryFixture.cs | 30 +- .../Annotations/FieldDefinitionAttribute.cs | 7 +- .../ArtistStats/AlbumStatistics.cs | 3 +- .../ArtistStats/ArtistStatisticsRepository.cs | 5 +- src/NzbDrone.Core/Backup/Backup.cs | 4 +- src/NzbDrone.Core/Backup/BackupService.cs | 2 +- src/NzbDrone.Core/Blacklisting/Blacklist.cs | 4 +- .../Blacklisting/BlacklistService.cs | 7 +- .../Datastore/Converters/GuidConverter.cs | 2 +- .../Converters/LanguageIntConverter.cs | 65 + .../036_update_with_quality_converters.cs | 4 +- ...e_to_episodeFiles_history_and_blacklist.cs | 97 + .../Migration/111_create_language_profiles.cs | 175 + .../Datastore/Migration/111_setup_music.cs | 106 - .../Datastore/Migration/112_music_history.cs | 36 - .../Datastore/Migration/112_setup_music.cs | 108 + .../Migration/113_music_blacklist.cs | 24 - .../Datastore/Migration/113_music_history.cs | 36 + .../Migration/114_music_blacklist.cs | 24 + .../Migration/114_remove_tv_naming.cs | 27 - .../115_change_drone_factory_variable_name.cs | 20 - .../Migration/115_remove_tv_naming.cs | 27 + .../116_change_drone_factory_variable_name.cs | 20 + .../Framework/NzbDroneMigrationBase.cs | 3 +- .../Framework/NzbDroneSqliteProcessor.cs | 3 +- src/NzbDrone.Core/Datastore/TableMapping.cs | 27 +- .../DownloadDecisionComparer.cs | 10 +- .../DownloadDecisionPriorizationService.cs | 3 +- .../LanguageUpgradableSpecification.cs | 75 + .../QualityUpgradableSpecification.cs | 72 - .../Specifications/CutoffSpecification.cs | 16 +- .../Specifications/LanguageSpecification.cs | 8 +- .../Specifications/QueueSpecification.cs | 21 +- .../RssSync/DelaySpecification.cs | 33 +- .../RssSync/HistorySpecification.cs | 12 +- .../RssSync/ProperSpecification.cs | 6 +- .../UpgradeDiskSpecification.cs | 15 +- .../DecisionEngine/UpgradableSpecification.cs | 109 + .../DiskSpace/DiskSpaceService.cs | 45 +- .../rTorrent/RTorrentDirectoryValidator.cs | 4 +- .../Download/DownloadClientItem.cs | 6 +- .../Download/DownloadFailedEvent.cs | 6 +- .../Download/FailedDownloadService.cs | 5 +- .../Download/Pending/PendingReleaseService.cs | 7 +- .../DownloadMonitoringService.cs | 15 +- .../TrackedDownloads/TrackedDownload.cs | 4 +- .../TrackedDownloadService.cs | 86 +- .../TrackedDownloadsRemovedEvent.cs | 15 + .../MediaBrowserMetadataSettings.cs | 4 +- .../Roksbox/RoksboxMetadataSettings.cs | 10 +- .../Extras/Subtitles/SubtitleFile.cs | 4 +- .../Extras/Subtitles/SubtitleService.cs | 3 +- src/NzbDrone.Core/History/History.cs | 6 +- .../History/HistoryRepository.cs | 12 +- src/NzbDrone.Core/History/HistoryService.cs | 28 +- .../IndexerSearch/AlbumSearchService.cs | 4 +- .../CutoffUnmetAlbumSearchCommand.cs | 26 + .../Instrumentation/ReconfigureLogging.cs | 4 +- src/NzbDrone.Core/Languages/Language.cs | 158 + .../Languages/LanguageComparer.cs | 27 + .../Languages/LanguagesBelowCutoff.cs | 17 + .../Commands/DownloadedAlbumsScanCommand.cs | 4 - .../MediaFiles/DiskScanService.cs | 2 +- src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 6 +- .../MediaFiles/MediaFileService.cs | 17 +- src/NzbDrone.Core/MediaFiles/TrackFile.cs | 4 +- .../TrackImport/ImportApprovedEpisodes.cs | 1 + .../TrackImport/ImportApprovedTracks.cs | 3 + .../TrackImport/ImportDecisionMaker.cs | 33 + .../Specifications/UpgradeSpecification.cs | 10 +- .../Messaging/Commands/Command.cs | 16 +- .../Messaging/Commands/CommandQueueManager.cs | 58 +- .../MetadataSource/SkyHook/SkyHookProxy.cs | 42 +- src/NzbDrone.Core/Music/AddArtistValidator.cs | 4 +- src/NzbDrone.Core/Music/AlbumRepository.cs | 19 +- src/NzbDrone.Core/Music/AlbumService.cs | 8 +- src/NzbDrone.Core/Music/Artist.cs | 10 +- src/NzbDrone.Core/Music/ArtistStatusType.cs | 8 + .../Notifications/NotificationDefinition.cs | 6 - .../Notifications/NotificationService.cs | 12 +- .../Notifications/Twitter/TwitterSettings.cs | 6 +- src/NzbDrone.Core/NzbDrone.Core.csproj | 45 +- src/NzbDrone.Core/Parser/IsoLanguage.cs | 4 +- src/NzbDrone.Core/Parser/IsoLanguages.cs | 3 +- src/NzbDrone.Core/Parser/Language.cs | 29 - src/NzbDrone.Core/Parser/LanguageParser.cs | 3 +- .../Parser/Model/LocalEpisode.cs | 4 +- src/NzbDrone.Core/Parser/Model/LocalTrack.cs | 4 +- .../Parser/Model/ParsedAlbumInfo.cs | 3 +- .../Parser/Model/ParsedEpisodeInfo.cs | 5 +- .../Parser/Model/ParsedTrackInfo.cs | 5 +- src/NzbDrone.Core/Parser/Parser.cs | 108 +- src/NzbDrone.Core/Parser/ParsingService.cs | 4 +- .../Profiles/Delay/DelayProfileService.cs | 81 +- .../Profiles/Languages/LanguageProfile.cs | 19 + .../LanguageProfileInUseException.cs | 13 + .../Languages/LanguageProfileRepository.cs | 23 + .../Languages/LanguageProfileService.cs | 95 + .../Profiles/Languages/ProfileLanguageItem.cs | 11 + src/NzbDrone.Core/Profiles/Profile.cs | 21 - .../Profiles/ProfileInUseException.cs | 13 - .../Profiles/ProfileQualityItem.cs | 11 - src/NzbDrone.Core/Profiles/Quality/Profile.cs | 19 + .../Profiles/Quality/ProfileInUseException.cs | 13 + .../Profiles/Quality/ProfileQualityItem.cs | 11 + .../{ => Quality}/ProfileRepository.cs | 4 +- .../Profiles/{ => Quality}/ProfileService.cs | 9 +- .../Qualities/QualityDefinitionService.cs | 16 +- src/NzbDrone.Core/Qualities/QualityModel.cs | 46 +- .../Qualities/QualityModelComparer.cs | 4 +- .../Queue/EstimatedCompletionTimeComparer.cs | 38 + src/NzbDrone.Core/Queue/Queue.cs | 5 +- src/NzbDrone.Core/Queue/QueueService.cs | 13 +- src/NzbDrone.Core/Queue/TimeleftComparer.cs | 38 + .../Restrictions/RestrictionService.cs | 2 +- .../SeriesStats/SeriesStatistics.cs | 3 +- .../SeriesStats/SeriesStatisticsRepository.cs | 5 +- src/NzbDrone.Core/Tags/TagDetails.cs | 18 + src/NzbDrone.Core/Tags/TagRepository.cs | 6 + src/NzbDrone.Core/Tags/TagService.cs | 46 +- .../ThingiProvider/IProviderFactory.cs | 1 + .../ThingiProvider/ProviderDefinition.cs | 9 +- .../ThingiProvider/ProviderFactory.cs | 10 +- .../Status/EscalationBackOff.cs | 18 + .../Status/ProviderStatusBase.cs | 23 + .../Status/ProviderStatusRepository.cs | 30 + .../Status/ProviderStatusServiceBase.cs | 138 + src/NzbDrone.Core/Tv/AddSeriesService.cs | 59 +- src/NzbDrone.Core/Tv/AddSeriesValidator.cs | 4 +- src/NzbDrone.Core/Tv/Episode.cs | 33 +- src/NzbDrone.Core/Tv/EpisodeCutoffService.cs | 27 +- src/NzbDrone.Core/Tv/EpisodeRepository.cs | 48 +- src/NzbDrone.Core/Tv/EpisodeService.cs | 8 +- .../Tv/Events/SeriesImportedEvent.cs | 15 + src/NzbDrone.Core/Tv/RefreshSeriesService.cs | 4 +- src/NzbDrone.Core/Tv/Series.cs | 10 +- src/NzbDrone.Core/Tv/SeriesAddedHandler.cs | 11 +- src/NzbDrone.Core/Tv/SeriesService.cs | 19 +- .../LanguageProfileExistsValidator.cs | 23 + .../Validation/LanguageValidator.cs | 21 - .../Validation/Paths/DroneFactoryValidator.cs | 28 - .../Validation/ProfileExistsValidator.cs | 2 +- .../Validation/RuleBuilderExtensions.cs | 13 +- src/NzbDrone.Host/MainAppContainerBuilder.cs | 12 +- src/NzbDrone.Host/NzbDrone.Host.csproj | 8 + .../Owin/MiddleWare/SignalRMiddleWare.cs | 4 +- .../Client/ClientBase.cs | 7 +- src/NzbDrone.Integration.Test/CorsFixture.cs | 4 +- .../IntegrationTest.cs | 4 +- .../NzbDrone.Integration.Test.csproj | 4 + .../NzbDronePersistentConnection.cs | 45 +- src/NzbDrone.SignalR/SignalRMessage.cs | 6 + .../AlbumStudio/AlbumStudioArtistResource.cs | 12 + .../AlbumStudio/AlbumStudioModule.cs | 55 + .../AlbumStudio/AlbumStudioResource.cs | 11 + src/Sonarr.Api.V3/Albums/AlbumModule.cs | 59 + .../Albums/AlbumModuleWithSignalR.cs | 148 + src/Sonarr.Api.V3/Albums/AlbumResource.cs | 65 + .../Albums/AlbumStatisticsResource.cs | 39 + .../Albums/AlbumsMonitoredResource.cs | 11 + .../Artist/AlternateTitleResource.cs | 9 + .../Artist/ArtistEditorDeleteResource.cs | 10 + .../Artist/ArtistEditorModule.cs | 85 + .../Artist/ArtistEditorResource.cs | 25 + .../Artist/ArtistImportModule.cs | 29 + .../Artist/ArtistLookupModule.cs | 45 + src/Sonarr.Api.V3/Artist/ArtistModule.cs | 234 + src/Sonarr.Api.V3/Artist/ArtistResource.cs | 257 + .../Blacklist/BlacklistModule.cs | 36 + .../Blacklist/BlacklistResource.cs | 50 + .../Calendar/CalendarFeedModule.cs | 101 + src/Sonarr.Api.V3/Calendar/CalendarModule.cs | 44 + src/Sonarr.Api.V3/Commands/CommandModule.cs | 70 + src/Sonarr.Api.V3/Commands/CommandResource.cs | 130 + .../Config/DownloadClientConfigModule.cs | 17 + .../Config/DownloadClientConfigResource.cs | 33 + src/Sonarr.Api.V3/Config/HostConfigModule.cs | 85 + .../Config/HostConfigResource.cs | 73 + .../Config/IndexerConfigModule.cs | 28 + .../Config/IndexerConfigResource.cs | 25 + .../Config/MediaManagementConfigModule.cs | 22 + .../Config/MediaManagementConfigResource.cs | 54 + .../Config/NamingConfigModule.cs | 114 + .../Config/NamingConfigResource.cs | 19 + .../Config/NamingExampleResource.cs | 63 + .../Config/SonarrConfigModule.cs | 52 + src/Sonarr.Api.V3/Config/UiConfigModule.cs | 21 + src/Sonarr.Api.V3/Config/UiConfigResource.cs | 39 + .../DiskSpace/DiskSpaceModule.cs | 23 + .../DiskSpace/DiskSpaceResource.cs | 28 + .../DownloadClient/DownloadClientModule.cs | 20 + .../DownloadClient/DownloadClientResource.cs | 38 + .../FileSystem/FileSystemModule.cs | 71 + src/Sonarr.Api.V3/Health/HealthModule.cs | 32 + src/Sonarr.Api.V3/Health/HealthResource.cs | 38 + src/Sonarr.Api.V3/History/HistoryModule.cs | 76 + src/Sonarr.Api.V3/History/HistoryResource.cs | 58 + src/Sonarr.Api.V3/Indexers/IndexerModule.cs | 20 + src/Sonarr.Api.V3/Indexers/IndexerResource.cs | 43 + src/Sonarr.Api.V3/Indexers/ReleaseModule.cs | 120 + .../Indexers/ReleaseModuleBase.cs | 42 + .../Indexers/ReleasePushModule.cs | 50 + src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 153 + src/Sonarr.Api.V3/Lidarr.Api.V3.csproj | 242 + src/Sonarr.Api.V3/Logs/LogFileModule.cs | 43 + src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs | 72 + src/Sonarr.Api.V3/Logs/LogFileResource.cs | 13 + src/Sonarr.Api.V3/Logs/LogModule.cs | 60 + src/Sonarr.Api.V3/Logs/LogResource.cs | 37 + src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs | 48 + .../ManualImport/ManualImportModule.cs | 41 + .../ManualImport/ManualImportResource.cs | 56 + .../MediaCovers/MediaCoverModule.cs | 47 + src/Sonarr.Api.V3/Metadata/MetadataModule.cs | 20 + .../Metadata/MetadataResource.cs | 34 + .../Notifications/NotificationModule.cs | 20 + .../Notifications/NotificationResource.cs | 57 + src/Sonarr.Api.V3/Parse/ParseModule.cs | 59 + src/Sonarr.Api.V3/Parse/ParseResource.cs | 16 + .../Profiles/Delay/DelayProfileModule.cs | 90 + .../Profiles/Delay/DelayProfileResource.cs | 63 + .../Language/LanguageProfileModule.cs | 57 + .../Language/LanguageProfileResource.cs | 76 + .../Language/LanguageProfileSchemaModule.cs | 37 + .../Profiles/Language/LanguageValidator.cs | 43 + .../Profiles/Quality/QualityProfileModule.cs | 55 + .../Quality/QualityProfileResource.cs | 78 + .../Quality/QualityProfileSchemaModule.cs | 34 + .../Quality/QualityProfileValidation.cs | 43 + src/Sonarr.Api.V3/Properties/AssemblyInfo.cs | 11 + src/Sonarr.Api.V3/ProviderModuleBase.cs | 187 + src/Sonarr.Api.V3/ProviderResource.cs | 70 + .../Qualities/QualityDefinitionModule.cs | 54 + .../Qualities/QualityDefinitionResource.cs | 62 + src/Sonarr.Api.V3/Queue/QueueActionModule.cs | 167 + src/Sonarr.Api.V3/Queue/QueueBulkResource.cs | 9 + src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs | 68 + src/Sonarr.Api.V3/Queue/QueueModule.cs | 113 + src/Sonarr.Api.V3/Queue/QueueResource.cs | 70 + src/Sonarr.Api.V3/Queue/QueueStatusModule.cs | 56 + .../Queue/QueueStatusResource.cs | 11 + .../RemotePathMappingModule.cs | 68 + .../RemotePathMappingResource.cs | 50 + .../Restrictions/RestrictionModule.cs | 60 + .../Restrictions/RestrictionResource.cs | 58 + .../RootFolders/RootFolderModule.cs | 58 + .../RootFolders/RootFolderResource.cs | 51 + src/Sonarr.Api.V3/SonarrV3FeedModule.cs | 12 + src/Sonarr.Api.V3/SonarrV3Module.cs | 12 + .../System/Backup/BackupModule.cs | 35 + .../System/Backup/BackupResource.cs | 14 + src/Sonarr.Api.V3/System/SystemModule.cs | 93 + src/Sonarr.Api.V3/System/Tasks/TaskModule.cs | 67 + .../System/Tasks/TaskResource.cs | 14 + src/Sonarr.Api.V3/Tags/TagDetailsModule.cs | 23 + src/Sonarr.Api.V3/Tags/TagDetailsResource.cs | 44 + src/Sonarr.Api.V3/Tags/TagModule.cs | 57 + src/Sonarr.Api.V3/Tags/TagResource.cs | 42 + .../TrackFiles/MediaInfoResource.cs | 28 + .../TrackFiles/TrackFileListResource.cs | 13 + .../TrackFiles/TrackFileModule.cs | 163 + .../TrackFiles/TrackFileResource.cs | 78 + src/Sonarr.Api.V3/Tracks/RenameTrackModule.cs | 43 + .../Tracks/RenameTrackResource.cs | 39 + src/Sonarr.Api.V3/Tracks/TrackModule.cs | 56 + .../Tracks/TrackModuleWithSignalR.cs | 132 + src/Sonarr.Api.V3/Tracks/TrackResource.cs | 68 + src/Sonarr.Api.V3/Update/UpdateModule.cs | 46 + src/Sonarr.Api.V3/Update/UpdateResource.cs | 53 + src/Sonarr.Api.V3/Wanted/CutoffModule.cs | 52 + src/Sonarr.Api.V3/Wanted/MissingModule.cs | 47 + src/Sonarr.Api.V3/app.config | 15 + src/Sonarr.Api.V3/packages.config | 10 + .../Authentication/1tews5g3.gd1~ | 0 .../Authentication/AuthenticationModule.cs | 5 +- .../Authentication/AuthenticationService.cs | 6 +- .../Authentication/EnableAuthInNancy.cs | 136 + .../Authentication/LoginResource.cs | 2 +- .../Authentication/NzbDroneUser.cs | 2 +- .../ClientSchema/Field.cs | 2 +- .../ClientSchema/SchemaBuilder.cs | 2 +- .../ClientSchema/SelectOption.cs | 2 +- .../ErrorManagement/ErrorHandler.cs | 4 +- .../ErrorManagement/ErrorModel.cs | 4 +- .../ErrorManagement/SonarrErrorPipeline.cs | 79 + .../Exceptions}/ApiException.cs | 5 +- .../Exceptions/InvalidApiKeyException.cs | 2 +- .../Extensions/AccessControlHeaders.cs | 2 +- .../Extensions/NancyJsonSerializer.cs | 2 +- .../Pipelines/CacheHeaderPipeline.cs | 4 +- .../Extensions/Pipelines/CorsPipeline.cs | 2 +- .../Extensions/Pipelines/GZipPipeline.cs | 6 +- .../Pipelines/IRegisterNancyPipeline.cs | 2 +- .../Pipelines/IfModifiedPipeline.cs | 4 +- .../Pipelines/RequestLoggingPipeline.cs | 184 +- .../Pipelines/SonarrVersionPipeline.cs | 25 + .../Extensions/Pipelines/UrlBasePipeline.cs | 0 .../Extensions/ReqResExtensions.cs | 2 +- .../Extensions/RequestExtensions.cs | 14 +- .../Frontend/CacheableSpecification.cs | 4 +- .../Frontend/Mappers/BackupFileMapper.cs | 2 +- .../Frontend/Mappers/BrowserConfig.cs | 34 + .../Frontend/Mappers/CacheBreakerProvider.cs | 2 +- .../Frontend/Mappers/FaviconMapper.cs | 6 +- .../Mappers/IMapHttpRequestsToDisk.cs | 2 +- .../Frontend/Mappers/IndexHtmlMapper.cs | 15 +- .../Frontend/Mappers/LogFileMapper.cs | 2 +- .../Frontend/Mappers/LoginHtmlMapper.cs | 71 + .../Frontend/Mappers/ManifestMapper.cs | 34 + .../Frontend/Mappers/MediaCoverMapper.cs | 2 +- .../Frontend/Mappers/RobotsTxtMapper.cs | 2 +- .../Frontend/Mappers/StaticResourceMapper.cs | 10 +- .../Mappers/StaticResourceMapperBase.cs | 4 +- .../Frontend/Mappers/UpdateLogFileMapper.cs | 2 +- .../Frontend/StaticResourceModule.cs | 6 +- .../LIdarrRestModuleWithSignalR.cs | 71 + src/Sonarr.Http/Lidarr.Http.csproj | 155 + src/Sonarr.Http/LidarrBootstrapper.cs | 59 + src/Sonarr.Http/LidarrRestModule.cs | 56 + src/Sonarr.Http/Mapping/MappingValidation.cs | 54 + .../Mapping/ResourceMappingException.cs | 15 + .../PagingResource.cs | 4 +- src/Sonarr.Http/Properties/AssemblyInfo.cs | 36 + .../REST/BadRequestException.cs | 4 +- .../REST/MethodNotAllowedException.cs | 4 +- .../REST/NotFoundException.cs | 4 +- .../REST/ResourceValidator.cs | 6 +- .../REST/RestModule.cs | 16 +- .../REST/RestResource.cs | 2 +- .../ResourceChangeMessage.cs | 4 +- .../TinyIoCNancyBootstrapper.cs | 2 +- .../Validation/EmptyCollectionValidator.cs | 2 +- .../Validation/RssSyncIntervalValidator.cs | 2 +- .../Validation/RuleBuilderExtensions.cs | 2 +- src/Sonarr.Http/app.config | 11 + src/Sonarr.Http/packages.config | 9 + src/UI/.idea/.name | 1 - src/UI/.idea/NzbDrone.UI.iml | 11 - src/UI/.idea/codeStyleSettings.xml | 59 - src/UI/.idea/dictionaries/Keivan.xml | 20 - src/UI/.idea/dictionaries/Keivan_Beigi.xml | 13 - src/UI/.idea/dictionaries/Mark.xml | 3 - src/UI/.idea/encodings.xml | 7 - .../inspectionProfiles/Project_Default.xml | 117 - .../inspectionProfiles/profiles_settings.xml | 7 - src/UI/.idea/jsLibraryMappings.xml | 8 - src/UI/.idea/jsLinters/jshint.xml | 72 - src/UI/.idea/jsLinters/jslint.xml | 13 - src/UI/.idea/misc.xml | 6 - src/UI/.idea/modules.xml | 8 - .../runConfigurations/Debug___Chrome.xml | 23 - .../runConfigurations/Debug___Firefox.xml | 23 - src/UI/.idea/scopes/NzbDrone.xml | 3 - src/UI/.idea/scopes/scope_settings.xml | 5 - src/UI/.idea/vcs.xml | 7 - src/UI/.jshintrc | 19 - src/UI/Activity/ActivityLayout.js | 84 - src/UI/Activity/ActivityLayoutTemplate.hbs | 11 - .../Blacklist/BlacklistActionsCell.js | 28 - .../Activity/Blacklist/BlacklistCollection.js | 47 - src/UI/Activity/Blacklist/BlacklistLayout.js | 114 - .../Blacklist/BlacklistLayoutTemplate.hbs | 11 - src/UI/Activity/Blacklist/BlacklistModel.js | 17 - .../Details/BlacklistDetailsLayout.js | 14 - .../BlacklistDetailsLayoutTemplate.hbs | 18 - .../Blacklist/Details/BlacklistDetailsView.js | 5 - .../Details/BlacklistDetailsViewTemplate.hbs | 23 - .../History/Details/HistoryDetailsAge.js | 22 - .../History/Details/HistoryDetailsLayout.js | 35 - .../Details/HistoryDetailsLayoutTemplate.hbs | 22 - .../History/Details/HistoryDetailsView.js | 6 - .../Details/HistoryDetailsViewTemplate.hbs | 103 - src/UI/Activity/History/HistoryCollection.js | 84 - src/UI/Activity/History/HistoryDetailsCell.js | 21 - src/UI/Activity/History/HistoryLayout.js | 146 - .../History/HistoryLayoutTemplate.hbs | 11 - src/UI/Activity/History/HistoryModel.js | 12 - src/UI/Activity/History/HistoryQualityCell.js | 30 - src/UI/Activity/Queue/ProgressCell.js | 23 - src/UI/Activity/Queue/QueueActionsCell.js | 59 - .../Queue/QueueActionsCellTemplate.hbs | 12 - src/UI/Activity/Queue/QueueCollection.js | 79 - src/UI/Activity/Queue/QueueLayout.js | 91 - src/UI/Activity/Queue/QueueLayoutTemplate.hbs | 11 - src/UI/Activity/Queue/QueueModel.js | 12 - src/UI/Activity/Queue/QueueStatusCell.js | 81 - .../Queue/QueueStatusCellTemplate.hbs | 8 - src/UI/Activity/Queue/QueueView.js | 40 - src/UI/Activity/Queue/RemoveFromQueueView.js | 34 - .../Queue/RemoveFromQueueViewTemplate.hbs | 49 - src/UI/Activity/Queue/TimeleftCell.js | 33 - src/UI/Activity/activity.less | 27 - src/UI/AddArtist/AddArtistCollection.js | 23 - src/UI/AddArtist/AddArtistLayout.js | 66 - src/UI/AddArtist/AddArtistLayoutTemplate.hbs | 18 - src/UI/AddArtist/AddArtistView.js | 183 - src/UI/AddArtist/AddArtistViewTemplate.hbs | 24 - .../AddArtist/ArtistTypeSelectionPartial.hbs | 3 - src/UI/AddArtist/BulkImport/ArtistPathCell.js | 7 - .../BulkImport/ArtistPathTemplate.hbs | 1 - .../BulkImport/BulkImportArtistNameCell.js | 21 - .../BulkImport/BulkImportCollection.js | 49 - .../BulkImport/BulkImportMonitorCell.js | 65 - .../BulkImportMonitorCellTemplate.hbs | 4 - .../BulkImport/BulkImportProfileCell.js | 32 - .../BulkImport/BulkImportProfileCellT.js | 77 - .../BulkImportProfileCellTemplate.hbs | 5 - .../BulkImport/BulkImportSelectAllCell.js | 54 - src/UI/AddArtist/BulkImport/BulkImportView.js | 191 - .../BulkImport/BulkImportViewTemplate.hbs | 13 - src/UI/AddArtist/BulkImport/EmptyView.js | 10 - .../BulkImport/EmptyViewTemplate.hbs | 3 - src/UI/AddArtist/BulkImport/ForeignIdCell.js | 57 - src/UI/AddArtist/EmptyView.js | 5 - src/UI/AddArtist/EmptyViewTemplate.hbs | 3 - src/UI/AddArtist/ErrorView.js | 13 - src/UI/AddArtist/ErrorViewTemplate.hbs | 7 - .../AddExistingArtistCollectionView.js | 51 - ...ddExistingArtistCollectionViewTemplate.hbs | 5 - .../Existing/UnmappedFolderCollection.js | 20 - .../AddArtist/Existing/UnmappedFolderModel.js | 3 - .../AddArtist/MonitoringTooltipTemplate.hbs | 18 - src/UI/AddArtist/NotFoundView.js | 13 - src/UI/AddArtist/NotFoundViewTemplate.hbs | 7 - .../RootFolders/RootFolderCollection.js | 10 - .../RootFolders/RootFolderCollectionView.js | 8 - .../RootFolderCollectionViewTemplate.hbs | 13 - .../RootFolders/RootFolderItemView.js | 28 - .../RootFolderItemViewTemplate.hbs | 9 - .../AddArtist/RootFolders/RootFolderLayout.js | 80 - .../RootFolders/RootFolderLayoutTemplate.hbs | 36 - .../AddArtist/RootFolders/RootFolderModel.js | 8 - .../RootFolderSelectionPartial.hbs | 11 - .../AddArtist/SearchResultCollectionView.js | 29 - src/UI/AddArtist/SearchResultView.js | 297 - src/UI/AddArtist/SearchResultViewTemplate.hbs | 146 - .../StartingAlbumSelectionPartial.hbs | 13 - src/UI/AddArtist/addArtist.less | 181 - src/UI/Album/AlbumDetailsLayout.js | 133 - src/UI/Album/AlbumDetailsLayoutTemplate.hbs | 35 - .../Album/History/AlbumHistoryActionsCell.js | 35 - .../Album/History/AlbumHistoryDetailsCell.js | 28 - src/UI/Album/History/AlbumHistoryLayout.js | 84 - .../History/AlbumHistoryLayoutTemplate.hbs | 1 - src/UI/Album/History/NoHistoryView.js | 5 - .../Album/History/NoHistoryViewTemplate.hbs | 3 - src/UI/Album/Search/AlbumSearchLayout.js | 82 - .../Search/AlbumSearchLayoutTemplate.hbs | 1 - src/UI/Album/Search/ButtonsView.js | 5 - src/UI/Album/Search/ButtonsViewTemplate.hbs | 4 - src/UI/Album/Search/ManualLayout.js | 86 - src/UI/Album/Search/ManualLayoutTemplate.hbs | 2 - src/UI/Album/Search/NoResultsView.js | 5 - src/UI/Album/Search/NoResultsViewTemplate.hbs | 1 - src/UI/Album/Summary/AlbumSummaryLayout.js | 119 - .../Summary/AlbumSummaryLayoutTemplate.hbs | 11 - src/UI/Album/Summary/NoFileView.js | 5 - src/UI/Album/Summary/NoFileViewTemplate.hbs | 3 - .../AlbumStudio/AlbumStudioCollectionView.js | 25 - src/UI/AlbumStudio/AlbumStudioFooterView.js | 116 - .../AlbumStudioFooterViewTemplate.hbs | 31 - src/UI/AlbumStudio/AlbumStudioLayout.js | 147 - .../AlbumStudio/AlbumStudioLayoutTemplate.hbs | 14 - src/UI/AlbumStudio/AlbumsCell.js | 59 - src/UI/AlbumStudio/AlbumsCellTemplate.hbs | 1 - src/UI/AlbumStudio/SingleAlbumCell.js | 64 - .../AlbumStudio/SingleAlbumCellTemplate.hbs | 30 - src/UI/AlbumStudio/albumstudio.less | 61 - src/UI/AppLayout.js | 20 - src/UI/Artist/AlbumCollection.js | 43 - src/UI/Artist/AlbumModel.js | 8 - src/UI/Artist/ArtistCollection.js | 145 - src/UI/Artist/ArtistController.js | 36 - src/UI/Artist/ArtistModel.js | 31 - src/UI/Artist/Delete/DeleteArtistTemplate.hbs | 50 - src/UI/Artist/Delete/DeleteArtistView.js | 41 - src/UI/Artist/Details/AlbumCollectionView.js | 46 - src/UI/Artist/Details/AlbumInfoView.js | 18 - .../Artist/Details/AlbumInfoViewTemplate.hbs | 42 - src/UI/Artist/Details/AlbumLayout.js | 348 - src/UI/Artist/Details/AlbumLayoutTemplate.hbs | 69 - src/UI/Artist/Details/ArtistDetailsLayout.js | 246 - .../Artist/Details/ArtistDetailsTemplate.hbs | 35 - src/UI/Artist/Details/InfoView.js | 18 - src/UI/Artist/Details/InfoViewTemplate.hbs | 70 - src/UI/Artist/Details/TrackNumberCell.js | 43 - .../Details/TrackNumberCellTemplate.hbs | 39 - src/UI/Artist/Details/TrackRatingCell.js | 19 - src/UI/Artist/Details/TrackWarningCell.js | 21 - src/UI/Artist/Edit/EditArtistView.js | 54 - src/UI/Artist/Edit/EditArtistViewTemplate.hbs | 104 - .../Artist/Editor/ArtistEditorFooterView.js | 126 - .../Editor/ArtistEditorFooterViewTemplate.hbs | 54 - src/UI/Artist/Editor/ArtistEditorLayout.js | 185 - .../Editor/ArtistEditorLayoutTemplate.hbs | 7 - .../Editor/Organize/OrganizeFilesView.js | 33 - .../Organize/OrganizeFilesViewTemplate.hbs | 25 - src/UI/Artist/Index/ArtistIndexItemView.js | 35 - src/UI/Artist/Index/ArtistIndexLayout.js | 360 - .../Index/ArtistIndexLayoutTemplate.hbs | 12 - src/UI/Artist/Index/EmptyTemplate.hbs | 16 - src/UI/Artist/Index/EmptyView.js | 5 - src/UI/Artist/Index/FooterModel.js | 4 - src/UI/Artist/Index/FooterView.js | 5 - src/UI/Artist/Index/FooterViewTemplate.hbs | 49 - .../Overview/ArtistOverviewCollectionView.js | 8 - .../ArtistOverviewCollectionViewTemplate.hbs | 1 - .../Index/Overview/ArtistOverviewItemView.js | 7 - .../ArtistOverviewItemViewTemplate.hbs | 59 - .../Posters/ArtistPostersCollectionView.js | 8 - .../ArtistPostersCollectionViewTemplate.hbs | 1 - .../Index/Posters/ArtistPostersItemView.js | 19 - .../Posters/ArtistPostersItemViewTemplate.hbs | 30 - src/UI/Artist/Index/TrackProgressPartial.hbs | 4 - src/UI/Artist/TrackCollection.js | 62 - src/UI/Artist/TrackFileCollection.js | 28 - src/UI/Artist/TrackFileModel.js | 3 - src/UI/Artist/TrackModel.js | 20 - src/UI/Artist/artist.less | 534 - src/UI/Calendar/CalendarCollection.js | 14 - src/UI/Calendar/CalendarFeedView.js | 60 - src/UI/Calendar/CalendarFeedViewTemplate.hbs | 57 - src/UI/Calendar/CalendarLayout.js | 96 - src/UI/Calendar/CalendarLayoutTemplate.hbs | 21 - src/UI/Calendar/CalendarView.js | 283 - src/UI/Calendar/UpcomingCollection.js | 28 - src/UI/Calendar/UpcomingCollectionView.js | 36 - src/UI/Calendar/UpcomingItemView.js | 28 - src/UI/Calendar/UpcomingItemViewTemplate.hbs | 18 - src/UI/Calendar/calendar.less | 258 - src/UI/Cells/AlbumFolderCell.js | 13 - src/UI/Cells/AlbumTitleCell.js | 29 - src/UI/Cells/ApprovalStatusCell.js | 33 - src/UI/Cells/ApprovalStatusCellTemplate.hbs | 11 - src/UI/Cells/ArtistActionsCell.js | 45 - src/UI/Cells/ArtistMonitoredCell.js | 39 - src/UI/Cells/ArtistStatusCell.js | 32 - src/UI/Cells/ArtistTitleCell.js | 6 - src/UI/Cells/ArtistTitleTemplate.hbs | 1 - src/UI/Cells/DeleteEpisodeFileCell.js | 27 - src/UI/Cells/Edit/QualityCellEditor.js | 74 - .../Cells/Edit/QualityCellEditorTemplate.hbs | 9 - src/UI/Cells/EpisodeActionsCell.js | 44 - src/UI/Cells/EpisodeFilePathCell.js | 19 - src/UI/Cells/EpisodeMonitoredCell.js | 57 - src/UI/Cells/EpisodeNumberCell.js | 71 - src/UI/Cells/EpisodeStatusCell.js | 127 - src/UI/Cells/EpisodeTitleCell.js | 29 - src/UI/Cells/EventTypeCell.js | 44 - src/UI/Cells/FileSizeCell.js | 13 - src/UI/Cells/IndexerCell.js | 11 - src/UI/Cells/NzbDroneCell.js | 61 - src/UI/Cells/ProfileCell.js | 29 - src/UI/Cells/QualityCell.js | 8 - src/UI/Cells/QualityCellTemplate.hbs | 5 - src/UI/Cells/RelativeDateCell.js | 34 - src/UI/Cells/RelativeTimeCell.js | 30 - src/UI/Cells/ReleaseTitleCell.js | 20 - src/UI/Cells/SeasonFolderCell.js | 14 - src/UI/Cells/SelectAllCell.js | 45 - src/UI/Cells/SeriesActionsCell.js | 45 - src/UI/Cells/SeriesStatusCell.js | 32 - src/UI/Cells/SeriesTitleCell.js | 6 - src/UI/Cells/SeriesTitleTemplate.hbs | 1 - src/UI/Cells/TemplatedCell.js | 21 - src/UI/Cells/ToggleCell.js | 48 - src/UI/Cells/TrackActionsCell.js | 44 - src/UI/Cells/TrackDurationCell.js | 13 - src/UI/Cells/TrackExplicitCell.js | 20 - src/UI/Cells/TrackMonitoredCell.js | 57 - src/UI/Cells/TrackProgressCell.js | 28 - src/UI/Cells/TrackProgressCellTemplate.hbs | 1 - src/UI/Cells/TrackStatusCell.js | 127 - src/UI/Cells/TrackTitleCell.js | 29 - src/UI/Cells/cells.less | 286 - src/UI/Commands/CommandCollection.js | 20 - src/UI/Commands/CommandController.js | 94 - .../CommandMessengerCollectionView.js | 11 - src/UI/Commands/CommandMessengerItemView.js | 45 - src/UI/Commands/CommandModel.js | 50 - src/UI/Config.js | 69 - src/UI/Content/Backgrid/backgrid.less | 3 - src/UI/Content/Backgrid/filter.less | 11 - src/UI/Content/Backgrid/paginator.less | 66 - src/UI/Content/Backgrid/selectall.less | 12 - src/UI/Content/Bootstrap/.csscomb.json | 304 - src/UI/Content/Bootstrap/.csslintrc | 19 - src/UI/Content/Bootstrap/alerts.less | 73 - src/UI/Content/Bootstrap/badges.less | 66 - src/UI/Content/Bootstrap/bootstrap.less | 56 - src/UI/Content/Bootstrap/breadcrumbs.less | 26 - src/UI/Content/Bootstrap/button-groups.less | 244 - src/UI/Content/Bootstrap/buttons.less | 166 - src/UI/Content/Bootstrap/carousel.less | 269 - src/UI/Content/Bootstrap/close.less | 34 - src/UI/Content/Bootstrap/code.less | 69 - .../Bootstrap/component-animations.less | 33 - src/UI/Content/Bootstrap/dropdowns.less | 216 - src/UI/Content/Bootstrap/forms.less | 607 - src/UI/Content/Bootstrap/glyphicons.less | 305 - src/UI/Content/Bootstrap/grid.less | 84 - src/UI/Content/Bootstrap/input-groups.less | 167 - src/UI/Content/Bootstrap/jumbotron.less | 52 - src/UI/Content/Bootstrap/labels.less | 64 - src/UI/Content/Bootstrap/list-group.less | 130 - src/UI/Content/Bootstrap/media.less | 66 - src/UI/Content/Bootstrap/mixins.less | 40 - src/UI/Content/Bootstrap/mixins/alerts.less | 14 - .../Bootstrap/mixins/background-variant.less | 9 - .../Bootstrap/mixins/border-radius.less | 18 - src/UI/Content/Bootstrap/mixins/buttons.less | 68 - .../Bootstrap/mixins/center-block.less | 7 - src/UI/Content/Bootstrap/mixins/clearfix.less | 22 - src/UI/Content/Bootstrap/mixins/forms.less | 85 - .../Content/Bootstrap/mixins/gradients.less | 59 - .../Bootstrap/mixins/grid-framework.less | 91 - src/UI/Content/Bootstrap/mixins/grid.less | 122 - .../Content/Bootstrap/mixins/hide-text.less | 21 - src/UI/Content/Bootstrap/mixins/image.less | 33 - src/UI/Content/Bootstrap/mixins/labels.less | 12 - .../Content/Bootstrap/mixins/list-group.less | 30 - .../Content/Bootstrap/mixins/nav-divider.less | 10 - .../Bootstrap/mixins/nav-vertical-align.less | 9 - src/UI/Content/Bootstrap/mixins/opacity.less | 8 - .../Content/Bootstrap/mixins/pagination.less | 24 - src/UI/Content/Bootstrap/mixins/panels.less | 24 - .../Bootstrap/mixins/progress-bar.less | 10 - .../Bootstrap/mixins/reset-filter.less | 8 - .../Content/Bootstrap/mixins/reset-text.less | 18 - src/UI/Content/Bootstrap/mixins/resize.less | 6 - .../mixins/responsive-visibility.less | 15 - src/UI/Content/Bootstrap/mixins/size.less | 10 - .../Content/Bootstrap/mixins/tab-focus.less | 9 - .../Content/Bootstrap/mixins/table-row.less | 28 - .../Bootstrap/mixins/text-emphasis.less | 9 - .../Bootstrap/mixins/text-overflow.less | 8 - .../Bootstrap/mixins/vendor-prefixes.less | 227 - src/UI/Content/Bootstrap/modals.less | 150 - src/UI/Content/Bootstrap/navbar.less | 660 - src/UI/Content/Bootstrap/navs.less | 242 - src/UI/Content/Bootstrap/normalize.less | 424 - src/UI/Content/Bootstrap/pager.less | 54 - src/UI/Content/Bootstrap/pagination.less | 89 - src/UI/Content/Bootstrap/panels.less | 271 - src/UI/Content/Bootstrap/popovers.less | 131 - src/UI/Content/Bootstrap/print.less | 101 - src/UI/Content/Bootstrap/progress-bars.less | 87 - .../Content/Bootstrap/responsive-embed.less | 35 - .../Bootstrap/responsive-utilities.less | 194 - src/UI/Content/Bootstrap/scaffolding.less | 161 - src/UI/Content/Bootstrap/tables.less | 234 - src/UI/Content/Bootstrap/theme.less | 291 - src/UI/Content/Bootstrap/thumbnails.less | 36 - src/UI/Content/Bootstrap/tooltip.less | 101 - src/UI/Content/Bootstrap/type.less | 302 - src/UI/Content/Bootstrap/utilities.less | 55 - src/UI/Content/Bootstrap/variables.less | 867 - src/UI/Content/Bootstrap/wells.less | 29 - src/UI/Content/FontAwesome/FontAwesome.otf | Bin 93888 -> 0 bytes src/UI/Content/FontAwesome/animated.less | 34 - .../Content/FontAwesome/bordered-pulled.less | 16 - src/UI/Content/FontAwesome/core.less | 13 - src/UI/Content/FontAwesome/fixed-width.less | 6 - src/UI/Content/FontAwesome/font-awesome.less | 17 - .../FontAwesome/fontawesome-webfont.eot | Bin 60767 -> 0 bytes .../FontAwesome/fontawesome-webfont.svg | 565 - .../FontAwesome/fontawesome-webfont.ttf | Bin 122092 -> 0 bytes .../FontAwesome/fontawesome-webfont.woff | Bin 71508 -> 0 bytes .../FontAwesome/fontawesome-webfont.woff2 | Bin 56780 -> 0 bytes src/UI/Content/FontAwesome/icons.less | 596 - src/UI/Content/FontAwesome/larger.less | 13 - src/UI/Content/FontAwesome/list.less | 19 - src/UI/Content/FontAwesome/mixins.less | 27 - src/UI/Content/FontAwesome/path.less | 15 - .../Content/FontAwesome/rotated-flipped.less | 20 - src/UI/Content/FontAwesome/stacked.less | 20 - src/UI/Content/FontAwesome/variables.less | 606 - src/UI/Content/Images/background/logo.png | Bin 15293 -> 0 bytes src/UI/Content/Images/cover-dark.png | Bin 1797 -> 0 bytes src/UI/Content/Images/favicon-debug.ico | Bin 16958 -> 0 bytes src/UI/Content/Images/logos/128.png | Bin 5799 -> 0 bytes src/UI/Content/Images/logos/48.png | Bin 2223 -> 0 bytes src/UI/Content/Images/logos/64.png | Bin 2798 -> 0 bytes src/UI/Content/Images/safari/logo.svg | 1 - src/UI/Content/Images/touch/114.png | Bin 5671 -> 0 bytes src/UI/Content/Images/touch/144.png | Bin 6594 -> 0 bytes src/UI/Content/Images/touch/57.png | Bin 2791 -> 0 bytes src/UI/Content/Images/touch/72.png | Bin 3351 -> 0 bytes src/UI/Content/Messenger/messenger.css | 101 - src/UI/Content/Messenger/messenger.flat.css | 462 - src/UI/Content/Overrides/bootstrap.less | 82 - .../Overrides/bootstrap.tagsinput.less | 35 - .../Overrides/bootstrap.toggle-switch.less | 33 - src/UI/Content/Overrides/browser.less | 17 - src/UI/Content/Overrides/fullcalendar.less | 49 - src/UI/Content/Overrides/messenger.less | 23 - src/UI/Content/badges.less | 37 - src/UI/Content/bootstrap.less | 3 - src/UI/Content/bootstrap.tagsinput.less | 50 - src/UI/Content/bootstrap.toggle-switch.css | 228 - src/UI/Content/checkbox-button.less | 33 - src/UI/Content/font.less | 47 - src/UI/Content/fonts/opensans-light.eot | Bin 19762 -> 0 bytes src/UI/Content/fonts/opensans-light.ttf | Bin 222412 -> 0 bytes src/UI/Content/fonts/opensans-light.woff | Bin 22656 -> 0 bytes src/UI/Content/fonts/opensans-regular.eot | Bin 19216 -> 0 bytes src/UI/Content/fonts/opensans-regular.ttf | Bin 217360 -> 0 bytes src/UI/Content/fonts/opensans-regular.woff | Bin 21956 -> 0 bytes src/UI/Content/fonts/opensans-semibold.eot | Bin 19716 -> 0 bytes src/UI/Content/fonts/opensans-semibold.ttf | Bin 221328 -> 0 bytes src/UI/Content/fonts/opensans-semibold.woff | Bin 22604 -> 0 bytes src/UI/Content/form.less | 133 - src/UI/Content/fullcalendar.css | 1413 -- src/UI/Content/icons.less | 505 - src/UI/Content/legend.less | 32 - src/UI/Content/mixins.less | 21 - src/UI/Content/navbar.less | 235 - src/UI/Content/overrides.less | 6 - src/UI/Content/prefixer.less | 344 - src/UI/Content/progress-bars.less | 39 - src/UI/Content/robots.txt | 2 - src/UI/Content/spinner.less | 130 - src/UI/Content/theme.less | 306 - src/UI/Content/typeahead.less | 152 - src/UI/Content/utilities.less | 19 - src/UI/Content/variables.less | 13 - src/UI/Content/zero.clipboard.swf | Bin 6580 -> 0 bytes src/UI/Controller.js | 61 - src/UI/Episode/EpisodeDetailsLayout.js | 130 - .../Episode/EpisodeDetailsLayoutTemplate.hbs | 35 - .../History/EpisodeHistoryActionsCell.js | 35 - .../History/EpisodeHistoryDetailsCell.js | 28 - .../Episode/History/EpisodeHistoryLayout.js | 84 - .../History/EpisodeHistoryLayoutTemplate.hbs | 1 - src/UI/Episode/History/NoHistoryView.js | 5 - .../Episode/History/NoHistoryViewTemplate.hbs | 3 - src/UI/Episode/Search/ButtonsView.js | 5 - src/UI/Episode/Search/ButtonsViewTemplate.hbs | 4 - src/UI/Episode/Search/EpisodeSearchLayout.js | 82 - .../Search/EpisodeSearchLayoutTemplate.hbs | 1 - src/UI/Episode/Search/ManualLayout.js | 86 - .../Episode/Search/ManualLayoutTemplate.hbs | 2 - src/UI/Episode/Search/NoResultsView.js | 5 - .../Episode/Search/NoResultsViewTemplate.hbs | 1 - .../Episode/Summary/EpisodeSummaryLayout.js | 119 - .../Summary/EpisodeSummaryLayoutTemplate.hbs | 14 - src/UI/Episode/Summary/NoFileView.js | 5 - src/UI/Episode/Summary/NoFileViewTemplate.hbs | 3 - src/UI/EpisodeFile/Editor/EmptyView.js | 5 - .../EpisodeFile/Editor/EmptyViewTemplate.hbs | 5 - .../Editor/EpisodeFileEditorLayout.js | 200 - .../EpisodeFileEditorLayoutTemplate.hbs | 28 - .../EpisodeFile/Editor/QualitySelectView.js | 35 - .../Editor/QualitySelectViewTemplate.hbs | 10 - src/UI/Form/ActionTemplate.hbs | 7 - src/UI/Form/CaptchaTemplate.hbs | 15 - src/UI/Form/CheckboxTemplate.hbs | 23 - src/UI/Form/FormBuilder.js | 66 - src/UI/Form/FormHelpPartial.hbs | 8 - src/UI/Form/FormMessage.js | 17 - src/UI/Form/HiddenTemplate.hbs | 1 - src/UI/Form/PasswordTemplate.hbs | 8 - src/UI/Form/PathTemplate.hbs | 8 - src/UI/Form/SelectTemplate.hbs | 12 - src/UI/Form/TagTemplate.hbs | 9 - src/UI/Form/TextboxTemplate.hbs | 8 - src/UI/Form/UrlTemplate.hbs | 8 - src/UI/Handlebars/Handlebars.Debug.js | 7 - src/UI/Handlebars/Helpers/Album.js | 75 - src/UI/Handlebars/Helpers/Artist.js | 117 - src/UI/Handlebars/Helpers/DateTime.js | 90 - src/UI/Handlebars/Helpers/EachReverse.js | 16 - src/UI/Handlebars/Helpers/Enumerable.js | 21 - src/UI/Handlebars/Helpers/Episode.js | 33 - src/UI/Handlebars/Helpers/Html.js | 40 - src/UI/Handlebars/Helpers/Numbers.js | 14 - src/UI/Handlebars/Helpers/Quality.js | 12 - src/UI/Handlebars/Helpers/Series.js | 96 - src/UI/Handlebars/Helpers/String.js | 7 - src/UI/Handlebars/Helpers/System.js | 18 - .../backbone.marionette.templates.js | 38 - src/UI/Health/HealthCollection.js | 13 - src/UI/Health/HealthModel.js | 3 - src/UI/Health/HealthView.js | 37 - src/UI/Hotkeys/Hotkeys.js | 34 - src/UI/Hotkeys/HotkeysView.js | 6 - src/UI/Hotkeys/HotkeysViewTemplate.hbs | 45 - src/UI/Hotkeys/hotkeys.less | 23 - src/UI/Instrumentation/ErrorHandler.js | 86 - src/UI/Instrumentation/StringFormat.js | 13 - src/UI/JsLibraries/backbone.backgrid.js | 2764 --- .../backbone.backgrid.paginator.js | 352 - .../backbone.backgrid.selectall.js | 243 - src/UI/JsLibraries/backbone.collectionview.js | 1072 -- src/UI/JsLibraries/backbone.deep.model.js | 437 - src/UI/JsLibraries/backbone.js | 1571 -- src/UI/JsLibraries/backbone.marionette.js | 2329 --- src/UI/JsLibraries/backbone.modelbinder.js | 576 - src/UI/JsLibraries/backbone.pageable.js | 1345 -- src/UI/JsLibraries/backbone.validation.js | 606 - src/UI/JsLibraries/backbone.wreqr.js | 276 - src/UI/JsLibraries/bootstrap.js | 2363 --- src/UI/JsLibraries/bootstrap.tagsinput.js | 617 - src/UI/JsLibraries/filesize.js | 141 - src/UI/JsLibraries/fullcalendar.js | 15591 ---------------- src/UI/JsLibraries/handlebars.helpers.js | 145 - src/UI/JsLibraries/handlebars.runtime.js | 660 - src/UI/JsLibraries/jquery-ui.js | 4233 ----- src/UI/JsLibraries/jquery.backstretch.js | 377 - src/UI/JsLibraries/jquery.dotdotdot.js | 632 - src/UI/JsLibraries/jquery.easypiechart.js | 357 - src/UI/JsLibraries/jquery.js | 10351 ---------- src/UI/JsLibraries/jquery.knob.js | 672 - src/UI/JsLibraries/jquery.signalR.js | 2193 --- src/UI/JsLibraries/locale/placeholder.txt | 1 - src/UI/JsLibraries/lodash.underscore.js | 4619 ----- src/UI/JsLibraries/messenger.js | 1263 -- src/UI/JsLibraries/moment.js | 3111 --- src/UI/JsLibraries/typeahead.js | 1716 -- src/UI/JsLibraries/zero.clipboard.js | 2581 --- src/UI/LifeCycle.js | 3 - src/UI/ManualImport/Cells/EpisodesCell.js | 46 - src/UI/ManualImport/Cells/PathCell.js | 16 - src/UI/ManualImport/Cells/QualityCell.js | 23 - src/UI/ManualImport/Cells/SeasonCell.js | 47 - src/UI/ManualImport/Cells/SeriesCell.js | 45 - src/UI/ManualImport/EmptyView.js | 5 - src/UI/ManualImport/EmptyViewTemplate.hbs | 1 - .../Episode/SelectEpisodeLayout.js | 81 - .../Episode/SelectEpisodeLayoutTemplate.hbs | 21 - .../ManualImport/Episode/SelectEpisodeRow.js | 20 - .../ManualImport/Folder/SelectFolderView.js | 84 - .../Folder/SelectFolderViewTemplate.hbs | 43 - src/UI/ManualImport/ManualImportCollection.js | 74 - src/UI/ManualImport/ManualImportLayout.js | 259 - .../ManualImportLayoutTemplate.hbs | 26 - src/UI/ManualImport/ManualImportModel.js | 4 - src/UI/ManualImport/ManualImportRow.js | 41 - .../Quality/SelectQualityLayout.js | 43 - .../Quality/SelectQualityLayoutTemplate.hbs | 19 - .../ManualImport/Quality/SelectQualityView.js | 37 - .../Quality/SelectQualityViewTemplate.hbs | 33 - .../ManualImport/Season/SelectSeasonLayout.js | 28 - .../Season/SelectSeasonLayoutTemplate.hbs | 29 - .../ManualImport/Series/SelectSeriesLayout.js | 101 - .../Series/SelectSeriesLayoutTemplate.hbs | 30 - src/UI/ManualImport/Series/SelectSeriesRow.js | 13 - .../Summary/ManualImportSummaryView.js | 20 - .../ManualImportSummaryViewTemplate.hbs | 19 - src/UI/ManualImport/manualimport.less | 63 - src/UI/Mixins/AsChangeTrackingModel.js | 22 - src/UI/Mixins/AsEditModalView.js | 114 - src/UI/Mixins/AsFilteredCollection.js | 79 - src/UI/Mixins/AsModelBoundView.js | 46 - src/UI/Mixins/AsNamedView.js | 31 - src/UI/Mixins/AsPageableCollection.js | 45 - src/UI/Mixins/AsPersistedStateCollection.js | 72 - src/UI/Mixins/AsSortedCollection.js | 130 - src/UI/Mixins/AsSortedCollectionView.js | 24 - src/UI/Mixins/AsValidatedView.js | 93 - src/UI/Mixins/AutoComplete.js | 51 - src/UI/Mixins/CopyToClipboard.js | 22 - src/UI/Mixins/DirectoryAutoComplete.js | 29 - src/UI/Mixins/FileBrowser.js | 32 - src/UI/Mixins/TagInput.js | 156 - src/UI/Mixins/backbone.signalr.mixin.js | 46 - src/UI/Navbar/NavbarLayout.js | 63 - src/UI/Navbar/NavbarLayoutTemplate.hbs | 44 - src/UI/Navbar/Search.js | 42 - src/UI/Profile/ProfileCollection.js | 13 - src/UI/Profile/ProfileModel.js | 9 - src/UI/Profile/ProfileSelectionPartial.hbs | 5 - src/UI/Quality/QualityDefinitionCollection.js | 7 - src/UI/Quality/QualityDefinitionModel.js | 14 - src/UI/Release/AgeCell.js | 33 - src/UI/Release/DownloadReportCell.js | 49 - src/UI/Release/PeersCell.js | 28 - src/UI/Release/ProtocolCell.js | 24 - src/UI/Release/ReleaseCollection.js | 56 - src/UI/Release/ReleaseLayout.js | 78 - src/UI/Release/ReleaseLayoutTemplate.hbs | 7 - src/UI/Release/ReleaseModel.js | 3 - src/UI/Rename/RenamePreviewCollection.js | 34 - src/UI/Rename/RenamePreviewCollectionView.js | 6 - .../RenamePreviewEmptyCollectionView.js | 6 - ...namePreviewEmptyCollectionViewTemplate.hbs | 3 - src/UI/Rename/RenamePreviewFormatView.js | 22 - .../RenamePreviewFormatViewTemplate.hbs | 3 - src/UI/Rename/RenamePreviewItemView.js | 39 - .../Rename/RenamePreviewItemViewTemplate.hbs | 20 - src/UI/Rename/RenamePreviewLayout.js | 124 - src/UI/Rename/RenamePreviewLayoutTemplate.hbs | 34 - src/UI/Rename/RenamePreviewModel.js | 3 - src/UI/Rename/rename.less | 42 - src/UI/Router.js | 25 - src/UI/Series/Delete/DeleteSeriesTemplate.hbs | 50 - src/UI/Series/Delete/DeleteSeriesView.js | 41 - src/UI/Series/Details/EpisodeNumberCell.js | 47 - .../Details/EpisodeNumberCellTemplate.hbs | 39 - src/UI/Series/Details/EpisodeWarningCell.js | 21 - src/UI/Series/Details/InfoView.js | 18 - src/UI/Series/Details/InfoViewTemplate.hbs | 73 - src/UI/Series/Details/SeasonCollectionView.js | 44 - src/UI/Series/Details/SeasonLayout.js | 301 - .../Series/Details/SeasonLayoutTemplate.hbs | 50 - src/UI/Series/Details/SeriesDetailsLayout.js | 263 - .../Series/Details/SeriesDetailsTemplate.hbs | 35 - src/UI/Series/Edit/EditSeriesView.js | 54 - src/UI/Series/Edit/EditSeriesViewTemplate.hbs | 104 - .../Editor/Organize/OrganizeFilesView.js | 33 - .../Organize/OrganizeFilesViewTemplate.hbs | 25 - .../Series/Editor/SeriesEditorFooterView.js | 126 - .../Editor/SeriesEditorFooterViewTemplate.hbs | 54 - src/UI/Series/Editor/SeriesEditorLayout.js | 184 - .../Editor/SeriesEditorLayoutTemplate.hbs | 7 - src/UI/Series/EpisodeCollection.js | 62 - src/UI/Series/EpisodeFileCollection.js | 28 - src/UI/Series/EpisodeFileModel.js | 3 - src/UI/Series/EpisodeModel.js | 20 - src/UI/Series/Index/EmptyTemplate.hbs | 16 - src/UI/Series/Index/EmptyView.js | 5 - .../Series/Index/EpisodeProgressPartial.hbs | 4 - src/UI/Series/Index/FooterModel.js | 4 - src/UI/Series/Index/FooterView.js | 5 - src/UI/Series/Index/FooterViewTemplate.hbs | 46 - .../Overview/SeriesOverviewCollectionView.js | 8 - .../SeriesOverviewCollectionViewTemplate.hbs | 1 - .../Index/Overview/SeriesOverviewItemView.js | 7 - .../SeriesOverviewItemViewTemplate.hbs | 59 - .../Posters/SeriesPostersCollectionView.js | 8 - .../SeriesPostersCollectionViewTemplate.hbs | 1 - .../Index/Posters/SeriesPostersItemView.js | 19 - .../Posters/SeriesPostersItemViewTemplate.hbs | 30 - src/UI/Series/Index/SeriesIndexItemView.js | 35 - src/UI/Series/Index/SeriesIndexLayout.js | 357 - .../Index/SeriesIndexLayoutTemplate.hbs | 12 - src/UI/Series/SeasonCollection.js | 10 - src/UI/Series/SeasonModel.js | 11 - src/UI/Series/SeriesCollection.js | 120 - src/UI/Series/SeriesController.js | 34 - src/UI/Series/SeriesModel.js | 31 - src/UI/Series/series.less | 477 - .../Add/DownloadClientAddCollectionView.js | 9 - ...ownloadClientAddCollectionViewTemplate.hbs | 14 - .../Add/DownloadClientAddItemView.js | 58 - .../Add/DownloadClientAddItemViewTemplate.hbs | 30 - .../Add/DownloadClientSchemaModal.js | 39 - .../Delete/DownloadClientDeleteView.js | 19 - .../DownloadClientDeleteViewTemplate.hbs | 13 - .../DownloadClientCollection.js | 25 - .../DownloadClientCollectionView.js | 25 - .../DownloadClientCollectionViewTemplate.hbs | 16 - .../DownloadClient/DownloadClientItemView.js | 24 - .../DownloadClientItemViewTemplate.hbs | 13 - .../DownloadClient/DownloadClientLayout.js | 32 - .../DownloadClientLayoutTemplate.hbs | 6 - .../DownloadClient/DownloadClientModel.js | 3 - .../DownloadClientSettingsModel.js | 7 - .../DownloadHandling/DownloadHandlingView.js | 50 - .../DownloadHandlingViewTemplate.hbs | 93 - .../DroneFactory/DroneFactoryView.js | 21 - .../DroneFactory/DroneFactoryViewTemplate.hbs | 29 - .../Edit/DownloadClientEditView.js | 56 - .../Edit/DownloadClientEditViewTemplate.hbs | 68 - .../RemotePathMappingCollection.js | 7 - .../RemotePathMappingCollectionView.js | 28 - ...emotePathMappingCollectionViewTemplate.hbs | 24 - .../RemotePathMappingDeleteView.js | 19 - .../RemotePathMappingDeleteViewTemplate.hbs | 13 - .../RemotePathMappingEditView.js | 45 - .../RemotePathMappingEditViewTemplate.hbs | 63 - .../RemotePathMappingItemView.js | 25 - .../RemotePathMappingItemViewTemplate.hbs | 12 - .../RemotePathMappingModel.js | 4 - .../DownloadClient/downloadclient.less | 33 - .../Settings/General/GeneralSettingsModel.js | 7 - src/UI/Settings/General/GeneralView.js | 136 - .../Settings/General/GeneralViewTemplate.hbs | 382 - .../Indexers/Add/IndexerAddCollectionView.js | 9 - .../Add/IndexerAddCollectionViewTemplate.hbs | 18 - .../Indexers/Add/IndexerAddItemView.js | 52 - .../Add/IndexerAddItemViewTemplate.hbs | 30 - .../Indexers/Add/IndexerSchemaModal.js | 39 - .../Indexers/Delete/IndexerDeleteView.js | 19 - .../Delete/IndexerDeleteViewTemplate.hbs | 13 - .../Settings/Indexers/Edit/IndexerEditView.js | 122 - .../Indexers/Edit/IndexerEditViewTemplate.hbs | 92 - src/UI/Settings/Indexers/IndexerCollection.js | 25 - .../Indexers/IndexerCollectionView.js | 25 - .../IndexerCollectionViewTemplate.hbs | 16 - src/UI/Settings/Indexers/IndexerItemView.js | 24 - .../Indexers/IndexerItemViewTemplate.hbs | 27 - src/UI/Settings/Indexers/IndexerLayout.js | 30 - .../Indexers/IndexerLayoutTemplate.hbs | 5 - src/UI/Settings/Indexers/IndexerModel.js | 3 - .../Settings/Indexers/IndexerSettingsModel.js | 7 - .../Indexers/Options/IndexerOptionsView.js | 12 - .../Options/IndexerOptionsViewTemplate.hbs | 40 - .../Restriction/RestrictionCollection.js | 7 - .../Restriction/RestrictionCollectionView.js | 26 - .../RestrictionCollectionViewTemplate.hbs | 24 - .../Restriction/RestrictionDeleteView.js | 19 - .../RestrictionDeleteViewTemplate.hbs | 13 - .../Restriction/RestrictionEditView.js | 55 - .../RestrictionEditViewTemplate.hbs | 60 - .../Restriction/RestrictionItemView.js | 28 - .../RestrictionItemViewTemplate.hbs | 12 - .../Indexers/Restriction/RestrictionModel.js | 4 - src/UI/Settings/Indexers/indexers.less | 33 - .../FileManagement/FileManagementView.js | 23 - .../FileManagementViewTemplate.hbs | 97 - .../MediaManagement/MediaManagementLayout.js | 28 - .../MediaManagementLayoutTemplate.hbs | 6 - .../MediaManagementSettingsModel.js | 7 - .../Naming/Basic/BasicNamingModel.js | 3 - .../Naming/Basic/BasicNamingView.js | 112 - .../Naming/Basic/BasicNamingViewTemplate.hbs | 98 - .../MediaManagement/Naming/NamingModel.js | 7 - .../Naming/NamingSampleModel.js | 3 - .../MediaManagement/Naming/NamingView.js | 72 - .../Naming/NamingViewTemplate.hbs | 153 - .../Partials/AbsoluteEpisodeNamingPartial.hbs | 8 - .../Naming/Partials/AirDateNamingPartial.hbs | 9 - .../Partials/AlbumTitleNamingPartial.hbs | 11 - .../Partials/ArtistNameNamingPartial.hbs | 11 - .../Naming/Partials/EpisodeNamingPartial.hbs | 7 - .../Partials/EpisodeTitleNamingPartial.hbs | 11 - .../Partials/MediaInfoNamingPartial.hbs | 11 - .../Partials/OriginalTitleNamingPartial.hbs | 1 - .../Naming/Partials/QualityNamingPartial.hbs | 11 - .../Partials/ReleaseGroupNamingPartial.hbs | 8 - .../Partials/ReleaseYearNamingPartial.hbs | 1 - .../Naming/Partials/SeasonNamingPartial.hbs | 7 - .../Partials/SeparatorNamingPartial.hbs | 10 - .../Partials/SeriesTitleNamingPartial.hbs | 11 - .../Naming/Partials/TrackNumNamingPartial.hbs | 7 - .../Partials/TrackTitleNamingPartial.hbs | 11 - .../Permissions/PermissionsView.js | 11 - .../Permissions/PermissionsViewTemplate.hbs | 74 - .../MediaManagement/Sorting/SortingView.js | 39 - .../Sorting/SortingViewTemplate.hbs | 115 - .../Settings/Metadata/MetadataCollection.js | 7 - .../Metadata/MetadataCollectionView.js | 9 - .../MetadataCollectionViewTemplate.hbs | 8 - src/UI/Settings/Metadata/MetadataEditView.js | 19 - .../Metadata/MetadataEditViewTemplate.hbs | 45 - src/UI/Settings/Metadata/MetadataItemView.js | 24 - .../Metadata/MetadataItemViewTemplate.hbs | 23 - src/UI/Settings/Metadata/MetadataLayout.js | 20 - .../Metadata/MetadataLayoutTemplate.hbs | 3 - src/UI/Settings/Metadata/MetadataModel.js | 3 - src/UI/Settings/Metadata/metadata.less | 37 - .../Add/NotificationAddCollectionView.js | 8 - .../NotificationAddCollectionViewTemplate.hbs | 14 - .../Add/NotificationAddItemView.js | 64 - .../Add/NotificationAddItemViewTemplate.hbs | 30 - .../Add/NotificationSchemaModal.js | 18 - .../Delete/NotificationDeleteView.js | 18 - .../Delete/NotificationDeleteViewTemplate.hbs | 13 - .../Edit/NotificationEditView.js | 141 - .../Edit/NotificationEditViewTemplate.hbs | 148 - .../Notifications/NotificationCollection.js | 7 - .../NotificationCollectionView.js | 25 - .../NotificationCollectionViewTemplate.hbs | 16 - .../Notifications/NotificationItemView.js | 24 - .../NotificationItemViewTemplate.hbs | 47 - .../Notifications/NotificationModel.js | 3 - .../Settings/Notifications/notifications.less | 37 - src/UI/Settings/Profile/AllowedLabeler.js | 19 - .../Profile/Delay/DelayProfileCollection.js | 7 - .../Delay/DelayProfileCollectionView.js | 13 - .../Profile/Delay/DelayProfileItemView.js | 25 - .../Delay/DelayProfileItemViewTemplate.hbs | 57 - .../Profile/Delay/DelayProfileLayout.js | 101 - .../Delay/DelayProfileLayoutTemplate.hbs | 24 - .../Profile/Delay/DelayProfileModel.js | 3 - .../Delay/Delete/DelayProfileDeleteView.js | 21 - .../Delete/DelayProfileDeleteViewTemplate.hbs | 13 - .../Delay/Edit/DelayProfileEditView.js | 122 - .../Edit/DelayProfileEditViewTemplate.hbs | 80 - src/UI/Settings/Profile/DeleteProfileView.js | 16 - .../Profile/DeleteProfileViewTemplate.hbs | 13 - .../Profile/Edit/EditProfileItemView.js | 5 - .../Edit/EditProfileItemViewTemplate.hbs | 3 - .../Profile/Edit/EditProfileLayout.js | 117 - .../Edit/EditProfileLayoutTemplate.hbs | 36 - .../Settings/Profile/Edit/EditProfileView.js | 28 - .../Profile/Edit/EditProfileViewTemplate.hbs | 45 - .../Edit/QualitySortableCollectionView.js | 17 - .../Profile/Language/LanguageCollection.js | 12 - .../Profile/Language/LanguageModel.js | 3 - src/UI/Settings/Profile/LanguageLabel.js | 15 - .../Profile/ProfileCollectionTemplate.hbs | 16 - .../Settings/Profile/ProfileCollectionView.js | 43 - src/UI/Settings/Profile/ProfileLayout.js | 28 - .../Profile/ProfileLayoutTemplate.hbs | 5 - .../Profile/ProfileSchemaCollection.js | 7 - src/UI/Settings/Profile/ProfileView.js | 35 - .../Settings/Profile/ProfileViewTemplate.hbs | 13 - src/UI/Settings/Profile/profile.less | 43 - src/UI/Settings/ProviderSettingsModelBase.js | 71 - .../QualityDefinitionCollectionTemplate.hbs | 16 - .../QualityDefinitionCollectionView.js | 10 - .../Definition/QualityDefinitionItemView.js | 95 - .../QualityDefinitionItemViewTemplate.hbs | 31 - src/UI/Settings/Quality/QualityLayout.js | 21 - .../Quality/QualityLayoutTemplate.hbs | 3 - src/UI/Settings/Quality/quality.less | 135 - src/UI/Settings/SettingsLayout.js | 252 - src/UI/Settings/SettingsLayoutTemplate.hbs | 49 - src/UI/Settings/SettingsModelBase.js | 34 - src/UI/Settings/ThingyAddCollectionView.js | 13 - src/UI/Settings/ThingyHeaderGroupView.js | 18 - .../ThingyHeaderGroupViewTemplate.hbs | 2 - src/UI/Settings/UI/UiSettingsModel.js | 7 - src/UI/Settings/UI/UiView.js | 22 - src/UI/Settings/UI/UiViewTemplate.hbs | 124 - src/UI/Settings/settings.less | 161 - src/UI/Settings/thingy.less | 65 - src/UI/Shared/ApiData.js | 17 - .../ControlPanel/ControlPanelController.js | 18 - .../Shared/ControlPanel/ControlPanelRegion.js | 41 - src/UI/Shared/FileBrowser/EmptyView.js | 5 - .../Shared/FileBrowser/EmptyViewTemplate.hbs | 3 - .../FileBrowser/FileBrowserCollection.js | 28 - .../Shared/FileBrowser/FileBrowserLayout.js | 162 - .../FileBrowser/FileBrowserLayoutTemplate.hbs | 26 - src/UI/Shared/FileBrowser/FileBrowserModel.js | 3 - .../Shared/FileBrowser/FileBrowserNameCell.js | 18 - src/UI/Shared/FileBrowser/FileBrowserRow.js | 24 - .../Shared/FileBrowser/FileBrowserTypeCell.js | 28 - src/UI/Shared/FileBrowser/filebrowser.less | 24 - src/UI/Shared/FormatHelpers.js | 93 - src/UI/Shared/Grid/HeaderCell.js | 155 - src/UI/Shared/Grid/JumpToPageTemplate.hbs | 9 - src/UI/Shared/Grid/Pager.js | 188 - src/UI/Shared/Grid/PagerTemplate.hbs | 16 - src/UI/Shared/LoadingView.js | 6 - src/UI/Shared/LoadingViewTemplate.hbs | 10 - src/UI/Shared/Messenger.js | 66 - src/UI/Shared/Modal/ModalController.js | 102 - src/UI/Shared/Modal/ModalRegion.js | 7 - src/UI/Shared/Modal/ModalRegion2.js | 30 - src/UI/Shared/Modal/ModalRegionBase.js | 65 - src/UI/Shared/NotFoundView.js | 5 - src/UI/Shared/NotFoundViewTemplate.hbs | 4 - src/UI/Shared/NzbDroneController.js | 67 - src/UI/Shared/SignalRBroadcaster.js | 76 - src/UI/Shared/Styles/card.less | 10 - .../Toolbar/Button/ButtonCollectionView.js | 22 - src/UI/Shared/Toolbar/Button/ButtonView.js | 85 - src/UI/Shared/Toolbar/ButtonCollection.js | 6 - src/UI/Shared/Toolbar/ButtonModel.js | 11 - src/UI/Shared/Toolbar/ButtonTemplate.hbs | 1 - .../Radio/RadioButtonCollectionView.js | 37 - .../Shared/Toolbar/Radio/RadioButtonView.js | 50 - src/UI/Shared/Toolbar/RadioButtonTemplate.hbs | 1 - .../Sorting/SortingButtonCollectionView.js | 38 - .../SortingButtonCollectionViewTemplate.hbs | 8 - .../Toolbar/Sorting/SortingButtonView.js | 70 - .../Sorting/SortingButtonViewTemplate.hbs | 4 - src/UI/Shared/Toolbar/ToolbarLayout.js | 108 - .../Shared/Toolbar/ToolbarLayoutTemplate.hbs | 2 - src/UI/Shared/Tooltip.js | 47 - src/UI/Shared/UiSettingsController.js | 26 - src/UI/Shared/UiSettingsModel.js | 29 - src/UI/Shared/VersionChangeMonitor.js | 13 - src/UI/Shared/piwikCheck.js | 12 - src/UI/Shims/backbone.backgrid.selectall.js | 4 - src/UI/Shims/backbone.collectionview.js | 4 - src/UI/Shims/backbone.deep.model.js | 4 - src/UI/Shims/backbone.js | 7 - src/UI/Shims/backbone.marionette.js | 10 - src/UI/Shims/backbone.validation.js | 8 - src/UI/Shims/backgrid.js | 19 - src/UI/Shims/backgrid.paginator.js | 5 - src/UI/Shims/handlebars.js | 1 - src/UI/Shims/jquery.js | 11 - src/UI/Shims/jquery.signalR.js | 4 - src/UI/Shims/messenger.js | 6 - src/UI/Shims/underscore.js | 4 - src/UI/System/Backup/BackupCollection.js | 15 - src/UI/System/Backup/BackupEmptyView.js | 5 - .../System/Backup/BackupEmptyViewTemplate.hbs | 1 - src/UI/System/Backup/BackupFilenameCell.js | 6 - .../Backup/BackupFilenameCellTemplate.hbs | 1 - src/UI/System/Backup/BackupLayout.js | 94 - src/UI/System/Backup/BackupLayoutTemplate.hbs | 10 - src/UI/System/Backup/BackupModel.js | 3 - src/UI/System/Backup/BackupTypeCell.js | 26 - src/UI/System/Info/About/AboutView.js | 10 - .../System/Info/About/AboutViewTemplate.hbs | 20 - .../Info/DiskSpace/DiskSpaceCollection.js | 7 - .../System/Info/DiskSpace/DiskSpaceLayout.js | 58 - .../DiskSpace/DiskSpaceLayoutTemplate.hbs | 5 - .../System/Info/DiskSpace/DiskSpaceModel.js | 3 - .../Info/DiskSpace/DiskSpacePathCell.js | 22 - src/UI/System/Info/Health/HealthCell.js | 12 - src/UI/System/Info/Health/HealthLayout.js | 57 - .../Info/Health/HealthLayoutTemplate.hbs | 6 - src/UI/System/Info/Health/HealthOkView.js | 5 - .../Info/Health/HealthOkViewTemplate.hbs | 3 - src/UI/System/Info/Health/HealthWikiCell.js | 24 - src/UI/System/Info/MoreInfo/MoreInfoView.js | 5 - .../Info/MoreInfo/MoreInfoViewTemplate.hbs | 32 - src/UI/System/Info/SystemInfoLayout.js | 24 - .../System/Info/SystemInfoLayoutTemplate.hbs | 15 - src/UI/System/Info/info.less | 3 - src/UI/System/Logs/Files/ContentsModel.js | 13 - src/UI/System/Logs/Files/ContentsView.js | 5 - .../Logs/Files/ContentsViewTemplate.hbs | 11 - src/UI/System/Logs/Files/DownloadLogCell.js | 12 - src/UI/System/Logs/Files/FilenameCell.js | 12 - src/UI/System/Logs/Files/LogFileCollection.js | 12 - src/UI/System/Logs/Files/LogFileLayout.js | 135 - .../Logs/Files/LogFileLayoutTemplate.hbs | 12 - src/UI/System/Logs/Files/LogFileModel.js | 3 - src/UI/System/Logs/Files/Row.js | 14 - src/UI/System/Logs/LogsCollection.js | 64 - src/UI/System/Logs/LogsLayout.js | 64 - src/UI/System/Logs/LogsLayoutTemplate.hbs | 17 - src/UI/System/Logs/LogsModel.js | 3 - .../Logs/Table/Details/LogDetailsView.js | 6 - .../Table/Details/LogDetailsViewTemplate.hbs | 23 - src/UI/System/Logs/Table/LogLevelCell.js | 12 - src/UI/System/Logs/Table/LogRow.js | 14 - src/UI/System/Logs/Table/LogTimeCell.js | 31 - src/UI/System/Logs/Table/LogsTableLayout.js | 175 - .../Logs/Table/LogsTableLayoutTemplate.hbs | 11 - .../System/Logs/Updates/LogFileCollection.js | 12 - src/UI/System/Logs/Updates/LogFileModel.js | 3 - src/UI/System/Logs/logs.less | 25 - src/UI/System/StatusModel.js | 9 - src/UI/System/SystemLayout.js | 150 - src/UI/System/SystemLayoutTemplate.hbs | 31 - src/UI/System/Task/ExecuteTaskCell.js | 30 - src/UI/System/Task/NextExecutionCell.js | 34 - src/UI/System/Task/TaskCollection.js | 15 - src/UI/System/Task/TaskIntervalCell.js | 21 - src/UI/System/Task/TaskLayout.js | 71 - src/UI/System/Task/TaskLayoutTemplate.hbs | 5 - src/UI/System/Task/TaskModel.js | 3 - src/UI/System/Update/EmptyView.js | 5 - src/UI/System/Update/EmptyViewTemplate.hbs | 1 - src/UI/System/Update/UpdateCollection.js | 7 - src/UI/System/Update/UpdateCollectionView.js | 8 - src/UI/System/Update/UpdateItemView.js | 31 - .../System/Update/UpdateItemViewTemplate.hbs | 43 - src/UI/System/Update/UpdateLayout.js | 29 - src/UI/System/Update/UpdateLayoutTemplate.hbs | 5 - src/UI/System/Update/UpdateModel.js | 3 - src/UI/System/Update/update.less | 51 - src/UI/Tags/TagCollection.js | 14 - src/UI/Tags/TagHelpers.js | 25 - src/UI/Tags/TagModel.js | 3 - src/UI/Wanted/ControlsColumnTemplate.hbs | 1 - src/UI/Wanted/Cutoff/CutoffUnmetCollection.js | 63 - src/UI/Wanted/Cutoff/CutoffUnmetLayout.js | 188 - .../Cutoff/CutoffUnmetLayoutTemplate.hbs | 11 - src/UI/Wanted/Missing/MissingCollection.js | 61 - src/UI/Wanted/Missing/MissingLayout.js | 240 - .../Wanted/Missing/MissingLayoutTemplate.hbs | 11 - src/UI/Wanted/WantedLayout.js | 68 - src/UI/Wanted/WantedLayoutTemplate.hbs | 10 - src/UI/app.js | 160 - src/UI/index.html | 101 - src/UI/jQuery/RouteBinder.js | 63 - src/UI/jQuery/ToTheTop.js | 23 - src/UI/jQuery/jquery.ajax.js | 23 - src/UI/jQuery/jquery.spin.js | 62 - src/UI/jQuery/jquery.validation.js | 105 - src/UI/login.html | 54 - src/UI/main.js | 60 - src/UI/polyfills.js | 39 - src/UI/reqres.js | 10 - src/UI/vendor.js | 34 - src/UI/vent.js | 42 - webpack.config.js | 33 - 2483 files changed, 101310 insertions(+), 111355 deletions(-) delete mode 100644 .idea/.name delete mode 100644 .idea/Sonarr.iml delete mode 100644 .idea/codeStyleSettings.xml delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/jsLibraryMappings.xml delete mode 100644 .idea/libraries/Sonarr_node_modules.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml create mode 100644 .npmrc create mode 100644 .yarnrc delete mode 100644 build.ps1 create mode 100644 frontend/.csscomb.json create mode 100644 frontend/.esformatter create mode 100644 frontend/.eslintignore create mode 100644 frontend/.eslintrc create mode 100644 frontend/.jsbeautifyrc create mode 100644 frontend/.stylelintrc create mode 100644 frontend/.tern-project create mode 100644 frontend/gulp/build.js create mode 100644 frontend/gulp/clean.js create mode 100644 frontend/gulp/copy.js create mode 100644 frontend/gulp/gulpFile.js create mode 100644 frontend/gulp/helpers/errorHandler.js create mode 100644 frontend/gulp/helpers/html-annotate-loader.js create mode 100644 frontend/gulp/helpers/paths.js create mode 100644 frontend/gulp/imageMin.js create mode 100644 frontend/gulp/start.js create mode 100644 frontend/gulp/stripBom.js create mode 100644 frontend/gulp/watch.js create mode 100644 frontend/gulp/webpack.js create mode 100644 frontend/src/.vscode/settings.json create mode 100644 frontend/src/Activity/Blacklist/Blacklist.js create mode 100644 frontend/src/Activity/Blacklist/BlacklistConnector.js create mode 100644 frontend/src/Activity/Blacklist/BlacklistDetailsModal.js create mode 100644 frontend/src/Activity/Blacklist/BlacklistRow.css create mode 100644 frontend/src/Activity/Blacklist/BlacklistRow.js create mode 100644 frontend/src/Activity/Blacklist/BlacklistRowConnector.js create mode 100644 frontend/src/Activity/History/Details/HistoryDetails.js create mode 100644 frontend/src/Activity/History/Details/HistoryDetailsConnector.js create mode 100644 frontend/src/Activity/History/Details/HistoryDetailsModal.css create mode 100644 frontend/src/Activity/History/Details/HistoryDetailsModal.js create mode 100644 frontend/src/Activity/History/History.js create mode 100644 frontend/src/Activity/History/HistoryConnector.js create mode 100644 frontend/src/Activity/History/HistoryEventTypeCell.css create mode 100644 frontend/src/Activity/History/HistoryEventTypeCell.js create mode 100644 frontend/src/Activity/History/HistoryRow.css create mode 100644 frontend/src/Activity/History/HistoryRow.js create mode 100644 frontend/src/Activity/History/HistoryRowConnector.js create mode 100644 frontend/src/Activity/Queue/ProtocolLabel.css create mode 100644 frontend/src/Activity/Queue/ProtocolLabel.js create mode 100644 frontend/src/Activity/Queue/Queue.js create mode 100644 frontend/src/Activity/Queue/QueueConnector.js create mode 100644 frontend/src/Activity/Queue/QueueDetails.js create mode 100644 frontend/src/Activity/Queue/QueueRow.css create mode 100644 frontend/src/Activity/Queue/QueueRow.js create mode 100644 frontend/src/Activity/Queue/QueueRowConnector.js create mode 100644 frontend/src/Activity/Queue/QueueStatusCell.css create mode 100644 frontend/src/Activity/Queue/QueueStatusCell.js create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.css create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemModal.js create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemsModal.css create mode 100644 frontend/src/Activity/Queue/RemoveQueueItemsModal.js create mode 100644 frontend/src/Activity/Queue/Status/QueueStatusConnector.js create mode 100644 frontend/src/Activity/Queue/TimeleftCell.css create mode 100644 frontend/src/Activity/Queue/TimeleftCell.js create mode 100644 frontend/src/Activity/activity.less create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeries.css create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeries.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.css create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContentConnector.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.css create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js create mode 100644 frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResultConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooterConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRowConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesSelected.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTableConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResultConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeriesConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.css create mode 100644 frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.js create mode 100644 frontend/src/AddArtist/ImportSeries/ImportSeries.js create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.css create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.js create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRowConnector.js create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.css create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js create mode 100644 frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js create mode 100644 frontend/src/AddArtist/SeriesMonitoringOptionsPopoverContent.js create mode 100644 frontend/src/AddArtist/SeriesTypePopoverContent.js create mode 100644 frontend/src/App/App.js create mode 100644 frontend/src/App/AppUpdatedModal.js create mode 100644 frontend/src/App/AppUpdatedModalConnector.js create mode 100644 frontend/src/App/AppUpdatedModalContent.css create mode 100644 frontend/src/App/AppUpdatedModalContent.js create mode 100644 frontend/src/App/AppUpdatedModalContentConnector.js create mode 100644 frontend/src/App/ConnectionLostModal.css create mode 100644 frontend/src/App/ConnectionLostModal.js create mode 100644 frontend/src/App/ConnectionLostModalConnector.js create mode 100644 frontend/src/Artist/ArtistNameLink.js create mode 100644 frontend/src/Artist/ArtistPoster.js create mode 100644 frontend/src/Artist/Delete/DeleteArtist.less create mode 100644 frontend/src/Artist/Delete/DeleteArtistModal.js create mode 100644 frontend/src/Artist/Delete/DeleteArtistModalContent.css create mode 100644 frontend/src/Artist/Delete/DeleteArtistModalContent.js create mode 100644 frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js create mode 100644 frontend/src/Artist/Details/EpisodeRow.css create mode 100644 frontend/src/Artist/Details/EpisodeRow.js create mode 100644 frontend/src/Artist/Details/EpisodeRowConnector.js create mode 100644 frontend/src/Artist/Details/SeriesAlternateTitles.css create mode 100644 frontend/src/Artist/Details/SeriesAlternateTitles.js create mode 100644 frontend/src/Artist/Details/SeriesDetails.css create mode 100644 frontend/src/Artist/Details/SeriesDetails.js create mode 100644 frontend/src/Artist/Details/SeriesDetailsConnector.js create mode 100644 frontend/src/Artist/Details/SeriesDetailsLinks.css create mode 100644 frontend/src/Artist/Details/SeriesDetailsLinks.js create mode 100644 frontend/src/Artist/Details/SeriesDetailsPageConnector.js create mode 100644 frontend/src/Artist/Details/SeriesDetailsSeason.css create mode 100644 frontend/src/Artist/Details/SeriesDetailsSeason.js create mode 100644 frontend/src/Artist/Details/SeriesDetailsSeasonConnector.js create mode 100644 frontend/src/Artist/Details/SeriesTags.css create mode 100644 frontend/src/Artist/Details/SeriesTags.js create mode 100644 frontend/src/Artist/Details/SeriesTagsConnector.js create mode 100644 frontend/src/Artist/Edit/EditArtistModal.js create mode 100644 frontend/src/Artist/Edit/EditArtistModalConnector.js create mode 100644 frontend/src/Artist/Edit/EditArtistModalContent.css create mode 100644 frontend/src/Artist/Edit/EditArtistModalContent.js create mode 100644 frontend/src/Artist/Edit/EditArtistModalContentConnector.js create mode 100644 frontend/src/Artist/Editor/Delete/DeleteArtistModal.js create mode 100644 frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css create mode 100644 frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js create mode 100644 frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js create mode 100644 frontend/src/Artist/Editor/Organize/OrganizeSeriesModal.js create mode 100644 frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.css create mode 100644 frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.js create mode 100644 frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContentConnector.js create mode 100644 frontend/src/Artist/Editor/SeriesEditor.js create mode 100644 frontend/src/Artist/Editor/SeriesEditorConnector.js create mode 100644 frontend/src/Artist/Editor/SeriesEditorFooter.css create mode 100644 frontend/src/Artist/Editor/SeriesEditorFooter.js create mode 100644 frontend/src/Artist/Editor/SeriesEditorFooterLabel.css create mode 100644 frontend/src/Artist/Editor/SeriesEditorFooterLabel.js create mode 100644 frontend/src/Artist/Editor/SeriesEditorRow.css create mode 100644 frontend/src/Artist/Editor/SeriesEditorRow.js create mode 100644 frontend/src/Artist/Editor/SeriesEditorRowConnector.js create mode 100644 frontend/src/Artist/Editor/Tags/TagsModal.js create mode 100644 frontend/src/Artist/Editor/Tags/TagsModalContent.css create mode 100644 frontend/src/Artist/Editor/Tags/TagsModalContent.js create mode 100644 frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js create mode 100644 frontend/src/Artist/Index/ArtistIndex.css create mode 100644 frontend/src/Artist/Index/ArtistIndex.js create mode 100644 frontend/src/Artist/Index/ArtistIndexConnector.js create mode 100644 frontend/src/Artist/Index/ArtistIndexFooter.css create mode 100644 frontend/src/Artist/Index/ArtistIndexFooter.js create mode 100644 frontend/src/Artist/Index/ArtistIndexItemConnector.js create mode 100644 frontend/src/Artist/Index/ArtistIndexPage.js create mode 100644 frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js create mode 100644 frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js create mode 100644 frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPoster.css create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPoster.js create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.css create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.js create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosters.css create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPosters.js create mode 100644 frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js create mode 100644 frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js create mode 100644 frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js create mode 100644 frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexHeader.css create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexHeader.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexRow.css create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexRow.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexTable.css create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexTable.js create mode 100644 frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js create mode 100644 frontend/src/Artist/Index/Table/ArtistStatusCell.css create mode 100644 frontend/src/Artist/Index/Table/ArtistStatusCell.js create mode 100644 frontend/src/Artist/Index/Table/artistIndexCellRenderers.js create mode 100644 frontend/src/Artist/NoArtist.css create mode 100644 frontend/src/Artist/NoArtist.js create mode 100644 frontend/src/Calendar/Agenda/Agenda.css create mode 100644 frontend/src/Calendar/Agenda/Agenda.js create mode 100644 frontend/src/Calendar/Agenda/AgendaConnector.js create mode 100644 frontend/src/Calendar/Agenda/AgendaEvent.css create mode 100644 frontend/src/Calendar/Agenda/AgendaEvent.js create mode 100644 frontend/src/Calendar/Agenda/AgendaEventConnector.js create mode 100644 frontend/src/Calendar/Calendar.css create mode 100644 frontend/src/Calendar/Calendar.js create mode 100644 frontend/src/Calendar/CalendarConnector.js create mode 100644 frontend/src/Calendar/CalendarPage.css create mode 100644 frontend/src/Calendar/CalendarPage.js create mode 100644 frontend/src/Calendar/CalendarPageConnector.js create mode 100644 frontend/src/Calendar/Day/CalendarDay.css create mode 100644 frontend/src/Calendar/Day/CalendarDay.js create mode 100644 frontend/src/Calendar/Day/CalendarDayConnector.js create mode 100644 frontend/src/Calendar/Day/CalendarDays.css create mode 100644 frontend/src/Calendar/Day/CalendarDays.js create mode 100644 frontend/src/Calendar/Day/CalendarDaysConnector.js create mode 100644 frontend/src/Calendar/Day/DayOfWeek.css create mode 100644 frontend/src/Calendar/Day/DayOfWeek.js create mode 100644 frontend/src/Calendar/Day/DaysOfWeek.css create mode 100644 frontend/src/Calendar/Day/DaysOfWeek.js create mode 100644 frontend/src/Calendar/Day/DaysOfWeekConnector.js create mode 100644 frontend/src/Calendar/Events/CalendarEvent.css create mode 100644 frontend/src/Calendar/Events/CalendarEvent.js create mode 100644 frontend/src/Calendar/Events/CalendarEventConnector.js create mode 100644 frontend/src/Calendar/Events/CalendarEventQueueDetails.js create mode 100644 frontend/src/Calendar/Header/CalendarHeader.css create mode 100644 frontend/src/Calendar/Header/CalendarHeader.js create mode 100644 frontend/src/Calendar/Header/CalendarHeaderConnector.js create mode 100644 frontend/src/Calendar/Header/CalendarHeaderViewButton.js create mode 100644 frontend/src/Calendar/Legend/Legend.css create mode 100644 frontend/src/Calendar/Legend/Legend.js create mode 100644 frontend/src/Calendar/Legend/LegendItem.css create mode 100644 frontend/src/Calendar/Legend/LegendItem.js create mode 100644 frontend/src/Calendar/calendarViews.js create mode 100644 frontend/src/Calendar/getStatusStyle.js create mode 100644 frontend/src/Calendar/iCal/CalendarLinkModal.js create mode 100644 frontend/src/Calendar/iCal/CalendarLinkModalContent.js create mode 100644 frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js create mode 100644 frontend/src/Commands/commandNames.js create mode 100644 frontend/src/Components/Alert.css create mode 100644 frontend/src/Components/Alert.js create mode 100644 frontend/src/Components/Card.css create mode 100644 frontend/src/Components/Card.js create mode 100644 frontend/src/Components/CircularProgressBar.css create mode 100644 frontend/src/Components/CircularProgressBar.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionList.css create mode 100644 frontend/src/Components/DescriptionList/DescriptionList.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItem.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemDescription.css create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemDescription.js create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemTitle.css create mode 100644 frontend/src/Components/DescriptionList/DescriptionListItemTitle.js create mode 100644 frontend/src/Components/DragPreviewLayer.css create mode 100644 frontend/src/Components/DragPreviewLayer.js create mode 100644 frontend/src/Components/FieldSet.css create mode 100644 frontend/src/Components/FieldSet.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModal.css create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModal.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContent.css create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContent.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js create mode 100644 frontend/src/Components/FileBrowser/FileBrowserRow.css create mode 100644 frontend/src/Components/FileBrowser/FileBrowserRow.js create mode 100644 frontend/src/Components/Form/CaptchaInput.css create mode 100644 frontend/src/Components/Form/CaptchaInput.js create mode 100644 frontend/src/Components/Form/CaptchaInputConnector.js create mode 100644 frontend/src/Components/Form/CheckInput.css create mode 100644 frontend/src/Components/Form/CheckInput.js create mode 100644 frontend/src/Components/Form/EnhancedSelectInput.css create mode 100644 frontend/src/Components/Form/EnhancedSelectInput.js create mode 100644 frontend/src/Components/Form/EnhancedSelectInputOption.css create mode 100644 frontend/src/Components/Form/EnhancedSelectInputOption.js create mode 100644 frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css create mode 100644 frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js create mode 100644 frontend/src/Components/Form/Form.css create mode 100644 frontend/src/Components/Form/Form.js create mode 100644 frontend/src/Components/Form/FormGroup.css create mode 100644 frontend/src/Components/Form/FormGroup.js create mode 100644 frontend/src/Components/Form/FormInputButton.css create mode 100644 frontend/src/Components/Form/FormInputButton.js create mode 100644 frontend/src/Components/Form/FormInputGroup.css create mode 100644 frontend/src/Components/Form/FormInputGroup.js create mode 100644 frontend/src/Components/Form/FormInputHelpText.css create mode 100644 frontend/src/Components/Form/FormInputHelpText.js create mode 100644 frontend/src/Components/Form/FormLabel.css create mode 100644 frontend/src/Components/Form/FormLabel.js create mode 100644 frontend/src/Components/Form/Input.css create mode 100644 frontend/src/Components/Form/LanguageProfileSelectInputConnector.js create mode 100644 frontend/src/Components/Form/MonitorEpisodesSelectInput.js create mode 100644 frontend/src/Components/Form/NumberInput.js create mode 100644 frontend/src/Components/Form/OAuthInput.js create mode 100644 frontend/src/Components/Form/OAuthInputConnector.js create mode 100644 frontend/src/Components/Form/PasswordInput.js create mode 100644 frontend/src/Components/Form/PathInput.css create mode 100644 frontend/src/Components/Form/PathInput.js create mode 100644 frontend/src/Components/Form/PathInputConnector.js create mode 100644 frontend/src/Components/Form/ProviderFieldFormGroup.js create mode 100644 frontend/src/Components/Form/QualityProfileSelectInputConnector.js create mode 100644 frontend/src/Components/Form/RootFolderSelectInput.js create mode 100644 frontend/src/Components/Form/RootFolderSelectInputConnector.js create mode 100644 frontend/src/Components/Form/RootFolderSelectInputOption.css create mode 100644 frontend/src/Components/Form/RootFolderSelectInputOption.js create mode 100644 frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css create mode 100644 frontend/src/Components/Form/RootFolderSelectInputSelectedValue.js create mode 100644 frontend/src/Components/Form/SelectInput.css create mode 100644 frontend/src/Components/Form/SelectInput.js create mode 100644 frontend/src/Components/Form/SeriesTypeSelectInput.js create mode 100644 frontend/src/Components/Form/TagInput.css create mode 100644 frontend/src/Components/Form/TagInput.js create mode 100644 frontend/src/Components/Form/TagInputConnector.js create mode 100644 frontend/src/Components/Form/TextInput.css create mode 100644 frontend/src/Components/Form/TextInput.js create mode 100644 frontend/src/Components/Form/TextTagInput.js create mode 100644 frontend/src/Components/Form/TextTagInputConnector.js create mode 100644 frontend/src/Components/HeartRating.css create mode 100644 frontend/src/Components/HeartRating.js create mode 100644 frontend/src/Components/Icon.css create mode 100644 frontend/src/Components/Icon.js create mode 100644 frontend/src/Components/Label.css create mode 100644 frontend/src/Components/Label.js create mode 100644 frontend/src/Components/Link/Button.css create mode 100644 frontend/src/Components/Link/Button.js create mode 100644 frontend/src/Components/Link/ClipboardButton.css create mode 100644 frontend/src/Components/Link/ClipboardButton.js create mode 100644 frontend/src/Components/Link/IconButton.css create mode 100644 frontend/src/Components/Link/IconButton.js create mode 100644 frontend/src/Components/Link/Link.css create mode 100644 frontend/src/Components/Link/Link.js create mode 100644 frontend/src/Components/Link/SpinnerButton.css create mode 100644 frontend/src/Components/Link/SpinnerButton.js create mode 100644 frontend/src/Components/Link/SpinnerErrorButton.css create mode 100644 frontend/src/Components/Link/SpinnerErrorButton.js create mode 100644 frontend/src/Components/Link/SpinnerIconButton.js create mode 100644 frontend/src/Components/Loading/LoadingIndicator.css create mode 100644 frontend/src/Components/Loading/LoadingIndicator.js create mode 100644 frontend/src/Components/Loading/LoadingMessage.css create mode 100644 frontend/src/Components/Loading/LoadingMessage.js create mode 100644 frontend/src/Components/Menu/FilterMenu.css create mode 100644 frontend/src/Components/Menu/FilterMenu.js create mode 100644 frontend/src/Components/Menu/FilterMenuItem.js create mode 100644 frontend/src/Components/Menu/Menu.css create mode 100644 frontend/src/Components/Menu/Menu.js create mode 100644 frontend/src/Components/Menu/MenuButton.css create mode 100644 frontend/src/Components/Menu/MenuButton.js create mode 100644 frontend/src/Components/Menu/MenuContent.css create mode 100644 frontend/src/Components/Menu/MenuContent.js create mode 100644 frontend/src/Components/Menu/MenuItem.css create mode 100644 frontend/src/Components/Menu/MenuItem.js create mode 100644 frontend/src/Components/Menu/SelectedMenuItem.css create mode 100644 frontend/src/Components/Menu/SelectedMenuItem.js create mode 100644 frontend/src/Components/Menu/SortMenu.js create mode 100644 frontend/src/Components/Menu/SortMenuItem.js create mode 100644 frontend/src/Components/Menu/ToolbarMenuButton.css create mode 100644 frontend/src/Components/Menu/ToolbarMenuButton.js create mode 100644 frontend/src/Components/Menu/ViewMenu.js create mode 100644 frontend/src/Components/Menu/ViewMenuItem.js create mode 100644 frontend/src/Components/Modal/ConfirmModal.js create mode 100644 frontend/src/Components/Modal/Modal.css create mode 100644 frontend/src/Components/Modal/Modal.js create mode 100644 frontend/src/Components/Modal/ModalBody.css create mode 100644 frontend/src/Components/Modal/ModalBody.js create mode 100644 frontend/src/Components/Modal/ModalContent.css create mode 100644 frontend/src/Components/Modal/ModalContent.js create mode 100644 frontend/src/Components/Modal/ModalFooter.css create mode 100644 frontend/src/Components/Modal/ModalFooter.js create mode 100644 frontend/src/Components/Modal/ModalHeader.css create mode 100644 frontend/src/Components/Modal/ModalHeader.js create mode 100644 frontend/src/Components/MonitorToggleButton.css create mode 100644 frontend/src/Components/MonitorToggleButton.js create mode 100644 frontend/src/Components/NotFound.css create mode 100644 frontend/src/Components/NotFound.js create mode 100644 frontend/src/Components/Page/ErrorPage.css create mode 100644 frontend/src/Components/Page/ErrorPage.js create mode 100644 frontend/src/Components/Page/Header/KeyboardShortcutsModal.js create mode 100644 frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css create mode 100644 frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js create mode 100644 frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js create mode 100644 frontend/src/Components/Page/Header/PageHeader.css create mode 100644 frontend/src/Components/Page/Header/PageHeader.js create mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenu.css create mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenu.js create mode 100644 frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js create mode 100644 frontend/src/Components/Page/Header/SeriesSearchInput.css create mode 100644 frontend/src/Components/Page/Header/SeriesSearchInput.js create mode 100644 frontend/src/Components/Page/Header/SeriesSearchInputConnector.js create mode 100644 frontend/src/Components/Page/Header/SeriesSearchResult.css create mode 100644 frontend/src/Components/Page/Header/SeriesSearchResult.js create mode 100644 frontend/src/Components/Page/LoadingPage.css create mode 100644 frontend/src/Components/Page/LoadingPage.js create mode 100644 frontend/src/Components/Page/Page.css create mode 100644 frontend/src/Components/Page/Page.js create mode 100644 frontend/src/Components/Page/PageConnector.js create mode 100644 frontend/src/Components/Page/PageContent.css create mode 100644 frontend/src/Components/Page/PageContent.js create mode 100644 frontend/src/Components/Page/PageContentBody.css create mode 100644 frontend/src/Components/Page/PageContentBody.js create mode 100644 frontend/src/Components/Page/PageContentBodyConnector.js create mode 100644 frontend/src/Components/Page/PageContentFooter.css create mode 100644 frontend/src/Components/Page/PageContentFooter.js create mode 100644 frontend/src/Components/Page/PageJumpBar.css create mode 100644 frontend/src/Components/Page/PageJumpBar.js create mode 100644 frontend/src/Components/Page/PageJumpBarItem.css create mode 100644 frontend/src/Components/Page/PageJumpBarItem.js create mode 100644 frontend/src/Components/Page/PageSectionContent.js create mode 100644 frontend/src/Components/Page/Sidebar/Messages/Message.css create mode 100644 frontend/src/Components/Page/Sidebar/Messages/Message.js create mode 100644 frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js create mode 100644 frontend/src/Components/Page/Sidebar/Messages/Messages.css create mode 100644 frontend/src/Components/Page/Sidebar/Messages/Messages.js create mode 100644 frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebar.css create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebar.js create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebarItem.css create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebarItem.js create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebarStatus.css create mode 100644 frontend/src/Components/Page/Sidebar/PageSidebarStatus.js create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbar.css create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbar.js create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarButton.css create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarButton.js create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarSection.css create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarSection.js create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css create mode 100644 frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js create mode 100644 frontend/src/Components/ProgressBar.css create mode 100644 frontend/src/Components/ProgressBar.js create mode 100644 frontend/src/Components/Router/Switch.js create mode 100644 frontend/src/Components/Scroller/OverlayScroller.css create mode 100644 frontend/src/Components/Scroller/OverlayScroller.js create mode 100644 frontend/src/Components/Scroller/Scroller.css create mode 100644 frontend/src/Components/Scroller/Scroller.js create mode 100644 frontend/src/Components/SignalRConnector.js create mode 100644 frontend/src/Components/SpinnerIcon.js create mode 100644 frontend/src/Components/Table/Cells/RelativeDateCell.css create mode 100644 frontend/src/Components/Table/Cells/RelativeDateCell.js create mode 100644 frontend/src/Components/Table/Cells/RelativeDateCellConnector.js create mode 100644 frontend/src/Components/Table/Cells/TableRowCell.css create mode 100644 frontend/src/Components/Table/Cells/TableRowCell.js create mode 100644 frontend/src/Components/Table/Cells/TableRowCellButton.css create mode 100644 frontend/src/Components/Table/Cells/TableRowCellButton.js create mode 100644 frontend/src/Components/Table/Cells/TableSelectCell.css create mode 100644 frontend/src/Components/Table/Cells/TableSelectCell.js create mode 100644 frontend/src/Components/Table/Cells/VirtualTableRowCell.css create mode 100644 frontend/src/Components/Table/Cells/VirtualTableRowCell.js create mode 100644 frontend/src/Components/Table/Cells/VirtualTableSelectCell.css create mode 100644 frontend/src/Components/Table/Cells/VirtualTableSelectCell.js create mode 100644 frontend/src/Components/Table/Table.css create mode 100644 frontend/src/Components/Table/Table.js create mode 100644 frontend/src/Components/Table/TableBody.js create mode 100644 frontend/src/Components/Table/TableHeader.js create mode 100644 frontend/src/Components/Table/TableHeaderCell.css create mode 100644 frontend/src/Components/Table/TableHeaderCell.js create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumn.css create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumn.js create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsModal.css create mode 100644 frontend/src/Components/Table/TableOptions/TableOptionsModal.js create mode 100644 frontend/src/Components/Table/TablePager.css create mode 100644 frontend/src/Components/Table/TablePager.js create mode 100644 frontend/src/Components/Table/TableRow.css create mode 100644 frontend/src/Components/Table/TableRow.js create mode 100644 frontend/src/Components/Table/TableRowButton.css create mode 100644 frontend/src/Components/Table/TableRowButton.js create mode 100644 frontend/src/Components/Table/TableSelectAllHeaderCell.css create mode 100644 frontend/src/Components/Table/TableSelectAllHeaderCell.js create mode 100644 frontend/src/Components/Table/VirtualTable.css create mode 100644 frontend/src/Components/Table/VirtualTable.js create mode 100644 frontend/src/Components/Table/VirtualTableBody.css create mode 100644 frontend/src/Components/Table/VirtualTableBody.js create mode 100644 frontend/src/Components/Table/VirtualTableHeader.css create mode 100644 frontend/src/Components/Table/VirtualTableHeader.js create mode 100644 frontend/src/Components/Table/VirtualTableHeaderCell.css create mode 100644 frontend/src/Components/Table/VirtualTableHeaderCell.js create mode 100644 frontend/src/Components/Table/VirtualTableRow.css create mode 100644 frontend/src/Components/Table/VirtualTableRow.js create mode 100644 frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css create mode 100644 frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js create mode 100644 frontend/src/Components/TagList.css create mode 100644 frontend/src/Components/TagList.js create mode 100644 frontend/src/Components/TagListConnector.js create mode 100644 frontend/src/Components/Tooltip/Popover.css create mode 100644 frontend/src/Components/Tooltip/Popover.js create mode 100644 frontend/src/Components/Tooltip/Tooltip.css create mode 100644 frontend/src/Components/Tooltip/Tooltip.js create mode 100644 frontend/src/Components/keyboardShortcuts.js create mode 100644 frontend/src/Components/withScrollPosition.js create mode 100644 frontend/src/Content/Fonts/FontAwesome.otf create mode 100644 frontend/src/Content/Fonts/Roboto-Light.ttf create mode 100644 frontend/src/Content/Fonts/Roboto-Light.woff create mode 100644 frontend/src/Content/Fonts/Roboto-Light.woff2 create mode 100644 frontend/src/Content/Fonts/Roboto-Regular.ttf create mode 100644 frontend/src/Content/Fonts/Roboto-Regular.woff create mode 100644 frontend/src/Content/Fonts/Roboto-Regular.woff2 rename src/UI/Content/fonts/ubuntumono-regular.eot => frontend/src/Content/Fonts/UbuntuMono-Regular.eot (100%) rename {src/UI/Content/fonts => frontend/src/Content/Fonts}/UbuntuMono-Regular.ttf (100%) rename src/UI/Content/fonts/ubuntumono-regular.woff => frontend/src/Content/Fonts/UbuntuMono-Regular.woff (100%) create mode 100644 frontend/src/Content/Fonts/font-awesome.css create mode 100644 frontend/src/Content/Fonts/fontawesome-webfont.eot create mode 100644 frontend/src/Content/Fonts/fontawesome-webfont.svg create mode 100644 frontend/src/Content/Fonts/fontawesome-webfont.ttf create mode 100644 frontend/src/Content/Fonts/fontawesome-webfont.woff create mode 100644 frontend/src/Content/Fonts/fontawesome-webfont.woff2 create mode 100644 frontend/src/Content/Fonts/fonts.css rename {src/UI => frontend/src}/Content/Images/404.png (100%) create mode 100644 frontend/src/Content/Images/Icons/android-chrome-192x192.png create mode 100644 frontend/src/Content/Images/Icons/android-chrome-512x512.png create mode 100644 frontend/src/Content/Images/Icons/apple-touch-icon.png create mode 100644 frontend/src/Content/Images/Icons/browserconfig.xml create mode 100644 frontend/src/Content/Images/Icons/favicon-16x16.png rename src/UI/Content/Images/logos/32.png => frontend/src/Content/Images/Icons/favicon-32x32.png (100%) create mode 100644 frontend/src/Content/Images/Icons/favicon-debug-16x16.png create mode 100644 frontend/src/Content/Images/Icons/favicon-debug-32x32.png create mode 100644 frontend/src/Content/Images/Icons/favicon-debug.ico rename {src/UI/Content/Images => frontend/src/Content/Images/Icons}/favicon.ico (100%) create mode 100644 frontend/src/Content/Images/Icons/manifest.json create mode 100644 frontend/src/Content/Images/Icons/mstile-144x144.png create mode 100644 frontend/src/Content/Images/Icons/mstile-150x150.png create mode 100644 frontend/src/Content/Images/Icons/mstile-310x150.png create mode 100644 frontend/src/Content/Images/Icons/mstile-310x310.png create mode 100644 frontend/src/Content/Images/Icons/mstile-70x70.png create mode 100644 frontend/src/Content/Images/Icons/safari-pinned-tab.svg create mode 100644 frontend/src/Content/Images/logo.svg rename {src/UI => frontend/src}/Content/Images/poster-dark.png (100%) create mode 100644 frontend/src/Episode/EpisodeDetailsModal.js create mode 100644 frontend/src/Episode/EpisodeDetailsModalContent.css create mode 100644 frontend/src/Episode/EpisodeDetailsModalContent.js create mode 100644 frontend/src/Episode/EpisodeDetailsModalContentConnector.js create mode 100644 frontend/src/Episode/EpisodeLanguage.js create mode 100644 frontend/src/Episode/EpisodeNumber.css create mode 100644 frontend/src/Episode/EpisodeNumber.js create mode 100644 frontend/src/Episode/EpisodeQuality.js create mode 100644 frontend/src/Episode/EpisodeSearchCell.css create mode 100644 frontend/src/Episode/EpisodeSearchCell.js create mode 100644 frontend/src/Episode/EpisodeSearchCellConnector.js create mode 100644 frontend/src/Episode/EpisodeStatus.css create mode 100644 frontend/src/Episode/EpisodeStatus.js create mode 100644 frontend/src/Episode/EpisodeStatusConnector.js create mode 100644 frontend/src/Episode/EpisodeTitleLink.css create mode 100644 frontend/src/Episode/EpisodeTitleLink.js create mode 100644 frontend/src/Episode/History/EpisodeHistory.js create mode 100644 frontend/src/Episode/History/EpisodeHistoryConnector.js create mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.css create mode 100644 frontend/src/Episode/History/EpisodeHistoryRow.js create mode 100644 frontend/src/Episode/SceneInfo.css create mode 100644 frontend/src/Episode/SceneInfo.js create mode 100644 frontend/src/Episode/Search/EpisodeSearch.css create mode 100644 frontend/src/Episode/Search/EpisodeSearch.js create mode 100644 frontend/src/Episode/Search/EpisodeSearchConnector.js create mode 100644 frontend/src/Episode/Search/InteractiveEpisodeSearch.js create mode 100644 frontend/src/Episode/Search/InteractiveEpisodeSearchConnector.js create mode 100644 frontend/src/Episode/Search/InteractiveEpisodeSearchRow.css create mode 100644 frontend/src/Episode/Search/InteractiveEpisodeSearchRow.js create mode 100644 frontend/src/Episode/Search/Peers.js create mode 100644 frontend/src/Episode/SeasonEpisodeNumber.css create mode 100644 frontend/src/Episode/SeasonEpisodeNumber.js create mode 100644 frontend/src/Episode/Summary/EpisodeAiring.js create mode 100644 frontend/src/Episode/Summary/EpisodeAiringConnector.js create mode 100644 frontend/src/Episode/Summary/EpisodeSummary.css create mode 100644 frontend/src/Episode/Summary/EpisodeSummary.js create mode 100644 frontend/src/Episode/Summary/EpisodeSummaryConnector.js create mode 100644 frontend/src/Episode/episodeEntities.js create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css create mode 100644 frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js create mode 100644 frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js create mode 100644 frontend/src/EpisodeFile/MediaInfo.js create mode 100644 frontend/src/EpisodeFile/MediaInfoConnector.js create mode 100644 frontend/src/EpisodeFile/mediaInfoTypes.js create mode 100644 frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js create mode 100644 frontend/src/Helpers/Props/Shapes/locationShape.js create mode 100644 frontend/src/Helpers/Props/Shapes/settingShape.js create mode 100644 frontend/src/Helpers/Props/align.js create mode 100644 frontend/src/Helpers/Props/filterTypes.js create mode 100644 frontend/src/Helpers/Props/icons.js create mode 100644 frontend/src/Helpers/Props/index.js create mode 100644 frontend/src/Helpers/Props/inputTypes.js create mode 100644 frontend/src/Helpers/Props/kinds.js create mode 100644 frontend/src/Helpers/Props/messageTypes.js create mode 100644 frontend/src/Helpers/Props/scrollDirections.js create mode 100644 frontend/src/Helpers/Props/sizes.js create mode 100644 frontend/src/Helpers/Props/sortDirections.js create mode 100644 frontend/src/Helpers/Props/tooltipPositions.js create mode 100644 frontend/src/Helpers/dragTypes.js create mode 100644 frontend/src/Helpers/elementChildren.js create mode 100644 frontend/src/Helpers/getDisplayName.js create mode 100644 frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js create mode 100644 frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.js create mode 100644 frontend/src/InteractiveImport/Episode/SelectEpisodeModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js create mode 100644 frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css create mode 100644 frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js create mode 100644 frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Folder/RecentFolderRow.js create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css create mode 100644 frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js create mode 100644 frontend/src/InteractiveImport/InteractiveImportModal.js create mode 100644 frontend/src/InteractiveImport/Quality/SelectQualityModal.js create mode 100644 frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js create mode 100644 frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Season/SelectSeasonModal.js create mode 100644 frontend/src/InteractiveImport/Season/SelectSeasonModalContent.js create mode 100644 frontend/src/InteractiveImport/Season/SelectSeasonModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Season/SelectSeasonRow.css create mode 100644 frontend/src/InteractiveImport/Season/SelectSeasonRow.js create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesModal.js create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesModalContentConnector.js create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesRow.css create mode 100644 frontend/src/InteractiveImport/Series/SelectSeriesRow.js create mode 100644 frontend/src/JsLibraries/jquery.js create mode 100644 frontend/src/JsLibraries/jquery.signalR.js create mode 100644 frontend/src/Organize/OrganizePreviewModal.js create mode 100644 frontend/src/Organize/OrganizePreviewModalConnector.js create mode 100644 frontend/src/Organize/OrganizePreviewModalContent.css create mode 100644 frontend/src/Organize/OrganizePreviewModalContent.js create mode 100644 frontend/src/Organize/OrganizePreviewModalContentConnector.js create mode 100644 frontend/src/Organize/OrganizePreviewRow.css create mode 100644 frontend/src/Organize/OrganizePreviewRow.js create mode 100644 frontend/src/SeasonPass/SeasonPass.js create mode 100644 frontend/src/SeasonPass/SeasonPassConnector.js create mode 100644 frontend/src/SeasonPass/SeasonPassFooter.css create mode 100644 frontend/src/SeasonPass/SeasonPassFooter.js create mode 100644 frontend/src/SeasonPass/SeasonPassRow.css create mode 100644 frontend/src/SeasonPass/SeasonPassRow.js create mode 100644 frontend/src/SeasonPass/SeasonPassRowConnector.js create mode 100644 frontend/src/SeasonPass/SeasonPassSeason.css create mode 100644 frontend/src/SeasonPass/SeasonPassSeason.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClientSettings.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js create mode 100644 frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js create mode 100644 frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js create mode 100644 frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js create mode 100644 frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js create mode 100644 frontend/src/Settings/General/GeneralSettings.js create mode 100644 frontend/src/Settings/General/GeneralSettingsConnector.js create mode 100644 frontend/src/Settings/Indexers/IndexerSettings.js create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js create mode 100644 frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js create mode 100644 frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js create mode 100644 frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js create mode 100644 frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css create mode 100644 frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js create mode 100644 frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js create mode 100644 frontend/src/Settings/Indexers/Indexers/Indexer.css create mode 100644 frontend/src/Settings/Indexers/Indexers/Indexer.js create mode 100644 frontend/src/Settings/Indexers/Indexers/Indexers.css create mode 100644 frontend/src/Settings/Indexers/Indexers/Indexers.js create mode 100644 frontend/src/Settings/Indexers/Indexers/IndexersConnector.js create mode 100644 frontend/src/Settings/Indexers/Options/IndexerOptions.js create mode 100644 frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css create mode 100644 frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/Restriction.css create mode 100644 frontend/src/Settings/Indexers/Restrictions/Restriction.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/Restrictions.css create mode 100644 frontend/src/Settings/Indexers/Restrictions/Restrictions.js create mode 100644 frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js create mode 100644 frontend/src/Settings/MediaManagement/MediaManagement.js create mode 100644 frontend/src/Settings/MediaManagement/MediaManagementConnector.js create mode 100644 frontend/src/Settings/MediaManagement/Naming/Naming.css create mode 100644 frontend/src/Settings/MediaManagement/Naming/Naming.js create mode 100644 frontend/src/Settings/MediaManagement/Naming/NamingConnector.js create mode 100644 frontend/src/Settings/MediaManagement/Naming/NamingModal.css create mode 100644 frontend/src/Settings/MediaManagement/Naming/NamingModal.js create mode 100644 frontend/src/Settings/MediaManagement/Naming/NamingOption.css create mode 100644 frontend/src/Settings/MediaManagement/Naming/NamingOption.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js create mode 100644 frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.css create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadata.js create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.css create mode 100644 frontend/src/Settings/Metadata/Metadata/Metadatas.js create mode 100644 frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js create mode 100644 frontend/src/Settings/Metadata/MetadataSettings.js create mode 100644 frontend/src/Settings/Notifications/NotificationSettings.js create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js create mode 100644 frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js create mode 100644 frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js create mode 100644 frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js create mode 100644 frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css create mode 100644 frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js create mode 100644 frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js create mode 100644 frontend/src/Settings/Notifications/Notifications/Notification.css create mode 100644 frontend/src/Settings/Notifications/Notifications/Notification.js create mode 100644 frontend/src/Settings/Notifications/Notifications/Notifications.css create mode 100644 frontend/src/Settings/Notifications/Notifications/Notifications.js create mode 100644 frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js create mode 100644 frontend/src/Settings/PendingChangesModal.js create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfile.css create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfile.js create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfiles.css create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfiles.js create mode 100644 frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js create mode 100644 frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js create mode 100644 frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css create mode 100644 frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js create mode 100644 frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js create mode 100644 frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js create mode 100644 frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css create mode 100644 frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js create mode 100644 frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfile.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfile.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItem.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItem.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItems.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileItems.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfiles.css create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfiles.js create mode 100644 frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js create mode 100644 frontend/src/Settings/Profiles/Profiles.js create mode 100644 frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js create mode 100644 frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js create mode 100644 frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css create mode 100644 frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js create mode 100644 frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfile.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfile.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItem.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItem.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItems.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItems.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfiles.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfiles.js create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinition.css create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinition.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitions.css create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitions.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js create mode 100644 frontend/src/Settings/Quality/Quality.js create mode 100644 frontend/src/Settings/SettingsToolbar.css create mode 100644 frontend/src/Settings/SettingsToolbar.js create mode 100644 frontend/src/Settings/SettingsToolbarConnector.js create mode 100644 frontend/src/Shared/piwikCheck.js create mode 100644 frontend/src/Shims/jquery.js create mode 100644 frontend/src/Shims/signalR.js create mode 100644 frontend/src/Store/Actions/Creators/createBatchToggleEpisodeMonitoredHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createFetchHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createRemoveItemHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createSaveHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createSaveProviderHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js create mode 100644 frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createTestProviderHandler.js create mode 100644 frontend/src/Store/Actions/Creators/createToggleEpisodeMonitoredHandler.js create mode 100644 frontend/src/Store/Actions/actionTypes.js create mode 100644 frontend/src/Store/Actions/addSeriesActionHandlers.js create mode 100644 frontend/src/Store/Actions/addSeriesActions.js create mode 100644 frontend/src/Store/Actions/appActions.js create mode 100644 frontend/src/Store/Actions/artistActionHandlers.js create mode 100644 frontend/src/Store/Actions/artistIndexActions.js create mode 100644 frontend/src/Store/Actions/baseActions.js create mode 100644 frontend/src/Store/Actions/blacklistActionHandlers.js create mode 100644 frontend/src/Store/Actions/blacklistActions.js create mode 100644 frontend/src/Store/Actions/calendarActionHandlers.js create mode 100644 frontend/src/Store/Actions/calendarActions.js create mode 100644 frontend/src/Store/Actions/captchaActionHandlers.js create mode 100644 frontend/src/Store/Actions/captchaActions.js create mode 100644 frontend/src/Store/Actions/commandActionHandlers.js create mode 100644 frontend/src/Store/Actions/commandActions.js create mode 100644 frontend/src/Store/Actions/episodeActionHandlers.js create mode 100644 frontend/src/Store/Actions/episodeActions.js create mode 100644 frontend/src/Store/Actions/episodeFileActionHandlers.js create mode 100644 frontend/src/Store/Actions/episodeFileActions.js create mode 100644 frontend/src/Store/Actions/episodeHistoryActionHandlers.js create mode 100644 frontend/src/Store/Actions/episodeHistoryActions.js create mode 100644 frontend/src/Store/Actions/historyActionHandlers.js create mode 100644 frontend/src/Store/Actions/historyActions.js create mode 100644 frontend/src/Store/Actions/importSeriesActionHandlers.js create mode 100644 frontend/src/Store/Actions/importSeriesActions.js create mode 100644 frontend/src/Store/Actions/interactiveImportActionHandlers.js create mode 100644 frontend/src/Store/Actions/interactiveImportActions.js create mode 100644 frontend/src/Store/Actions/oAuthActionHandlers.js create mode 100644 frontend/src/Store/Actions/oAuthActions.js create mode 100644 frontend/src/Store/Actions/organizePreviewActionHandlers.js create mode 100644 frontend/src/Store/Actions/organizePreviewActions.js create mode 100644 frontend/src/Store/Actions/pathActionHandlers.js create mode 100644 frontend/src/Store/Actions/pathActions.js create mode 100644 frontend/src/Store/Actions/queueActionHandlers.js create mode 100644 frontend/src/Store/Actions/queueActions.js create mode 100644 frontend/src/Store/Actions/releaseActionHandlers.js create mode 100644 frontend/src/Store/Actions/releaseActions.js create mode 100644 frontend/src/Store/Actions/rootFolderActionHandlers.js create mode 100644 frontend/src/Store/Actions/rootFolderActions.js create mode 100644 frontend/src/Store/Actions/seasonPassActionHandlers.js create mode 100644 frontend/src/Store/Actions/seasonPassActions.js create mode 100644 frontend/src/Store/Actions/seriesActions.js create mode 100644 frontend/src/Store/Actions/seriesEditorActionHandlers.js create mode 100644 frontend/src/Store/Actions/seriesEditorActions.js create mode 100644 frontend/src/Store/Actions/settingsActionHandlers.js create mode 100644 frontend/src/Store/Actions/settingsActions.js create mode 100644 frontend/src/Store/Actions/systemActionHandlers.js create mode 100644 frontend/src/Store/Actions/systemActions.js create mode 100644 frontend/src/Store/Actions/tagActionHandlers.js create mode 100644 frontend/src/Store/Actions/tagActions.js create mode 100644 frontend/src/Store/Actions/wantedActionHandlers.js create mode 100644 frontend/src/Store/Actions/wantedActions.js create mode 100644 frontend/src/Store/Middleware/middlewares.js create mode 100644 frontend/src/Store/Middleware/persistState.js create mode 100644 frontend/src/Store/Reducers/Creators/createAddItemReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createClearReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createReducers.js create mode 100644 frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createUpdateReducer.js create mode 100644 frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js create mode 100644 frontend/src/Store/Reducers/addSeriesReducers.js create mode 100644 frontend/src/Store/Reducers/appReducers.js create mode 100644 frontend/src/Store/Reducers/artistIndexReducers.js create mode 100644 frontend/src/Store/Reducers/blacklistReducers.js create mode 100644 frontend/src/Store/Reducers/calendarReducers.js create mode 100644 frontend/src/Store/Reducers/captchaReducers.js create mode 100644 frontend/src/Store/Reducers/commandReducers.js create mode 100644 frontend/src/Store/Reducers/episodeFileReducers.js create mode 100644 frontend/src/Store/Reducers/episodeHistoryReducers.js create mode 100644 frontend/src/Store/Reducers/episodeReducers.js create mode 100644 frontend/src/Store/Reducers/historyReducers.js create mode 100644 frontend/src/Store/Reducers/importSeriesReducers.js create mode 100644 frontend/src/Store/Reducers/index.js create mode 100644 frontend/src/Store/Reducers/interactiveImportReducers.js create mode 100644 frontend/src/Store/Reducers/oAuthReducers.js create mode 100644 frontend/src/Store/Reducers/organizePreviewReducers.js create mode 100644 frontend/src/Store/Reducers/pathReducers.js create mode 100644 frontend/src/Store/Reducers/queueReducers.js create mode 100644 frontend/src/Store/Reducers/releaseReducers.js create mode 100644 frontend/src/Store/Reducers/rootFolderReducers.js create mode 100644 frontend/src/Store/Reducers/seasonPassReducers.js create mode 100644 frontend/src/Store/Reducers/seriesEditorReducers.js create mode 100644 frontend/src/Store/Reducers/seriesReducers.js create mode 100644 frontend/src/Store/Reducers/settingsReducers.js create mode 100644 frontend/src/Store/Reducers/systemReducers.js create mode 100644 frontend/src/Store/Reducers/tagReducers.js create mode 100644 frontend/src/Store/Reducers/wantedReducers.js create mode 100644 frontend/src/Store/Selectors/createAllSeriesSelector.js create mode 100644 frontend/src/Store/Selectors/createArtistSelector.js create mode 100644 frontend/src/Store/Selectors/createClientSideCollectionSelector.js create mode 100644 frontend/src/Store/Selectors/createCommandExecutingSelector.js create mode 100644 frontend/src/Store/Selectors/createCommandSelector.js create mode 100644 frontend/src/Store/Selectors/createCommandsSelector.js create mode 100644 frontend/src/Store/Selectors/createDimensionsSelector.js create mode 100644 frontend/src/Store/Selectors/createEpisodeFileSelector.js create mode 100644 frontend/src/Store/Selectors/createEpisodeSelector.js create mode 100644 frontend/src/Store/Selectors/createExistingSeriesSelector.js create mode 100644 frontend/src/Store/Selectors/createImportSeriesItemSelector.js create mode 100644 frontend/src/Store/Selectors/createLanguageProfileSelector.js create mode 100644 frontend/src/Store/Selectors/createProfileInUseSelector.js create mode 100644 frontend/src/Store/Selectors/createProviderSettingsSelector.js create mode 100644 frontend/src/Store/Selectors/createQualityProfileSelector.js create mode 100644 frontend/src/Store/Selectors/createQueueItemSelector.js create mode 100644 frontend/src/Store/Selectors/createSettingsSectionSelector.js create mode 100644 frontend/src/Store/Selectors/createSystemStatusSelector.js create mode 100644 frontend/src/Store/Selectors/createTagsSelector.js create mode 100644 frontend/src/Store/Selectors/createUISettingsSelector.js create mode 100644 frontend/src/Store/Selectors/selectSettings.js create mode 100644 frontend/src/Store/connectSection.js create mode 100644 frontend/src/Store/createAppStore.js create mode 100644 frontend/src/Store/scrollPositions.js rename src/UI/Shared/Styles/clickable.less => frontend/src/Styles/Mixins/clickable.css (100%) create mode 100644 frontend/src/Styles/Mixins/cover.css create mode 100644 frontend/src/Styles/Mixins/linkOverlay.css create mode 100644 frontend/src/Styles/Mixins/scroller.css create mode 100644 frontend/src/Styles/Mixins/truncate.css create mode 100644 frontend/src/Styles/Variables/animations.js create mode 100644 frontend/src/Styles/Variables/colors.js create mode 100644 frontend/src/Styles/Variables/dimensions.js create mode 100644 frontend/src/Styles/Variables/fonts.js create mode 100644 frontend/src/Styles/globals.css create mode 100644 frontend/src/Styles/scaffolding.css create mode 100644 frontend/src/System/Backup/Backups.css create mode 100644 frontend/src/System/Backup/Backups.js create mode 100644 frontend/src/System/Backup/BackupsConnector.js create mode 100644 frontend/src/System/Events/LogsTable.js create mode 100644 frontend/src/System/Events/LogsTableConnector.js create mode 100644 frontend/src/System/Events/LogsTableDetailsModal.css create mode 100644 frontend/src/System/Events/LogsTableDetailsModal.js create mode 100644 frontend/src/System/Events/LogsTableRow.css create mode 100644 frontend/src/System/Events/LogsTableRow.js create mode 100644 frontend/src/System/Logs/Files/LogFiles.js create mode 100644 frontend/src/System/Logs/Files/LogFilesConnector.js create mode 100644 frontend/src/System/Logs/Files/LogFilesTableRow.css create mode 100644 frontend/src/System/Logs/Files/LogFilesTableRow.js create mode 100644 frontend/src/System/Logs/Logs.js create mode 100644 frontend/src/System/Logs/LogsNavMenu.js create mode 100644 frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js create mode 100644 frontend/src/System/Status/About/About.js create mode 100644 frontend/src/System/Status/About/AboutConnector.js create mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.css create mode 100644 frontend/src/System/Status/DiskSpace/DiskSpace.js create mode 100644 frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js create mode 100644 frontend/src/System/Status/Health/Health.css create mode 100644 frontend/src/System/Status/Health/Health.js create mode 100644 frontend/src/System/Status/Health/HealthConnector.js create mode 100644 frontend/src/System/Status/Health/HealthStatusConnector.js create mode 100644 frontend/src/System/Status/MoreInfo/MoreInfo.js create mode 100644 frontend/src/System/Status/Status.js create mode 100644 frontend/src/System/Tasks/TaskRow.css create mode 100644 frontend/src/System/Tasks/TaskRow.js create mode 100644 frontend/src/System/Tasks/TaskRowConnector.js create mode 100644 frontend/src/System/Tasks/Tasks.js create mode 100644 frontend/src/System/Tasks/TasksConnector.js create mode 100644 frontend/src/System/Updates/UpdateChanges.css create mode 100644 frontend/src/System/Updates/UpdateChanges.js create mode 100644 frontend/src/System/Updates/Updates.css create mode 100644 frontend/src/System/Updates/Updates.js create mode 100644 frontend/src/System/Updates/UpdatesConnector.js create mode 100644 frontend/src/Utilities/Array/sortByName.js create mode 100644 frontend/src/Utilities/Command/findCommand.js create mode 100644 frontend/src/Utilities/Command/index.js create mode 100644 frontend/src/Utilities/Command/isCommandComplete.js create mode 100644 frontend/src/Utilities/Command/isCommandExecuting.js create mode 100644 frontend/src/Utilities/Command/isCommandFailed.js create mode 100644 frontend/src/Utilities/Command/isSameCommand.js create mode 100644 frontend/src/Utilities/Constants/keyCodes.js create mode 100644 frontend/src/Utilities/Date/formatDate.js create mode 100644 frontend/src/Utilities/Date/formatDateTime.js create mode 100644 frontend/src/Utilities/Date/formatTime.js create mode 100644 frontend/src/Utilities/Date/formatTimeSpan.js create mode 100644 frontend/src/Utilities/Date/getRelativeDate.js create mode 100644 frontend/src/Utilities/Date/isAfter.js create mode 100644 frontend/src/Utilities/Date/isBefore.js create mode 100644 frontend/src/Utilities/Date/isInNextWeek.js create mode 100644 frontend/src/Utilities/Date/isSameWeek.js create mode 100644 frontend/src/Utilities/Date/isToday.js create mode 100644 frontend/src/Utilities/Date/isTomorrow.js create mode 100644 frontend/src/Utilities/Date/isYesterday.js create mode 100644 frontend/src/Utilities/Episode/updateEpisodes.js create mode 100644 frontend/src/Utilities/Number/formatAge.js create mode 100644 frontend/src/Utilities/Number/formatBytes.js create mode 100644 frontend/src/Utilities/Number/padNumber.js create mode 100644 frontend/src/Utilities/Object/getErrorMessage.js create mode 100644 frontend/src/Utilities/Object/hasDifferentItems.js create mode 100644 frontend/src/Utilities/Object/selectUniqueIds.js create mode 100644 frontend/src/Utilities/ResolutionUtility.js create mode 100644 frontend/src/Utilities/Series/getMonitoringOptions.js create mode 100644 frontend/src/Utilities/Series/getNewSeries.js create mode 100644 frontend/src/Utilities/Series/getProgressBarKind.js create mode 100644 frontend/src/Utilities/State/getProviderState.js create mode 100644 frontend/src/Utilities/State/getSectionState.js create mode 100644 frontend/src/Utilities/State/selectProviderSchema.js create mode 100644 frontend/src/Utilities/State/updateSectionState.js create mode 100644 frontend/src/Utilities/String/combinePath.js create mode 100644 frontend/src/Utilities/String/split.js create mode 100644 frontend/src/Utilities/String/titleCase.js create mode 100644 frontend/src/Utilities/Table/areAllSelected.js create mode 100644 frontend/src/Utilities/Table/getSelectedIds.js create mode 100644 frontend/src/Utilities/Table/getToggledRange.js create mode 100644 frontend/src/Utilities/Table/removeOldSelectedState.js create mode 100644 frontend/src/Utilities/Table/selectAll.js create mode 100644 frontend/src/Utilities/Table/toggleSelected.js create mode 100644 frontend/src/Utilities/getPathWithUrlBase.js create mode 100644 frontend/src/Utilities/getUniqueElementId.js create mode 100644 frontend/src/Utilities/isMobile.js create mode 100644 frontend/src/Utilities/pages.js create mode 100644 frontend/src/Utilities/requestAction.js create mode 100644 frontend/src/Utilities/sectionTypes.js create mode 100644 frontend/src/Utilities/serverSideCollectionHandlers.js create mode 100644 frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js create mode 100644 frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js create mode 100644 frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css create mode 100644 frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js create mode 100644 frontend/src/Wanted/Missing/Missing.js create mode 100644 frontend/src/Wanted/Missing/MissingConnector.js create mode 100644 frontend/src/Wanted/Missing/MissingRow.css create mode 100644 frontend/src/Wanted/Missing/MissingRow.js create mode 100644 frontend/src/index.css create mode 100644 frontend/src/index.html create mode 100644 frontend/src/index.js create mode 100644 frontend/src/jQuery/jquery.ajax.js create mode 100644 frontend/src/login.html rename {src/UI => frontend/src}/oauth.html (84%) create mode 100644 frontend/src/polyfills.js create mode 100644 frontend/src/preload.js create mode 100644 frontend/src/vendor.js create mode 100644 npm-shrinkwrap.json create mode 100644 src/Lidarr.sln delete mode 100644 src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs delete mode 100644 src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs delete mode 100644 src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs delete mode 100644 src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs delete mode 100644 src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs delete mode 100644 src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs delete mode 100644 src/NzbDrone.Api/NancyBootstrapper.cs delete mode 100644 src/NzbDrone.Api/NzbDroneRestModule.cs delete mode 100644 src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs create mode 100644 src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs create mode 100644 src/NzbDrone.Core.Test/Languages/LanguageFixture.cs create mode 100644 src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs create mode 100644 src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/102_add_language_to_episodeFiles_history_and_blacklist.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/111_create_language_profiles.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/112_music_history.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/112_setup_music.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/113_music_history.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/114_music_blacklist.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs delete mode 100644 src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/115_remove_tv_naming.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/116_change_drone_factory_variable_name.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs delete mode 100644 src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs create mode 100644 src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs create mode 100644 src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs create mode 100644 src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs create mode 100644 src/NzbDrone.Core/Languages/Language.cs create mode 100644 src/NzbDrone.Core/Languages/LanguageComparer.cs create mode 100644 src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs create mode 100644 src/NzbDrone.Core/Music/ArtistStatusType.cs delete mode 100644 src/NzbDrone.Core/Parser/Language.cs create mode 100644 src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs create mode 100644 src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs create mode 100644 src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs create mode 100644 src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs create mode 100644 src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs delete mode 100644 src/NzbDrone.Core/Profiles/Profile.cs delete mode 100644 src/NzbDrone.Core/Profiles/ProfileInUseException.cs delete mode 100644 src/NzbDrone.Core/Profiles/ProfileQualityItem.cs create mode 100644 src/NzbDrone.Core/Profiles/Quality/Profile.cs create mode 100644 src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs create mode 100644 src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs rename src/NzbDrone.Core/Profiles/{ => Quality}/ProfileRepository.cs (88%) rename src/NzbDrone.Core/Profiles/{ => Quality}/ProfileService.cs (95%) create mode 100644 src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs create mode 100644 src/NzbDrone.Core/Queue/TimeleftComparer.cs create mode 100644 src/NzbDrone.Core/Tags/TagDetails.cs create mode 100644 src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs create mode 100644 src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs create mode 100644 src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs create mode 100644 src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs create mode 100644 src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs create mode 100644 src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs delete mode 100644 src/NzbDrone.Core/Validation/LanguageValidator.cs delete mode 100644 src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs create mode 100644 src/Sonarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs create mode 100644 src/Sonarr.Api.V3/AlbumStudio/AlbumStudioModule.cs create mode 100644 src/Sonarr.Api.V3/AlbumStudio/AlbumStudioResource.cs create mode 100644 src/Sonarr.Api.V3/Albums/AlbumModule.cs create mode 100644 src/Sonarr.Api.V3/Albums/AlbumModuleWithSignalR.cs create mode 100644 src/Sonarr.Api.V3/Albums/AlbumResource.cs create mode 100644 src/Sonarr.Api.V3/Albums/AlbumStatisticsResource.cs create mode 100644 src/Sonarr.Api.V3/Albums/AlbumsMonitoredResource.cs create mode 100644 src/Sonarr.Api.V3/Artist/AlternateTitleResource.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistEditorDeleteResource.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistEditorModule.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistEditorResource.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistImportModule.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistLookupModule.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistModule.cs create mode 100644 src/Sonarr.Api.V3/Artist/ArtistResource.cs create mode 100644 src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs create mode 100644 src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs create mode 100644 src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs create mode 100644 src/Sonarr.Api.V3/Calendar/CalendarModule.cs create mode 100644 src/Sonarr.Api.V3/Commands/CommandModule.cs create mode 100644 src/Sonarr.Api.V3/Commands/CommandResource.cs create mode 100644 src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs create mode 100644 src/Sonarr.Api.V3/Config/HostConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/HostConfigResource.cs create mode 100644 src/Sonarr.Api.V3/Config/IndexerConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/IndexerConfigResource.cs create mode 100644 src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs create mode 100644 src/Sonarr.Api.V3/Config/NamingConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/NamingConfigResource.cs create mode 100644 src/Sonarr.Api.V3/Config/NamingExampleResource.cs create mode 100644 src/Sonarr.Api.V3/Config/SonarrConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/UiConfigModule.cs create mode 100644 src/Sonarr.Api.V3/Config/UiConfigResource.cs create mode 100644 src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs create mode 100644 src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs create mode 100644 src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs create mode 100644 src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs create mode 100644 src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs create mode 100644 src/Sonarr.Api.V3/Health/HealthModule.cs create mode 100644 src/Sonarr.Api.V3/Health/HealthResource.cs create mode 100644 src/Sonarr.Api.V3/History/HistoryModule.cs create mode 100644 src/Sonarr.Api.V3/History/HistoryResource.cs create mode 100644 src/Sonarr.Api.V3/Indexers/IndexerModule.cs create mode 100644 src/Sonarr.Api.V3/Indexers/IndexerResource.cs create mode 100644 src/Sonarr.Api.V3/Indexers/ReleaseModule.cs create mode 100644 src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs create mode 100644 src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs create mode 100644 src/Sonarr.Api.V3/Indexers/ReleaseResource.cs create mode 100644 src/Sonarr.Api.V3/Lidarr.Api.V3.csproj create mode 100644 src/Sonarr.Api.V3/Logs/LogFileModule.cs create mode 100644 src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs create mode 100644 src/Sonarr.Api.V3/Logs/LogFileResource.cs create mode 100644 src/Sonarr.Api.V3/Logs/LogModule.cs create mode 100644 src/Sonarr.Api.V3/Logs/LogResource.cs create mode 100644 src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs create mode 100644 src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs create mode 100644 src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs create mode 100644 src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs create mode 100644 src/Sonarr.Api.V3/Metadata/MetadataModule.cs create mode 100644 src/Sonarr.Api.V3/Metadata/MetadataResource.cs create mode 100644 src/Sonarr.Api.V3/Notifications/NotificationModule.cs create mode 100644 src/Sonarr.Api.V3/Notifications/NotificationResource.cs create mode 100644 src/Sonarr.Api.V3/Parse/ParseModule.cs create mode 100644 src/Sonarr.Api.V3/Parse/ParseResource.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Language/LanguageProfileResource.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Language/LanguageValidator.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs create mode 100644 src/Sonarr.Api.V3/Properties/AssemblyInfo.cs create mode 100644 src/Sonarr.Api.V3/ProviderModuleBase.cs create mode 100644 src/Sonarr.Api.V3/ProviderResource.cs create mode 100644 src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs create mode 100644 src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueActionModule.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueBulkResource.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueModule.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueResource.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueStatusModule.cs create mode 100644 src/Sonarr.Api.V3/Queue/QueueStatusResource.cs create mode 100644 src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs create mode 100644 src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs create mode 100644 src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs create mode 100644 src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs create mode 100644 src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs create mode 100644 src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs create mode 100644 src/Sonarr.Api.V3/SonarrV3FeedModule.cs create mode 100644 src/Sonarr.Api.V3/SonarrV3Module.cs create mode 100644 src/Sonarr.Api.V3/System/Backup/BackupModule.cs create mode 100644 src/Sonarr.Api.V3/System/Backup/BackupResource.cs create mode 100644 src/Sonarr.Api.V3/System/SystemModule.cs create mode 100644 src/Sonarr.Api.V3/System/Tasks/TaskModule.cs create mode 100644 src/Sonarr.Api.V3/System/Tasks/TaskResource.cs create mode 100644 src/Sonarr.Api.V3/Tags/TagDetailsModule.cs create mode 100644 src/Sonarr.Api.V3/Tags/TagDetailsResource.cs create mode 100644 src/Sonarr.Api.V3/Tags/TagModule.cs create mode 100644 src/Sonarr.Api.V3/Tags/TagResource.cs create mode 100644 src/Sonarr.Api.V3/TrackFiles/MediaInfoResource.cs create mode 100644 src/Sonarr.Api.V3/TrackFiles/TrackFileListResource.cs create mode 100644 src/Sonarr.Api.V3/TrackFiles/TrackFileModule.cs create mode 100644 src/Sonarr.Api.V3/TrackFiles/TrackFileResource.cs create mode 100644 src/Sonarr.Api.V3/Tracks/RenameTrackModule.cs create mode 100644 src/Sonarr.Api.V3/Tracks/RenameTrackResource.cs create mode 100644 src/Sonarr.Api.V3/Tracks/TrackModule.cs create mode 100644 src/Sonarr.Api.V3/Tracks/TrackModuleWithSignalR.cs create mode 100644 src/Sonarr.Api.V3/Tracks/TrackResource.cs create mode 100644 src/Sonarr.Api.V3/Update/UpdateModule.cs create mode 100644 src/Sonarr.Api.V3/Update/UpdateResource.cs create mode 100644 src/Sonarr.Api.V3/Wanted/CutoffModule.cs create mode 100644 src/Sonarr.Api.V3/Wanted/MissingModule.cs create mode 100644 src/Sonarr.Api.V3/app.config create mode 100644 src/Sonarr.Api.V3/packages.config rename src/{NzbDrone.Api => Sonarr.Http}/Authentication/1tews5g3.gd1~ (100%) rename src/{NzbDrone.Api => Sonarr.Http}/Authentication/AuthenticationModule.cs (89%) rename src/{NzbDrone.Api => Sonarr.Http}/Authentication/AuthenticationService.cs (95%) create mode 100644 src/Sonarr.Http/Authentication/EnableAuthInNancy.cs rename src/{NzbDrone.Api => Sonarr.Http}/Authentication/LoginResource.cs (81%) rename src/{NzbDrone.Api => Sonarr.Http}/Authentication/NzbDroneUser.cs (85%) rename src/{NzbDrone.Api => Sonarr.Http}/ClientSchema/Field.cs (92%) rename src/{NzbDrone.Api => Sonarr.Http}/ClientSchema/SchemaBuilder.cs (99%) rename src/{NzbDrone.Api => Sonarr.Http}/ClientSchema/SelectOption.cs (76%) rename src/{NzbDrone.Api => Sonarr.Http}/ErrorManagement/ErrorHandler.cs (93%) rename src/{NzbDrone.Api => Sonarr.Http}/ErrorManagement/ErrorModel.cs (83%) create mode 100644 src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs rename src/{NzbDrone.Api/ErrorManagement => Sonarr.Http/Exceptions}/ApiException.cs (90%) rename src/{NzbDrone.Api => Sonarr.Http}/Exceptions/InvalidApiKeyException.cs (87%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/AccessControlHeaders.cs (92%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/NancyJsonSerializer.cs (93%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/CacheHeaderPipeline.cs (92%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/CorsPipeline.cs (97%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/GZipPipeline.cs (98%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/IRegisterNancyPipeline.cs (78%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/IfModifiedPipeline.cs (93%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/RequestLoggingPipeline.cs (91%) create mode 100644 src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/Pipelines/UrlBasePipeline.cs (100%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/ReqResExtensions.cs (98%) rename src/{NzbDrone.Api => Sonarr.Http}/Extensions/RequestExtensions.cs (76%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/CacheableSpecification.cs (93%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/BackupFileMapper.cs (95%) create mode 100644 src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/CacheBreakerProvider.cs (96%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/FaviconMapper.cs (88%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/IMapHttpRequestsToDisk.cs (84%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/IndexHtmlMapper.cs (87%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/LogFileMapper.cs (95%) create mode 100644 src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs create mode 100644 src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/MediaCoverMapper.cs (97%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/RobotsTxtMapper.cs (96%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/StaticResourceMapper.cs (83%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/StaticResourceMapperBase.cs (97%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/Mappers/UpdateLogFileMapper.cs (95%) rename src/{NzbDrone.Api => Sonarr.Http}/Frontend/StaticResourceModule.cs (95%) create mode 100644 src/Sonarr.Http/LIdarrRestModuleWithSignalR.cs create mode 100644 src/Sonarr.Http/Lidarr.Http.csproj create mode 100644 src/Sonarr.Http/LidarrBootstrapper.cs create mode 100644 src/Sonarr.Http/LidarrRestModule.cs create mode 100644 src/Sonarr.Http/Mapping/MappingValidation.cs create mode 100644 src/Sonarr.Http/Mapping/ResourceMappingException.cs rename src/{NzbDrone.Api => Sonarr.Http}/PagingResource.cs (95%) create mode 100644 src/Sonarr.Http/Properties/AssemblyInfo.cs rename src/{NzbDrone.Api => Sonarr.Http}/REST/BadRequestException.cs (76%) rename src/{NzbDrone.Api => Sonarr.Http}/REST/MethodNotAllowedException.cs (78%) rename src/{NzbDrone.Api => Sonarr.Http}/REST/NotFoundException.cs (76%) rename src/{NzbDrone.Api => Sonarr.Http}/REST/ResourceValidator.cs (95%) rename src/{NzbDrone.Api => Sonarr.Http}/REST/RestModule.cs (92%) rename src/{NzbDrone.Api => Sonarr.Http}/REST/RestResource.cs (91%) rename src/{NzbDrone.Api => Sonarr.Http}/ResourceChangeMessage.cs (93%) rename src/{NzbDrone.Api => Sonarr.Http}/TinyIoCNancyBootstrapper.cs (99%) rename src/{NzbDrone.Api => Sonarr.Http}/Validation/EmptyCollectionValidator.cs (94%) rename src/{NzbDrone.Api => Sonarr.Http}/Validation/RssSyncIntervalValidator.cs (95%) rename src/{NzbDrone.Api => Sonarr.Http}/Validation/RuleBuilderExtensions.cs (97%) create mode 100644 src/Sonarr.Http/app.config create mode 100644 src/Sonarr.Http/packages.config delete mode 100644 src/UI/.idea/.name delete mode 100644 src/UI/.idea/NzbDrone.UI.iml delete mode 100644 src/UI/.idea/codeStyleSettings.xml delete mode 100644 src/UI/.idea/dictionaries/Keivan.xml delete mode 100644 src/UI/.idea/dictionaries/Keivan_Beigi.xml delete mode 100644 src/UI/.idea/dictionaries/Mark.xml delete mode 100644 src/UI/.idea/encodings.xml delete mode 100644 src/UI/.idea/inspectionProfiles/Project_Default.xml delete mode 100644 src/UI/.idea/inspectionProfiles/profiles_settings.xml delete mode 100644 src/UI/.idea/jsLibraryMappings.xml delete mode 100644 src/UI/.idea/jsLinters/jshint.xml delete mode 100644 src/UI/.idea/jsLinters/jslint.xml delete mode 100644 src/UI/.idea/misc.xml delete mode 100644 src/UI/.idea/modules.xml delete mode 100644 src/UI/.idea/runConfigurations/Debug___Chrome.xml delete mode 100644 src/UI/.idea/runConfigurations/Debug___Firefox.xml delete mode 100644 src/UI/.idea/scopes/NzbDrone.xml delete mode 100644 src/UI/.idea/scopes/scope_settings.xml delete mode 100644 src/UI/.idea/vcs.xml delete mode 100644 src/UI/.jshintrc delete mode 100644 src/UI/Activity/ActivityLayout.js delete mode 100644 src/UI/Activity/ActivityLayoutTemplate.hbs delete mode 100644 src/UI/Activity/Blacklist/BlacklistActionsCell.js delete mode 100644 src/UI/Activity/Blacklist/BlacklistCollection.js delete mode 100644 src/UI/Activity/Blacklist/BlacklistLayout.js delete mode 100644 src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs delete mode 100644 src/UI/Activity/Blacklist/BlacklistModel.js delete mode 100644 src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js delete mode 100644 src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs delete mode 100644 src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js delete mode 100644 src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs delete mode 100644 src/UI/Activity/History/Details/HistoryDetailsAge.js delete mode 100644 src/UI/Activity/History/Details/HistoryDetailsLayout.js delete mode 100644 src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs delete mode 100644 src/UI/Activity/History/Details/HistoryDetailsView.js delete mode 100644 src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs delete mode 100644 src/UI/Activity/History/HistoryCollection.js delete mode 100644 src/UI/Activity/History/HistoryDetailsCell.js delete mode 100644 src/UI/Activity/History/HistoryLayout.js delete mode 100644 src/UI/Activity/History/HistoryLayoutTemplate.hbs delete mode 100644 src/UI/Activity/History/HistoryModel.js delete mode 100644 src/UI/Activity/History/HistoryQualityCell.js delete mode 100644 src/UI/Activity/Queue/ProgressCell.js delete mode 100644 src/UI/Activity/Queue/QueueActionsCell.js delete mode 100644 src/UI/Activity/Queue/QueueActionsCellTemplate.hbs delete mode 100644 src/UI/Activity/Queue/QueueCollection.js delete mode 100644 src/UI/Activity/Queue/QueueLayout.js delete mode 100644 src/UI/Activity/Queue/QueueLayoutTemplate.hbs delete mode 100644 src/UI/Activity/Queue/QueueModel.js delete mode 100644 src/UI/Activity/Queue/QueueStatusCell.js delete mode 100644 src/UI/Activity/Queue/QueueStatusCellTemplate.hbs delete mode 100644 src/UI/Activity/Queue/QueueView.js delete mode 100644 src/UI/Activity/Queue/RemoveFromQueueView.js delete mode 100644 src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs delete mode 100644 src/UI/Activity/Queue/TimeleftCell.js delete mode 100644 src/UI/Activity/activity.less delete mode 100644 src/UI/AddArtist/AddArtistCollection.js delete mode 100644 src/UI/AddArtist/AddArtistLayout.js delete mode 100644 src/UI/AddArtist/AddArtistLayoutTemplate.hbs delete mode 100644 src/UI/AddArtist/AddArtistView.js delete mode 100644 src/UI/AddArtist/AddArtistViewTemplate.hbs delete mode 100644 src/UI/AddArtist/ArtistTypeSelectionPartial.hbs delete mode 100644 src/UI/AddArtist/BulkImport/ArtistPathCell.js delete mode 100644 src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportCollection.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportProfileCell.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportView.js delete mode 100644 src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs delete mode 100644 src/UI/AddArtist/BulkImport/EmptyView.js delete mode 100644 src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs delete mode 100644 src/UI/AddArtist/BulkImport/ForeignIdCell.js delete mode 100644 src/UI/AddArtist/EmptyView.js delete mode 100644 src/UI/AddArtist/EmptyViewTemplate.hbs delete mode 100644 src/UI/AddArtist/ErrorView.js delete mode 100644 src/UI/AddArtist/ErrorViewTemplate.hbs delete mode 100644 src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js delete mode 100644 src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs delete mode 100644 src/UI/AddArtist/Existing/UnmappedFolderCollection.js delete mode 100644 src/UI/AddArtist/Existing/UnmappedFolderModel.js delete mode 100644 src/UI/AddArtist/MonitoringTooltipTemplate.hbs delete mode 100644 src/UI/AddArtist/NotFoundView.js delete mode 100644 src/UI/AddArtist/NotFoundViewTemplate.hbs delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderCollection.js delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderCollectionView.js delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderItemView.js delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderLayout.js delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderModel.js delete mode 100644 src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs delete mode 100644 src/UI/AddArtist/SearchResultCollectionView.js delete mode 100644 src/UI/AddArtist/SearchResultView.js delete mode 100644 src/UI/AddArtist/SearchResultViewTemplate.hbs delete mode 100644 src/UI/AddArtist/StartingAlbumSelectionPartial.hbs delete mode 100644 src/UI/AddArtist/addArtist.less delete mode 100644 src/UI/Album/AlbumDetailsLayout.js delete mode 100644 src/UI/Album/AlbumDetailsLayoutTemplate.hbs delete mode 100644 src/UI/Album/History/AlbumHistoryActionsCell.js delete mode 100644 src/UI/Album/History/AlbumHistoryDetailsCell.js delete mode 100644 src/UI/Album/History/AlbumHistoryLayout.js delete mode 100644 src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs delete mode 100644 src/UI/Album/History/NoHistoryView.js delete mode 100644 src/UI/Album/History/NoHistoryViewTemplate.hbs delete mode 100644 src/UI/Album/Search/AlbumSearchLayout.js delete mode 100644 src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs delete mode 100644 src/UI/Album/Search/ButtonsView.js delete mode 100644 src/UI/Album/Search/ButtonsViewTemplate.hbs delete mode 100644 src/UI/Album/Search/ManualLayout.js delete mode 100644 src/UI/Album/Search/ManualLayoutTemplate.hbs delete mode 100644 src/UI/Album/Search/NoResultsView.js delete mode 100644 src/UI/Album/Search/NoResultsViewTemplate.hbs delete mode 100644 src/UI/Album/Summary/AlbumSummaryLayout.js delete mode 100644 src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs delete mode 100644 src/UI/Album/Summary/NoFileView.js delete mode 100644 src/UI/Album/Summary/NoFileViewTemplate.hbs delete mode 100644 src/UI/AlbumStudio/AlbumStudioCollectionView.js delete mode 100644 src/UI/AlbumStudio/AlbumStudioFooterView.js delete mode 100644 src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs delete mode 100644 src/UI/AlbumStudio/AlbumStudioLayout.js delete mode 100644 src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs delete mode 100644 src/UI/AlbumStudio/AlbumsCell.js delete mode 100644 src/UI/AlbumStudio/AlbumsCellTemplate.hbs delete mode 100644 src/UI/AlbumStudio/SingleAlbumCell.js delete mode 100644 src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs delete mode 100644 src/UI/AlbumStudio/albumstudio.less delete mode 100644 src/UI/AppLayout.js delete mode 100644 src/UI/Artist/AlbumCollection.js delete mode 100644 src/UI/Artist/AlbumModel.js delete mode 100644 src/UI/Artist/ArtistCollection.js delete mode 100644 src/UI/Artist/ArtistController.js delete mode 100644 src/UI/Artist/ArtistModel.js delete mode 100644 src/UI/Artist/Delete/DeleteArtistTemplate.hbs delete mode 100644 src/UI/Artist/Delete/DeleteArtistView.js delete mode 100644 src/UI/Artist/Details/AlbumCollectionView.js delete mode 100644 src/UI/Artist/Details/AlbumInfoView.js delete mode 100644 src/UI/Artist/Details/AlbumInfoViewTemplate.hbs delete mode 100644 src/UI/Artist/Details/AlbumLayout.js delete mode 100644 src/UI/Artist/Details/AlbumLayoutTemplate.hbs delete mode 100644 src/UI/Artist/Details/ArtistDetailsLayout.js delete mode 100644 src/UI/Artist/Details/ArtistDetailsTemplate.hbs delete mode 100644 src/UI/Artist/Details/InfoView.js delete mode 100644 src/UI/Artist/Details/InfoViewTemplate.hbs delete mode 100644 src/UI/Artist/Details/TrackNumberCell.js delete mode 100644 src/UI/Artist/Details/TrackNumberCellTemplate.hbs delete mode 100644 src/UI/Artist/Details/TrackRatingCell.js delete mode 100644 src/UI/Artist/Details/TrackWarningCell.js delete mode 100644 src/UI/Artist/Edit/EditArtistView.js delete mode 100644 src/UI/Artist/Edit/EditArtistViewTemplate.hbs delete mode 100644 src/UI/Artist/Editor/ArtistEditorFooterView.js delete mode 100644 src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs delete mode 100644 src/UI/Artist/Editor/ArtistEditorLayout.js delete mode 100644 src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs delete mode 100644 src/UI/Artist/Editor/Organize/OrganizeFilesView.js delete mode 100644 src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/ArtistIndexItemView.js delete mode 100644 src/UI/Artist/Index/ArtistIndexLayout.js delete mode 100644 src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs delete mode 100644 src/UI/Artist/Index/EmptyTemplate.hbs delete mode 100644 src/UI/Artist/Index/EmptyView.js delete mode 100644 src/UI/Artist/Index/FooterModel.js delete mode 100644 src/UI/Artist/Index/FooterView.js delete mode 100644 src/UI/Artist/Index/FooterViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js delete mode 100644 src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/Overview/ArtistOverviewItemView.js delete mode 100644 src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js delete mode 100644 src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/Posters/ArtistPostersItemView.js delete mode 100644 src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs delete mode 100644 src/UI/Artist/Index/TrackProgressPartial.hbs delete mode 100644 src/UI/Artist/TrackCollection.js delete mode 100644 src/UI/Artist/TrackFileCollection.js delete mode 100644 src/UI/Artist/TrackFileModel.js delete mode 100644 src/UI/Artist/TrackModel.js delete mode 100644 src/UI/Artist/artist.less delete mode 100644 src/UI/Calendar/CalendarCollection.js delete mode 100644 src/UI/Calendar/CalendarFeedView.js delete mode 100644 src/UI/Calendar/CalendarFeedViewTemplate.hbs delete mode 100644 src/UI/Calendar/CalendarLayout.js delete mode 100644 src/UI/Calendar/CalendarLayoutTemplate.hbs delete mode 100644 src/UI/Calendar/CalendarView.js delete mode 100644 src/UI/Calendar/UpcomingCollection.js delete mode 100644 src/UI/Calendar/UpcomingCollectionView.js delete mode 100644 src/UI/Calendar/UpcomingItemView.js delete mode 100644 src/UI/Calendar/UpcomingItemViewTemplate.hbs delete mode 100644 src/UI/Calendar/calendar.less delete mode 100644 src/UI/Cells/AlbumFolderCell.js delete mode 100644 src/UI/Cells/AlbumTitleCell.js delete mode 100644 src/UI/Cells/ApprovalStatusCell.js delete mode 100644 src/UI/Cells/ApprovalStatusCellTemplate.hbs delete mode 100644 src/UI/Cells/ArtistActionsCell.js delete mode 100644 src/UI/Cells/ArtistMonitoredCell.js delete mode 100644 src/UI/Cells/ArtistStatusCell.js delete mode 100644 src/UI/Cells/ArtistTitleCell.js delete mode 100644 src/UI/Cells/ArtistTitleTemplate.hbs delete mode 100644 src/UI/Cells/DeleteEpisodeFileCell.js delete mode 100644 src/UI/Cells/Edit/QualityCellEditor.js delete mode 100644 src/UI/Cells/Edit/QualityCellEditorTemplate.hbs delete mode 100644 src/UI/Cells/EpisodeActionsCell.js delete mode 100644 src/UI/Cells/EpisodeFilePathCell.js delete mode 100644 src/UI/Cells/EpisodeMonitoredCell.js delete mode 100644 src/UI/Cells/EpisodeNumberCell.js delete mode 100644 src/UI/Cells/EpisodeStatusCell.js delete mode 100644 src/UI/Cells/EpisodeTitleCell.js delete mode 100644 src/UI/Cells/EventTypeCell.js delete mode 100644 src/UI/Cells/FileSizeCell.js delete mode 100644 src/UI/Cells/IndexerCell.js delete mode 100644 src/UI/Cells/NzbDroneCell.js delete mode 100644 src/UI/Cells/ProfileCell.js delete mode 100644 src/UI/Cells/QualityCell.js delete mode 100644 src/UI/Cells/QualityCellTemplate.hbs delete mode 100644 src/UI/Cells/RelativeDateCell.js delete mode 100644 src/UI/Cells/RelativeTimeCell.js delete mode 100644 src/UI/Cells/ReleaseTitleCell.js delete mode 100644 src/UI/Cells/SeasonFolderCell.js delete mode 100644 src/UI/Cells/SelectAllCell.js delete mode 100644 src/UI/Cells/SeriesActionsCell.js delete mode 100644 src/UI/Cells/SeriesStatusCell.js delete mode 100644 src/UI/Cells/SeriesTitleCell.js delete mode 100644 src/UI/Cells/SeriesTitleTemplate.hbs delete mode 100644 src/UI/Cells/TemplatedCell.js delete mode 100644 src/UI/Cells/ToggleCell.js delete mode 100644 src/UI/Cells/TrackActionsCell.js delete mode 100644 src/UI/Cells/TrackDurationCell.js delete mode 100644 src/UI/Cells/TrackExplicitCell.js delete mode 100644 src/UI/Cells/TrackMonitoredCell.js delete mode 100644 src/UI/Cells/TrackProgressCell.js delete mode 100644 src/UI/Cells/TrackProgressCellTemplate.hbs delete mode 100644 src/UI/Cells/TrackStatusCell.js delete mode 100644 src/UI/Cells/TrackTitleCell.js delete mode 100644 src/UI/Cells/cells.less delete mode 100644 src/UI/Commands/CommandCollection.js delete mode 100644 src/UI/Commands/CommandController.js delete mode 100644 src/UI/Commands/CommandMessengerCollectionView.js delete mode 100644 src/UI/Commands/CommandMessengerItemView.js delete mode 100644 src/UI/Commands/CommandModel.js delete mode 100644 src/UI/Config.js delete mode 100644 src/UI/Content/Backgrid/backgrid.less delete mode 100644 src/UI/Content/Backgrid/filter.less delete mode 100644 src/UI/Content/Backgrid/paginator.less delete mode 100644 src/UI/Content/Backgrid/selectall.less delete mode 100644 src/UI/Content/Bootstrap/.csscomb.json delete mode 100644 src/UI/Content/Bootstrap/.csslintrc delete mode 100644 src/UI/Content/Bootstrap/alerts.less delete mode 100644 src/UI/Content/Bootstrap/badges.less delete mode 100644 src/UI/Content/Bootstrap/bootstrap.less delete mode 100644 src/UI/Content/Bootstrap/breadcrumbs.less delete mode 100644 src/UI/Content/Bootstrap/button-groups.less delete mode 100644 src/UI/Content/Bootstrap/buttons.less delete mode 100644 src/UI/Content/Bootstrap/carousel.less delete mode 100644 src/UI/Content/Bootstrap/close.less delete mode 100644 src/UI/Content/Bootstrap/code.less delete mode 100644 src/UI/Content/Bootstrap/component-animations.less delete mode 100644 src/UI/Content/Bootstrap/dropdowns.less delete mode 100644 src/UI/Content/Bootstrap/forms.less delete mode 100644 src/UI/Content/Bootstrap/glyphicons.less delete mode 100644 src/UI/Content/Bootstrap/grid.less delete mode 100644 src/UI/Content/Bootstrap/input-groups.less delete mode 100644 src/UI/Content/Bootstrap/jumbotron.less delete mode 100644 src/UI/Content/Bootstrap/labels.less delete mode 100644 src/UI/Content/Bootstrap/list-group.less delete mode 100644 src/UI/Content/Bootstrap/media.less delete mode 100644 src/UI/Content/Bootstrap/mixins.less delete mode 100644 src/UI/Content/Bootstrap/mixins/alerts.less delete mode 100644 src/UI/Content/Bootstrap/mixins/background-variant.less delete mode 100644 src/UI/Content/Bootstrap/mixins/border-radius.less delete mode 100644 src/UI/Content/Bootstrap/mixins/buttons.less delete mode 100644 src/UI/Content/Bootstrap/mixins/center-block.less delete mode 100644 src/UI/Content/Bootstrap/mixins/clearfix.less delete mode 100644 src/UI/Content/Bootstrap/mixins/forms.less delete mode 100644 src/UI/Content/Bootstrap/mixins/gradients.less delete mode 100644 src/UI/Content/Bootstrap/mixins/grid-framework.less delete mode 100644 src/UI/Content/Bootstrap/mixins/grid.less delete mode 100644 src/UI/Content/Bootstrap/mixins/hide-text.less delete mode 100644 src/UI/Content/Bootstrap/mixins/image.less delete mode 100644 src/UI/Content/Bootstrap/mixins/labels.less delete mode 100644 src/UI/Content/Bootstrap/mixins/list-group.less delete mode 100644 src/UI/Content/Bootstrap/mixins/nav-divider.less delete mode 100644 src/UI/Content/Bootstrap/mixins/nav-vertical-align.less delete mode 100644 src/UI/Content/Bootstrap/mixins/opacity.less delete mode 100644 src/UI/Content/Bootstrap/mixins/pagination.less delete mode 100644 src/UI/Content/Bootstrap/mixins/panels.less delete mode 100644 src/UI/Content/Bootstrap/mixins/progress-bar.less delete mode 100644 src/UI/Content/Bootstrap/mixins/reset-filter.less delete mode 100644 src/UI/Content/Bootstrap/mixins/reset-text.less delete mode 100644 src/UI/Content/Bootstrap/mixins/resize.less delete mode 100644 src/UI/Content/Bootstrap/mixins/responsive-visibility.less delete mode 100644 src/UI/Content/Bootstrap/mixins/size.less delete mode 100644 src/UI/Content/Bootstrap/mixins/tab-focus.less delete mode 100644 src/UI/Content/Bootstrap/mixins/table-row.less delete mode 100644 src/UI/Content/Bootstrap/mixins/text-emphasis.less delete mode 100644 src/UI/Content/Bootstrap/mixins/text-overflow.less delete mode 100644 src/UI/Content/Bootstrap/mixins/vendor-prefixes.less delete mode 100644 src/UI/Content/Bootstrap/modals.less delete mode 100644 src/UI/Content/Bootstrap/navbar.less delete mode 100644 src/UI/Content/Bootstrap/navs.less delete mode 100644 src/UI/Content/Bootstrap/normalize.less delete mode 100644 src/UI/Content/Bootstrap/pager.less delete mode 100644 src/UI/Content/Bootstrap/pagination.less delete mode 100644 src/UI/Content/Bootstrap/panels.less delete mode 100644 src/UI/Content/Bootstrap/popovers.less delete mode 100644 src/UI/Content/Bootstrap/print.less delete mode 100644 src/UI/Content/Bootstrap/progress-bars.less delete mode 100644 src/UI/Content/Bootstrap/responsive-embed.less delete mode 100644 src/UI/Content/Bootstrap/responsive-utilities.less delete mode 100644 src/UI/Content/Bootstrap/scaffolding.less delete mode 100644 src/UI/Content/Bootstrap/tables.less delete mode 100644 src/UI/Content/Bootstrap/theme.less delete mode 100644 src/UI/Content/Bootstrap/thumbnails.less delete mode 100644 src/UI/Content/Bootstrap/tooltip.less delete mode 100644 src/UI/Content/Bootstrap/type.less delete mode 100644 src/UI/Content/Bootstrap/utilities.less delete mode 100644 src/UI/Content/Bootstrap/variables.less delete mode 100644 src/UI/Content/Bootstrap/wells.less delete mode 100644 src/UI/Content/FontAwesome/FontAwesome.otf delete mode 100644 src/UI/Content/FontAwesome/animated.less delete mode 100644 src/UI/Content/FontAwesome/bordered-pulled.less delete mode 100644 src/UI/Content/FontAwesome/core.less delete mode 100644 src/UI/Content/FontAwesome/fixed-width.less delete mode 100644 src/UI/Content/FontAwesome/font-awesome.less delete mode 100644 src/UI/Content/FontAwesome/fontawesome-webfont.eot delete mode 100644 src/UI/Content/FontAwesome/fontawesome-webfont.svg delete mode 100644 src/UI/Content/FontAwesome/fontawesome-webfont.ttf delete mode 100644 src/UI/Content/FontAwesome/fontawesome-webfont.woff delete mode 100644 src/UI/Content/FontAwesome/fontawesome-webfont.woff2 delete mode 100644 src/UI/Content/FontAwesome/icons.less delete mode 100644 src/UI/Content/FontAwesome/larger.less delete mode 100644 src/UI/Content/FontAwesome/list.less delete mode 100644 src/UI/Content/FontAwesome/mixins.less delete mode 100644 src/UI/Content/FontAwesome/path.less delete mode 100644 src/UI/Content/FontAwesome/rotated-flipped.less delete mode 100644 src/UI/Content/FontAwesome/stacked.less delete mode 100644 src/UI/Content/FontAwesome/variables.less delete mode 100644 src/UI/Content/Images/background/logo.png delete mode 100644 src/UI/Content/Images/cover-dark.png delete mode 100644 src/UI/Content/Images/favicon-debug.ico delete mode 100644 src/UI/Content/Images/logos/128.png delete mode 100644 src/UI/Content/Images/logos/48.png delete mode 100644 src/UI/Content/Images/logos/64.png delete mode 100644 src/UI/Content/Images/safari/logo.svg delete mode 100644 src/UI/Content/Images/touch/114.png delete mode 100644 src/UI/Content/Images/touch/144.png delete mode 100644 src/UI/Content/Images/touch/57.png delete mode 100644 src/UI/Content/Images/touch/72.png delete mode 100644 src/UI/Content/Messenger/messenger.css delete mode 100644 src/UI/Content/Messenger/messenger.flat.css delete mode 100644 src/UI/Content/Overrides/bootstrap.less delete mode 100644 src/UI/Content/Overrides/bootstrap.tagsinput.less delete mode 100644 src/UI/Content/Overrides/bootstrap.toggle-switch.less delete mode 100644 src/UI/Content/Overrides/browser.less delete mode 100644 src/UI/Content/Overrides/fullcalendar.less delete mode 100644 src/UI/Content/Overrides/messenger.less delete mode 100644 src/UI/Content/badges.less delete mode 100644 src/UI/Content/bootstrap.less delete mode 100644 src/UI/Content/bootstrap.tagsinput.less delete mode 100644 src/UI/Content/bootstrap.toggle-switch.css delete mode 100644 src/UI/Content/checkbox-button.less delete mode 100644 src/UI/Content/font.less delete mode 100644 src/UI/Content/fonts/opensans-light.eot delete mode 100644 src/UI/Content/fonts/opensans-light.ttf delete mode 100644 src/UI/Content/fonts/opensans-light.woff delete mode 100644 src/UI/Content/fonts/opensans-regular.eot delete mode 100644 src/UI/Content/fonts/opensans-regular.ttf delete mode 100644 src/UI/Content/fonts/opensans-regular.woff delete mode 100644 src/UI/Content/fonts/opensans-semibold.eot delete mode 100644 src/UI/Content/fonts/opensans-semibold.ttf delete mode 100644 src/UI/Content/fonts/opensans-semibold.woff delete mode 100644 src/UI/Content/form.less delete mode 100644 src/UI/Content/fullcalendar.css delete mode 100644 src/UI/Content/icons.less delete mode 100644 src/UI/Content/legend.less delete mode 100644 src/UI/Content/mixins.less delete mode 100644 src/UI/Content/navbar.less delete mode 100644 src/UI/Content/overrides.less delete mode 100644 src/UI/Content/prefixer.less delete mode 100644 src/UI/Content/progress-bars.less delete mode 100644 src/UI/Content/robots.txt delete mode 100644 src/UI/Content/spinner.less delete mode 100644 src/UI/Content/theme.less delete mode 100644 src/UI/Content/typeahead.less delete mode 100644 src/UI/Content/utilities.less delete mode 100644 src/UI/Content/variables.less delete mode 100644 src/UI/Content/zero.clipboard.swf delete mode 100644 src/UI/Controller.js delete mode 100644 src/UI/Episode/EpisodeDetailsLayout.js delete mode 100644 src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs delete mode 100644 src/UI/Episode/History/EpisodeHistoryActionsCell.js delete mode 100644 src/UI/Episode/History/EpisodeHistoryDetailsCell.js delete mode 100644 src/UI/Episode/History/EpisodeHistoryLayout.js delete mode 100644 src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs delete mode 100644 src/UI/Episode/History/NoHistoryView.js delete mode 100644 src/UI/Episode/History/NoHistoryViewTemplate.hbs delete mode 100644 src/UI/Episode/Search/ButtonsView.js delete mode 100644 src/UI/Episode/Search/ButtonsViewTemplate.hbs delete mode 100644 src/UI/Episode/Search/EpisodeSearchLayout.js delete mode 100644 src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs delete mode 100644 src/UI/Episode/Search/ManualLayout.js delete mode 100644 src/UI/Episode/Search/ManualLayoutTemplate.hbs delete mode 100644 src/UI/Episode/Search/NoResultsView.js delete mode 100644 src/UI/Episode/Search/NoResultsViewTemplate.hbs delete mode 100644 src/UI/Episode/Summary/EpisodeSummaryLayout.js delete mode 100644 src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs delete mode 100644 src/UI/Episode/Summary/NoFileView.js delete mode 100644 src/UI/Episode/Summary/NoFileViewTemplate.hbs delete mode 100644 src/UI/EpisodeFile/Editor/EmptyView.js delete mode 100644 src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs delete mode 100644 src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js delete mode 100644 src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs delete mode 100644 src/UI/EpisodeFile/Editor/QualitySelectView.js delete mode 100644 src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs delete mode 100644 src/UI/Form/ActionTemplate.hbs delete mode 100644 src/UI/Form/CaptchaTemplate.hbs delete mode 100644 src/UI/Form/CheckboxTemplate.hbs delete mode 100644 src/UI/Form/FormBuilder.js delete mode 100644 src/UI/Form/FormHelpPartial.hbs delete mode 100644 src/UI/Form/FormMessage.js delete mode 100644 src/UI/Form/HiddenTemplate.hbs delete mode 100644 src/UI/Form/PasswordTemplate.hbs delete mode 100644 src/UI/Form/PathTemplate.hbs delete mode 100644 src/UI/Form/SelectTemplate.hbs delete mode 100644 src/UI/Form/TagTemplate.hbs delete mode 100644 src/UI/Form/TextboxTemplate.hbs delete mode 100644 src/UI/Form/UrlTemplate.hbs delete mode 100644 src/UI/Handlebars/Handlebars.Debug.js delete mode 100644 src/UI/Handlebars/Helpers/Album.js delete mode 100644 src/UI/Handlebars/Helpers/Artist.js delete mode 100644 src/UI/Handlebars/Helpers/DateTime.js delete mode 100644 src/UI/Handlebars/Helpers/EachReverse.js delete mode 100644 src/UI/Handlebars/Helpers/Enumerable.js delete mode 100644 src/UI/Handlebars/Helpers/Episode.js delete mode 100644 src/UI/Handlebars/Helpers/Html.js delete mode 100644 src/UI/Handlebars/Helpers/Numbers.js delete mode 100644 src/UI/Handlebars/Helpers/Quality.js delete mode 100644 src/UI/Handlebars/Helpers/Series.js delete mode 100644 src/UI/Handlebars/Helpers/String.js delete mode 100644 src/UI/Handlebars/Helpers/System.js delete mode 100644 src/UI/Handlebars/backbone.marionette.templates.js delete mode 100644 src/UI/Health/HealthCollection.js delete mode 100644 src/UI/Health/HealthModel.js delete mode 100644 src/UI/Health/HealthView.js delete mode 100644 src/UI/Hotkeys/Hotkeys.js delete mode 100644 src/UI/Hotkeys/HotkeysView.js delete mode 100644 src/UI/Hotkeys/HotkeysViewTemplate.hbs delete mode 100644 src/UI/Hotkeys/hotkeys.less delete mode 100644 src/UI/Instrumentation/ErrorHandler.js delete mode 100644 src/UI/Instrumentation/StringFormat.js delete mode 100644 src/UI/JsLibraries/backbone.backgrid.js delete mode 100644 src/UI/JsLibraries/backbone.backgrid.paginator.js delete mode 100644 src/UI/JsLibraries/backbone.backgrid.selectall.js delete mode 100644 src/UI/JsLibraries/backbone.collectionview.js delete mode 100644 src/UI/JsLibraries/backbone.deep.model.js delete mode 100644 src/UI/JsLibraries/backbone.js delete mode 100644 src/UI/JsLibraries/backbone.marionette.js delete mode 100644 src/UI/JsLibraries/backbone.modelbinder.js delete mode 100644 src/UI/JsLibraries/backbone.pageable.js delete mode 100644 src/UI/JsLibraries/backbone.validation.js delete mode 100644 src/UI/JsLibraries/backbone.wreqr.js delete mode 100644 src/UI/JsLibraries/bootstrap.js delete mode 100644 src/UI/JsLibraries/bootstrap.tagsinput.js delete mode 100644 src/UI/JsLibraries/filesize.js delete mode 100644 src/UI/JsLibraries/fullcalendar.js delete mode 100644 src/UI/JsLibraries/handlebars.helpers.js delete mode 100644 src/UI/JsLibraries/handlebars.runtime.js delete mode 100644 src/UI/JsLibraries/jquery-ui.js delete mode 100644 src/UI/JsLibraries/jquery.backstretch.js delete mode 100644 src/UI/JsLibraries/jquery.dotdotdot.js delete mode 100644 src/UI/JsLibraries/jquery.easypiechart.js delete mode 100644 src/UI/JsLibraries/jquery.js delete mode 100644 src/UI/JsLibraries/jquery.knob.js delete mode 100644 src/UI/JsLibraries/jquery.signalR.js delete mode 100644 src/UI/JsLibraries/locale/placeholder.txt delete mode 100644 src/UI/JsLibraries/lodash.underscore.js delete mode 100644 src/UI/JsLibraries/messenger.js delete mode 100644 src/UI/JsLibraries/moment.js delete mode 100644 src/UI/JsLibraries/typeahead.js delete mode 100644 src/UI/JsLibraries/zero.clipboard.js delete mode 100644 src/UI/LifeCycle.js delete mode 100644 src/UI/ManualImport/Cells/EpisodesCell.js delete mode 100644 src/UI/ManualImport/Cells/PathCell.js delete mode 100644 src/UI/ManualImport/Cells/QualityCell.js delete mode 100644 src/UI/ManualImport/Cells/SeasonCell.js delete mode 100644 src/UI/ManualImport/Cells/SeriesCell.js delete mode 100644 src/UI/ManualImport/EmptyView.js delete mode 100644 src/UI/ManualImport/EmptyViewTemplate.hbs delete mode 100644 src/UI/ManualImport/Episode/SelectEpisodeLayout.js delete mode 100644 src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs delete mode 100644 src/UI/ManualImport/Episode/SelectEpisodeRow.js delete mode 100644 src/UI/ManualImport/Folder/SelectFolderView.js delete mode 100644 src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs delete mode 100644 src/UI/ManualImport/ManualImportCollection.js delete mode 100644 src/UI/ManualImport/ManualImportLayout.js delete mode 100644 src/UI/ManualImport/ManualImportLayoutTemplate.hbs delete mode 100644 src/UI/ManualImport/ManualImportModel.js delete mode 100644 src/UI/ManualImport/ManualImportRow.js delete mode 100644 src/UI/ManualImport/Quality/SelectQualityLayout.js delete mode 100644 src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs delete mode 100644 src/UI/ManualImport/Quality/SelectQualityView.js delete mode 100644 src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs delete mode 100644 src/UI/ManualImport/Season/SelectSeasonLayout.js delete mode 100644 src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs delete mode 100644 src/UI/ManualImport/Series/SelectSeriesLayout.js delete mode 100644 src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs delete mode 100644 src/UI/ManualImport/Series/SelectSeriesRow.js delete mode 100644 src/UI/ManualImport/Summary/ManualImportSummaryView.js delete mode 100644 src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs delete mode 100644 src/UI/ManualImport/manualimport.less delete mode 100644 src/UI/Mixins/AsChangeTrackingModel.js delete mode 100644 src/UI/Mixins/AsEditModalView.js delete mode 100644 src/UI/Mixins/AsFilteredCollection.js delete mode 100644 src/UI/Mixins/AsModelBoundView.js delete mode 100644 src/UI/Mixins/AsNamedView.js delete mode 100644 src/UI/Mixins/AsPageableCollection.js delete mode 100644 src/UI/Mixins/AsPersistedStateCollection.js delete mode 100644 src/UI/Mixins/AsSortedCollection.js delete mode 100644 src/UI/Mixins/AsSortedCollectionView.js delete mode 100644 src/UI/Mixins/AsValidatedView.js delete mode 100644 src/UI/Mixins/AutoComplete.js delete mode 100644 src/UI/Mixins/CopyToClipboard.js delete mode 100644 src/UI/Mixins/DirectoryAutoComplete.js delete mode 100644 src/UI/Mixins/FileBrowser.js delete mode 100644 src/UI/Mixins/TagInput.js delete mode 100644 src/UI/Mixins/backbone.signalr.mixin.js delete mode 100644 src/UI/Navbar/NavbarLayout.js delete mode 100644 src/UI/Navbar/NavbarLayoutTemplate.hbs delete mode 100644 src/UI/Navbar/Search.js delete mode 100644 src/UI/Profile/ProfileCollection.js delete mode 100644 src/UI/Profile/ProfileModel.js delete mode 100644 src/UI/Profile/ProfileSelectionPartial.hbs delete mode 100644 src/UI/Quality/QualityDefinitionCollection.js delete mode 100644 src/UI/Quality/QualityDefinitionModel.js delete mode 100644 src/UI/Release/AgeCell.js delete mode 100644 src/UI/Release/DownloadReportCell.js delete mode 100644 src/UI/Release/PeersCell.js delete mode 100644 src/UI/Release/ProtocolCell.js delete mode 100644 src/UI/Release/ReleaseCollection.js delete mode 100644 src/UI/Release/ReleaseLayout.js delete mode 100644 src/UI/Release/ReleaseLayoutTemplate.hbs delete mode 100644 src/UI/Release/ReleaseModel.js delete mode 100644 src/UI/Rename/RenamePreviewCollection.js delete mode 100644 src/UI/Rename/RenamePreviewCollectionView.js delete mode 100644 src/UI/Rename/RenamePreviewEmptyCollectionView.js delete mode 100644 src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs delete mode 100644 src/UI/Rename/RenamePreviewFormatView.js delete mode 100644 src/UI/Rename/RenamePreviewFormatViewTemplate.hbs delete mode 100644 src/UI/Rename/RenamePreviewItemView.js delete mode 100644 src/UI/Rename/RenamePreviewItemViewTemplate.hbs delete mode 100644 src/UI/Rename/RenamePreviewLayout.js delete mode 100644 src/UI/Rename/RenamePreviewLayoutTemplate.hbs delete mode 100644 src/UI/Rename/RenamePreviewModel.js delete mode 100644 src/UI/Rename/rename.less delete mode 100644 src/UI/Router.js delete mode 100644 src/UI/Series/Delete/DeleteSeriesTemplate.hbs delete mode 100644 src/UI/Series/Delete/DeleteSeriesView.js delete mode 100644 src/UI/Series/Details/EpisodeNumberCell.js delete mode 100644 src/UI/Series/Details/EpisodeNumberCellTemplate.hbs delete mode 100644 src/UI/Series/Details/EpisodeWarningCell.js delete mode 100644 src/UI/Series/Details/InfoView.js delete mode 100644 src/UI/Series/Details/InfoViewTemplate.hbs delete mode 100644 src/UI/Series/Details/SeasonCollectionView.js delete mode 100644 src/UI/Series/Details/SeasonLayout.js delete mode 100644 src/UI/Series/Details/SeasonLayoutTemplate.hbs delete mode 100644 src/UI/Series/Details/SeriesDetailsLayout.js delete mode 100644 src/UI/Series/Details/SeriesDetailsTemplate.hbs delete mode 100644 src/UI/Series/Edit/EditSeriesView.js delete mode 100644 src/UI/Series/Edit/EditSeriesViewTemplate.hbs delete mode 100644 src/UI/Series/Editor/Organize/OrganizeFilesView.js delete mode 100644 src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs delete mode 100644 src/UI/Series/Editor/SeriesEditorFooterView.js delete mode 100644 src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs delete mode 100644 src/UI/Series/Editor/SeriesEditorLayout.js delete mode 100644 src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs delete mode 100644 src/UI/Series/EpisodeCollection.js delete mode 100644 src/UI/Series/EpisodeFileCollection.js delete mode 100644 src/UI/Series/EpisodeFileModel.js delete mode 100644 src/UI/Series/EpisodeModel.js delete mode 100644 src/UI/Series/Index/EmptyTemplate.hbs delete mode 100644 src/UI/Series/Index/EmptyView.js delete mode 100644 src/UI/Series/Index/EpisodeProgressPartial.hbs delete mode 100644 src/UI/Series/Index/FooterModel.js delete mode 100644 src/UI/Series/Index/FooterView.js delete mode 100644 src/UI/Series/Index/FooterViewTemplate.hbs delete mode 100644 src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js delete mode 100644 src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs delete mode 100644 src/UI/Series/Index/Overview/SeriesOverviewItemView.js delete mode 100644 src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs delete mode 100644 src/UI/Series/Index/Posters/SeriesPostersCollectionView.js delete mode 100644 src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs delete mode 100644 src/UI/Series/Index/Posters/SeriesPostersItemView.js delete mode 100644 src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs delete mode 100644 src/UI/Series/Index/SeriesIndexItemView.js delete mode 100644 src/UI/Series/Index/SeriesIndexLayout.js delete mode 100644 src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs delete mode 100644 src/UI/Series/SeasonCollection.js delete mode 100644 src/UI/Series/SeasonModel.js delete mode 100644 src/UI/Series/SeriesCollection.js delete mode 100644 src/UI/Series/SeriesController.js delete mode 100644 src/UI/Series/SeriesModel.js delete mode 100644 src/UI/Series/series.less delete mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js delete mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js delete mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js delete mode 100644 src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js delete mode 100644 src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientCollection.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientCollectionView.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientItemView.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientLayout.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientModel.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js delete mode 100644 src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js delete mode 100644 src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js delete mode 100644 src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs delete mode 100644 src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js delete mode 100644 src/UI/Settings/DownloadClient/downloadclient.less delete mode 100644 src/UI/Settings/General/GeneralSettingsModel.js delete mode 100644 src/UI/Settings/General/GeneralView.js delete mode 100644 src/UI/Settings/General/GeneralViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js delete mode 100644 src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Add/IndexerAddItemView.js delete mode 100644 src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Add/IndexerSchemaModal.js delete mode 100644 src/UI/Settings/Indexers/Delete/IndexerDeleteView.js delete mode 100644 src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Edit/IndexerEditView.js delete mode 100644 src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/IndexerCollection.js delete mode 100644 src/UI/Settings/Indexers/IndexerCollectionView.js delete mode 100644 src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/IndexerItemView.js delete mode 100644 src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/IndexerLayout.js delete mode 100644 src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/IndexerModel.js delete mode 100644 src/UI/Settings/Indexers/IndexerSettingsModel.js delete mode 100644 src/UI/Settings/Indexers/Options/IndexerOptionsView.js delete mode 100644 src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionCollection.js delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionEditView.js delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionItemView.js delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Indexers/Restriction/RestrictionModel.js delete mode 100644 src/UI/Settings/Indexers/indexers.less delete mode 100644 src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js delete mode 100644 src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs delete mode 100644 src/UI/Settings/MediaManagement/MediaManagementLayout.js delete mode 100644 src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs delete mode 100644 src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/NamingModel.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/NamingView.js delete mode 100644 src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs delete mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsView.js delete mode 100644 src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs delete mode 100644 src/UI/Settings/MediaManagement/Sorting/SortingView.js delete mode 100644 src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs delete mode 100644 src/UI/Settings/Metadata/MetadataCollection.js delete mode 100644 src/UI/Settings/Metadata/MetadataCollectionView.js delete mode 100644 src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Metadata/MetadataEditView.js delete mode 100644 src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs delete mode 100644 src/UI/Settings/Metadata/MetadataItemView.js delete mode 100644 src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Metadata/MetadataLayout.js delete mode 100644 src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Metadata/MetadataModel.js delete mode 100644 src/UI/Settings/Metadata/metadata.less delete mode 100644 src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js delete mode 100644 src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/Add/NotificationAddItemView.js delete mode 100644 src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/Add/NotificationSchemaModal.js delete mode 100644 src/UI/Settings/Notifications/Delete/NotificationDeleteView.js delete mode 100644 src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/Edit/NotificationEditView.js delete mode 100644 src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/NotificationCollection.js delete mode 100644 src/UI/Settings/Notifications/NotificationCollectionView.js delete mode 100644 src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/NotificationItemView.js delete mode 100644 src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Notifications/NotificationModel.js delete mode 100644 src/UI/Settings/Notifications/notifications.less delete mode 100644 src/UI/Settings/Profile/AllowedLabeler.js delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileCollection.js delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileItemView.js delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileLayout.js delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Delay/DelayProfileModel.js delete mode 100644 src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js delete mode 100644 src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js delete mode 100644 src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/DeleteProfileView.js delete mode 100644 src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileItemView.js delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileLayout.js delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileView.js delete mode 100644 src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js delete mode 100644 src/UI/Settings/Profile/Language/LanguageCollection.js delete mode 100644 src/UI/Settings/Profile/Language/LanguageModel.js delete mode 100644 src/UI/Settings/Profile/LanguageLabel.js delete mode 100644 src/UI/Settings/Profile/ProfileCollectionTemplate.hbs delete mode 100644 src/UI/Settings/Profile/ProfileCollectionView.js delete mode 100644 src/UI/Settings/Profile/ProfileLayout.js delete mode 100644 src/UI/Settings/Profile/ProfileLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Profile/ProfileSchemaCollection.js delete mode 100644 src/UI/Settings/Profile/ProfileView.js delete mode 100644 src/UI/Settings/Profile/ProfileViewTemplate.hbs delete mode 100644 src/UI/Settings/Profile/profile.less delete mode 100644 src/UI/Settings/ProviderSettingsModelBase.js delete mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs delete mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js delete mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js delete mode 100644 src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs delete mode 100644 src/UI/Settings/Quality/QualityLayout.js delete mode 100644 src/UI/Settings/Quality/QualityLayoutTemplate.hbs delete mode 100644 src/UI/Settings/Quality/quality.less delete mode 100644 src/UI/Settings/SettingsLayout.js delete mode 100644 src/UI/Settings/SettingsLayoutTemplate.hbs delete mode 100644 src/UI/Settings/SettingsModelBase.js delete mode 100644 src/UI/Settings/ThingyAddCollectionView.js delete mode 100644 src/UI/Settings/ThingyHeaderGroupView.js delete mode 100644 src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs delete mode 100644 src/UI/Settings/UI/UiSettingsModel.js delete mode 100644 src/UI/Settings/UI/UiView.js delete mode 100644 src/UI/Settings/UI/UiViewTemplate.hbs delete mode 100644 src/UI/Settings/settings.less delete mode 100644 src/UI/Settings/thingy.less delete mode 100644 src/UI/Shared/ApiData.js delete mode 100644 src/UI/Shared/ControlPanel/ControlPanelController.js delete mode 100644 src/UI/Shared/ControlPanel/ControlPanelRegion.js delete mode 100644 src/UI/Shared/FileBrowser/EmptyView.js delete mode 100644 src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserCollection.js delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserLayout.js delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserModel.js delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserNameCell.js delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserRow.js delete mode 100644 src/UI/Shared/FileBrowser/FileBrowserTypeCell.js delete mode 100644 src/UI/Shared/FileBrowser/filebrowser.less delete mode 100644 src/UI/Shared/FormatHelpers.js delete mode 100644 src/UI/Shared/Grid/HeaderCell.js delete mode 100644 src/UI/Shared/Grid/JumpToPageTemplate.hbs delete mode 100644 src/UI/Shared/Grid/Pager.js delete mode 100644 src/UI/Shared/Grid/PagerTemplate.hbs delete mode 100644 src/UI/Shared/LoadingView.js delete mode 100644 src/UI/Shared/LoadingViewTemplate.hbs delete mode 100644 src/UI/Shared/Messenger.js delete mode 100644 src/UI/Shared/Modal/ModalController.js delete mode 100644 src/UI/Shared/Modal/ModalRegion.js delete mode 100644 src/UI/Shared/Modal/ModalRegion2.js delete mode 100644 src/UI/Shared/Modal/ModalRegionBase.js delete mode 100644 src/UI/Shared/NotFoundView.js delete mode 100644 src/UI/Shared/NotFoundViewTemplate.hbs delete mode 100644 src/UI/Shared/NzbDroneController.js delete mode 100644 src/UI/Shared/SignalRBroadcaster.js delete mode 100644 src/UI/Shared/Styles/card.less delete mode 100644 src/UI/Shared/Toolbar/Button/ButtonCollectionView.js delete mode 100644 src/UI/Shared/Toolbar/Button/ButtonView.js delete mode 100644 src/UI/Shared/Toolbar/ButtonCollection.js delete mode 100644 src/UI/Shared/Toolbar/ButtonModel.js delete mode 100644 src/UI/Shared/Toolbar/ButtonTemplate.hbs delete mode 100644 src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js delete mode 100644 src/UI/Shared/Toolbar/Radio/RadioButtonView.js delete mode 100644 src/UI/Shared/Toolbar/RadioButtonTemplate.hbs delete mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js delete mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs delete mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonView.js delete mode 100644 src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs delete mode 100644 src/UI/Shared/Toolbar/ToolbarLayout.js delete mode 100644 src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs delete mode 100644 src/UI/Shared/Tooltip.js delete mode 100644 src/UI/Shared/UiSettingsController.js delete mode 100644 src/UI/Shared/UiSettingsModel.js delete mode 100644 src/UI/Shared/VersionChangeMonitor.js delete mode 100644 src/UI/Shared/piwikCheck.js delete mode 100644 src/UI/Shims/backbone.backgrid.selectall.js delete mode 100644 src/UI/Shims/backbone.collectionview.js delete mode 100644 src/UI/Shims/backbone.deep.model.js delete mode 100644 src/UI/Shims/backbone.js delete mode 100644 src/UI/Shims/backbone.marionette.js delete mode 100644 src/UI/Shims/backbone.validation.js delete mode 100644 src/UI/Shims/backgrid.js delete mode 100644 src/UI/Shims/backgrid.paginator.js delete mode 100644 src/UI/Shims/handlebars.js delete mode 100644 src/UI/Shims/jquery.js delete mode 100644 src/UI/Shims/jquery.signalR.js delete mode 100644 src/UI/Shims/messenger.js delete mode 100644 src/UI/Shims/underscore.js delete mode 100644 src/UI/System/Backup/BackupCollection.js delete mode 100644 src/UI/System/Backup/BackupEmptyView.js delete mode 100644 src/UI/System/Backup/BackupEmptyViewTemplate.hbs delete mode 100644 src/UI/System/Backup/BackupFilenameCell.js delete mode 100644 src/UI/System/Backup/BackupFilenameCellTemplate.hbs delete mode 100644 src/UI/System/Backup/BackupLayout.js delete mode 100644 src/UI/System/Backup/BackupLayoutTemplate.hbs delete mode 100644 src/UI/System/Backup/BackupModel.js delete mode 100644 src/UI/System/Backup/BackupTypeCell.js delete mode 100644 src/UI/System/Info/About/AboutView.js delete mode 100644 src/UI/System/Info/About/AboutViewTemplate.hbs delete mode 100644 src/UI/System/Info/DiskSpace/DiskSpaceCollection.js delete mode 100644 src/UI/System/Info/DiskSpace/DiskSpaceLayout.js delete mode 100644 src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs delete mode 100644 src/UI/System/Info/DiskSpace/DiskSpaceModel.js delete mode 100644 src/UI/System/Info/DiskSpace/DiskSpacePathCell.js delete mode 100644 src/UI/System/Info/Health/HealthCell.js delete mode 100644 src/UI/System/Info/Health/HealthLayout.js delete mode 100644 src/UI/System/Info/Health/HealthLayoutTemplate.hbs delete mode 100644 src/UI/System/Info/Health/HealthOkView.js delete mode 100644 src/UI/System/Info/Health/HealthOkViewTemplate.hbs delete mode 100644 src/UI/System/Info/Health/HealthWikiCell.js delete mode 100644 src/UI/System/Info/MoreInfo/MoreInfoView.js delete mode 100644 src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs delete mode 100644 src/UI/System/Info/SystemInfoLayout.js delete mode 100644 src/UI/System/Info/SystemInfoLayoutTemplate.hbs delete mode 100644 src/UI/System/Info/info.less delete mode 100644 src/UI/System/Logs/Files/ContentsModel.js delete mode 100644 src/UI/System/Logs/Files/ContentsView.js delete mode 100644 src/UI/System/Logs/Files/ContentsViewTemplate.hbs delete mode 100644 src/UI/System/Logs/Files/DownloadLogCell.js delete mode 100644 src/UI/System/Logs/Files/FilenameCell.js delete mode 100644 src/UI/System/Logs/Files/LogFileCollection.js delete mode 100644 src/UI/System/Logs/Files/LogFileLayout.js delete mode 100644 src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs delete mode 100644 src/UI/System/Logs/Files/LogFileModel.js delete mode 100644 src/UI/System/Logs/Files/Row.js delete mode 100644 src/UI/System/Logs/LogsCollection.js delete mode 100644 src/UI/System/Logs/LogsLayout.js delete mode 100644 src/UI/System/Logs/LogsLayoutTemplate.hbs delete mode 100644 src/UI/System/Logs/LogsModel.js delete mode 100644 src/UI/System/Logs/Table/Details/LogDetailsView.js delete mode 100644 src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs delete mode 100644 src/UI/System/Logs/Table/LogLevelCell.js delete mode 100644 src/UI/System/Logs/Table/LogRow.js delete mode 100644 src/UI/System/Logs/Table/LogTimeCell.js delete mode 100644 src/UI/System/Logs/Table/LogsTableLayout.js delete mode 100644 src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs delete mode 100644 src/UI/System/Logs/Updates/LogFileCollection.js delete mode 100644 src/UI/System/Logs/Updates/LogFileModel.js delete mode 100644 src/UI/System/Logs/logs.less delete mode 100644 src/UI/System/StatusModel.js delete mode 100644 src/UI/System/SystemLayout.js delete mode 100644 src/UI/System/SystemLayoutTemplate.hbs delete mode 100644 src/UI/System/Task/ExecuteTaskCell.js delete mode 100644 src/UI/System/Task/NextExecutionCell.js delete mode 100644 src/UI/System/Task/TaskCollection.js delete mode 100644 src/UI/System/Task/TaskIntervalCell.js delete mode 100644 src/UI/System/Task/TaskLayout.js delete mode 100644 src/UI/System/Task/TaskLayoutTemplate.hbs delete mode 100644 src/UI/System/Task/TaskModel.js delete mode 100644 src/UI/System/Update/EmptyView.js delete mode 100644 src/UI/System/Update/EmptyViewTemplate.hbs delete mode 100644 src/UI/System/Update/UpdateCollection.js delete mode 100644 src/UI/System/Update/UpdateCollectionView.js delete mode 100644 src/UI/System/Update/UpdateItemView.js delete mode 100644 src/UI/System/Update/UpdateItemViewTemplate.hbs delete mode 100644 src/UI/System/Update/UpdateLayout.js delete mode 100644 src/UI/System/Update/UpdateLayoutTemplate.hbs delete mode 100644 src/UI/System/Update/UpdateModel.js delete mode 100644 src/UI/System/Update/update.less delete mode 100644 src/UI/Tags/TagCollection.js delete mode 100644 src/UI/Tags/TagHelpers.js delete mode 100644 src/UI/Tags/TagModel.js delete mode 100644 src/UI/Wanted/ControlsColumnTemplate.hbs delete mode 100644 src/UI/Wanted/Cutoff/CutoffUnmetCollection.js delete mode 100644 src/UI/Wanted/Cutoff/CutoffUnmetLayout.js delete mode 100644 src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs delete mode 100644 src/UI/Wanted/Missing/MissingCollection.js delete mode 100644 src/UI/Wanted/Missing/MissingLayout.js delete mode 100644 src/UI/Wanted/Missing/MissingLayoutTemplate.hbs delete mode 100644 src/UI/Wanted/WantedLayout.js delete mode 100644 src/UI/Wanted/WantedLayoutTemplate.hbs delete mode 100644 src/UI/app.js delete mode 100644 src/UI/index.html delete mode 100644 src/UI/jQuery/RouteBinder.js delete mode 100644 src/UI/jQuery/ToTheTop.js delete mode 100644 src/UI/jQuery/jquery.ajax.js delete mode 100644 src/UI/jQuery/jquery.spin.js delete mode 100644 src/UI/jQuery/jquery.validation.js delete mode 100644 src/UI/login.html delete mode 100644 src/UI/main.js delete mode 100644 src/UI/polyfills.js delete mode 100644 src/UI/reqres.js delete mode 100644 src/UI/vendor.js delete mode 100644 src/UI/vent.js diff --git a/.gitignore b/.gitignore index a340f3295..e52e5375c 100644 --- a/.gitignore +++ b/.gitignore @@ -120,7 +120,7 @@ _tests/ setup/Output/ *.~is -UI.Phantom/ +UI/ #VS outout folders bin @@ -135,5 +135,3 @@ _start _temp_*/**/* src/.idea/ -/npm_start.bat -/npm_start.bat diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index e402c1d9d..000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Lidarr \ No newline at end of file diff --git a/.idea/Sonarr.iml b/.idea/Sonarr.iml deleted file mode 100644 index aeec84bf6..000000000 --- a/.idea/Sonarr.iml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index 7598f4c8e..000000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba45..000000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml deleted file mode 100644 index b8387eb1b..000000000 --- a/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Sonarr_node_modules.xml b/.idea/libraries/Sonarr_node_modules.xml deleted file mode 100644 index 4eeebc5cc..000000000 --- a/.idea/libraries/Sonarr_node_modules.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 19f74da8e..000000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 7cc2cf51b..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4..000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..ad5884817 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +save-prefix="" diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..fdd705c63 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +save-prefix "" diff --git a/build.ps1 b/build.ps1 deleted file mode 100644 index 45b8ce783..000000000 --- a/build.ps1 +++ /dev/null @@ -1 +0,0 @@ -Write-Warning "DEPRECATED -- Please use build.sh instead." \ No newline at end of file diff --git a/build.sh b/build.sh index 1775ee34d..a61280bde 100755 --- a/build.sh +++ b/build.sh @@ -39,9 +39,6 @@ CleanFolder() find $path -name "FluentValidation.resources.dll" -exec rm "{}" \; find $path -name "App.config" -exec rm "{}" \; - echo "Removing .less files" - find $path -name "*.less" -exec rm "{}" \; - echo "Removing vshost files" find $path -name "*.vshost.exe" -exec rm "{}" \; @@ -102,11 +99,11 @@ Build() RunGulp() { echo "##teamcity[progressStart 'npm install']" - npm-cache install npm || CheckExitCode npm install + npm-cache install npm || CheckExitCode npm install --no-optional --no-bin-links echo "##teamcity[progressFinish 'npm install']" echo "##teamcity[progressStart 'Running gulp']" - CheckExitCode npm run build + CheckExitCode npm run build -- --production echo "##teamcity[progressFinish 'Running gulp']" } diff --git a/frontend/.csscomb.json b/frontend/.csscomb.json new file mode 100644 index 000000000..a82e49732 --- /dev/null +++ b/frontend/.csscomb.json @@ -0,0 +1,25 @@ +{ + "remove-empty-rulesets": true, + "always-semicolon": true, + "color-case": "lower", + "block-indent": " ", + "color-shorthand": false, + "element-case": "lower", + "eof-newline": true, + "leading-zero": true, + "quotes": "double", + "sort-order-fallback": "abc", + "space-before-colon": "", + "space-after-colon": " ", + "space-before-combinator": " ", + "space-after-combinator": " ", + "space-between-declarations": "\n", + "space-before-opening-brace": " ", + "space-after-opening-brace": "\n", + "space-after-selector-delimiter": " ", + "space-before-selector-delimiter": "", + "space-before-closing-brace": "\n", + "strip-spaces": true, + "tab-size": true, + "unitless-zero": false +} diff --git a/frontend/.esformatter b/frontend/.esformatter new file mode 100644 index 000000000..600bb0751 --- /dev/null +++ b/frontend/.esformatter @@ -0,0 +1,335 @@ +{ + "indent": { + "value": " ", + "FunctionExpression": 1, + "ArrayExpression": 1, + "ObjectExpression": 1 + }, + "lineBreak": { + "value": "\n", + + "before": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": ">=1", + "ArrowFunctionExpressionOpeningBrace": 0, + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": ">=1", + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": 0, + "CatchClosingBrace": ">=1", + "CatchKeyword": 0, + "CatchOpeningBrace": 0, + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": 0, + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": ">=1", + "DoWhileStatementOpeningBrace": 0, + "ElseIfStatement": 0, + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": 0, + "ElseStatement": 0, + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": 0, + "EmptyStatement": -1, + "EndOfFile": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": 0, + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 0, + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": "<2", + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 0, + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace":0, + "IIFEClosingParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": 0, + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": -1, + "MethodDefinition": ">=1", + "ObjectExpressionClosingBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": "<=2", + "PropertyValue": 0, + "ReturnStatement": -1, + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": 0, + "ThisExpression": -1, + "ThrowStatement": ">=1", + "TryClosingBrace": ">=1", + "TryKeyword": -1, + "TryOpeningBrace": 0, + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": 0, + "VariableDeclarationWithoutInit": ">=1", + "VariableName": ">=1", + "VariableValue": 0, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": 0 + }, + + "after": { + "ArrayPatternClosing": 0, + "ArrayPatternComma": 0, + "ArrayPatternOpening": 0, + "ArrowFunctionExpressionArrow": 0, + "ArrowFunctionExpressionClosingBrace": -1, + "ArrowFunctionExpressionOpeningBrace": ">=1", + "AssignmentExpression": ">=1", + "AssignmentOperator": 0, + "BlockStatement": 0, + "BreakKeyword": -1, + "CallExpression": -1, + "CallExpressionClosingParentheses": -1, + "CallExpressionOpeningParentheses": -1, + "CatchClosingBrace": ">=0", + "CatchKeyword": 0, + "CatchOpeningBrace": ">=1", + "ClassDeclaration": ">=1", + "ClassDeclarationClosingBrace": ">=1", + "ClassDeclarationOpeningBrace": ">=1", + "ConditionalExpression": ">=1", + "DeleteOperator": ">=1", + "DoWhileStatement": ">=1", + "DoWhileStatementClosingBrace": 0, + "DoWhileStatementOpeningBrace": ">=1", + "ElseIfStatement": ">=1", + "ElseIfStatementClosingBrace": ">=1", + "ElseIfStatementOpeningBrace": ">=1", + "ElseStatement": ">=1", + "ElseStatementClosingBrace": ">=1", + "ElseStatementOpeningBrace": ">=1", + "EmptyStatement": -1, + "FinallyClosingBrace": ">=1", + "FinallyKeyword": -1, + "FinallyOpeningBrace": ">=1", + "ForInStatement": ">=1", + "ForInStatementClosingBrace": ">=1", + "ForInStatementExpressionClosing": -1, + "ForInStatementExpressionOpening": "<2", + "ForInStatementOpeningBrace": ">=1", + "ForStatement": ">=1", + "ForStatementClosingBrace": ">=1", + "ForStatementExpressionClosing": -1, + "ForStatementExpressionOpening": "<2", + "ForStatementOpeningBrace": ">=1", + "FunctionDeclaration": ">=1", + "FunctionDeclarationClosingBrace": ">=1", + "FunctionDeclarationOpeningBrace": ">=1", + "FunctionExpression": 0, + "FunctionExpressionClosingBrace": -1, + "FunctionExpressionOpeningBrace": 1, + "IIFEOpeningParentheses": 0, + "IfStatement": ">=1", + "IfStatementClosingBrace": ">=1", + "IfStatementOpeningBrace": ">=1", + "LogicalExpression": -1, + "MemberExpressionClosing": 0, + "MemberExpressionOpening": 0, + "MemberExpressionPeriod": 0, + "MethodDefinition": ">=1", + "ObjectExpressionOpeningBrace": "<=1", + "ObjectPatternClosingBrace": 0, + "ObjectPatternComma": 0, + "ObjectPatternOpeningBrace": 0, + "ParameterDefault": 0, + "Property": -1, + "PropertyName": 0, + "ReturnStatement": -1, + "SwitchCaseColon": ">=1", + "SwitchClosingBrace": ">=1", + "SwitchOpeningBrace": ">=1", + "ThisExpression": 0, + "ThrowStatement": ">=1", + "TryClosingBrace": 0, + "TryKeyword": -1, + "TryOpeningBrace": ">=1", + "VariableDeclaration": ">=1", + "VariableDeclarationSemiColon": ">=1", + "VariableValue": -1, + "WhileStatement": ">=1", + "WhileStatementClosingBrace": ">=1", + "WhileStatementOpeningBrace": ">=1" + } + }, + "whiteSpace": { + "value": " ", + "removeTrailing": 1, + "before": { + "ArgumentComma": 0, + "ArgumentList": 0, + "ArgumentListArrayExpression": 0, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 0, + "ArrayExpressionOpening": 1, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 1, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 0, + "ConditionalExpressionAlternate": 1, + "ConditionalExpressionConsequent": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementConditional": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionClosingParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 0, + "ForInStatementExpressionOpening": 1, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 0, + "ForStatementExpressionOpening": 1, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 0, + "FunctionDeclarationClosingBrace": 1, + "FunctionDeclarationOpeningBrace": 1, + "FunctionExpressionClosingBrace": 1, + "FunctionExpressionOpeningBrace": 1, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 1, + "IfStatementOpeningBrace": 1, + "LineComment": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionClosing": 0, + "ObjectExpressionClosingBrace": 1, + "ParameterComma": 0, + "ParameterList": 0, + "Property": 1, + "PropertyName": 1, + "PropertyValue": 1, + "SwitchDiscriminantClosing": 0, + "SwitchDiscriminantOpening": 1, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "VariableValue": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 0, + "WhileStatementConditionalOpening": 1, + "WhileStatementOpeningBrace": 1 + }, + "after": { + "ArgumentComma": 1, + "ArgumentList": 0, + "ArgumentListArrayExpression": 1, + "ArgumentListFunctionExpression": 1, + "ArgumentListObjectExpression": 0, + "ArrayExpressionClosing": 0, + "ArrayExpressionComma": 1, + "ArrayExpressionOpening": 0, + "AssignmentOperator": 1, + "BinaryExpression": 0, + "BinaryExpressionOperator": 1, + "BlockComment": 1, + "CallExpression": 0, + "CatchClosingBrace": 1, + "CatchKeyword": 1, + "CatchOpeningBrace": 1, + "CatchParameterList": 0, + "CommaOperator": 1, + "ConditionalExpressionConsequent": 1, + "ConditionalExpressionTest": 1, + "DoWhileStatementBody": 1, + "DoWhileStatementClosingBrace": 1, + "DoWhileStatementOpeningBrace": 1, + "ElseIfStatementClosingBrace": 1, + "ElseIfStatementOpeningBrace": 1, + "ElseStatementClosingBrace": 1, + "ElseStatementOpeningBrace": 1, + "EmptyStatement": 0, + "ExpressionOpeningParentheses": 0, + "FinallyClosingBrace": 1, + "FinallyKeyword": -1, + "FinallyOpeningBrace": 1, + "ForInStatement": 1, + "ForInStatementClosingBrace": 1, + "ForInStatementExpressionClosing": 1, + "ForInStatementExpressionOpening": 0, + "ForInStatementOpeningBrace": 1, + "ForStatement": 1, + "ForStatementClosingBrace": 1, + "ForStatementExpressionClosing": 1, + "ForStatementExpressionOpening": 0, + "ForStatementOpeningBrace": 1, + "ForStatementSemicolon": 1, + "FunctionDeclarationClosingBrace": 0, + "FunctionDeclarationOpeningBrace": 0, + "FunctionExpressionClosingBrace": 0, + "FunctionExpressionOpeningBrace": 0, + "FunctionName": 0, + "FunctionReservedWord": 0, + "IfStatementClosingBrace": 1, + "IfStatementConditionalClosing": 0, + "IfStatementConditionalOpening": 0, + "IfStatementOpeningBrace": 1, + "LogicalExpressionOperator": 1, + "MemberExpressionOpening": 0, + "ObjectExpressionClosingBrace": 0, + "ObjectExpressionOpeningBrace": 1, + "ParameterComma": 1, + "ParameterList": 0, + "PropertyName": 0, + "PropertyValue": 0, + "SwitchDiscriminantClosing": 1, + "SwitchDiscriminantOpening": 0, + "ThrowKeyword": 1, + "TryClosingBrace": 1, + "TryKeyword": -1, + "TryOpeningBrace": 1, + "UnaryExpressionOperator": 0, + "VariableName": 1, + "WhileStatementClosingBrace": 1, + "WhileStatementConditionalClosing": 1, + "WhileStatementConditionalOpening": 0, + "WhileStatementOpeningBrace": 1 + } + } +} diff --git a/frontend/.eslintignore b/frontend/.eslintignore new file mode 100644 index 000000000..d4b43f836 --- /dev/null +++ b/frontend/.eslintignore @@ -0,0 +1 @@ +**/JsLibraries/** diff --git a/frontend/.eslintrc b/frontend/.eslintrc new file mode 100644 index 000000000..574918d05 --- /dev/null +++ b/frontend/.eslintrc @@ -0,0 +1,288 @@ +{ + "parser": "babel-eslint", + + "env": { + "browser": true, + "commonjs": true, + "node": true, + "es6": true + }, + + "globals": { + "expect": false, + "chai": false, + "sinon": false, + }, + + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module", + "ecmaFeatures": { + "modules": true, + "impliedStrict": true + }, + }, + + "plugins": [ + "filenames", + "react" + ], + + "rules": { + "filenames/match-exported": ["error"], + + # ECMAScript 6 + + arrow-body-style: [0], + arrow-parens: ["error", "always"], + arrow-spacing: ["error", { "before": true, "after": true }], + constructor-super: "error", + generator-star-spacing: "off", + no-class-assign: "error", + no-confusing-arrow: "error", + no-const-assign: "error", + no-dupe-class-members: "error", + no-duplicate-imports: "error", + no-new-symbol: "error", + no-this-before-super: "error", + no-useless-escape: "error", + no-useless-computed-key: "error", + no-useless-constructor: "error", + no-var: "warn", + object-shorthand: ["error", "properties"], + prefer-arrow-callback: "error", + prefer-const: "warn", + prefer-reflect: "off", + prefer-rest-params: "off", + prefer-spread: "warn", + prefer-template: "error", + require-yield: "off", + template-curly-spacing: ["error", "never"], + yield-star-spacing: "off", + + # Possible Errors + + "comma-dangle": "error", + "no-cond-assign": "error", + "no-console": "off", + "no-constant-condition": "warn", + "no-control-regex": "error", + "no-debugger": "off", + "no-dupe-args": "error", + "no-dupe-keys": "error", + "no-duplicate-case": "error", + "no-empty": "warn", + "no-empty-character-class": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-extra-parens": ["error", "functions"], + "no-extra-semi": "error", + "no-func-assign": "error", + "no-inner-declarations": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-negated-in-lhs": "error", + "no-obj-calls": "error", + "no-regex-spaces": "error", + "no-sparse-arrays": "error", + "no-unexpected-multiline": "error", + "no-unreachable": "warn", + "no-unsafe-finally": "error", + "use-isnan": "error", + "valid-jsdoc": "off", + "valid-typeof": "error", + + # Best Practices + + "accessor-pairs": "off", + "array-callback-return": "warn", + "block-scoped-var": "warn", + "consistent-return": "off", + "curly": "error", + "default-case": "error", + "dot-location": ["error", "property"], + "dot-notation": "error", + "eqeqeq": ["error", "smart"], + "guard-for-in": "error", + "no-alert": "warn", + "no-caller": "error", + "no-case-declarations": "error", + "no-div-regex": "error", + "no-else-return": "error", + "no-empty-function": ["error", {"allow": ["arrowFunctions"]}], + no-empty-pattern: "error", + "no-eval": "error", + "no-extend-native": "error", + "no-extra-bind": "error", + "no-fallthrough": "error", + "no-floating-decimal": "error", + "no-implicit-coercion": ["error", { + "boolean": false, + "number": true, + "string": true, + "allow": [/* "!!", "~", "*", "+" */] + }], + no-implicit-globals: "error", + no-implied-eval: "error", + no-invalid-this: "off", + no-iterator: "error", + no-labels: "error", + no-lone-blocks: "error", + no-loop-func: "error", + no-magic-numbers: ["off", {"ignoreArrayIndexes": true, "ignore": [0, 1] }], + no-multi-spaces: "error", + no-multi-str: "error", + "no-native-reassign": ["error", {"exceptions": ["console"]}], + no-new: "off", + no-new-func: "error", + no-new-wrappers: "error", + no-octal: "error", + no-octal-escape: "error", + no-param-reassign: "off", + no-process-env: "off", + no-proto: "error", + no-redeclare: "error", + no-return-assign: "warn", + no-script-url: "error", + no-self-assign: "error", + no-self-compare: "error", + no-sequences: "error", + no-throw-literal: "error", + no-unmodified-loop-condition: "error", + no-unused-expressions: "error", + no-unused-labels: "error", + no-useless-call: "error", + no-useless-concat: "error", + no-void: "error", + no-warning-comments: "off", + no-with: "error", + radix: ["error", "as-needed"], + vars-on-top: "off", + wrap-iife: ["error", "inside"], + yoda: "error", + + # Strict Mode + + strict: ["error", "never"], + + # Variables + + init-declarations: ["error", "always"], + no-catch-shadow: "error", + no-delete-var: "error", + no-label-var: "error", + no-restricted-globals: "off", + no-shadow: "error", + no-shadow-restricted-names: "error", + no-undef: "error", + no-undef-init: "off", + no-undefined: "off", + no-unused-vars: ["error", { "args": "none" }], + no-use-before-define: "error", + + # Node.js and CommonJS + + callback-return: "warn", + global-require: "error", + handle-callback-err: "warn", + no-mixed-requires: "error", + no-new-require: "error", + no-path-concat: "error", + no-process-exit: "error", + + # Stylistic Issues + + array-bracket-spacing: ["error", "never"], + block-spacing: ["error", "always"], + brace-style: ["error", "1tbs", { "allowSingleLine": false }], + camelcase: "off", + comma-spacing: ["error", {"before": false, "after": true}], + comma-style: ["error", "last"], + computed-property-spacing: ["error", "never"], + consistent-this: ["error", "self"], + eol-last: "error", + func-names: "off", + func-style: ["error", "declaration"], + indent: ["error", 2, {"SwitchCase": 1}], + key-spacing: ["error", {"beforeColon": false, "afterColon": true}], + keyword-spacing: ["error", {before: true, after: true}], + lines-around-comment: ["error", { "beforeBlockComment": true, "afterBlockComment": false }], + max-depth: ["error", {"maximum": 5}], + max-nested-callbacks: ["error", 4], + max-params: ["error", 4], + max-statements: "off", + max-statements-per-line: ["error", { "max": 1 }], + new-cap: ["error", {"capIsNewExceptions": ["$.Deferred"]}], + new-parens: "error", + newline-after-var: "off", + newline-before-return: "off", + newline-per-chained-call: "off", + no-array-constructor: "error", + no-bitwise: "error", + no-continue: "error", + no-inline-comments: "off", + no-lonely-if: "warn", + no-mixed-spaces-and-tabs: "error", + no-multiple-empty-lines: ["error", {max: 1}], + no-negated-condition: "warn", + no-nested-ternary: "error", + no-new-object: "error", + no-plusplus: "off", + no-restricted-syntax: "off", + no-spaced-func: "error", + no-ternary: "off", + no-trailing-spaces: "error", + no-underscore-dangle: ["error", { "allowAfterThis": true }], + no-unneeded-ternary: "error", + no-whitespace-before-property: "error", + object-curly-spacing: ["error", "always"], + one-var: ["error", "never"], + one-var-declaration-per-line: ["error", "always"], + operator-assignment: ["off", "never"], + operator-linebreak: ["error", "after"], + padded-blocks: ["error", "never"], + quote-props: ["error", "consistent"], + quotes: ["error", "single"], + require-jsdoc: "off", + semi: "error", + semi-spacing: ["error", { "before": false, "after": true }], + sort-vars: "off", + space-before-blocks: ["error", "always"], + space-before-function-paren: ["error", "never"], + space-in-parens: "off", + space-infix-ops: "off", + space-unary-ops: "off", + spaced-comment: "error", + wrap-regex: "error", + + # React + + "react/jsx-boolean-value": [2, "always"], + "react/jsx-uses-vars": 2, + "react/jsx-closing-bracket-location": 2, + "react/jsx-space-before-closing": 2, + "react/jsx-curly-spacing": [2, "never"], + "react/jsx-equals-spacing": [2, "never"], + "react/jsx-indent-props": [2, 2], + "react/jsx-indent": [2, 2], + "react/jsx-key": 2, + "react/jsx-no-bind": [2, { allowArrowFunctions: true }], + "react/jsx-no-duplicate-props": [2, { "ignoreCase": true }], + "react/jsx-max-props-per-line": [2, { "maximum": 2 }], + "react/jsx-handler-names": [2, { "eventHandlerPrefix": "on", "eventHandlerPropPrefix": "on" }], + "react/jsx-no-undef": 2, + "react/jsx-pascal-case": 2, + "react/jsx-uses-react": 2, + "react/no-did-mount-set-state": 2, + "react/no-did-update-set-state": 2, + "react/no-direct-mutation-state": 2, + "react/no-multi-comp": [2, { "ignoreStateless": true }], + "react/no-unknown-property": 2, + "react/prefer-es6-class": 2, + "react/prop-types": 2, + "react/react-in-jsx-scope": 2, + "react/self-closing-comp": 2, + "react/sort-comp": 2, + "react/wrap-multilines": 2 + } +} diff --git a/frontend/.jsbeautifyrc b/frontend/.jsbeautifyrc new file mode 100644 index 000000000..50aa6aa29 --- /dev/null +++ b/frontend/.jsbeautifyrc @@ -0,0 +1,12 @@ +{ + "js": { + "indent_size": 2, + "indent_char": " ", + "indent_level": 2, + "indent_with_tabs": false, + "preserve_newlines": true, + "brace_style": "collapse", + "max_preserve_newlines": 2, + "jslint_happy": true + } +} \ No newline at end of file diff --git a/frontend/.stylelintrc b/frontend/.stylelintrc new file mode 100644 index 000000000..5c7c74f43 --- /dev/null +++ b/frontend/.stylelintrc @@ -0,0 +1,392 @@ +{ +"plugins": [ + "stylelint-order" +], +"rules": { + "at-rule-empty-line-before": [ + "always", + { + "except": [ + "inside-block" + ] + } + ], + "at-rule-name-case": "lower", + "at-rule-name-newline-after": "always-multi-line", + "at-rule-name-space-after": "always", + "at-rule-no-unknown": [ + true, + { + "ignoreAtRules": [ + "/^add\\-mixin$/", + "/^define\\-mixin$/" + ] + } + ], + "at-rule-no-vendor-prefix": true, + "at-rule-semicolon-newline-after": "always", + "at-rule-semicolon-space-before": "never", + "block-closing-brace-empty-line-before": "never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always", + "block-closing-brace-space-after": "always-single-line", + "block-closing-brace-space-before": "always-single-line", + "block-no-empty": true, + "block-opening-brace-newline-after": "always", + "block-opening-brace-newline-before": "never-single-line", + "block-opening-brace-space-after": "always-single-line", + "block-opening-brace-space-before": "always", + "color-hex-case": "lower", + "color-hex-length": "short", + "color-named": "never", + "color-no-invalid-hex": true, + "comment-whitespace-inside": "always", + "declaration-bang-space-after": "never", + "declaration-bang-space-before": "always", + "declaration-block-no-duplicate-properties": [ + true, + { + "ignoreProperties": [ + "composes" + ] + } + ], + "declaration-block-no-redundant-longhand-properties": true, + "declaration-block-no-shorthand-property-overrides": true, + "declaration-block-semicolon-newline-after": "always", + "declaration-block-semicolon-newline-before": "never-multi-line", + "declaration-block-semicolon-space-before": "never", + "declaration-block-single-line-max-declarations": 1, + "declaration-block-trailing-semicolon": "always", + "declaration-colon-space-after": "always", + "declaration-colon-space-before": "never", + "font-family-name-quotes": "always-unless-keyword", + "function-calc-no-unspaced-operator": true, + "function-comma-newline-after": "never-multi-line", + "function-comma-newline-before": "never-multi-line", + "function-comma-space-after": "always", + "function-comma-space-before": "never", + "function-linear-gradient-no-nonstandard-direction": true, + "function-name-case": "lower", + "function-parentheses-newline-inside": "never-multi-line", + "function-parentheses-space-inside": "never", + "function-url-quotes": "always", + "function-url-scheme-blacklist": [ + "data" + ], + "function-whitespace-after": "always", + "indentation": 2, + "keyframe-declaration-no-important": true, + "length-zero-no-unit": true, + "max-empty-lines": 1, + "max-line-length": [ + 100, + { + "ignore": [ + "non-comments" + ] + } + ], + "max-nesting-depth": 2, + "media-feature-colon-space-after": "always", + "media-feature-colon-space-before": "never", + "media-feature-name-case": "lower", + "media-feature-name-no-vendor-prefix": true, + "media-feature-range-operator-space-after": "always", + "media-feature-range-operator-space-before": "always", + "no-empty-source": true, + "no-eol-whitespace": true, + "no-extra-semicolons": true, + "no-invalid-double-slash-comments": true, + "no-missing-end-of-source-newline": true, + "number-leading-zero": "always", + "number-no-trailing-zeros": true, + "order/order": [ + "custom-properties", + "dollar-variables", + { + "hasBlock": false, + "name": "add-mixin", + "type": "at-rule" + }, + "declarations", + "rules", + "at-rules" + ], + "order/properties-order": [ + { + "emptyLineBefore": "always", + "properties": [ + "composes" + ] + }, + { + "emptyLineBefore": "always", + "properties": [ + "position", + "top", + "right", + "bottom", + "left", + "z-index", + "display", + "visibility", + "align-content", + "align-items", + "align-self", + "justify-content", + "flex", + "flex-direction", + "flex-order", + "flex-pack", + "flex-align", + "flex-grow", + "flex-shrink", + "flex-basis", + "flex-wrap", + "flex-flow", + "float", + "clear", + "overflow", + "overflow-x", + "overflow-y", + "-webkit-overflow-scrolling", + "clip", + "box-sizing", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "min-width", + "min-height", + "max-width", + "max-height", + "width", + "height", + "outline", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "border", + "border-spacing", + "border-collapse", + "border-width", + "border-style", + "border-color", + "border-top", + "border-top-width", + "border-top-style", + "border-top-color", + "border-right", + "border-right-width", + "border-right-style", + "border-right-color", + "border-bottom", + "border-bottom-width", + "border-bottom-style", + "border-bottom-color", + "border-left", + "border-left-width", + "border-left-style", + "border-left-color", + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "border-image", + "border-image-source", + "border-image-slice", + "border-image-width", + "border-image-outset", + "border-image-repeat", + "border-top-image", + "border-right-image", + "border-bottom-image", + "border-left-image", + "border-corner-image", + "border-top-left-image", + "border-top-right-image", + "border-bottom-right-image", + "border-bottom-left-image", + "background", + "background-color", + "background-image", + "background-attachment", + "background-position", + "background-position-x", + "background-position-y", + "background-clip", + "background-origin", + "background-size", + "background-repeat", + "box-decoration-break", + "box-shadow", + "color", + "table-layout", + "caption-side", + "empty-cells", + "list-style", + "list-style-position", + "list-style-type", + "list-style-image", + "quotes", + "content", + "counter-increment", + "counter-reset", + "-ms-writing-mode", + "vertical-align", + "text-align", + "text-align-last", + "text-decoration", + "text-emphasis", + "text-emphasis-position", + "text-emphasis-style", + "text-emphasis-color", + "text-indent", + "text-justify", + "text-outline", + "text-transform", + "text-wrap", + "text-overflow", + "text-overflow-ellipsis", + "text-overflow-mode", + "text-shadow", + "white-space", + "word-spacing", + "word-wrap", + "word-break", + "tab-size", + "hyphens", + "letter-spacing", + "font", + "font-weight", + "font-style", + "font-variant", + "font-size-adjust", + "font-stretch", + "font-size", + "font-family", + "font-smoothing", + "-moz-osx-font-smoothing", + "-webkit-font-smoothing", + "src", + "line-height", + "opacity", + "filter", + "resize", + "cursor", + "appearance", + "nav-index", + "nav-up", + "nav-right", + "nav-down", + "nav-left", + "transition", + "transition-delay", + "transition-timing-function", + "transition-duration", + "transition-property", + "transform", + "transform-origin", + "transform-style", + "backface-visibility", + "animation", + "animation-name", + "animation-duration", + "animation-play-state", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "animation-fill-mode", + "pointer-events", + "user-select", + "touch-action", + "-webkit-tap-highlight-color", + "unicode-bidi", + "direction", + "columns", + "column-span", + "column-width", + "column-count", + "column-fill", + "column-gap", + "column-rule", + "column-rule-width", + "column-rule-style", + "column-rule-color", + "break-before", + "break-inside", + "break-after", + "page-break-before", + "page-break-inside", + "page-break-after", + "orphans", + "widows", + "zoom", + "max-zoom", + "min-zoom", + "user-zoom", + "orientation" + ] + } + ], + "property-case": "lower", + "property-no-vendor-prefix": true, + "rule-empty-line-before": [ + "always", + { + "except": [ + "first-nested" + ], + "ignore": [ + "after-comment" + ] + } + ], + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", + "selector-attribute-quotes": "never", + "selector-class-pattern": "^[A-Za-z0-9]+$", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-descendant-combinator-no-non-space": true, + "selector-list-comma-newline-after": "always", + "selector-list-comma-newline-before": "never-multi-line", + "selector-list-comma-space-before": "never", + "selector-max-attribute": 0, + "selector-max-class": 3, + "selector-max-compound-selectors": 3, + "selector-max-empty-lines": 0, + "selector-max-id": 0, + "selector-max-universal": 0, + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-parentheses-space-inside": "never", + "selector-pseudo-element-case": "lower", + "selector-pseudo-element-colon-notation": "double", + "selector-pseudo-element-no-unknown": true, + "selector-type-case": "lower", + "selector-type-no-unknown": true, + "shorthand-property-no-redundant-values": true, + "string-no-newline": true, + "string-quotes": "single", + "time-min-milliseconds": 100, + "unit-case": "lower", + "unit-no-unknown": true, + "value-list-comma-newline-after": "never-multi-line", + "value-list-comma-newline-before": "never-multi-line", + "value-list-comma-space-after": "always", + "value-list-comma-space-before": "never", + "value-list-max-empty-lines": 0, + "value-no-vendor-prefix": true + } +} diff --git a/frontend/.tern-project b/frontend/.tern-project new file mode 100644 index 000000000..aa9d76407 --- /dev/null +++ b/frontend/.tern-project @@ -0,0 +1,7 @@ +{ + "ecmaVersion": 6, + "libs": [ + "browser", + "jquery" + ] +} diff --git a/frontend/gulp/build.js b/frontend/gulp/build.js new file mode 100644 index 000000000..cfeb5d138 --- /dev/null +++ b/frontend/gulp/build.js @@ -0,0 +1,15 @@ +const gulp = require('gulp'); +const runSequence = require('run-sequence'); + +require('./clean'); +require('./copy'); + +gulp.task('build', () => { + return runSequence('clean', [ + 'webpack', + 'copyHtml', + 'copyFonts', + 'copyImages', + 'copyJs' + ]); +}); diff --git a/frontend/gulp/clean.js b/frontend/gulp/clean.js new file mode 100644 index 000000000..ac2e4026f --- /dev/null +++ b/frontend/gulp/clean.js @@ -0,0 +1,8 @@ +const gulp = require('gulp'); +const del = require('del'); + +const paths = require('./helpers/paths'); + +gulp.task('clean', () => { + return del([paths.dest.root]); +}); diff --git a/frontend/gulp/copy.js b/frontend/gulp/copy.js new file mode 100644 index 000000000..d1d47c97e --- /dev/null +++ b/frontend/gulp/copy.js @@ -0,0 +1,45 @@ +var path = require('path'); +var gulp = require('gulp'); +var print = require('gulp-print'); +var cache = require('gulp-cached'); +var livereload = require('gulp-livereload'); +var paths = require('./helpers/paths.js'); + +gulp.task('copyJs', () => { + return gulp.src( + [ + path.join(paths.src.root, 'polyfills.js') + ]) + .pipe(cache('copyJs')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.root)) + .pipe(livereload()); +}); + +gulp.task('copyHtml', () => { + return gulp.src(paths.src.html) + .pipe(cache('copyHtml')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.root)) + .pipe(livereload()); +}); + +gulp.task('copyFonts', () => { + return gulp.src( + path.join(paths.src.fonts, '**', '*.*') + ) + .pipe(cache('copyFonts')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.fonts)) + .pipe(livereload()); +}); + +gulp.task('copyImages', () => { + return gulp.src( + path.join(paths.src.images, '**', '*.*') + ) + .pipe(cache('copyImages')) + .pipe(print()) + .pipe(gulp.dest(paths.dest.images)) + .pipe(livereload()); +}); diff --git a/frontend/gulp/gulpFile.js b/frontend/gulp/gulpFile.js new file mode 100644 index 000000000..744dd8d7e --- /dev/null +++ b/frontend/gulp/gulpFile.js @@ -0,0 +1,8 @@ +require('./build.js'); +require('./clean.js'); +require('./copy.js'); +require('./imageMin.js'); +require('./start.js'); +require('./stripBom.js'); +require('./watch.js'); +require('./webpack.js'); diff --git a/frontend/gulp/helpers/errorHandler.js b/frontend/gulp/helpers/errorHandler.js new file mode 100644 index 000000000..f3e1c113b --- /dev/null +++ b/frontend/gulp/helpers/errorHandler.js @@ -0,0 +1,6 @@ +const gulpUtil = require('gulp-util'); + +module.exports = function errorHandler(error) { + gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`)); + this.emit('end'); +}; diff --git a/frontend/gulp/helpers/html-annotate-loader.js b/frontend/gulp/helpers/html-annotate-loader.js new file mode 100644 index 000000000..6c7ce10b8 --- /dev/null +++ b/frontend/gulp/helpers/html-annotate-loader.js @@ -0,0 +1,15 @@ +const path = require('path'); +const rootPath = path.resolve(__dirname + '/../../src/'); +module.exports = function(source) { + if (this.cacheable) { + this.cacheable(); + } + + const resourcePath = this.resourcePath.replace(rootPath, ''); + const wrappedSource =` + + ${source} + `; + + return wrappedSource; +}; diff --git a/frontend/gulp/helpers/paths.js b/frontend/gulp/helpers/paths.js new file mode 100644 index 000000000..b96b5aaeb --- /dev/null +++ b/frontend/gulp/helpers/paths.js @@ -0,0 +1,23 @@ +const root = './frontend/src/'; + +const paths = { + src: { + root, + html: root + '*.html', + scripts: root + '**/*.js', + content: root + 'Content/', + fonts: root + 'Content/Fonts/', + images: root + 'Content/Images/', + exclude: { + libs: `!${root}JsLibraries/**` + } + }, + dest: { + root: './_output/UI/', + content: './_output/UI/Content/', + fonts: './_output/UI/Content/Fonts/', + images: './_output/UI/Content/Images/' + } +}; + +module.exports = paths; diff --git a/frontend/gulp/imageMin.js b/frontend/gulp/imageMin.js new file mode 100644 index 000000000..828143f28 --- /dev/null +++ b/frontend/gulp/imageMin.js @@ -0,0 +1,15 @@ +var gulp = require('gulp'); +var print = require('gulp-print'); +var paths = require('./helpers/paths.js'); + +gulp.task('imageMin', () => { + var imagemin = require('gulp-imagemin'); + return gulp.src(paths.src.images) + .pipe(imagemin({ + progressive: false, + optimizationLevel: 4, + svgoPlugins: [{ removeViewBox: false }] + })) + .pipe(print()) + .pipe(gulp.dest(paths.src.content + 'Images/')); +}); diff --git a/frontend/gulp/start.js b/frontend/gulp/start.js new file mode 100644 index 000000000..eda9f0dba --- /dev/null +++ b/frontend/gulp/start.js @@ -0,0 +1,104 @@ +// will download and run sonarr (server) in a non-windows enviroment +// you can use this if you don't care about the server code and just want to work +// with the web code. + +var http = require('http'); +var gulp = require('gulp'); +var fs = require('fs'); +var targz = require('tar.gz'); +var del = require('del'); +var spawn = require('child_process').spawn; + +function download(url, dest, cb) { + console.log('Downloading ' + url + ' to ' + dest); + var file = fs.createWriteStream(dest); + http.get(url, function(response) { + response.pipe(file); + file.on('finish', function() { + console.log('Download completed'); + file.close(cb); + }); + }); +} + +function getLatest(cb) { + var branch = 'develop'; + process.argv.forEach(function(val) { + var branchMatch = /branch=([\S]*)/.exec(val); + if (branchMatch && branchMatch.length > 1) { + branch = branchMatch[1]; + } + }); + + var url = 'http://services.sonarr.tv/v1/update/' + branch + '?os=osx'; + + console.log('Checking for latest version:', url); + + http.get(url, function(res) { + var data = ''; + + res.on('data', function(chunk) { + data += chunk; + }); + + res.on('end', function() { + var updatePackage = JSON.parse(data).updatePackage; + console.log('Latest version available: ' + updatePackage.version + ' Release Date: ' + updatePackage.releaseDate); + cb(updatePackage); + }); + }).on('error', function(e) { + console.log('problem with request: ' + e.message); + }); +} + +function extract(source, dest, cb) { + console.log('extracting download page to ' + dest); + new targz().extract(source, dest, function(err) { + if (err) { + console.log(err); + } + console.log('Update package extracted.'); + cb(); + }); +} + +gulp.task('getSonarr', function() { + try { + fs.mkdirSync('./_start/'); + } catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } + } + + getLatest(function(updatePackage) { + var packagePath = './_start/' + updatePackage.filename; + var dirName = './_start/' + updatePackage.version; + download(updatePackage.url, packagePath, function() { + extract(packagePath, dirName, function() { + // clean old binaries + console.log('Cleaning old binaries'); + del.sync(['./_output/*', '!./_output/UI/']); + console.log('copying binaries to target'); + gulp.src(dirName + '/NzbDrone/*.*') + .pipe(gulp.dest('./_output/')); + }); + }); + }); +}); + +gulp.task('startSonarr', function() { + var ls = spawn('mono', ['--debug', './_output/NzbDrone.exe']); + + ls.stdout.on('data', function(data) { + process.stdout.write(data); + }); + + ls.stderr.on('data', function(data) { + process.stdout.write(data); + }); + + ls.on('close', function(code) { + console.log('child process exited with code ' + code); + }); +}); diff --git a/frontend/gulp/stripBom.js b/frontend/gulp/stripBom.js new file mode 100644 index 000000000..080b86dfe --- /dev/null +++ b/frontend/gulp/stripBom.js @@ -0,0 +1,13 @@ +const gulp = require('gulp'); +const paths = require('./helpers/paths.js'); +const stripbom = require('gulp-stripbom'); + +function stripBom(dest) { + gulp.src([paths.src.scripts, paths.src.exclude.libs]) + .pipe(stripbom({ showLog: false })) + .pipe(gulp.dest(dest)); +} + +gulp.task('stripBom', () => { + stripBom(paths.src.root); +}); diff --git a/frontend/gulp/watch.js b/frontend/gulp/watch.js new file mode 100644 index 000000000..dae893c38 --- /dev/null +++ b/frontend/gulp/watch.js @@ -0,0 +1,27 @@ +var gulp = require('gulp'); +var livereload = require('gulp-livereload'); +var watch = require('gulp-watch'); +var paths = require('./helpers/paths.js'); + +require('./copy.js'); +require('./webpack.js'); + +function watchTask(glob, task) { + var options = { + name: `watch: ${task}`, + verbose: true + }; + return watch(glob, options, () => { + gulp.start(task); + }); +} + +gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => { + livereload.listen(); + + gulp.start('webpackWatch'); + + watchTask(paths.src.html, 'copyHtml'); + watchTask(paths.src.fonts + '**/*.*', 'copyFonts'); + watchTask(paths.src.images + '**/*.*', 'copyImages'); +}); diff --git a/frontend/gulp/webpack.js b/frontend/gulp/webpack.js new file mode 100644 index 000000000..f0c017aea --- /dev/null +++ b/frontend/gulp/webpack.js @@ -0,0 +1,159 @@ +const _ = require('lodash'); +const gulp = require('gulp'); +const simpleVars = require('postcss-simple-vars'); +const nested = require('postcss-nested'); +const autoprefixer = require('autoprefixer'); +const webpackStream = require('webpack-stream'); +const livereload = require('gulp-livereload'); +const path = require('path'); +const webpack = require('webpack'); +const errorHandler = require('./helpers/errorHandler'); +const reload = require('require-nocache')(module); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); + +const uiFolder = 'UI'; +const root = path.join(__dirname, '..', 'src'); +const isProduction = process.argv.indexOf('--production') > -1; + +console.log('ROOT:', root); +console.log('isProduction:', isProduction); + +const cssVariables = [ + '../src/Styles/Variables/colors', + '../src/Styles/Variables/dimensions', + '../src/Styles/Variables/fonts', + '../src/Styles/Variables/animations' +].map(require.resolve); + +const config = { + devtool: '#source-map', + stats: { + children: false + }, + watchOptions: { + ignored: /node_modules/ + }, + entry: { + preload: 'preload.js', + vendor: 'vendor.js', + index: 'index.js' + }, + resolve: { + root: [ + root, + path.join(root, 'Shims'), + path.join(root, 'JsLibraries') + ] + }, + output: { + filename: path.join('_output', uiFolder, '[name].js'), + sourceMapFilename: '[file].map' + }, + plugins: [ + new ExtractTextPlugin(path.join('_output', uiFolder, 'Content', 'styles.css'), { allChunks: true }), + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor' + }), + new webpack.DefinePlugin({ + __DEV__: !isProduction, + 'process.env': { + NODE_ENV: isProduction ? JSON.stringify('production') : JSON.stringify('development') + } + }) + ], + resolveLoader: { + modulesDirectories: [ + 'node_modules', + 'gulp/webpack/' + ] + }, + eslint: { + formatter: function(results) { + return JSON.stringify(results); + } + }, + module: { + loaders: [ + { + test: /\.js?$/, + exclude: /(node_modules|JsLibraries)/, + loader: 'babel', + query: { + plugins: ['transform-class-properties'], + presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'], + env: { + development: { + plugins: ['transform-react-jsx-source'] + } + } + } + }, + + // CSS Modules + { + test: /\.css$/, + exclude: /(node_modules|globals.css)/, + loader: ExtractTextPlugin.extract('style', 'css-loader?modules&importLoaders=1&sourceMap&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader') + }, + + // Global styles + { + test: /\.css$/, + include: /(node_modules|globals.css)/, + loader: 'style!css-loader' + }, + + // Fonts + { + test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'url?limit=10240&mimetype=application/font-woff&emitFile=false&name=Content/Fonts/[name].[ext]' + }, + { + test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, + loader: 'file-loader?emitFile=false&name=Content/Fonts/[name].[ext]' + } + ] + }, + postcss: function(wpack) { + cssVariables.forEach(wpack.addDependency); + + return [ + simpleVars({ + variables: function() { + return cssVariables.reduce(function(obj, vars) { + return _.extend(obj, reload(vars)); + }, {}); + } + }), + nested(), + autoprefixer({ + browsers: [ + 'Chrome >= 30', + 'Firefox >= 30', + 'Safari >= 6', + 'Edge >= 12', + 'Explorer >= 10', + 'iOS >= 7', + 'Android >= 4.4' + ] + }) + ]; + } +}; + +gulp.task('webpack', () => { + return gulp.src('index.js') + .pipe(webpackStream(config)) + .pipe(gulp.dest('')); +}); + +gulp.task('webpackWatch', () => { + config.watch = true; + return gulp.src('') + .pipe(webpackStream(config)) + .on('error', errorHandler) + .pipe(gulp.dest('')) + .on('error', errorHandler) + .pipe(livereload()) + .on('error', errorHandler); +}); diff --git a/frontend/src/.vscode/settings.json b/frontend/src/.vscode/settings.json new file mode 100644 index 000000000..0fb2bf460 --- /dev/null +++ b/frontend/src/.vscode/settings.json @@ -0,0 +1,4 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.insertFinalNewline": true +} \ No newline at end of file diff --git a/frontend/src/Activity/Blacklist/Blacklist.js b/frontend/src/Activity/Blacklist/Blacklist.js new file mode 100644 index 000000000..e3ecd2ff7 --- /dev/null +++ b/frontend/src/Activity/Blacklist/Blacklist.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import BlacklistRowConnector from './BlacklistRowConnector'; + +class Blacklist extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isClearingBlacklistExecuting, + onClearBlacklistPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load blacklist
+ } + + { + isPopulated && !error && !items.length && +
+ No history blacklist +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } +} + +Blacklist.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isClearingBlacklistExecuting: PropTypes.bool.isRequired, + onClearBlacklistPress: PropTypes.func.isRequired +}; + +export default Blacklist; diff --git a/frontend/src/Activity/Blacklist/BlacklistConnector.js b/frontend/src/Activity/Blacklist/BlacklistConnector.js new file mode 100644 index 000000000..8056639f6 --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistConnector.js @@ -0,0 +1,120 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as blacklistActions from 'Store/Actions/blacklistActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import Blacklist from './Blacklist'; + +function createMapStateToProps() { + return createSelector( + (state) => state.blacklist, + createCommandsSelector(), + (blacklist, commands) => { + const isClearingBlacklistExecuting = _.some(commands, { name: commandNames.CLEAR_BLACKLIST }); + + return { + isClearingBlacklistExecuting, + ...blacklist + }; + } + ); +} + +const mapDispatchToProps = { + ...blacklistActions, + executeCommand +}; + +class BlacklistConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.gotoBlacklistFirstPage(); + } + + componentDidUpdate(prevProps) { + if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) { + this.props.gotoBlacklistFirstPage(); + } + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoBlacklistFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoBlacklistPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoBlacklistNextPage(); + } + + onLastPagePress = () => { + this.props.gotoBlacklistLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoBlacklistPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setBlacklistSort({ sortKey }); + } + + onTableOptionChange = (payload) => { + this.props.setBlacklistTableOption(payload); + + if (payload.pageSize) { + this.props.gotoBlacklistFirstPage(); + } + } + + onClearBlacklistPress = () => { + this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +BlacklistConnector.propTypes = { + isClearingBlacklistExecuting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchBlacklist: PropTypes.func.isRequired, + gotoBlacklistFirstPage: PropTypes.func.isRequired, + gotoBlacklistPreviousPage: PropTypes.func.isRequired, + gotoBlacklistNextPage: PropTypes.func.isRequired, + gotoBlacklistLastPage: PropTypes.func.isRequired, + gotoBlacklistPage: PropTypes.func.isRequired, + setBlacklistSort: PropTypes.func.isRequired, + setBlacklistTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector); diff --git a/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js b/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js new file mode 100644 index 000000000..356512a9d --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistDetailsModal.js @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +class BlacklistDetailsModal extends Component { + + // + // Render + + render() { + const { + isOpen, + sourceTitle, + protocol, + indexer, + message, + onModalClose + } = this.props; + + return ( + + + + Details + + + + + + + + + { + !!message && + + } + + { + !!message && + + } + + + + + + + + + ); + } +} + +BlacklistDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + sourceTitle: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + message: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default BlacklistDetailsModal; diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.css b/frontend/src/Activity/Blacklist/BlacklistRow.css new file mode 100644 index 000000000..030dfe98a --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRow.css @@ -0,0 +1,18 @@ +.language, +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} + +.indexer { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 80px; +} + +.details { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/Blacklist/BlacklistRow.js b/frontend/src/Activity/Blacklist/BlacklistRow.js new file mode 100644 index 000000000..50d71c30f --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRow.js @@ -0,0 +1,175 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import EpisodeLanguage from 'Episode/EpisodeLanguage'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import BlacklistDetailsModal from './BlacklistDetailsModal'; +import styles from './BlacklistRow.css'; + +class BlacklistRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onDetailsPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + series, + sourceTitle, + language, + quality, + date, + protocol, + indexer, + message, + columns + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'series.sortTitle') { + return ( + + + + ); + } + + if (name === 'sourceTitle') { + return ( + + {sourceTitle} + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'date') { + return ( + + ); + } + + if (name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (name === 'details') { + return ( + + + + ); + } + }) + } + + + + ); + } + +} + +BlacklistRow.propTypes = { + id: PropTypes.number.isRequired, + series: PropTypes.object.isRequired, + sourceTitle: PropTypes.string.isRequired, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + date: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + message: PropTypes.string, + columns: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default BlacklistRow; diff --git a/frontend/src/Activity/Blacklist/BlacklistRowConnector.js b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js new file mode 100644 index 000000000..0cf173c9e --- /dev/null +++ b/frontend/src/Activity/Blacklist/BlacklistRowConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import BlacklistRow from './BlacklistRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (series) => { + return { + series + }; + } + ); +} + +export default connect(createMapStateToProps)(BlacklistRow); diff --git a/frontend/src/Activity/History/Details/HistoryDetails.js b/frontend/src/Activity/History/Details/HistoryDetails.js new file mode 100644 index 000000000..9c08c3895 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetails.js @@ -0,0 +1,237 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import Link from 'Components/Link/Link'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; + +function HistoryDetails(props) { + const { + eventType, + sourceTitle, + data, + shortDateFormat, + timeFormat + } = props; + + if (eventType === 'grabbed') { + const { + indexer, + releaseGroup, + nzbInfoUrl, + downloadClient, + downloadId, + age, + ageHours, + ageMinutes, + publishedDate + } = data; + + return ( + + + + { + !!indexer && + + } + + { + !!releaseGroup && + + } + + { + !!nzbInfoUrl && + + + Info URL + + + + {nzbInfoUrl} + + + } + + { + !!downloadClient && + + } + + { + !!downloadId && + + } + + { + !!indexer && + + } + + { + !!publishedDate && + + } + + ); + } + + if (eventType === 'downloadFailed') { + const { + message + } = data; + + return ( + + + + { + !!message && + + } + + ); + } + + if (eventType === 'downloadFolderImported') { + const { + droppedPath, + importedPath + } = data; + + return ( + + + + { + !!droppedPath && + + } + + { + !!importedPath && + + } + + ); + } + + if (eventType === 'episodeFileDeleted') { + const { + reason + } = data; + + let reasonMessage = ''; + + switch (reason) { + case 'Manual': + reasonMessage = 'File was deleted by via UI'; + break; + case 'MissingFromDisk': + reasonMessage = 'Sonarr was unable to find the file on disk so it was removed'; + break; + case 'Upgrade': + reasonMessage = 'File was deleted to import an upgrade'; + break; + default: + reasonMessage = ''; + } + + return ( + + + + + + ); + } + + if (eventType === 'episodeFileRenamed') { + const { + sourcePath, + sourceRelativePath, + path, + relativePath + } = data; + + return ( + + + + + + + + + + ); + } +} + +HistoryDetails.propTypes = { + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default HistoryDetails; diff --git a/frontend/src/Activity/History/Details/HistoryDetailsConnector.js b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js new file mode 100644 index 000000000..0848c7905 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsConnector.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import HistoryDetails from './HistoryDetails'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'shortDateFormat', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps)(HistoryDetails); diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.css b/frontend/src/Activity/History/Details/HistoryDetailsModal.css new file mode 100644 index 000000000..bdcb7f918 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.css @@ -0,0 +1,5 @@ +.markAsFailedButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Activity/History/Details/HistoryDetailsModal.js b/frontend/src/Activity/History/Details/HistoryDetailsModal.js new file mode 100644 index 000000000..2cf9294f6 --- /dev/null +++ b/frontend/src/Activity/History/Details/HistoryDetailsModal.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import HistoryDetails from './HistoryDetails'; +import styles from './HistoryDetailsModal.css'; + +function getHeaderTitle(eventType) { + switch (eventType) { + case 'grabbed': + return 'Grabbed'; + case 'downloadFailed': + return 'Download Failed'; + case 'downloadFolderImported': + return 'Episode Imported'; + case 'episodeFileDeleted': + return 'Episode File Deleted'; + case 'episodeFileRenamed': + return 'Episode File Renamed'; + default: + return 'Unknown'; + } +} + +function HistoryDetailsModal(props) { + const { + isOpen, + eventType, + sourceTitle, + data, + isMarkingAsFailed, + shortDateFormat, + timeFormat, + onMarkAsFailedPress, + onModalClose + } = props; + + return ( + + + + {getHeaderTitle(eventType)} + + + + + + + + { + eventType === 'grabbed' && + + Mark as Failed + + } + + + + + + ); +} + +HistoryDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + isMarkingAsFailed: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +HistoryDetailsModal.defaultProps = { + isMarkingAsFailed: false +}; + +export default HistoryDetailsModal; diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js new file mode 100644 index 000000000..af2fb4f3f --- /dev/null +++ b/frontend/src/Activity/History/History.js @@ -0,0 +1,195 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import HistoryRowConnector from './HistoryRowConnector'; + +class History extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + filterKey, + filterValue, + totalRecords, + isEpisodesFetching, + isEpisodesPopulated, + episodesError, + onFilterSelect, + onFirstPagePress, + ...otherProps + } = this.props; + + const isFetchingAny = isFetching || isEpisodesFetching; + const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length); + const hasError = error || episodesError; + + return ( + + + + + + + + + + + All + + + + Grabbed + + + + Imported + + + + Failed + + + + Deleted + + + + Renamed + + + + + + + + { + isFetchingAny && !isAllPopulated && + + } + + { + !isFetchingAny && hasError && +
Unable to load history
+ } + + { + // If history isPopulated and it's empty show no history found and don't + // wait for the episodes to populate because they are never coming. + + isPopulated && !hasError && !items.length && +
+ No history found +
+ } + + { + isAllPopulated && !hasError && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } +} + +History.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.string, + totalRecords: PropTypes.number, + isEpisodesFetching: PropTypes.bool.isRequired, + isEpisodesPopulated: PropTypes.bool.isRequired, + episodesError: PropTypes.object, + onFilterSelect: PropTypes.func.isRequired, + onFirstPagePress: PropTypes.func.isRequired +}; + +export default History; diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js new file mode 100644 index 000000000..9034dab84 --- /dev/null +++ b/frontend/src/Activity/History/HistoryConnector.js @@ -0,0 +1,128 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import * as historyActions from 'Store/Actions/historyActions'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import History from './History'; + +function createMapStateToProps() { + return createSelector( + (state) => state.history, + (state) => state.episodes, + (history, episodes) => { + return { + isEpisodesFetching: episodes.isFetching, + isEpisodesPopulated: episodes.isPopulated, + episodesError: episodes.error, + ...history + }; + } + ); +} + +const mapDispatchToProps = { + ...historyActions, + fetchEpisodes, + clearEpisodes +}; + +class HistoryConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.gotoHistoryFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const episodeIds = selectUniqueIds(this.props.items, 'episodeId'); + this.props.fetchEpisodes({ episodeIds }); + } + } + + componentWillUnmount() { + this.props.clearHistory(); + this.props.clearEpisodes(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoHistoryFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoHistoryPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoHistoryNextPage(); + } + + onLastPagePress = () => { + this.props.gotoHistoryLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoHistoryPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setHistorySort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setHistoryFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setHistoryTableOption(payload); + + if (payload.pageSize) { + this.props.gotoHistoryFirstPage(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +HistoryConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchHistory: PropTypes.func.isRequired, + gotoHistoryFirstPage: PropTypes.func.isRequired, + gotoHistoryPreviousPage: PropTypes.func.isRequired, + gotoHistoryNextPage: PropTypes.func.isRequired, + gotoHistoryLastPage: PropTypes.func.isRequired, + gotoHistoryPage: PropTypes.func.isRequired, + setHistorySort: PropTypes.func.isRequired, + setHistoryFilter: PropTypes.func.isRequired, + setHistoryTableOption: PropTypes.func.isRequired, + clearHistory: PropTypes.func.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector); diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.css b/frontend/src/Activity/History/HistoryEventTypeCell.css new file mode 100644 index 000000000..086354783 --- /dev/null +++ b/frontend/src/Activity/History/HistoryEventTypeCell.css @@ -0,0 +1,3 @@ +.cell { + width: 35px; +} diff --git a/frontend/src/Activity/History/HistoryEventTypeCell.js b/frontend/src/Activity/History/HistoryEventTypeCell.js new file mode 100644 index 000000000..f013b3f55 --- /dev/null +++ b/frontend/src/Activity/History/HistoryEventTypeCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './HistoryEventTypeCell.css'; + +function getIconName(eventType) { + switch (eventType) { + case 'grabbed': + return icons.DOWNLOADING; + case 'seriesFolderImported': + return icons.DRIVE; + case 'downloadFolderImported': + return icons.DOWNLOADED; + case 'downloadFailed': + return icons.DOWNLOADING; + case 'episodeFileDeleted': + return icons.DELETE; + case 'episodeFileRenamed': + return icons.ORGANIZE; + default: + return icons.UNKNOWN; + } +} + +function getIconKind(eventType) { + switch (eventType) { + case 'downloadFailed': + return kinds.DANGER; + default: + return kinds.DEFAULT; + } +} + +function getTooltip(eventType, data) { + switch (eventType) { + case 'grabbed': + return `Episode grabbed from ${data.indexer} and sent to ${data.downloadClient}`; + case 'seriesFolderImported': + return 'Episode imported from series folder'; + case 'downloadFolderImported': + return 'Episode downloaded successfully and picked up from download client'; + case 'downloadFailed': + return 'Episode download failed'; + case 'episodeFileDeleted': + return 'Episode file deleted'; + case 'episodeFileRenamed': + return 'Episode file renamed'; + default: + return 'Unknown event'; + } +} + +function HistoryEventTypeCell({ eventType, data }) { + const iconName = getIconName(eventType); + const iconKind = getIconKind(eventType); + const tooltip = getTooltip(eventType, data); + + return ( + + + + ); +} + +HistoryEventTypeCell.propTypes = { + eventType: PropTypes.string.isRequired, + data: PropTypes.object +}; + +HistoryEventTypeCell.defaultProps = { + data: {} +}; + +export default HistoryEventTypeCell; diff --git a/frontend/src/Activity/History/HistoryRow.css b/frontend/src/Activity/History/HistoryRow.css new file mode 100644 index 000000000..83586af58 --- /dev/null +++ b/frontend/src/Activity/History/HistoryRow.css @@ -0,0 +1,23 @@ +.downloadClient { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 120px; +} + +.indexer { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 80px; +} + +.releaseGroup { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 110px; +} + +.details { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/History/HistoryRow.js b/frontend/src/Activity/History/HistoryRow.js new file mode 100644 index 000000000..905bb5ffa --- /dev/null +++ b/frontend/src/Activity/History/HistoryRow.js @@ -0,0 +1,254 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import episodeEntities from 'Episode/episodeEntities'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import EpisodeLanguage from 'Episode/EpisodeLanguage'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import HistoryEventTypeCell from './HistoryEventTypeCell'; +import HistoryDetailsModal from './Details/HistoryDetailsModal'; +import styles from './HistoryRow.css'; + +class HistoryRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if ( + prevProps.isMarkingAsFailed && + !this.props.isMarkingAsFailed && + !this.props.markAsFailedError + ) { + this.setState({ isDetailsModalOpen: false }); + } + } + + // + // Listeners + + onDetailsPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + episodeId, + series, + episode, + language, + quality, + eventType, + sourceTitle, + date, + data, + isMarkingAsFailed, + columns, + shortDateFormat, + timeFormat, + onMarkAsFailedPress + } = this.props; + + if (!episode) { + return null; + } + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'eventType') { + return ( + + ); + } + + if (name === 'series.sortTitle') { + return ( + + + + ); + } + + if (name === 'episode') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'date') { + return ( + + ); + } + + if (name === 'downloadClient') { + return ( + + {data.downloadClient} + + ); + } + + if (name === 'indexer') { + return ( + + {data.indexer} + + ); + } + + if (name === 'releaseGroup') { + return ( + + {data.releaseGroup} + + ); + } + + if (name === 'details') { + return ( + + + + ); + } + }) + } + + + + ); + } + +} + +HistoryRow.propTypes = { + episodeId: PropTypes.number, + series: PropTypes.object.isRequired, + episode: PropTypes.object, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + eventType: PropTypes.string.isRequired, + sourceTitle: PropTypes.string.isRequired, + date: PropTypes.string.isRequired, + data: PropTypes.object.isRequired, + isMarkingAsFailed: PropTypes.bool, + markAsFailedError: PropTypes.object, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onMarkAsFailedPress: PropTypes.func.isRequired +}; + +export default HistoryRow; diff --git a/frontend/src/Activity/History/HistoryRowConnector.js b/frontend/src/Activity/History/HistoryRowConnector.js new file mode 100644 index 000000000..7ffcdbb57 --- /dev/null +++ b/frontend/src/Activity/History/HistoryRowConnector.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import HistoryRow from './HistoryRow'; + +function createMapStateToProps() { + return createSelector( + createEpisodeSelector(), + createUISettingsSelector(), + (episode, uiSettings) => { + return { + episode, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +const mapDispatchToProps = { + fetchHistory, + markAsFailed +}; + +class HistoryRowConnector extends Component { + + componentDidUpdate(prevProps) { + if ( + prevProps.isMarkingAsFailed && + !this.props.isMarkingAsFailed && + !this.props.markAsFailedError + ) { + this.props.fetchHistory(); + } + } + + // + // Listeners + + onMarkAsFailedPress = () => { + this.props.markAsFailed({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +HistoryRowConnector.propTypes = { + id: PropTypes.number.isRequired, + isMarkingAsFailed: PropTypes.bool, + markAsFailedError: PropTypes.object, + fetchHistory: PropTypes.func.isRequired, + markAsFailed: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector); diff --git a/frontend/src/Activity/Queue/ProtocolLabel.css b/frontend/src/Activity/Queue/ProtocolLabel.css new file mode 100644 index 000000000..15e8e4fc6 --- /dev/null +++ b/frontend/src/Activity/Queue/ProtocolLabel.css @@ -0,0 +1,13 @@ +.torrent { + composes: label from 'Components/Label.css'; + + border-color: $torrentColor; + background-color: $torrentColor; +} + +.usenet { + composes: label from 'Components/Label.css'; + + border-color: $usenetColor; + background-color: $usenetColor; +} diff --git a/frontend/src/Activity/Queue/ProtocolLabel.js b/frontend/src/Activity/Queue/ProtocolLabel.js new file mode 100644 index 000000000..e8a08943c --- /dev/null +++ b/frontend/src/Activity/Queue/ProtocolLabel.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Label from 'Components/Label'; +import styles from './ProtocolLabel.css'; + +function ProtocolLabel({ protocol }) { + const protocolName = protocol === 'usenet' ? 'nzb' : protocol; + + return ( + + ); +} + +ProtocolLabel.propTypes = { + protocol: PropTypes.string.isRequired +}; + +export default ProtocolLabel; diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js new file mode 100644 index 000000000..621e4ac37 --- /dev/null +++ b/frontend/src/Activity/Queue/Queue.js @@ -0,0 +1,243 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { icons } from 'Helpers/Props'; +import episodeEntities from 'Episode/episodeEntities'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import RemoveQueueItemsModal from './RemoveQueueItemsModal'; +import QueueRowConnector from './QueueRowConnector'; + +class Queue extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isPendingSelected: false, + isConfirmRemoveModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState({ selectedState: {} }); + return; + } + + const selectedIds = this.getSelectedIds(); + const isPendingSelected = _.some(this.props.items, (item) => { + return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay'; + }); + + if (isPendingSelected !== this.state.isPendingSelected) { + this.setState({ isPendingSelected }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onGrabSelectedPress = () => { + this.props.onGrabSelectedPress(this.getSelectedIds()); + } + + onRemoveSelectedPress = () => { + this.setState({ isConfirmRemoveModalOpen: true }); + } + + onRemoveSelectedConfirmed = (blacklist) => { + this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist); + this.setState({ isConfirmRemoveModalOpen: false }); + } + + onConfirmRemoveModalClose = () => { + this.setState({ isConfirmRemoveModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + isEpisodesPopulated, + columns, + totalRecords, + isGrabbing, + isRemoving, + isCheckForFinishedDownloadExecuting, + onRefreshPress, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmRemoveModalOpen, + isPendingSelected + } = this.state; + + const isRefreshing = isFetching || isCheckForFinishedDownloadExecuting; + const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length); + const selectedCount = this.getSelectedIds().length; + const disableSelectedActions = selectedCount === 0; + + return ( + + + + + + + + + + + + + + + { + isRefreshing && !isAllPopulated && + + } + + { + !isRefreshing && error && +
+ Failed to load Queue +
+ } + + { + isAllPopulated && !error && !items.length && +
+ Queue is empty +
+ } + + { + isAllPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+ + +
+ ); + } +} + +Queue.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isEpisodesPopulated: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isGrabbing: PropTypes.bool.isRequired, + isRemoving: PropTypes.bool.isRequired, + isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onGrabSelectedPress: PropTypes.func.isRequired, + onRemoveSelectedPress: PropTypes.func.isRequired +}; + +export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js new file mode 100644 index 000000000..0828a0246 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -0,0 +1,153 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as queueActions from 'Store/Actions/queueActions'; +import { clearEpisodes } from 'Store/Actions/episodeActions'; +import * as commandNames from 'Commands/commandNames'; +import Queue from './Queue'; + +function createMapStateToProps() { + return createSelector( + (state) => state.queue.paged, + (state) => state.queue.queueEpisodes, + createCommandsSelector(), + (queue, queueEpisodes, commands) => { + const isCheckForFinishedDownloadExecuting = _.some(commands, { name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD }); + + return { + isCheckForFinishedDownloadExecuting, + isEpisodesPopulated: queueEpisodes.isPopulated, + ...queue + }; + } + ); +} + +const mapDispatchToProps = { + ...queueActions, + clearEpisodes, + executeCommand +}; + +class QueueConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.gotoQueueFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const episodes = _.uniqBy(_.reduce(this.props.items, (result, item) => { + result.push(item.episode); + + return result; + }, []), ({ id }) => id); + + this.props.clearEpisodes(); + this.props.setQueueEpisodes({ episodes }); + } + } + + componentWillUnmount() { + this.props.clearQueue(); + this.props.clearEpisodes(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoQueueFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoQueuePreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoQueueNextPage(); + } + + onLastPagePress = () => { + this.props.gotoQueueLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoQueuePage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setQueueSort({ sortKey }); + } + + onTableOptionChange = (payload) => { + this.props.setQueueTableOption(payload); + + if (payload.pageSize) { + this.props.gotoQueueFirstPage(); + } + } + + onRefreshPress = () => { + this.props.executeCommand({ + name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD + }); + } + + onGrabSelectedPress = (ids) => { + this.props.grabQueueItems({ ids }); + } + + onRemoveSelectedPress = (ids, blacklist) => { + this.props.removeQueueItems({ ids, blacklist }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueueConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchQueue: PropTypes.func.isRequired, + gotoQueueFirstPage: PropTypes.func.isRequired, + gotoQueuePreviousPage: PropTypes.func.isRequired, + gotoQueueNextPage: PropTypes.func.isRequired, + gotoQueueLastPage: PropTypes.func.isRequired, + gotoQueuePage: PropTypes.func.isRequired, + setQueueSort: PropTypes.func.isRequired, + setQueueTableOption: PropTypes.func.isRequired, + clearQueue: PropTypes.func.isRequired, + setQueueEpisodes: PropTypes.func.isRequired, + grabQueueItems: PropTypes.func.isRequired, + removeQueueItems: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueConnector); diff --git a/frontend/src/Activity/Queue/QueueDetails.js b/frontend/src/Activity/Queue/QueueDetails.js new file mode 100644 index 000000000..f6e360c0a --- /dev/null +++ b/frontend/src/Activity/Queue/QueueDetails.js @@ -0,0 +1,97 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; + +function QueueDetails(props) { + const { + title, + size, + sizeleft, + estimatedCompletionTime, + status: queueStatus, + errorMessage, + progressBar + } = props; + + const status = queueStatus.toLowerCase(); + + const progress = (100 - sizeleft / size * 100); + + if (status === 'pending') { + return ( + + ); + } + + if (status === 'completed') { + if (errorMessage) { + return ( + + ); + } + + // TODO: show an icon when download is complete, but not imported yet? + } + + if (errorMessage) { + return ( + + ); + } + + if (status === 'failed') { + return ( + + ); + } + + if (status === 'warning') { + return ( + + ); + } + + if (progress < 5) { + return ( + + ); + } + + return progressBar; +} + +QueueDetails.propTypes = { + title: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + estimatedCompletionTime: PropTypes.string, + status: PropTypes.string.isRequired, + errorMessage: PropTypes.string, + progressBar: PropTypes.node.isRequired +}; + +export default QueueDetails; diff --git a/frontend/src/Activity/Queue/QueueRow.css b/frontend/src/Activity/Queue/QueueRow.css new file mode 100644 index 000000000..6aa4a1622 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRow.css @@ -0,0 +1,23 @@ +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.protocol { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} + +.progress { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 70px; +} diff --git a/frontend/src/Activity/Queue/QueueRow.js b/frontend/src/Activity/Queue/QueueRow.js new file mode 100644 index 000000000..54b0f4276 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRow.js @@ -0,0 +1,348 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import QueueStatusCell from './QueueStatusCell'; +import TimeleftCell from './TimeleftCell'; +import RemoveQueueItemModal from './RemoveQueueItemModal'; +import styles from './QueueRow.css'; + +class QueueRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isRemoveQueueItemModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + // + // Listeners + + onRemoveQueueItemPress = () => { + this.setState({ isRemoveQueueItemModalOpen: true }); + } + + onRemoveQueueItemModalConfirmed = (blacklist) => { + this.props.onRemoveQueueItemPress(blacklist); + this.setState({ isRemoveQueueItemModalOpen: false }); + } + + onRemoveQueueItemModalClose = () => { + this.setState({ isRemoveQueueItemModalOpen: false }); + } + + onInteractiveImportPress = () => { + this.setState({ isInteractiveImportModalOpen: true }); + } + + onInteractiveImportModalClose = () => { + this.setState({ isInteractiveImportModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + downloadId, + episodeEntity, + title, + status, + trackedDownloadStatus, + statusMessages, + errorMessage, + series, + episode, + quality, + protocol, + indexer, + downloadClient, + estimatedCompletionTime, + timeleft, + size, + sizeleft, + showRelativeDates, + shortDateFormat, + timeFormat, + isGrabbing, + grabError, + isRemoving, + isSelected, + columns, + onSelectedChange, + onGrabPress + } = this.props; + + const { + isRemoveQueueItemModalOpen, + isInteractiveImportModalOpen + } = this.state; + + const progress = 100 - (sizeleft / size * 100); + const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning'; + const isPending = status === 'Delay' || status === 'DownloadClientUnavailable'; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'series.sortTitle') { + return ( + + + + ); + } + + if (name === 'series') { + return ( + + + + ); + } + + if (name === 'episode') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'quality') { + return ( + + + + ); + } + + if (name === 'protocol') { + return ( + + + + ); + } + + if (name === 'indexer') { + return ( + + {indexer} + + ); + } + + if (name === 'downloadClient') { + return ( + + {downloadClient} + + ); + } + + if (name === 'estimatedCompletionTime') { + return ( + + ); + } + + if (name === 'progress') { + return ( + + { + !!progress && + + } + + ); + } + + if (name === 'actions') { + return ( + + { + showInteractiveImport && + + } + + { + isPending && + + } + + + + ); + } + }) + } + + + + + + ); + } + +} + +QueueRow.propTypes = { + id: PropTypes.number.isRequired, + downloadId: PropTypes.string, + episodeEntity: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + trackedDownloadStatus: PropTypes.string, + statusMessages: PropTypes.arrayOf(PropTypes.object), + errorMessage: PropTypes.string.isRequired, + series: PropTypes.object.isRequired, + episode: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + protocol: PropTypes.string.isRequired, + indexer: PropTypes.string, + downloadClient: PropTypes.string, + estimatedCompletionTime: PropTypes.string, + timeleft: PropTypes.string, + size: PropTypes.number, + sizeleft: PropTypes.number, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isGrabbing: PropTypes.bool.isRequired, + grabError: PropTypes.object, + isRemoving: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired, + onGrabPress: PropTypes.func.isRequired, + onRemoveQueueItemPress: PropTypes.func.isRequired +}; + +QueueRow.defaultProps = { + isGrabbing: false, + isRemoving: false +}; + +export default QueueRow; diff --git a/frontend/src/Activity/Queue/QueueRowConnector.js b/frontend/src/Activity/Queue/QueueRowConnector.js new file mode 100644 index 000000000..0da6a1abc --- /dev/null +++ b/frontend/src/Activity/Queue/QueueRowConnector.js @@ -0,0 +1,76 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import QueueRow from './QueueRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createEpisodeSelector(), + createUISettingsSelector(), + (series, episode, uiSettings) => { + const result = _.pick(uiSettings, [ + 'showRelativeDates', + 'shortDateFormat', + 'timeFormat' + ]); + + result.series = series; + result.episode = episode; + + return result; + } + ); +} + +const mapDispatchToProps = { + grabQueueItem, + removeQueueItem +}; + +class QueueRowConnector extends Component { + + // + // Listeners + + onGrabPress = () => { + this.props.grabQueueItem({ id: this.props.id }); + } + + onRemoveQueueItemPress = (blacklist) => { + this.props.removeQueueItem({ id: this.props.id, blacklist }); + } + + // + // Render + + render() { + if (!this.props.episode) { + return null; + } + + return ( + + ); + } +} + +QueueRowConnector.propTypes = { + id: PropTypes.number.isRequired, + episodeEntity: PropTypes.string.isRequired, + episode: PropTypes.object, + grabQueueItem: PropTypes.func.isRequired, + removeQueueItem: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector); diff --git a/frontend/src/Activity/Queue/QueueStatusCell.css b/frontend/src/Activity/Queue/QueueStatusCell.css new file mode 100644 index 000000000..6291ec949 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatusCell.css @@ -0,0 +1,5 @@ +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} diff --git a/frontend/src/Activity/Queue/QueueStatusCell.js b/frontend/src/Activity/Queue/QueueStatusCell.js new file mode 100644 index 000000000..f8cbc65ff --- /dev/null +++ b/frontend/src/Activity/Queue/QueueStatusCell.js @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Popover from 'Components/Tooltip/Popover'; +import styles from './QueueStatusCell.css'; + +function getDetailedPopoverBody(statusMessages) { + return ( +
+ { + statusMessages.map(({ title, messages }) => { + return ( +
+ {title} +
    + { + messages.map((message) => { + return ( +
  • + {message} +
  • + ); + }) + } +
+
+ ); + }) + } +
+ ); +} + +function QueueStatusCell(props) { + const { + sourceTitle, + status, + trackedDownloadStatus = 'Ok', + statusMessages, + errorMessage + } = props; + + const hasWarning = trackedDownloadStatus === 'Warning'; + const hasError = trackedDownloadStatus === 'Error'; + + // status === 'downloading' + let iconName = icons.DOWNLOADING; + let iconKind = kinds.DEFAULT; + let title = 'Downloading'; + + if (hasWarning) { + iconKind = kinds.WARNING; + } + + if (status === 'Paused') { + iconName = icons.PAUSED; + title = 'Paused'; + } + + if (status === 'Queued') { + iconName = icons.QUEUED; + title = 'Queued'; + } + + if (status === 'Completed') { + iconName = icons.DOWNLOADED; + title = 'Downloaded'; + } + + if (status === 'Delay') { + iconName = icons.PENDING; + title = 'Pending'; + } + + if (status === 'DownloadClientUnavailable') { + iconName = icons.PENDING; + iconKind = kinds.WARNING; + title = 'Pending - Download client is unavailable'; + } + + if (status === 'Failed') { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = 'Download failed'; + } + + if (status === 'Warning') { + iconName = icons.DOWNLOADING; + iconKind = kinds.WARNING; + title = `Download warning: ${errorMessage || 'check download client for more details'}`; + } + + if (hasError) { + if (status === 'Completed') { + iconName = icons.DOWNLOAD; + iconKind = kinds.DANGER; + title = `Import failed: ${sourceTitle}`; + } else { + iconName = icons.DOWNLOADING; + iconKind = kinds.DANGER; + title = 'Download failed'; + } + } + + return ( + + + } + title={title} + body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle} + position={tooltipPositions.RIGHT} + /> + + ); +} + +QueueStatusCell.propTypes = { + sourceTitle: PropTypes.string.isRequired, + status: PropTypes.string.isRequired, + trackedDownloadStatus: PropTypes.string, + statusMessages: PropTypes.arrayOf(PropTypes.object), + errorMessage: PropTypes.string +}; + +export default QueueStatusCell; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.css b/frontend/src/Activity/Queue/RemoveQueueItemModal.css new file mode 100644 index 000000000..c9ef59ec1 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.css @@ -0,0 +1,3 @@ +.message { + margin-bottom: 30px; +} diff --git a/frontend/src/Activity/Queue/RemoveQueueItemModal.js b/frontend/src/Activity/Queue/RemoveQueueItemModal.js new file mode 100644 index 000000000..52c2bc1cc --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemModal.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './RemoveQueueItemModal.css'; + +class RemoveQueueItemModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + blacklist: false + }; + } + + // + // Listeners + + onBlacklistChange = ({ value }) => { + this.setState({ blacklist: value }); + } + + onRemoveQueueItemConfirmed = () => { + const blacklist = this.state.blacklist; + + this.setState({ blacklist: false }); + this.props.onRemovePress(blacklist); + } + + onModalClose = () => { + this.setState({ blacklist: false }); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + isOpen, + sourceTitle + } = this.props; + + const blacklist = this.state.blacklist; + + return ( + + + + Remove - {sourceTitle} + + + +
+ Are you sure you want to remove '{sourceTitle}' from the queue? +
+ + + Blacklist Release + + + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + sourceTitle: PropTypes.string.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemModal; diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.css b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css new file mode 100644 index 000000000..c9ef59ec1 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.css @@ -0,0 +1,3 @@ +.message { + margin-bottom: 30px; +} diff --git a/frontend/src/Activity/Queue/RemoveQueueItemsModal.js b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js new file mode 100644 index 000000000..8e8009ab1 --- /dev/null +++ b/frontend/src/Activity/Queue/RemoveQueueItemsModal.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './RemoveQueueItemsModal.css'; + +class RemoveQueueItemsModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + blacklist: false + }; + } + + // + // Listeners + + onBlacklistChange = ({ value }) => { + this.setState({ blacklist: value }); + } + + onRemoveQueueItemConfirmed = () => { + const blacklist = this.state.blacklist; + + this.setState({ blacklist: false }); + this.props.onRemovePress(blacklist); + } + + onModalClose = () => { + this.setState({ blacklist: false }); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + isOpen, + selectedCount + } = this.props; + + const blacklist = this.state.blacklist; + + return ( + + + + Remove Selected Item{selectedCount > 1 ? 's' : ''} + + + +
+ Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue? +
+ + + Blacklist Release + + + +
+ + + + + + +
+
+ ); + } +} + +RemoveQueueItemsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + selectedCount: PropTypes.number.isRequired, + onRemovePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default RemoveQueueItemsModal; diff --git a/frontend/src/Activity/Queue/Status/QueueStatusConnector.js b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js new file mode 100644 index 000000000..c8419a8f8 --- /dev/null +++ b/frontend/src/Activity/Queue/Status/QueueStatusConnector.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQueueStatus } from 'Store/Actions/queueActions'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app, + (state) => state.queue.queueStatus, + (app, status) => { + return { + isConnected: app.isConnected, + isReconnecting: app.isReconnecting, + isPopulated: status.isPopulated, + ...status.item + }; + } + ); +} + +const mapDispatchToProps = { + fetchQueueStatus +}; + +class QueueStatusConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.fetchQueueStatus(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isConnected && prevProps.isReconnecting) { + this.props.fetchQueueStatus(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +QueueStatusConnector.propTypes = { + isConnected: PropTypes.bool.isRequired, + isReconnecting: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchQueueStatus: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector); diff --git a/frontend/src/Activity/Queue/TimeleftCell.css b/frontend/src/Activity/Queue/TimeleftCell.css new file mode 100644 index 000000000..eb58cf297 --- /dev/null +++ b/frontend/src/Activity/Queue/TimeleftCell.css @@ -0,0 +1,5 @@ +.timeleft { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Activity/Queue/TimeleftCell.js b/frontend/src/Activity/Queue/TimeleftCell.js new file mode 100644 index 000000000..c9515f172 --- /dev/null +++ b/frontend/src/Activity/Queue/TimeleftCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatTime from 'Utilities/Date/formatTime'; +import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './TimeleftCell.css'; + +function TimeleftCell(props) { + const { + estimatedCompletionTime, + timeleft, + status, + size, + sizeleft, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (status === 'Delay') { + const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); + + return ( + + - + + ); + } + + if (status === 'DownloadClientUnavailable') { + const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); + const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); + + return ( + + - + + ); + } + + if (!timeleft) { + return ( + + - + + ); + } + + const totalSize = formatBytes(size); + const remainingSize = formatBytes(sizeleft); + + return ( + + {formatTimeSpan(timeleft)} + + ); +} + +TimeleftCell.propTypes = { + estimatedCompletionTime: PropTypes.string, + timeleft: PropTypes.string, + status: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default TimeleftCell; diff --git a/frontend/src/Activity/activity.less b/frontend/src/Activity/activity.less new file mode 100644 index 000000000..c6d9b6d2a --- /dev/null +++ b/frontend/src/Activity/activity.less @@ -0,0 +1,27 @@ + +.queue-status-cell .popover { + max-width: 800px; +} + +.queue { + .protocol-cell { + text-align: center; + width: 80px; + } + + .episode-number-cell { + min-width: 90px; + } +} + +.remove-from-queue-modal { + .form-horizontal { + margin-top: 20px; + } +} + +.history-detail-modal { + .info { + word-wrap: break-word; + } +} diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeries.css b/frontend/src/AddArtist/AddNewSeries/AddNewSeries.css new file mode 100644 index 000000000..c1ec4fbe3 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeries.css @@ -0,0 +1,54 @@ +.searchContainer { + display: flex; + margin-bottom: 10px; +} + +.searchIconContainer { + width: 58px; + height: 46px; + border: 1px solid $inputBorderColor; + border-right: none; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: #edf1f2; + text-align: center; + line-height: 46px; +} + +.searchInput { + composes: text from 'Components/Form/TextInput.css'; + + height: 46px; + border-radius: 0; + font-size: 18px; +} + +.clearLookupButton { + border: 1px solid $inputBorderColor; + border-left: none; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.message { + margin-top: 30px; + text-align: center; +} + +.helpText { + margin-bottom: 10px; + font-weight: 300; + font-size: 24px; +} + +.noResults { + margin-bottom: 10px; + font-weight: 300; + font-size: 30px; +} + +.searchResults { + margin-top: 30px; +} diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeries.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeries.js new file mode 100644 index 000000000..c027dc906 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeries.js @@ -0,0 +1,184 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import TextInput from 'Components/Form/TextInput'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector'; +import styles from './AddNewSeries.css'; + +class AddNewSeries extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + term: props.term || '', + isFetching: false + }; + } + + componentDidMount() { + const term = this.state.term; + + if (term) { + this.props.onSeriesLookupChange(term); + } + } + + componentDidUpdate(prevProps) { + const { + term, + isFetching + } = this.props; + + if (term && term !== prevProps.term) { + this.setState({ + term, + isFetching: true + }); + this.props.onSeriesLookupChange(term); + } else if (isFetching !== prevProps.isFetching) { + this.setState({ + isFetching + }); + } + } + + // + // Listeners + + onSearchInputChange = ({ value }) => { + const hasValue = !!value.trim(); + + this.setState({ term: value, isFetching: hasValue }, () => { + if (hasValue) { + this.props.onSeriesLookupChange(value); + } else { + this.props.onClearSeriesLookup(); + } + }); + } + + onClearSeriesLookupPress = () => { + this.setState({ term: '' }); + this.props.onClearSeriesLookup(); + } + + // + // Render + + render() { + const { + error, + items + } = this.props; + + const term = this.state.term; + const isFetching = this.state.isFetching; + + return ( + + +
+
+ +
+ + + + +
+ + { + isFetching && + + } + + { + !isFetching && !!error && +
Failed to load search results, please try again.
+ } + + { + !isFetching && !error && !!items.length && +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+ } + + { + !isFetching && !error && !items.length && !!term && +
+
Couldn't find any results for '{term}'
+
You can also search using MusicBrainz ID of a show. eg. lidarr:71663
+
+ + Why can't I find my artist? + +
+
+ } + + { + !term && +
+
It's easy to add a new artist, just start typing the name the artist you want to add.
+
You can also search using MusicBrainz ID of a show. eg. lidarr:71663
+
+ } + +
+ +
+
+
+ ); + } +} + +AddNewSeries.propTypes = { + term: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isAdding: PropTypes.bool.isRequired, + addError: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeriesLookupChange: PropTypes.func.isRequired, + onClearSeriesLookup: PropTypes.func.isRequired +}; + +export default AddNewSeries; diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js new file mode 100644 index 000000000..491bfe6f9 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import queryString from 'query-string'; +import { lookupSeries, clearAddSeries } from 'Store/Actions/addSeriesActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import AddNewSeries from './AddNewSeries'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addSeries, + (state) => state.routing.location, + (addSeries, location) => { + const query = queryString.parse(location.search); + + return { + term: query.term, + ...addSeries + }; + } + ); +} + +const mapDispatchToProps = { + lookupSeries, + clearAddSeries, + fetchRootFolders +}; + +class AddNewSeriesConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._seriesLookupTimeout = null; + } + + componentDidMount() { + this.props.fetchRootFolders(); + } + + componentWillUnmount() { + if (this._seriesLookupTimeout) { + clearTimeout(this._seriesLookupTimeout); + } + + this.props.clearAddSeries(); + } + + // + // Listeners + + onSeriesLookupChange = (term) => { + if (this._seriesLookupTimeout) { + clearTimeout(this._seriesLookupTimeout); + } + + if (term.trim() === '') { + this.props.clearAddSeries(); + } else { + this._seriesLookupTimeout = setTimeout(() => { + this.props.lookupSeries({ term }); + }, 300); + } + } + + onClearSeriesLookup = () => { + this.props.clearAddSeries(); + } + + // + // Render + + render() { + const { + term, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AddNewSeriesConnector.propTypes = { + term: PropTypes.string, + lookupSeries: PropTypes.func.isRequired, + clearAddSeries: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesConnector); diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js new file mode 100644 index 000000000..cb603e7a6 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddNewSeriesModalContentConnector from './AddNewSeriesModalContentConnector'; + +function AddNewSeriesModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +AddNewSeriesModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNewSeriesModal; diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.css b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.css new file mode 100644 index 000000000..90526c529 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.css @@ -0,0 +1,74 @@ +.container { + display: flex; +} + +.year { + margin-left: 5px; + color: $disabledColor; +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.info { + flex-grow: 1; +} + +.overview { + margin-bottom: 30px; +} + +.labelIcon { + margin-left: 8px; +} + +.searchForMissingEpisodesLabelContainer { + display: flex; + margin-top: 2px; +} + +.searchForMissingEpisodesLabel { + margin-right: 8px; + font-weight: normal; +} + +.searchForMissingEpisodesContainer { + composes: container from 'Components/Form/CheckInput.css'; + + flex: 0 1 0; +} + +.searchForMissingEpisodesInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin-top: 0; +} + +.modalFooter { + composes: modalFooter from 'Components/Modal/ModalFooter.css'; +} + +.addButton { + composes: button from 'Components/Link/SpinnerButton.css'; + composes: truncate from 'Styles/mixins/truncate.css'; +} + +.hideLanguageProfile { + composes: group from 'Components/Form/FormGroup.css'; + + display: none; +} + +@media only screen and (max-width: $breakpointSmall) { + .modalFooter { + display: block; + text-align: center; + } + + .addButton { + margin-top: 10px; + } +} diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js new file mode 100644 index 000000000..3c42627e1 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js @@ -0,0 +1,265 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import CheckInput from 'Components/Form/CheckInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Popover from 'Components/Tooltip/Popover'; +import ArtistPoster from 'Artist/ArtistPoster'; +import SeriesMonitoringOptionsPopoverContent from 'AddArtist/SeriesMonitoringOptionsPopoverContent'; +import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent'; +import styles from './AddNewSeriesModalContent.css'; + +class AddNewSeriesModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + searchForMissingEpisodes: false + }; + } + + // + // Listeners + + onSearchForMissingEpisodesChange = ({ value }) => { + this.setState({ searchForMissingEpisodes: value }); + } + + onQualityProfileIdChange = ({ value }) => { + this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) }); + } + + onLanguageProfileIdChange = ({ value }) => { + this.props.onInputChange({ name: 'languageProfileId', value: parseInt(value) }); + } + + onAddSeriesPress = () => { + this.props.onAddSeriesPress(this.state.searchForMissingEpisodes); + } + + // + // Render + + render() { + const { + artistName, + year, + overview, + images, + isAdding, + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + seriesType, + albumFolder, + tags, + showLanguageProfile, + isSmallScreen, + onModalClose, + onInputChange + } = this.props; + + return ( + + + {artistName} + + { + !name.contains(year) && + ({year}) + } + + + +
+ { + !isSmallScreen && +
+ +
+ } + +
+
+ {overview} +
+ +
+ + Root Folder + + + + + + + Monitor + + + } + title="Monitoring Options" + body={} + position={tooltipPositions.RIGHT} + /> + + + + + + + Quality Profile + + + + + + Language Profile + + + + + + + Series Type + + + } + title="Series Types" + body={} + position={tooltipPositions.RIGHT} + /> + + + + + + + Album Folder + + + + + + Tags + + + +
+
+
+
+ + + + + + Add {artistName} + + +
+ ); + } +} + +AddNewSeriesModalContent.propTypes = { + artistName: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + overview: PropTypes.string, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isAdding: PropTypes.bool.isRequired, + addError: PropTypes.object, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + languageProfileId: PropTypes.object, + seriesType: PropTypes.object.isRequired, + albumFolder: PropTypes.object.isRequired, + tags: PropTypes.object.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired, + onAddSeriesPress: PropTypes.func.isRequired +}; + +export default AddNewSeriesModalContent; diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContentConnector.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContentConnector.js new file mode 100644 index 000000000..d9fa91776 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContentConnector.js @@ -0,0 +1,105 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import selectSettings from 'Store/Selectors/selectSettings'; +import AddNewSeriesModalContent from './AddNewSeriesModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addSeries, + (state) => state.settings.languageProfiles, + createDimensionsSelector(), + (addSeriesState, languageProfiles, dimensions) => { + const { + isAdding, + addError, + defaults + } = addSeriesState; + + const { + settings, + validationErrors, + validationWarnings + } = selectSettings(defaults, {}, addError); + + return { + isAdding, + addError, + showLanguageProfile: languageProfiles.length > 1, + isSmallScreen: dimensions.isSmallScreen, + validationErrors, + validationWarnings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setAddSeriesDefault, + addSeries +}; + +class AddNewSeriesModalContentConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setAddSeriesDefault({ [name]: value }); + } + + onAddSeriesPress = (searchForMissingEpisodes) => { + const { + foreignArtistId, + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + albumFolder, + tags + } = this.props; + + this.props.addSeries({ + foreignArtistId, + rootFolderPath: rootFolderPath.value, + monitor: monitor.value, + qualityProfileId: qualityProfileId.value, + languageProfileId: languageProfileId.value, + albumFolder: albumFolder.value, + tags: tags.value, + searchForMissingEpisodes + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddNewSeriesModalContentConnector.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + rootFolderPath: PropTypes.object, + monitor: PropTypes.object.isRequired, + qualityProfileId: PropTypes.object, + languageProfileId: PropTypes.object, + albumFolder: PropTypes.object.isRequired, + tags: PropTypes.object.isRequired, + onModalClose: PropTypes.func.isRequired, + setAddSeriesDefault: PropTypes.func.isRequired, + addSeries: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesModalContentConnector); diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.css b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.css new file mode 100644 index 000000000..38ccffb4d --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.css @@ -0,0 +1,40 @@ +.searchResult { + display: flex; + margin: 20px 0; + padding: 20px; + width: 100%; + background-color: $white; + color: inherit; + transition: background 500ms; + + &:hover { + background-color: #eaf2ff; + color: inherit; + text-decoration: none; + } +} + +.poster { + flex: 0 0 170px; + margin-right: 20px; + height: 250px; +} + +.title { + font-weight: 300; + font-size: 36px; +} + +.year { + margin-left: 10px; + color: $disabledColor; +} + +.alreadyExistsIcon { + margin-left: 10px; + color: #37bc9b; +} + +.overview { + margin-top: 20px; +} diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js new file mode 100644 index 000000000..428a1a5fb --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js @@ -0,0 +1,169 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import HeartRating from 'Components/HeartRating'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import ArtistPoster from 'Artist/ArtistPoster'; +import AddNewSeriesModal from './AddNewSeriesModal'; +import styles from './AddNewSeriesSearchResult.css'; + +class AddNewSeriesSearchResult extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNewAddSeriesModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (!prevProps.isExistingSeries && this.props.isExistingSeries) { + this.onAddSerisModalClose(); + } + } + + // + // Listeners + + onPress = () => { + this.setState({ isNewAddSeriesModalOpen: true }); + } + + onAddSerisModalClose = () => { + this.setState({ isNewAddSeriesModalOpen: false }); + } + + // + // Render + + render() { + const { + foreignArtistId, + artistName, + nameSlug, + year, + network, + status, + overview, + seasonCount, + ratings, + images, + isExistingSeries, + isSmallScreen + } = this.props; + + const linkProps = isExistingSeries ? { to: `/series/${nameSlug}` } : { onPress: this.onPress }; + let seasons = '1 Season'; + + if (seasonCount > 1) { + seasons = `${seasonCount} Seasons`; + } + + return ( + + { + !isSmallScreen && + + } + +
+
+ {artistName} + + { + !name.contains(year) && !!year && + ({year}) + } + + { + isExistingSeries && + + } +
+ +
+ + + { + !!network && + + } + + { + !!seasonCount && + + } + + { + status === 'ended' && + + } +
+ +
+ {overview} +
+
+ + + + ); + } +} + +AddNewSeriesSearchResult.propTypes = { + foreignArtistId: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + year: PropTypes.number, + network: PropTypes.string, + status: PropTypes.string.isRequired, + overview: PropTypes.string, + seasonCount: PropTypes.number, + ratings: PropTypes.object.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + isExistingSeries: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired +}; + +export default AddNewSeriesSearchResult; diff --git a/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResultConnector.js b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResultConnector.js new file mode 100644 index 000000000..5ba942270 --- /dev/null +++ b/frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResultConnector.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import AddNewSeriesSearchResult from './AddNewSeriesSearchResult'; + +function createMapStateToProps() { + return createSelector( + createExistingSeriesSelector(), + createDimensionsSelector(), + (isExistingSeries, dimensions) => { + return { + isExistingSeries, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(AddNewSeriesSearchResult); diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js new file mode 100644 index 000000000..0f0e2ce1f --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import ImportSeriesTableConnector from './ImportSeriesTableConnector'; +import ImportSeriesFooterConnector from './ImportSeriesFooterConnector'; + +class ImportSeries extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + contentBody: null, + scrollTop: 0 + }; + } + + // + // Control + + setContentBodyRef = (ref) => { + this.setState({ contentBody: ref }); + } + + // + // Listeners + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState, { parseIds: false }); + } + + onSelectAllChange = ({ value }) => { + // Only select non-dupes + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onRemoveSelectedStateItem = (id) => { + this.setState((state) => { + const selectedState = Object.assign({}, state.selectedState); + delete selectedState[id]; + + return { + ...state, + selectedState + }; + }); + } + + onInputChange = ({ name, value }) => { + this.props.onInputChange(this.getSelectedIds(), name, value); + } + + onImportPress = () => { + this.props.onImportPress(this.getSelectedIds()); + } + + onScroll = ({ scrollTop }) => { + this.setState({ scrollTop }); + } + + // + // Render + + render() { + const { + rootFolderId, + path, + rootFoldersFetching, + rootFoldersPopulated, + rootFoldersError, + unmappedFolders, + showLanguageProfile + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + contentBody + } = this.state; + + return ( + + + { + rootFoldersFetching && !rootFoldersPopulated && + + } + + { + !rootFoldersFetching && !!rootFoldersError && +
Unable to load root folders
+ } + + { + !rootFoldersError && rootFoldersPopulated && !unmappedFolders.length && +
+ All series in {path} have been imported +
+ } + + { + !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody && + + } +
+ + { + !rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && + + } +
+ ); + } +} + +ImportSeries.propTypes = { + rootFolderId: PropTypes.number.isRequired, + path: PropTypes.string, + rootFoldersFetching: PropTypes.bool.isRequired, + rootFoldersPopulated: PropTypes.bool.isRequired, + rootFoldersError: PropTypes.object, + unmappedFolders: PropTypes.arrayOf(PropTypes.object), + items: PropTypes.arrayOf(PropTypes.object), + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onImportPress: PropTypes.func.isRequired +}; + +ImportSeries.defaultProps = { + unmappedFolders: [] +}; + +export default ImportSeries; diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesConnector.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesConnector.js new file mode 100644 index 000000000..55f153681 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesConnector.js @@ -0,0 +1,121 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setImportSeriesValue, importSeries, clearImportSeries } from 'Store/Actions/importSeriesActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import { setAddSeriesDefault } from 'Store/Actions/addSeriesActions'; +import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape'; +import ImportSeries from './ImportSeries'; + +function createMapStateToProps() { + return createSelector( + (state, { match }) => match, + (state) => state.rootFolders, + (state) => state.addSeries, + (state) => state.importSeries, + (state) => state.settings.languageProfiles, + (match, rootFolders, addSeries, importSeriesState, languageProfiles) => { + const { + isFetching: rootFoldersFetching, + isPopulated: rootFoldersPopulated, + error: rootFoldersError, + items + } = rootFolders; + + const rootFolderId = parseInt(match.params.rootFolderId); + + const result = { + rootFolderId, + rootFoldersFetching, + rootFoldersPopulated, + rootFoldersError, + showLanguageProfile: languageProfiles.items.length > 1 + }; + + if (items.length) { + const rootFolder = _.find(items, { id: rootFolderId }); + + return { + ...result, + ...rootFolder, + items: importSeriesState.items + }; + } + + return result; + } + ); +} + +const mapDispatchToProps = { + setImportSeriesValue, + importSeries, + clearImportSeries, + fetchRootFolders, + setAddSeriesDefault +}; + +class ImportSeriesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.rootFoldersPopulated) { + this.props.fetchRootFolders(); + } + } + + componentWillUnmount() { + this.props.clearImportSeries(); + } + + // + // Listeners + + onInputChange = (ids, name, value) => { + this.props.setAddSeriesDefault({ [name]: value }); + + ids.forEach((id) => { + this.props.setImportSeriesValue({ + id, + [name]: value + }); + }); + } + + onImportPress = (ids) => { + this.props.importSeries({ ids }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +const routeMatchShape = createRouteMatchShape({ + rootFolderId: PropTypes.string.isRequired +}); + +ImportSeriesConnector.propTypes = { + match: routeMatchShape.isRequired, + rootFoldersPopulated: PropTypes.bool.isRequired, + setImportSeriesValue: PropTypes.func.isRequired, + importSeries: PropTypes.func.isRequired, + clearImportSeries: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired, + setAddSeriesDefault: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesConnector); diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.css b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.css new file mode 100644 index 000000000..1df1b8c90 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.css @@ -0,0 +1,27 @@ +.inputContainer { + margin-right: 20px; + min-width: 150px; +} + +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.importButtonContainer { + display: flex; + align-items: center; +} + +.importButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + height: 35px; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin: 0 10px 0 12px; + text-align: left; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js new file mode 100644 index 000000000..98c4c7ff2 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js @@ -0,0 +1,263 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import CheckInput from 'Components/Form/CheckInput'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import styles from './ImportSeriesFooter.css'; + +const MIXED = 'mixed'; + +class ImportSeriesFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultSeasonFolder, + defaultSeriesType + } = props; + + this.state = { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + seriesType: defaultSeriesType, + seasonFolder: defaultSeasonFolder + }; + } + + componentDidUpdate(prevProps, prevState) { + const { + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultSeriesType, + defaultSeasonFolder, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + isSeriesTypeMixed, + isSeasonFolderMixed + } = this.props; + + const { + monitor, + qualityProfileId, + languageProfileId, + seriesType, + seasonFolder + } = this.state; + + const newState = {}; + + if (isMonitorMixed && monitor !== MIXED) { + newState.monitor = MIXED; + } else if (!isMonitorMixed && monitor !== defaultMonitor) { + newState.monitor = defaultMonitor; + } + + if (isQualityProfileIdMixed && qualityProfileId !== MIXED) { + newState.qualityProfileId = MIXED; + } else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) { + newState.qualityProfileId = defaultQualityProfileId; + } + + if (isLanguageProfileIdMixed && languageProfileId !== MIXED) { + newState.languageProfileId = MIXED; + } else if (!isLanguageProfileIdMixed && languageProfileId !== defaultLanguageProfileId) { + newState.languageProfileId = defaultLanguageProfileId; + } + + if (isSeriesTypeMixed && seriesType !== MIXED) { + newState.seriesType = MIXED; + } else if (!isSeriesTypeMixed && seriesType !== defaultSeriesType) { + newState.seriesType = defaultSeriesType; + } + + if (isSeasonFolderMixed && seasonFolder != null) { + newState.seasonFolder = null; + } else if (!isSeasonFolderMixed && seasonFolder !== defaultSeasonFolder) { + newState.seasonFolder = defaultSeasonFolder; + } + + if (!_.isEmpty(newState)) { + this.setState(newState); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + this.props.onInputChange({ name, value }); + } + + // + // Render + + render() { + const { + selectedCount, + isImporting, + isLookingUpSeries, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + isSeriesTypeMixed, + showLanguageProfile, + onImportPress + } = this.props; + + const { + monitor, + qualityProfileId, + languageProfileId, + seriesType, + seasonFolder + } = this.state; + + return ( + +
+
+ Monitor +
+ + +
+ +
+
+ Quality Profile +
+ + +
+ + { + showLanguageProfile && + +
+
+ Language Profile +
+ + +
+ } + +
+
+ Series Type +
+ + +
+ +
+
+ Season Folder +
+ + +
+ +
+
+   +
+ +
+ + Import {selectedCount} Series + + + { + isLookingUpSeries && + + } + + { + isLookingUpSeries && + 'Processing Folders' + } +
+
+
+ ); + } +} + +ImportSeriesFooter.propTypes = { + selectedCount: PropTypes.number.isRequired, + isImporting: PropTypes.bool.isRequired, + isLookingUpSeries: PropTypes.bool.isRequired, + defaultMonitor: PropTypes.string.isRequired, + defaultQualityProfileId: PropTypes.number, + defaultLanguageProfileId: PropTypes.number, + defaultSeriesType: PropTypes.string.isRequired, + defaultSeasonFolder: PropTypes.bool.isRequired, + isMonitorMixed: PropTypes.bool.isRequired, + isQualityProfileIdMixed: PropTypes.bool.isRequired, + isLanguageProfileIdMixed: PropTypes.bool.isRequired, + isSeriesTypeMixed: PropTypes.bool.isRequired, + isSeasonFolderMixed: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onImportPress: PropTypes.func.isRequired +}; + +export default ImportSeriesFooter; diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooterConnector.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooterConnector.js new file mode 100644 index 000000000..00fb7835a --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooterConnector.js @@ -0,0 +1,57 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import ImportSeriesFooter from './ImportSeriesFooter'; + +function isMixed(items, selectedIds, defaultValue, key) { + return _.some(items, (series) => { + return selectedIds.indexOf(series.id) > -1 && series[key] !== defaultValue; + }); +} + +function createMapStateToProps() { + return createSelector( + (state) => state.addSeries, + (state) => state.importSeries, + (state, { selectedIds }) => selectedIds, + (addSeries, importSeries, selectedIds) => { + const { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + seriesType: defaultSeriesType, + seasonFolder: defaultSeasonFolder + } = addSeries.defaults; + + const items = importSeries.items; + + const isLookingUpSeries = _.some(importSeries.items, (series) => { + return !series.isPopulated && series.error == null; + }); + + const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); + const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId'); + const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId'); + const isSeriesTypeMixed = isMixed(items, selectedIds, defaultSeriesType, 'seriesType'); + const isSeasonFolderMixed = isMixed(items, selectedIds, defaultSeasonFolder, 'seasonFolder'); + + return { + selectedCount: selectedIds.length, + isImporting: importSeries.isImporting, + isLookingUpSeries, + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultSeriesType, + defaultSeasonFolder, + isMonitorMixed, + isQualityProfileIdMixed, + isLanguageProfileIdMixed, + isSeriesTypeMixed, + isSeasonFolderMixed + }; + } + ); +} + +export default connect(createMapStateToProps)(ImportSeriesFooter); diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.css b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.css new file mode 100644 index 000000000..36a57ea73 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.css @@ -0,0 +1,45 @@ +.folder { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 200px; +} + +.monitor { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 200px; + min-width: 185px; +} + +.qualityProfile, +.languageProfile { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 250px; + min-width: 170px; +} + +.seriesType { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 200px; + min-width: 120px; +} + +.seasonFolder { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 150px; + min-width: 120px; +} + +.series { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 1 400px; + min-width: 300px; +} + +.detailsIcon { + margin-left: 8px; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js new file mode 100644 index 000000000..a01343f97 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Popover from 'Components/Tooltip/Popover'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; +import SeriesMonitoringOptionsPopoverContent from 'AddArtist/SeriesMonitoringOptionsPopoverContent'; +import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent'; +import styles from './ImportSeriesHeader.css'; + +function ImportSeriesHeader(props) { + const { + showLanguageProfile, + allSelected, + allUnselected, + onSelectAllChange + } = props; + + return ( + + + + + Folder + + + + Monitor + + + } + title="Monitoring Options" + body={} + position={tooltipPositions.RIGHT} + /> + + + + Quality Profile + + + { + showLanguageProfile && + + Language Profile + + } + + + Series Type + + + } + title="Series Type" + body={} + position={tooltipPositions.RIGHT} + /> + + + + Season Folder + + + + Series + + + ); +} + +ImportSeriesHeader.propTypes = { + showLanguageProfile: PropTypes.bool.isRequired, + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default ImportSeriesHeader; diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.css b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.css new file mode 100644 index 000000000..10329ea1c --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.css @@ -0,0 +1,52 @@ +.selectInput { + composes: input from 'Components/Form/CheckInput.css'; +} + +.folder { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 200px; + line-height: 36px; +} + +.monitor { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 200px; + min-width: 185px; +} + +.qualityProfile, +.languageProfile { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 250px; + min-width: 170px; +} + +.seriesType { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 200px; + min-width: 120px; +} + +.seasonFolder { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 150px; + min-width: 120px; +} + +.series { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 1 400px; + min-width: 300px; +} + +.hideLanguageProfile { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + display: none; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js new file mode 100644 index 000000000..f8733d174 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js @@ -0,0 +1,121 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; +import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector'; +import styles from './ImportSeriesRow.css'; + +function ImportSeriesRow(props) { + const { + style, + id, + monitor, + qualityProfileId, + languageProfileId, + seasonFolder, + seriesType, + selectedSeries, + isExistingSeries, + showLanguageProfile, + isSelected, + onSelectedChange, + onInputChange + } = props; + + return ( + + + + + {id} + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +ImportSeriesRow.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.string.isRequired, + monitor: PropTypes.string.isRequired, + qualityProfileId: PropTypes.number.isRequired, + languageProfileId: PropTypes.number.isRequired, + seriesType: PropTypes.string.isRequired, + seasonFolder: PropTypes.bool.isRequired, + selectedSeries: PropTypes.object, + isExistingSeries: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + queued: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +ImportSeriesRow.defaultsProps = { + items: [] +}; + +export default ImportSeriesRow; diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRowConnector.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRowConnector.js new file mode 100644 index 000000000..cdc5bac4b --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRowConnector.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import ImportSeriesRow from './ImportSeriesRow'; + +function createImportSeriesItemSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.importSeries.items, + (id, items) => { + return _.find(items, { id }) || {}; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createImportSeriesItemSelector(), + createAllSeriesSelector(), + (item, series) => { + const selectedSeries = item && item.selectedSeries; + const isExistingSeries = !!selectedSeries && _.some(series, { tvdbId: selectedSeries.tvdbId }); + + return { + ...item, + isExistingSeries + }; + } + ); +} + +const mapDispatchToProps = { + queueLookupSeries, + setImportSeriesValue +}; + +class ImportSeriesRowConnector extends Component { + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setImportSeriesValue({ + id: this.props.id, + [name]: value + }); + } + + // + // Render + + render() { + // Don't show the row until we have the information we require for it. + + const { + items, + monitor, + seriesType, + seasonFolder + } = this.props; + + if (!items || !monitor || !seriesType || !seasonFolder == null) { + return null; + } + + return ( + + ); + } +} + +ImportSeriesRowConnector.propTypes = { + rootFolderId: PropTypes.number.isRequired, + id: PropTypes.string.isRequired, + monitor: PropTypes.string, + seriesType: PropTypes.string, + seasonFolder: PropTypes.bool, + items: PropTypes.arrayOf(PropTypes.object), + queueLookupSeries: PropTypes.func.isRequired, + setImportSeriesValue: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRowConnector); diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesSelected.css b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesSelected.css new file mode 100644 index 000000000..efc6dccb3 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesSelected.css @@ -0,0 +1,3 @@ +.input { + composes: input from 'Components/Form/CheckInput.css'; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js new file mode 100644 index 000000000..7db302686 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js @@ -0,0 +1,213 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import VirtualTable from 'Components/Table/VirtualTable'; +import ImportSeriesHeader from './ImportSeriesHeader'; +import ImportSeriesRowConnector from './ImportSeriesRowConnector'; + +class ImportSeriesTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._table = null; + } + + componentDidMount() { + const { + unmappedFolders, + defaultMonitor, + defaultQualityProfileId, + defaultLanguageProfileId, + defaultSeriesType, + defaultSeasonFolder, + onSeriesLookup, + onSetImportSeriesValue + } = this.props; + + const values = { + monitor: defaultMonitor, + qualityProfileId: defaultQualityProfileId, + languageProfileId: defaultLanguageProfileId, + seriesType: defaultSeriesType, + seasonFolder: defaultSeasonFolder + }; + + unmappedFolders.forEach((unmappedFolder) => { + const id = unmappedFolder.name; + + onSeriesLookup(id, unmappedFolder.path); + + onSetImportSeriesValue({ + id, + ...values + }); + }); + } + + // This isn't great, but it's the most reliable way to ensure the items + // are checked off even if they aren't actually visible since the cells + // are virtualized. + + componentDidUpdate(prevProps) { + const { + items, + selectedState, + onSelectedChange, + onRemoveSelectedStateItem + } = this.props; + + prevProps.items.forEach((prevItem) => { + const { + id + } = prevItem; + + const item = _.find(items, { id }); + + if (!item) { + onRemoveSelectedStateItem(id); + return; + } + + const selectedSeries = item.selectedSeries; + const isSelected = selectedState[id]; + + const isExistingSeries = !!selectedSeries && + _.some(prevProps.allSeries, { tvdbId: selectedSeries.tvdbId }); + + // Props doesn't have a selected series or + // the selected series is an existing series. + if ((selectedSeries && !prevItem.selectedSeries) || (isExistingSeries && !prevItem.selectedSeries)) { + onSelectedChange({ id, value: false }); + + return; + } + + // State is selected, but a series isn't selected or + // the selected series is an existing series. + if (isSelected && (!selectedSeries || isExistingSeries)) { + onSelectedChange({ id, value: false }); + + return; + } + + // A series is being selected that wasn't previously selected. + if (selectedSeries && selectedSeries !== prevItem.selectedSeries) { + onSelectedChange({ id, value: true }); + + return; + } + }); + + // Forces the table to re-render if the selected state + // has changed otherwise it will be stale. + + if (prevProps.selectedState !== selectedState && this._table) { + this._table.forceUpdateGrid(); + } + } + + // + // Control + + setTableRef = (ref) => { + this._table = ref; + } + + rowRenderer = ({ key, rowIndex, style }) => { + const { + rootFolderId, + items, + selectedState, + showLanguageProfile, + onSelectedChange + } = this.props; + + const item = items[rowIndex]; + + return ( + + ); + } + + // + // Render + + render() { + const { + items, + allSelected, + allUnselected, + isSmallScreen, + contentBody, + showLanguageProfile, + scrollTop, + onSelectAllChange, + onScroll + } = this.props; + + if (!items.length) { + return null; + } + + return ( + + } + onScroll={onScroll} + /> + ); + } +} + +ImportSeriesTable.propTypes = { + rootFolderId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object), + unmappedFolders: PropTypes.arrayOf(PropTypes.object), + defaultMonitor: PropTypes.string.isRequired, + defaultQualityProfileId: PropTypes.number, + defaultLanguageProfileId: PropTypes.number, + defaultSeriesType: PropTypes.string.isRequired, + defaultSeasonFolder: PropTypes.bool.isRequired, + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + selectedState: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + allSeries: PropTypes.arrayOf(PropTypes.object), + contentBody: PropTypes.object.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + scrollTop: PropTypes.number.isRequired, + onSelectAllChange: PropTypes.func.isRequired, + onSelectedChange: PropTypes.func.isRequired, + onRemoveSelectedStateItem: PropTypes.func.isRequired, + onSeriesLookup: PropTypes.func.isRequired, + onSetImportSeriesValue: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ImportSeriesTable; diff --git a/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTableConnector.js b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTableConnector.js new file mode 100644 index 000000000..a09d5fa80 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTableConnector.js @@ -0,0 +1,44 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import ImportSeriesTable from './ImportSeriesTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.addSeries, + (state) => state.importSeries, + (state) => state.app.dimensions, + createAllSeriesSelector(), + (addSeries, importSeries, dimensions, allSeries) => { + return { + defaultMonitor: addSeries.defaults.monitor, + defaultQualityProfileId: addSeries.defaults.qualityProfileId, + defaultLanguageProfileId: addSeries.defaults.languageProfileId, + defaultSeriesType: addSeries.defaults.seriesType, + defaultSeasonFolder: addSeries.defaults.seasonFolder, + items: importSeries.items, + isSmallScreen: dimensions.isSmallScreen, + allSeries + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSeriesLookup(name, path) { + dispatch(queueLookupSeries({ + name, + path, + term: name + })); + }, + + onSetImportSeriesValue(values) { + dispatch(setImportSeriesValue(values)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ImportSeriesTable); diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.css b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.css new file mode 100644 index 000000000..83aa37175 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.css @@ -0,0 +1,8 @@ +.series { + padding: 10px 20px; + width: 100%; + + &:hover { + background-color: $menuItemHoverColor; + } +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.js b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.js new file mode 100644 index 000000000..d82cdc924 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResult.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import ImportSeriesTitle from './ImportSeriesTitle'; +import styles from './ImportSeriesSearchResult.css'; + +class ImportSeriesSearchResult extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.tvdbId); + } + + // + // Render + + render() { + const { + title, + year, + network, + isExistingSeries + } = this.props; + + return ( + + + + ); + } +} + +ImportSeriesSearchResult.propTypes = { + tvdbId: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + network: PropTypes.string, + isExistingSeries: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default ImportSeriesSearchResult; diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResultConnector.js b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResultConnector.js new file mode 100644 index 000000000..81bb3059b --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSearchResultConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector'; +import ImportSeriesSearchResult from './ImportSeriesSearchResult'; + +function createMapStateToProps() { + return createSelector( + createExistingSeriesSelector(), + (isExistingSeries) => { + return { + isExistingSeries + }; + } + ); +} + +export default connect(createMapStateToProps)(ImportSeriesSearchResult); diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css new file mode 100644 index 000000000..be0acafca --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.css @@ -0,0 +1,72 @@ +.tether { + z-index: 2000; +} + +.button { + composes: link from 'Components/Link/Link.css'; + + position: relative; + display: flex; + align-items: center; + padding: 6px 16px; + width: 100%; + height: 35px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; +} + +.loading { + display: inline-block; +} + +.warningIcon { + margin-right: 8px; +} + +.existing { + margin-left: 5px; +} + +.dropdownArrowContainer { + position: absolute; + right: 16px; +} + +.contentContainer { + margin-top: 4px; + padding: 0 8px; + width: 400px; +} + +.content { + padding: 4px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} + +.searchContainer { + display: flex; +} + +.searchIconContainer { + width: 58px; + border: 1px solid $inputBorderColor; + border-right: none; + border-radius: 4px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + background-color: #edf1f2; + text-align: center; + line-height: 33px; +} + +.searchInput { + composes: text from 'Components/Form/TextInput.css'; + + /*border-left: 0;*/ + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js new file mode 100644 index 000000000..df095a79b --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeries.js @@ -0,0 +1,268 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import TetherComponent from 'react-tether'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import TextInput from 'Components/Form/TextInput'; +import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnector'; +import ImportSeriesTitle from './ImportSeriesTitle'; +import styles from './ImportSeriesSelectSeries.css'; + +const tetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ], + attachment: 'top center', + targetAttachment: 'bottom center' +}; + +class ImportSeriesSelectSeries extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._seriesLookupTimeout = null; + + this.state = { + term: props.id, + isOpen: false + }; + } + + // + // Control + + _setButtonRef = (ref) => { + this._buttonRef = ref; + } + + _setContentRef = (ref) => { + this._contentRef = ref; + } + + _addListener() { + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const button = ReactDOM.findDOMNode(this._buttonRef); + const content = ReactDOM.findDOMNode(this._contentRef); + + if (!button) { + return; + } + + if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) { + this.setState({ isOpen: false }); + this._removeListener(); + } + } + + onPress = () => { + if (this.state.isOpen) { + this._removeListener(); + } else { + this._addListener(); + } + + this.setState({ isOpen: !this.state.isOpen }); + } + + onSearchInputChange = ({ value }) => { + if (this._seriesLookupTimeout) { + clearTimeout(this._seriesLookupTimeout); + } + + this.setState({ term: value }, () => { + this._seriesLookupTimeout = setTimeout(() => { + this.props.onSearchInputChange(value); + }, 200); + }); + } + + onSeriesSelect = (tvdbId) => { + this.setState({ isOpen: false }); + + this.props.onSeriesSelect(tvdbId); + } + + // + // Render + + render() { + const { + selectedSeries, + isExistingSeries, + isFetching, + isPopulated, + error, + items, + queued, + onSeriesSelect + } = this.props; + + const errorMessage = error && + error.responseJSON && + error.responseJSON.message; + + return ( + + + { + queued && !isPopulated && + + } + + { + isPopulated && selectedSeries && isExistingSeries && + + } + + { + isPopulated && selectedSeries && + + } + + { + isPopulated && !selectedSeries && +
+ + + No match found! +
+ } + + { + !isFetching && !!error && +
+ + + Search failed, please try again later. +
+ } + +
+ +
+ + + { + this.state.isOpen && +
+
+
+
+ +
+ + +
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
+
+ } +
+ ); + } +} + +ImportSeriesSelectSeries.propTypes = { + id: PropTypes.string.isRequired, + selectedSeries: PropTypes.object, + isExistingSeries: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + queued: PropTypes.bool.isRequired, + onSearchInputChange: PropTypes.func.isRequired, + onSeriesSelect: PropTypes.func.isRequired +}; + +ImportSeriesSelectSeries.defaultProps = { + isFetching: true, + isPopulated: false, + items: [], + queued: true +}; + +export default ImportSeriesSelectSeries; diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeriesConnector.js b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeriesConnector.js new file mode 100644 index 000000000..ac4ac79b5 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesSelectSeriesConnector.js @@ -0,0 +1,71 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions'; +import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector'; +import ImportSeriesSelectSeries from './ImportSeriesSelectSeries'; + +function createMapStateToProps() { + return createSelector( + createImportSeriesItemSelector(), + (item) => { + return item; + } + ); +} + +const mapDispatchToProps = { + queueLookupSeries, + setImportSeriesValue +}; + +class ImportSeriesSelectSeriesConnector extends Component { + + // + // Listeners + + onSearchInputChange = (term) => { + this.props.queueLookupSeries({ + name: this.props.id, + term + }); + } + + onSeriesSelect = (tvdbId) => { + const { + id, + items + } = this.props; + + this.props.setImportSeriesValue({ + id, + selectedSeries: _.find(items, { tvdbId }) + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportSeriesSelectSeriesConnector.propTypes = { + id: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object), + selectedSeries: PropTypes.object, + isSelected: PropTypes.bool, + queueLookupSeries: PropTypes.func.isRequired, + setImportSeriesValue: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesSelectSeriesConnector); diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.css b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.css new file mode 100644 index 000000000..f6ae0f4e6 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.css @@ -0,0 +1,17 @@ +.titleContainer { + display: flex; + align-items: center; +} + +.title { + margin-right: 5px; +} + +.year { + margin-left: 5px; + color: $disabledColor; +} + +.existing { + margin-left: 5px; +} diff --git a/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.js b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.js new file mode 100644 index 000000000..3cb6a55dc --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/Import/SelectSeries/ImportSeriesTitle.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; +import styles from './ImportSeriesTitle.css'; + +function ImportSeriesTitle(props) { + const { + title, + year, + network, + isExistingSeries + } = props; + + return ( +
+
+ {title} + + { + !title.contains(year) && + ({year}) + } +
+ + { + !!network && + + } + + { + isExistingSeries && + + } +
+ ); +} + +ImportSeriesTitle.propTypes = { + title: PropTypes.string.isRequired, + year: PropTypes.number.isRequired, + network: PropTypes.string, + isExistingSeries: PropTypes.bool.isRequired +}; + +export default ImportSeriesTitle; diff --git a/frontend/src/AddArtist/ImportSeries/ImportSeries.js b/frontend/src/AddArtist/ImportSeries/ImportSeries.js new file mode 100644 index 000000000..0ee3eaf47 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/ImportSeries.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import { Route } from 'react-router-dom'; +import Switch from 'Components/Router/Switch'; +import ImportSeriesSelectFolderConnector from 'AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector'; +import ImportSeriesConnector from 'AddArtist/ImportSeries/Import/ImportSeriesConnector'; + +class ImportSeries extends Component { + + // + // Render + + render() { + return ( + + + + + + ); + } +} + +export default ImportSeries; diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.css b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.css new file mode 100644 index 000000000..d9c5ccb01 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.css @@ -0,0 +1,18 @@ +.link { + composes: link from 'Components/Link/Link.css'; + + display: block; +} + +.freeSpace, +.unmappedFolders { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 45px; +} diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.js b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.js new file mode 100644 index 000000000..3ba5669e3 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRow.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './ImportSeriesRootFolderRow.css'; + +function ImportSeriesRootFolderRow(props) { + const { + id, + path, + freeSpace, + unmappedFolders, + onDeletePress + } = props; + + const unmappedFoldersCount = unmappedFolders.length || '-'; + + return ( + + + + {path} + + + + + {formatBytes(freeSpace) || '-'} + + + + {unmappedFoldersCount} + + + + + + + ); +} + +ImportSeriesRootFolderRow.propTypes = { + id: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + freeSpace: PropTypes.number.isRequired, + unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired, + onDeletePress: PropTypes.func.isRequired +}; + +ImportSeriesRootFolderRow.defaultProps = { + freeSpace: 0, + unmappedFolders: [] +}; + +export default ImportSeriesRootFolderRow; diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRowConnector.js b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRowConnector.js new file mode 100644 index 000000000..f0fb03921 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesRootFolderRowConnector.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { deleteRootFolder } from 'Store/Actions/rootFolderActions'; +import ImportSeriesRootFolderRow from './ImportSeriesRootFolderRow'; + +function createMapStateToProps() { + return createSelector( + () => { + return { + }; + } + ); +} + +const mapDispatchToProps = { + deleteRootFolder +}; + +class ImportSeriesRootFolderRowConnector extends Component { + + // + // Listeners + + onDeletePress = () => { + this.props.deleteRootFolder({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportSeriesRootFolderRowConnector.propTypes = { + id: PropTypes.number.isRequired, + deleteRootFolder: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRootFolderRowConnector); diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.css b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.css new file mode 100644 index 000000000..030da96fb --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.css @@ -0,0 +1,32 @@ +.header { + margin-bottom: 40px; + text-align: center; + font-weight: 300; + font-size: 36px; +} + +.tips { + font-size: 20px; +} + +.tip { + font-size: $defaultFontSize; +} + +.code { + font-size: 12px; + font-family: $monoSpaceFontFamily; +} + +.recentFolders { + margin-top: 40px; +} + +.startImport { + margin-top: 40px; + text-align: center; +} + +.importButtonIcon { + margin-right: 8px; +} diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js new file mode 100644 index 000000000..798a0c940 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolder.js @@ -0,0 +1,188 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import ImportSeriesRootFolderRowConnector from './ImportSeriesRootFolderRowConnector'; +import styles from './ImportSeriesSelectFolder.css'; + +const rootFolderColumns = [ + { + name: 'path', + label: 'Path', + isVisible: true + }, + { + name: 'freeSpace', + label: 'Free Space', + isVisible: true + }, + { + name: 'unmappedFolders', + label: 'Unmapped Folders', + isVisible: true + }, + { + name: 'actions', + isVisible: true + } +]; + +class ImportSeriesSelectFolder extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddNewRootFolderModalOpen: false + }; + } + + // + // Lifecycle + + onAddNewRootFolderPress = () => { + this.setState({ isAddNewRootFolderModalOpen: true }); + } + + onNewRootFolderSelect = ({ value }) => { + this.props.onNewRootFolderSelect(value); + } + + onAddRootFolderModalClose = () => { + this.setState({ isAddNewRootFolderModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items + } = this.props; + + return ( + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load root folders
+ } + + { + !error && isPopulated && +
+
+ Import series you already have +
+ +
+ Some tips to ensure the import goes smoothly: +
    +
  • + Make sure your files include the quality in the name. eg. episode.s02e15.bluray.mkv +
  • +
  • + Point Sonarr to the folder containing all of your tv shows not a specific one. eg. "\tv shows\" and not "\tv shows\the simpsons\" +
  • +
+
+ + { + items.length > 0 ? +
+
+ + + { + items.map((rootFolder) => { + return ( + + ); + }) + } + +
+
+ + +
: + +
+ +
+ } + + +
+ } +
+
+ ); + } +} + +ImportSeriesSelectFolder.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onNewRootFolderSelect: PropTypes.func.isRequired, + onDeleteRootFolderPress: PropTypes.func.isRequired +}; + +export default ImportSeriesSelectFolder; diff --git a/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js new file mode 100644 index 000000000..b9f82e376 --- /dev/null +++ b/frontend/src/AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector.js @@ -0,0 +1,87 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { push } from 'react-router-redux'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions'; +import ImportSeriesSelectFolder from './ImportSeriesSelectFolder'; + +function createMapStateToProps() { + return createSelector( + (state) => state.rootFolders, + (rootFolders) => { + return rootFolders; + } + ); +} + +const mapDispatchToProps = { + fetchRootFolders, + addRootFolder, + deleteRootFolder, + push +}; + +class ImportSeriesSelectFolderConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRootFolders(); + } + + componentDidUpdate(prevProps) { + const { + items, + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id); + + if (newRootFolders.length === 1) { + this.props.push(`${window.Sonarr.urlBase}/add/import/${newRootFolders[0].id}`); + } + } + } + + // + // Listeners + + onNewRootFolderSelect = (path) => { + this.props.addRootFolder({ path }); + } + + onDeleteRootFolderPress = (id) => { + this.props.deleteRootFolder({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ImportSeriesSelectFolderConnector.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchRootFolders: PropTypes.func.isRequired, + addRootFolder: PropTypes.func.isRequired, + deleteRootFolder: PropTypes.func.isRequired, + push: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesSelectFolderConnector); diff --git a/frontend/src/AddArtist/SeriesMonitoringOptionsPopoverContent.js b/frontend/src/AddArtist/SeriesMonitoringOptionsPopoverContent.js new file mode 100644 index 000000000..dcd5a4da5 --- /dev/null +++ b/frontend/src/AddArtist/SeriesMonitoringOptionsPopoverContent.js @@ -0,0 +1,46 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; + +function SeriesMonitoringOptionsPopoverContent() { + return ( + + + + + + + + + + + + + + + + ); +} + +export default SeriesMonitoringOptionsPopoverContent; diff --git a/frontend/src/AddArtist/SeriesTypePopoverContent.js b/frontend/src/AddArtist/SeriesTypePopoverContent.js new file mode 100644 index 000000000..e57d49a9e --- /dev/null +++ b/frontend/src/AddArtist/SeriesTypePopoverContent.js @@ -0,0 +1,26 @@ +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; + +function SeriesTypePopoverContent() { + return ( + + + + + + + + ); +} + +export default SeriesTypePopoverContent; diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js new file mode 100644 index 000000000..bf5920ebf --- /dev/null +++ b/frontend/src/App/App.js @@ -0,0 +1,245 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DocumentTitle from 'react-document-title'; +import { Provider } from 'react-redux'; +import { Route, Redirect } from 'react-router-dom'; +import { ConnectedRouter } from 'react-router-redux'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; +import NotFound from 'Components/NotFound'; +import Switch from 'Components/Router/Switch'; +import PageConnector from 'Components/Page/PageConnector'; +import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector'; +import AddNewSeriesConnector from 'AddArtist/AddNewSeries/AddNewSeriesConnector'; +import ImportSeries from 'AddArtist/ImportSeries/ImportSeries'; +import SeriesEditorConnector from 'Artist/Editor/SeriesEditorConnector'; +import SeasonPassConnector from 'SeasonPass/SeasonPassConnector'; +import SeriesDetailsPageConnector from 'Artist/Details/SeriesDetailsPageConnector'; +import CalendarPageConnector from 'Calendar/CalendarPageConnector'; +import HistoryConnector from 'Activity/History/HistoryConnector'; +import QueueConnector from 'Activity/Queue/QueueConnector'; +import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector'; +import MissingConnector from 'Wanted/Missing/MissingConnector'; +import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; +import UISettingsConnector from 'Settings/UI/UISettingsConnector'; +import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector'; +import Profiles from 'Settings/Profiles/Profiles'; +import Quality from 'Settings/Quality/Quality'; +import IndexerSettings from 'Settings/Indexers/IndexerSettings'; +import DownloadClientSettings from 'Settings/DownloadClients/DownloadClientSettings'; +import NotificationSettings from 'Settings/Notifications/NotificationSettings'; +import MetadataSettings from 'Settings/Metadata/MetadataSettings'; +import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; +import Status from 'System/Status/Status'; +import TasksConnector from 'System/Tasks/TasksConnector'; +import BackupsConnector from 'System/Backup/BackupsConnector'; +import UpdatesConnector from 'System/Updates/UpdatesConnector'; +import LogsTableConnector from 'System/Events/LogsTableConnector'; +import Logs from 'System/Logs/Logs'; + +function App({ store, history }) { + return ( + + + + + + {/* + Series + */} + + + + { + window.Sonarr.urlBase && + { + return ( + + ); + }} + /> + } + + + + + + + + + + + + {/* + Calendar + */} + + + + {/* + Activity + */} + + + + + + + + {/* + Wanted + */} + + + + + + {/* + Settings + */} + + + + + + + + + + + + + + + + + + + + {/* + System + */} + + + + + + + + + + + + + + {/* + Not Found + */} + + + + + + + + + ); +} + +App.propTypes = { + store: PropTypes.object.isRequired, + history: PropTypes.object.isRequired +}; + +export default App; diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js new file mode 100644 index 000000000..fe48e67f4 --- /dev/null +++ b/frontend/src/App/AppUpdatedModal.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector'; + +function AppUpdatedModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +AppUpdatedModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + version: PropTypes.string.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModal; diff --git a/frontend/src/App/AppUpdatedModalConnector.js b/frontend/src/App/AppUpdatedModalConnector.js new file mode 100644 index 000000000..bfa0bde38 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppUpdatedModal from './AppUpdatedModal'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + (version) => { + return { + version + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModal); diff --git a/frontend/src/App/AppUpdatedModalContent.css b/frontend/src/App/AppUpdatedModalContent.css new file mode 100644 index 000000000..459ddafc0 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.css @@ -0,0 +1,15 @@ +.version { + margin: 0 3px; + font-weight: bold; +} + +.maintenance { + margin-top: 20px; +} + +.changes { + margin-top: 20px; + padding-bottom: 5px; + font-size: 18px; + border-bottom: 1px solid #e5e5e5; +} diff --git a/frontend/src/App/AppUpdatedModalContent.js b/frontend/src/App/AppUpdatedModalContent.js new file mode 100644 index 000000000..123267501 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContent.js @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import UpdateChanges from 'System/Updates/UpdateChanges'; +import styles from './AppUpdatedModalContent.css'; + +function AppUpdatedModalContent(props) { + const { + version, + isPopulated, + error, + items, + onSeeChangesPress, + onModalClose + } = props; + + const update = items[0]; + + return ( + + + Sonarr Updated + + + +
+ Version {version} of Sonarr has been installed, in order to get the latest changes you'll need to reload Sonarr. +
+ + { + isPopulated && !error && !!update && +
+ { + !update.changes && +
Maintenance release
+ } + + { + !!update.changes && +
+
+ What's new? +
+ + + + +
+ } +
+ } + + { + !isPopulated && !error && + + } +
+ + + + + + +
+ ); +} + +AppUpdatedModalContent.propTypes = { + isOpen: PropTypes.bool.isRequired, + version: PropTypes.string.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeeChangesPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AppUpdatedModalContent; diff --git a/frontend/src/App/AppUpdatedModalContentConnector.js b/frontend/src/App/AppUpdatedModalContentConnector.js new file mode 100644 index 000000000..7acd56f41 --- /dev/null +++ b/frontend/src/App/AppUpdatedModalContentConnector.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import AppUpdatedModalContent from './AppUpdatedModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.version, + (state) => state.system.updates, + (version, updates) => { + const { + isPopulated, + error, + items + } = updates; + + return { + version, + isPopulated, + error, + items + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchUpdates() { + dispatch(fetchUpdates()); + }, + + onSeeChangesPress() { + window.location = `${window.Sonarr.urlBase}/system/updates`; + } + }; +} + +class AppUpdatedModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate() { + this.props.dispatchFetchUpdates(); + } + + // + // Render + + render() { + const { + dispatchFetchUpdates, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +AppUpdatedModalContentConnector.propTypes = { + dispatchFetchUpdates: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector); diff --git a/frontend/src/App/ConnectionLostModal.css b/frontend/src/App/ConnectionLostModal.css new file mode 100644 index 000000000..f0a9d220f --- /dev/null +++ b/frontend/src/App/ConnectionLostModal.css @@ -0,0 +1,3 @@ +.automatic { + margin-top: 20px; +} diff --git a/frontend/src/App/ConnectionLostModal.js b/frontend/src/App/ConnectionLostModal.js new file mode 100644 index 000000000..9ebed8ed3 --- /dev/null +++ b/frontend/src/App/ConnectionLostModal.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './ConnectionLostModal.css'; + +function ConnectionLostModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + Connnection Lost + + + +
+ Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality. +
+ +
+ Sonarr will try to connect automatically, or you can click reload below. +
+
+ + + +
+
+ ); +} + +ConnectionLostModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ConnectionLostModal; diff --git a/frontend/src/App/ConnectionLostModalConnector.js b/frontend/src/App/ConnectionLostModalConnector.js new file mode 100644 index 000000000..8ab8e3cd0 --- /dev/null +++ b/frontend/src/App/ConnectionLostModalConnector.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import ConnectionLostModal from './ConnectionLostModal'; + +function createMapDispatchToProps(dispatch, props) { + return { + onModalClose() { + location.reload(); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal); diff --git a/frontend/src/Artist/ArtistNameLink.js b/frontend/src/Artist/ArtistNameLink.js new file mode 100644 index 000000000..506881cac --- /dev/null +++ b/frontend/src/Artist/ArtistNameLink.js @@ -0,0 +1,20 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'Components/Link/Link'; + +function ArtistNameLink({ nameSlug, artistName }) { + const link = `/series/${nameSlug}`; + + return ( + + {artistName} + + ); +} + +ArtistNameLink.propTypes = { + nameSlug: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired +}; + +export default ArtistNameLink; diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js new file mode 100644 index 000000000..80bc979f9 --- /dev/null +++ b/frontend/src/Artist/ArtistPoster.js @@ -0,0 +1,160 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LazyLoad from 'react-lazyload'; + +const posterPlaceholder = 'data:image/png;base64,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'; + +function findPoster(images) { + return _.find(images, { coverType: 'poster' }); +} + +function getPosterUrl(poster, size) { + if (poster) { + // Remove protocol + let url = poster.url.replace(/^https?:/, ''); + url = url.replace('poster.jpg', `poster-${size}.jpg`); + + return url; + } +} + +class ArtistPoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const pixelRatio = Math.floor(window.devicePixelRatio); + + const { + images, + size + } = props; + + const poster = findPoster(images); + + this.state = { + pixelRatio, + poster, + posterUrl: getPosterUrl(poster, pixelRatio * size), + hasError: false, + isLoaded: false + }; + } + + componentDidUpdate(prevProps) { + const { + images, + size + } = this.props; + + const { + pixelRatio + } = this.state; + + const poster = findPoster(images); + + if (poster && poster.url !== this.state.poster.url) { + this.setState({ + poster, + posterUrl: getPosterUrl(poster, pixelRatio * size), + hasError: false, + isLoaded: false + }); + } + } + + // + // Listeners + + onError = () => { + this.setState({ hasError: true }); + } + + onLoad = () => { + this.setState({ isLoaded: true }); + } + + // + // Render + + render() { + const { + className, + style, + size, + lazy, + overflow + } = this.props; + + const { + posterUrl, + hasError, + isLoaded + } = this.state; + + if (hasError || !posterUrl) { + return ( + + ); + } + + if (lazy) { + return ( + + } + > + + + ); + } + + return ( + + ); + } +} + +ArtistPoster.propTypes = { + className: PropTypes.string, + style: PropTypes.object, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + size: PropTypes.number.isRequired, + lazy: PropTypes.bool.isRequired, + overflow: PropTypes.bool.isRequired +}; + +ArtistPoster.defaultProps = { + size: 250, + lazy: true, + overflow: false +}; + +export default ArtistPoster; diff --git a/frontend/src/Artist/Delete/DeleteArtist.less b/frontend/src/Artist/Delete/DeleteArtist.less new file mode 100644 index 000000000..72670081b --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtist.less @@ -0,0 +1,39 @@ +@import "Content/icons"; + +.delete-series-modal { + + i { + margin-right : 5px; + //.fa-icon-color(white); + + } + + .path { + white-space : nowrap; + font-size : 16px; + padding-bottom : 20px; + } + + .delete-files-info, + .delete-label { + color : @brand-danger-dark; + } + + .delete-files-info { + display : none; + } + + .checkbox { + display : inline-block; + } + + .c-checkbox:hover .check { + border-color : @brand-danger-dark; + } + + input[type=checkbox]:checked + span { + background-color : @brand-danger-dark; + border-color : @brand-danger-dark; + } + +} \ No newline at end of file diff --git a/frontend/src/Artist/Delete/DeleteArtistModal.js b/frontend/src/Artist/Delete/DeleteArtistModal.js new file mode 100644 index 000000000..5b6490c66 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector'; + +function DeleteArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteArtistModal; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.css b/frontend/src/Artist/Delete/DeleteArtistModalContent.css new file mode 100644 index 000000000..dbfef0871 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.css @@ -0,0 +1,12 @@ +.pathContainer { + margin-bottom: 20px; +} + +.pathIcon { + margin-right: 8px; +} + +.deleteFilesMessage { + margin-top: 20px; + color: $dangerColor; +} diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Delete/DeleteArtistModalContent.js new file mode 100644 index 000000000..d30a589de --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './DeleteArtistModalContent.css'; + +class DeleteArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + } + + onDeleteSeriesConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + + this.setState({ deleteFiles: false }); + this.props.onDeletePress(deleteFiles); + } + + // + // Render + + render() { + const { + artistName, + path, + trackFileCount, + sizeOnDisk, + onModalClose + } = this.props; + + const deleteFiles = this.state.deleteFiles; + let deleteFilesLabel = `Delete ${trackFileCount} Track Files`; + let deleteFilesHelpText = 'Delete the track files and artist folder'; + + if (trackFileCount === 0) { + deleteFilesLabel = 'Delete Artist Folder'; + deleteFilesHelpText = 'Delete the artist folder and it\'s contents'; + } + + return ( + + + Delete - {artistName} + + + +
+ + + {path} +
+ + + {deleteFilesLabel} + + + + + { + deleteFiles && +
+
The artist folder {path} and all it's content will be deleted.
+ + { + !!trackFileCount && +
{trackFileCount} track files totaling {formatBytes(sizeOnDisk)}
+ } +
+ } + +
+ + + + + + +
+ ); + } +} + +DeleteArtistModalContent.propTypes = { + artistName: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + trackFileCount: PropTypes.number.isRequired, + sizeOnDisk: PropTypes.number.isRequired, + onDeletePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +DeleteArtistModalContent.defaultProps = { + trackFileCount: 0 +}; + +export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js new file mode 100644 index 000000000..4790780e7 --- /dev/null +++ b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { deleteArtist } from 'Store/Actions/seriesActions'; +import DeleteArtistModalContent from './DeleteArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (series) => { + return series; + } + ); +} + +const mapDispatchToProps = { + deleteArtist +}; + +class DeleteArtistModalContentConnector extends Component { + + // + // Listeners + + onDeletePress = (deleteFiles) => { + this.props.deleteArtist({ + id: this.props.artistId, + deleteFiles + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DeleteArtistModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + onModalClose: PropTypes.func.isRequired, + deleteArtist: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DeleteArtistModalContentConnector); diff --git a/frontend/src/Artist/Details/EpisodeRow.css b/frontend/src/Artist/Details/EpisodeRow.css new file mode 100644 index 000000000..fc7bf0397 --- /dev/null +++ b/frontend/src/Artist/Details/EpisodeRow.css @@ -0,0 +1,26 @@ +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; +} + +.monitored { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 42px; +} + +.episodeNumber { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.language, +.audio, +.video, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Artist/Details/EpisodeRow.js b/frontend/src/Artist/Details/EpisodeRow.js new file mode 100644 index 000000000..5063db63b --- /dev/null +++ b/frontend/src/Artist/Details/EpisodeRow.js @@ -0,0 +1,266 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; +import EpisodeNumber from 'Episode/EpisodeNumber'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; +import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector'; +import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes'; + +import styles from './EpisodeRow.css'; + +class EpisodeRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onManualSearchPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + onMonitorEpisodePress = (monitored, options) => { + this.props.onMonitorEpisodePress(this.props.id, monitored, options); + } + + // + // Render + + render() { + const { + id, + artistId, + episodeFileId, + monitored, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + airDateUtc, + title, + unverifiedSceneNumbering, + isSaving, + seriesMonitored, + seriesType, + episodeFilePath, + episodeFileRelativePath, + alternateTitles, + columns + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'monitored') { + return ( + + + + ); + } + + if (name === 'episodeNumber') { + return ( + + + + ); + } + + if (name === 'title') { + return ( + + + + ); + } + + if (name === 'path') { + return ( + + { + episodeFilePath + } + + ); + } + + if (name === 'relativePath') { + return ( + + { + episodeFileRelativePath + } + + ); + } + + if (name === 'airDateUtc') { + return ( + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'audioInfo') { + return ( + + + + ); + } + + if (name === 'videoCodec') { + return ( + + + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + + return null; + }) + } + + ); + } +} + +EpisodeRow.propTypes = { + id: PropTypes.number.isRequired, + artistId: PropTypes.number.isRequired, + episodeFileId: PropTypes.number, + monitored: PropTypes.bool.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + airDateUtc: PropTypes.string, + title: PropTypes.string.isRequired, + isSaving: PropTypes.bool, + unverifiedSceneNumbering: PropTypes.bool, + seriesMonitored: PropTypes.bool.isRequired, + seriesType: PropTypes.string.isRequired, + episodeFilePath: PropTypes.string, + episodeFileRelativePath: PropTypes.string, + mediaInfo: PropTypes.object, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onMonitorEpisodePress: PropTypes.func.isRequired +}; + +export default EpisodeRow; diff --git a/frontend/src/Artist/Details/EpisodeRowConnector.js b/frontend/src/Artist/Details/EpisodeRowConnector.js new file mode 100644 index 000000000..e827c17b5 --- /dev/null +++ b/frontend/src/Artist/Details/EpisodeRowConnector.js @@ -0,0 +1,29 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import EpisodeRow from './EpisodeRow'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state, { sceneSeasonNumber }) => sceneSeasonNumber, + createArtistSelector(), + createEpisodeFileSelector(), + createCommandsSelector(), + (id, sceneSeasonNumber, series, episodeFile, commands) => { + const alternateTitles = sceneSeasonNumber ? _.filter(series.alternateTitles, { sceneSeasonNumber }) : []; + + return { + seriesMonitored: series.monitored, + seriesType: series.seriesType, + episodeFilePath: episodeFile ? episodeFile.path : null, + episodeFileRelativePath: episodeFile ? episodeFile.relativePath : null, + alternateTitles + }; + } + ); +} +export default connect(createMapStateToProps)(EpisodeRow); diff --git a/frontend/src/Artist/Details/SeriesAlternateTitles.css b/frontend/src/Artist/Details/SeriesAlternateTitles.css new file mode 100644 index 000000000..1af1ae68b --- /dev/null +++ b/frontend/src/Artist/Details/SeriesAlternateTitles.css @@ -0,0 +1,3 @@ +.alternateTitle { + white-space: nowrap; +} diff --git a/frontend/src/Artist/Details/SeriesAlternateTitles.js b/frontend/src/Artist/Details/SeriesAlternateTitles.js new file mode 100644 index 000000000..18d016579 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesAlternateTitles.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './SeriesAlternateTitles.css'; + +function SeriesAlternateTitles({ alternateTitles }) { + return ( +
    + { + alternateTitles.map((alternateTitle) => { + return ( +
  • + {alternateTitle} +
  • + ); + }) + } +
+ ); +} + +SeriesAlternateTitles.propTypes = { + alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export default SeriesAlternateTitles; diff --git a/frontend/src/Artist/Details/SeriesDetails.css b/frontend/src/Artist/Details/SeriesDetails.css new file mode 100644 index 000000000..7a2daccb2 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetails.css @@ -0,0 +1,119 @@ +.innerContentBody { + padding: 0; +} + +.header { + position: relative; + width: 100%; + height: 425px; +} + +.backdrop { + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + background-size: cover; +} + +.backdropOverlay { + position: absolute; + width: 100%; + height: 100%; + background: $black; + opacity: 0.7; +} + +.headerContent { + display: flex; + padding: 30px; + width: 100%; + height: 100%; + color: $white; +} + +.poster { + flex-shrink: 0; + margin-right: 35px; + width: 250px; + height: 368px; +} + +.info { + flex-grow: 1; + overflow: hidden; +} + +.titleContainer { + display: flex; + justify-content: space-between; +} + +.title { + margin-bottom: 5px; + font-weight: 300; + font-size: 50px; + line-height: 50px; +} + +.alternateTitlesIconContainer { + margin-left: 20px; + line-height: 50px; +} + +.seriesNavigationButtons { + white-space: no-wrap; +} + +.seriesNavigationButton { + composes: button from 'Components/Link/IconButton.css'; + + margin-left: 5px; + color: #e1e2e3; + white-space: nowrap; +} + +.details { + font-weight: 300; + font-size: 20px; +} + +.runtime { + margin-right: 15px; +} + +.detailsLabel { + composes: label from 'Components/Label.css'; + + margin: 5px 10px 5px 0; +} + +.sizeOnDisk, +.qualityProfileName, +.network, +.links, +.tags { + margin-left: 8px; + font-weight: 300; + font-size: 17px; +} + +.contentContainer { + padding: 20px; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentContainer { + padding: 20px 0; + } + + .headerContent { + padding: 15px; + } +} + +@media only screen and (max-width: $breakpointLarge) { + .poster { + display: none; + } +} diff --git a/frontend/src/Artist/Details/SeriesDetails.js b/frontend/src/Artist/Details/SeriesDetails.js new file mode 100644 index 000000000..bdf1c2d53 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetails.js @@ -0,0 +1,576 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; +import HeartRating from 'Components/HeartRating'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Label from 'Components/Label'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import Popover from 'Components/Tooltip/Popover'; +import Tooltip from 'Components/Tooltip/Tooltip'; +import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal'; +import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import ArtistPoster from 'Artist/ArtistPoster'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import SeriesAlternateTitles from './SeriesAlternateTitles'; +import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector'; +import SeriesTagsConnector from './SeriesTagsConnector'; +import SeriesDetailsLinks from './SeriesDetailsLinks'; +import styles from './SeriesDetails.css'; + +function getFanartUrl(images) { + const fanartImage = _.find(images, { coverType: 'fanart' }); + if (fanartImage) { + // Remove protocol + return fanartImage.url.replace(/^https?:/, ''); + } +} + +function getExpandedState(newState) { + return { + allExpanded: newState.allSelected, + allCollapsed: newState.allUnselected, + expandedState: newState.selectedState + }; +} + +class SeriesDetails extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOrganizeModalOpen: false, + isManageEpisodesOpen: false, + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false, + allExpanded: false, + allCollapsed: false, + expandedState: {} + }; + } + + // + // Listeners + + onOrganizePress = () => { + this.setState({ isOrganizeModalOpen: true }); + } + + onOrganizeModalClose = () => { + this.setState({ isOrganizeModalOpen: false }); + } + + onManageEpisodesPress = () => { + this.setState({ isManageEpisodesOpen: true }); + } + + onManageEpisodesModalClose = () => { + this.setState({ isManageEpisodesOpen: false }); + } + + onEditSeriesPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditSeriesModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteSeriesPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteSeriesModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onExpandAllPress = () => { + const { + allExpanded, + expandedState + } = this.state; + + this.setState(getExpandedState(selectAll(expandedState, !allExpanded))); + } + + onExpandPress = (seasonNumber, isExpanded) => { + this.setState((state) => { + const convertedState = { + allSelected: state.allExpanded, + allUnselected: state.allCollapsed, + selectedState: state.expandedState + }; + + const newState = toggleSelected(convertedState, [], seasonNumber, isExpanded, false); + + return getExpandedState(newState); + }); + } + + // + // Render + + render() { + const { + id, + tvdbId, + tvMazeId, + imdbId, + title, + runtime, + ratings, + sizeOnDisk, + episodeFileCount, + qualityProfileId, + monitored, + status, + network, + overview, + images, + seasons, + alternateTitles, + tags, + isRefreshing, + isSearching, + isFetching, + isPopulated, + episodesError, + episodeFilesError, + previousSeries, + nextSeries, + onRefreshPress, + onSearchPress + } = this.props; + + const { + isOrganizeModalOpen, + isManageEpisodesOpen, + isEditArtistModalOpen, + isDeleteArtistModalOpen, + allExpanded, + allCollapsed, + expandedState + } = this.state; + + const continuing = status === 'continuing'; + + let episodeFilesCountMessage = 'No episode files'; + + if (episodeFileCount === 1) { + episodeFilesCountMessage = '1 episode file'; + } else if (episodeFileCount > 1) { + episodeFilesCountMessage = `${episodeFileCount} episode files`; + } + + let expandIcon = icons.EXPAND_INDETERMINATE; + + if (allExpanded) { + expandIcon = icons.COLLAPSE; + } else if (allCollapsed) { + expandIcon = icons.EXPAND; + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + +
+
+
+ {title} + + { + !!alternateTitles.length && + + + } + title="Alternate Titles" + body={} + position={tooltipPositions.BOTTOM} + /> + + } +
+ +
+ + + +
+
+ +
+
+ { + !!runtime && + + {runtime} Minutes + + } + + +
+
+ +
+ + + + + + + + + { + !!network && + + } + + + + + + Links + + + } + tooltip={ + + } + kind={kinds.INVERSE} + position={tooltipPositions.BOTTOM} + /> + + { + !!tags.length && + + + + + Tags + + + } + tooltip={} + kind={kinds.INVERSE} + position={tooltipPositions.BOTTOM} + /> + + } +
+ +
+ {overview} +
+
+
+
+ +
+ { + !isPopulated && !episodesError && !episodeFilesError && + + } + + { + !isFetching && episodesError && +
Loading episodes failed
+ } + + { + !isFetching && episodeFilesError && +
Loading episode files failed
+ } + + { + isPopulated && !!seasons.length && +
+ { + seasons.slice(0).reverse().map((season) => { + return ( + + ); + }) + } +
+ } + + { + isPopulated && !seasons.length && +
+ No episode information is available. +
+ } + +
+ + + + + + + + + + + ); + } +} + +SeriesDetails.propTypes = { + id: PropTypes.number.isRequired, + tvdbId: PropTypes.number.isRequired, + tvMazeId: PropTypes.number, + imdbId: PropTypes.string, + title: PropTypes.string.isRequired, + runtime: PropTypes.number.isRequired, + ratings: PropTypes.object.isRequired, + sizeOnDisk: PropTypes.number.isRequired, + episodeFileCount: PropTypes.number, + qualityProfileId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + network: PropTypes.string, + overview: PropTypes.string.isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + seasons: PropTypes.arrayOf(PropTypes.object).isRequired, + alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + isRefreshing: PropTypes.bool.isRequired, + isSearching: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + episodesError: PropTypes.object, + episodeFilesError: PropTypes.object, + previousSeries: PropTypes.object.isRequired, + nextSeries: PropTypes.object.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +export default SeriesDetails; diff --git a/frontend/src/Artist/Details/SeriesDetailsConnector.js b/frontend/src/Artist/Details/SeriesDetailsConnector.js new file mode 100644 index 000000000..e7892341a --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsConnector.js @@ -0,0 +1,184 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions'; +import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import SeriesDetails from './SeriesDetails'; + +function createMapStateToProps() { + return createSelector( + (state, { titleSlug }) => titleSlug, + (state) => state.episodes, + (state) => state.episodeFiles, + createAllSeriesSelector(), + createCommandsSelector(), + (titleSlug, episodes, episodeFiles, allSeries, commands) => { + const sortedSeries = _.orderBy(allSeries, 'sortTitle'); + const seriesIndex = _.findIndex(sortedSeries, { titleSlug }); + const series = sortedSeries[seriesIndex]; + + if (!series) { + return {}; + } + + const previousSeries = sortedSeries[seriesIndex - 1] || _.last(sortedSeries); + const nextSeries = sortedSeries[seriesIndex + 1] || _.first(sortedSeries); + const isSeriesRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_SERIES, artistId: series.id }); + const allSeriesRefreshing = _.some(commands, (command) => command.name === commandNames.REFRESH_SERIES && !command.body.artistId); + const isRefreshing = isSeriesRefreshing || allSeriesRefreshing; + const isSearching = !!findCommand(commands, { name: commandNames.SERIES_SEARCH, artistId: series.id }); + const isRenamingFiles = !!findCommand(commands, { name: commandNames.RENAME_FILES, artistId: series.id }); + const isRenamingSeriesCommand = findCommand(commands, { name: commandNames.RENAME_SERIES }); + const isRenamingSeries = !!(isRenamingSeriesCommand && isRenamingSeriesCommand.body.artistId.indexOf(series.id) > -1); + + const isFetching = episodes.isFetching || episodeFiles.isFetching; + const isPopulated = episodes.isPopulated && episodeFiles.isPopulated; + const episodesError = episodes.error; + const episodeFilesError = episodeFiles.error; + const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => { + if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) && + (alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) { + acc.push(alternateTitle.title); + } + + return acc; + }, []); + + return { + ...series, + alternateTitles, + isRefreshing, + isSearching, + isRenamingFiles, + isRenamingSeries, + isFetching, + isPopulated, + episodesError, + episodeFilesError, + previousSeries, + nextSeries + }; + } + ); +} + +const mapDispatchToProps = { + fetchEpisodes, + clearEpisodes, + fetchEpisodeFiles, + clearEpisodeFiles, + fetchQueueDetails, + clearQueueDetails, + executeCommand +}; + +class SeriesDetailsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this._populate(); + } + + componentDidUpdate(prevProps) { + const { + id, + isRefreshing, + isRenamingFiles, + isRenamingSeries + } = this.props; + + if ( + (prevProps.isRefreshing && !isRefreshing) || + (prevProps.isRenamingFiles && !isRenamingFiles) || + (prevProps.isRenamingSeries && !isRenamingSeries) + ) { + this._populate(); + } + + // If the id has changed we need to clear the episodes/episode + // files and fetch from the server. + + if (prevProps.id !== id) { + this._unpopulate(); + this._populate(); + } + } + + componentWillUnmount() { + this._unpopulate(); + } + + // + // Control + + _populate() { + const artistId = this.props.id; + + this.props.fetchEpisodes({ artistId }); + this.props.fetchEpisodeFiles({ artistId }); + this.props.fetchQueueDetails({ artistId }); + } + + _unpopulate() { + this.props.clearEpisodes(); + this.props.clearEpisodeFiles(); + this.props.clearQueueDetails(); + } + + // + // Listeners + + onRefreshPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_SERIES, + artistId: this.props.id + }); + } + + onSearchPress = () => { + this.props.executeCommand({ + name: commandNames.SERIES_SEARCH, + artistId: this.props.id + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SeriesDetailsConnector.propTypes = { + id: PropTypes.number.isRequired, + titleSlug: PropTypes.string.isRequired, + isRefreshing: PropTypes.bool.isRequired, + isRenamingFiles: PropTypes.bool.isRequired, + isRenamingSeries: PropTypes.bool.isRequired, + fetchEpisodes: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + fetchEpisodeFiles: PropTypes.func.isRequired, + clearEpisodeFiles: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsConnector); diff --git a/frontend/src/Artist/Details/SeriesDetailsLinks.css b/frontend/src/Artist/Details/SeriesDetailsLinks.css new file mode 100644 index 000000000..0f65b9154 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsLinks.css @@ -0,0 +1,13 @@ +.links { + margin: 0; +} + +.link { + white-space: nowrap; +} + +.linkLabel { + composes: label from 'Components/Label.css'; + + cursor: pointer; +} diff --git a/frontend/src/Artist/Details/SeriesDetailsLinks.js b/frontend/src/Artist/Details/SeriesDetailsLinks.js new file mode 100644 index 000000000..9cacdb1a3 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsLinks.js @@ -0,0 +1,84 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import styles from './SeriesDetailsLinks.css'; + +function SeriesDetailsLinks(props) { + const { + tvdbId, + tvMazeId, + imdbId + } = props; + + return ( +
+ + + + + + + + + { + !!tvMazeId && + + + + } + + { + !!imdbId && + + + + } +
+ ); +} + +SeriesDetailsLinks.propTypes = { + tvdbId: PropTypes.number.isRequired, + tvMazeId: PropTypes.number, + imdbId: PropTypes.string +}; + +export default SeriesDetailsLinks; diff --git a/frontend/src/Artist/Details/SeriesDetailsPageConnector.js b/frontend/src/Artist/Details/SeriesDetailsPageConnector.js new file mode 100644 index 000000000..bf440a532 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsPageConnector.js @@ -0,0 +1,76 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { push } from 'react-router-redux'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import NotFound from 'Components/NotFound'; +import SeriesDetailsConnector from './SeriesDetailsConnector'; + +function createMapStateToProps() { + return createSelector( + (state, { match }) => match, + createAllSeriesSelector(), + (match, allSeries) => { + const titleSlug = match.params.titleSlug; + const seriesIndex = _.findIndex(allSeries, { titleSlug }); + + if (seriesIndex > -1) { + return { + titleSlug + }; + } + + return {}; + } + ); +} + +const mapDispatchToProps = { + push +}; + +class SeriesDetailsPageConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + if (!this.props.titleSlug) { + this.props.push(`${window.Sonarr.urlBase}/`); + return; + } + } + + // + // Render + + render() { + const { + titleSlug + } = this.props; + + if (!titleSlug) { + return ( + + ); + } + + return ( + + ); + } +} + +SeriesDetailsPageConnector.propTypes = { + titleSlug: PropTypes.string, + match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired, + push: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsPageConnector); diff --git a/frontend/src/Artist/Details/SeriesDetailsSeason.css b/frontend/src/Artist/Details/SeriesDetailsSeason.css new file mode 100644 index 000000000..b6cffa8f1 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsSeason.css @@ -0,0 +1,113 @@ +.season { + margin-bottom: 20px; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; + + &:last-of-type { + margin-bottom: 0; + } +} + +.header { + position: relative; + display: flex; + align-items: center; + width: 100%; + font-size: 24px; +} + +.seasonNumber { + margin-right: 10px; + margin-left: 5px; +} + +.episodeCountContainer { + margin-left: 10px; + vertical-align: text-bottom; +} + +.expandButton { + composes: link from 'Components/Link/Link.css'; + + flex-grow: 1; + margin: 0 20px; + text-align: center; +} + +.left { + display: flex; + align-items: center; + flex: 0 1 300px; +} + +.left, +.actions { + padding: 15px 10px; +} + +.actionsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + flex: 0 0 45px; +} + +.actionsMenuContent { + composes: menuContent from 'Components/Menu/MenuContent.css'; + + white-space: nowrap; + font-size: 14px; +} + +.actionMenuIcon { + margin-right: 8px; +} + +.actionButton { + composes: button from 'Components/Link/IconButton.css'; + + width: 30px; +} + +.episodes { + padding-top: 15px; + border-top: 1px solid $borderColor; +} + +.collapseButtonContainer { + padding: 10px 15px; + width: 100%; + border-top: 1px solid $borderColor; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + background-color: #fafafa; + text-align: center; +} + +.expandButtonIcon { + composes: actionButton; + + position: absolute; + top: 50%; + left: 50%; + margin-top: -12px; + margin-left: -15px; +} + +.noEpisodes { + margin-bottom: 15px; + text-align: center; +} + +@media only screen and (max-width: $breakpointSmall) { + .season { + border-right: 0; + border-left: 0; + border-radius: 0; + } + + .expandButtonIcon { + position: static; + margin: 0; + } +} diff --git a/frontend/src/Artist/Details/SeriesDetailsSeason.js b/frontend/src/Artist/Details/SeriesDetailsSeason.js new file mode 100644 index 000000000..9b1e8c03b --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsSeason.js @@ -0,0 +1,393 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import isAfter from 'Utilities/Date/isAfter'; +import isBefore from 'Utilities/Date/isBefore'; +import getToggledRange from 'Utilities/Table/getToggledRange'; +import { align, icons, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import IconButton from 'Components/Link/IconButton'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal'; +import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; +import EpisodeRowConnector from './EpisodeRowConnector'; +import styles from './SeriesDetailsSeason.css'; + +function getSeasonStatistics(episodes) { + let episodeCount = 0; + let episodeFileCount = 0; + let totalEpisodeCount = 0; + + episodes.forEach((episode) => { + if (episode.episodeFileId || (episode.monitored && isBefore(episode.airDateUtc))) { + episodeCount++; + } + + if (episode.episodeFileId) { + episodeFileCount++; + } + + totalEpisodeCount++; + }); + + return { + episodeCount, + episodeFileCount, + totalEpisodeCount + }; +} + +function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) { + if (episodeFileCount === episodeCount && episodeCount > 0) { + return kinds.SUCCESS; + } + + if (!monitored) { + return kinds.WARNING; + } + + return kinds.DANGER; +} + +class SeriesDetailsSeason extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOrganizeModalOpen: false, + isManageEpisodesOpen: false, + lastToggledEpisode: null + }; + } + + componentDidMount() { + this._expandByDefault(); + } + + componentDidUpdate(prevProps) { + if (prevProps.artistId !== this.props.artistId) { + this._expandByDefault(); + } + } + + // + // Control + + _expandByDefault() { + const { + seasonNumber, + onExpandPress, + items + } = this.props; + + const expand = _.some(items, (item) => { + return isAfter(item.airDateUtc) || + isAfter(item.airDateUtc, { days: -30 }); + }); + + onExpandPress(seasonNumber, expand && seasonNumber > 0); + } + + // + // Listeners + + onOrganizePress = () => { + this.setState({ isOrganizeModalOpen: true }); + } + + onOrganizeModalClose = () => { + this.setState({ isOrganizeModalOpen: false }); + } + + onManageEpisodesPress = () => { + this.setState({ isManageEpisodesOpen: true }); + } + + onManageEpisodesModalClose = () => { + this.setState({ isManageEpisodesOpen: false }); + } + + onExpandPress = () => { + const { + seasonNumber, + isExpanded + } = this.props; + + this.props.onExpandPress(seasonNumber, !isExpanded); + } + + onMonitorEpisodePress = (episodeId, monitored, { shiftKey }) => { + const lastToggled = this.state.lastToggledEpisode; + const episodeIds = [episodeId]; + + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(this.props.items, episodeId, lastToggled); + const items = this.props.items; + + for (let i = lower; i < upper; i++) { + episodeIds.push(items[i].id); + } + } + + this.setState({ lastToggledEpisode: episodeId }); + + this.props.onMonitorEpisodePress(_.uniq(episodeIds), monitored); + } + + // + // Render + + render() { + const { + artistId, + monitored, + seasonNumber, + items, + columns, + isSaving, + isExpanded, + isSearching, + seriesMonitored, + isSmallScreen, + onTableOptionChange, + onMonitorSeasonPress, + onSearchPress + } = this.props; + + const { + episodeCount, + episodeFileCount, + totalEpisodeCount + } = getSeasonStatistics(items); + + const { + isOrganizeModalOpen, + isManageEpisodesOpen + } = this.state; + + return ( +
+
+
+ + + { + seasonNumber === 0 ? + + Specials + : + + Season {seasonNumber} + + } + + +
+ + + + { + !isSmallScreen && +   + } + + + { + isSmallScreen ? + + + + + + + + + + Search + + + + + + Preview Rename + + + + + + Manage Episodes + + + : + +
+ + + + + +
+ } + +
+ +
+ { + isExpanded && +
+ { + items.length ? + + + { + items.map((item) => { + return ( + + ); + }) + } + +
: + +
+ No episodes in this season +
+ } +
+ +
+
+ } +
+ + + + +
+ ); + } +} + +SeriesDetailsSeason.propTypes = { + artistId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + seasonNumber: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool, + isExpanded: PropTypes.bool, + isSearching: PropTypes.bool.isRequired, + seriesMonitored: PropTypes.bool.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired, + onMonitorSeasonPress: PropTypes.func.isRequired, + onExpandPress: PropTypes.func.isRequired, + onMonitorEpisodePress: PropTypes.func.isRequired, + onSearchPress: PropTypes.func.isRequired +}; + +export default SeriesDetailsSeason; diff --git a/frontend/src/Artist/Details/SeriesDetailsSeasonConnector.js b/frontend/src/Artist/Details/SeriesDetailsSeasonConnector.js new file mode 100644 index 000000000..71dfaeddd --- /dev/null +++ b/frontend/src/Artist/Details/SeriesDetailsSeasonConnector.js @@ -0,0 +1,118 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { toggleSeasonMonitored } from 'Store/Actions/seriesActions'; +import { toggleEpisodesMonitored, setEpisodesTableOption } from 'Store/Actions/episodeActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import SeriesDetailsSeason from './SeriesDetailsSeason'; + +function createMapStateToProps() { + return createSelector( + (state, { seasonNumber }) => seasonNumber, + (state) => state.episodes, + createArtistSelector(), + createCommandsSelector(), + createDimensionsSelector(), + (seasonNumber, episodes, series, commands, dimensions) => { + const isSearching = !!findCommand(commands, { + name: commandNames.SEASON_SEARCH, + artistId: series.id, + seasonNumber + }); + + const episodesInSeason = _.filter(episodes.items, { seasonNumber }); + const sortedEpisodes = _.orderBy(episodesInSeason, 'episodeNumber', 'desc'); + + return { + items: sortedEpisodes, + columns: episodes.columns, + isSearching, + seriesMonitored: series.monitored, + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +const mapDispatchToProps = { + toggleSeasonMonitored, + toggleEpisodesMonitored, + setEpisodesTableOption, + executeCommand +}; + +class SeriesDetailsSeasonConnector extends Component { + + // + // Listeners + + onTableOptionChange = (payload) => { + this.props.setEpisodesTableOption(payload); + } + + onMonitorSeasonPress = (monitored) => { + const { + artistId, + seasonNumber + } = this.props; + + this.props.toggleSeasonMonitored({ + artistId, + seasonNumber, + monitored + }); + } + + onSearchPress = () => { + const { + artistId, + seasonNumber + } = this.props; + + this.props.executeCommand({ + name: commandNames.SEASON_SEARCH, + artistId, + seasonNumber + }); + } + + onMonitorEpisodePress = (episodeIds, monitored) => { + this.props.toggleEpisodesMonitored({ + episodeIds, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SeriesDetailsSeasonConnector.propTypes = { + artistId: PropTypes.number.isRequired, + seasonNumber: PropTypes.number.isRequired, + toggleSeasonMonitored: PropTypes.func.isRequired, + toggleEpisodesMonitored: PropTypes.func.isRequired, + setEpisodesTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsSeasonConnector); diff --git a/frontend/src/Artist/Details/SeriesTags.css b/frontend/src/Artist/Details/SeriesTags.css new file mode 100644 index 000000000..ec340a041 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesTags.css @@ -0,0 +1,8 @@ +.tags { + margin: 0; + padding-left: 20px; +} + +.tag { + white-space: nowrap; +} diff --git a/frontend/src/Artist/Details/SeriesTags.js b/frontend/src/Artist/Details/SeriesTags.js new file mode 100644 index 000000000..4897937dd --- /dev/null +++ b/frontend/src/Artist/Details/SeriesTags.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; +import styles from './SeriesTags.css'; + +function SeriesTags({ tags }) { + return ( +
+ { + tags.map((tag) => { + return ( + + ); + }) + } +
+ ); +} + +SeriesTags.propTypes = { + tags: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export default SeriesTags; diff --git a/frontend/src/Artist/Details/SeriesTagsConnector.js b/frontend/src/Artist/Details/SeriesTagsConnector.js new file mode 100644 index 000000000..354b2cec2 --- /dev/null +++ b/frontend/src/Artist/Details/SeriesTagsConnector.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import SeriesTags from './SeriesTags'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createTagsSelector(), + (series, tagList) => { + const tags = _.reduce(series.tags, (acc, tag) => { + const matchingTag = _.find(tagList, { id: tag }); + + if (matchingTag) { + acc.push(matchingTag.label); + } + + return acc; + }, []); + + return { + tags + }; + } + ); +} + +export default connect(createMapStateToProps)(SeriesTags); diff --git a/frontend/src/Artist/Edit/EditArtistModal.js b/frontend/src/Artist/Edit/EditArtistModal.js new file mode 100644 index 000000000..6e99a2f53 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditArtistModalContentConnector from './EditArtistModalContentConnector'; + +function EditArtistModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditArtistModal; diff --git a/frontend/src/Artist/Edit/EditArtistModalConnector.js b/frontend/src/Artist/Edit/EditArtistModalConnector.js new file mode 100644 index 000000000..83a3323bf --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditArtistModal from './EditArtistModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditArtistModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'series' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditArtistModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(EditArtistModalConnector); diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.css b/frontend/src/Artist/Edit/EditArtistModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js new file mode 100644 index 000000000..b8494c525 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -0,0 +1,165 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditArtistModalContent.css'; + +class EditArtistModalContent extends Component { + + // + // Render + + render() { + const { + artistName, + item, + isSaving, + showLanguageProfile, + onInputChange, + onSavePress, + onModalClose, + onDeleteSeriesPress, + ...otherProps + } = this.props; + + const { + monitored, + albumFolder, + qualityProfileId, + languageProfileId, + // seriesType, + path, + tags + } = item; + + return ( + + + Edit - {artistName} + + + +
+ + Monitored + + + + + + Use Album Folder + + + + + + Quality Profile + + + + + { + showLanguageProfile && + + Language Profile + + + + } + + + Path + + + + + + Tags + + + +
+
+ + + + + + + Save + + +
+ ); + } +} + +EditArtistModalContent.propTypes = { + artistId: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + item: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteSeriesPress: PropTypes.func.isRequired +}; + +export default EditArtistModalContent; diff --git a/frontend/src/Artist/Edit/EditArtistModalContentConnector.js b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js new file mode 100644 index 000000000..9bd312233 --- /dev/null +++ b/frontend/src/Artist/Edit/EditArtistModalContentConnector.js @@ -0,0 +1,98 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { setSeriesValue, saveArtist } from 'Store/Actions/seriesActions'; +import EditArtistModalContent from './EditArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.series, + (state) => state.settings.languageProfiles, + createArtistSelector(), + (seriesState, languageProfiles, series) => { + const { + isSaving, + saveError, + pendingChanges + } = seriesState; + + const seriesSettings = _.pick(series, [ + 'monitored', + 'albumFolder', + 'qualityProfileId', + 'languageProfileId', + // 'seriesType', + 'path', + 'tags' + ]); + + const settings = selectSettings(seriesSettings, pendingChanges, saveError); + + return { + artistName: series.artistName, + isSaving, + saveError, + pendingChanges, + item: settings.settings, + showLanguageProfile: languageProfiles.items.length > 1, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setSeriesValue, + saveArtist +}; + +class EditArtistModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setSeriesValue({ name, value }); + } + + onSavePress = () => { + this.props.saveArtist({ id: this.props.artistId }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditArtistModalContentConnector.propTypes = { + artistId: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + setSeriesValue: PropTypes.func.isRequired, + saveArtist: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditArtistModalContentConnector); diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js new file mode 100644 index 000000000..11fd79d5d --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import DeleteArtistModalContentConnector from './DeleteArtistModalContentConnector'; + +function DeleteArtistModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteArtistModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteArtistModal; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css new file mode 100644 index 000000000..950fdc27d --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.css @@ -0,0 +1,13 @@ +.message { + margin-top: 20px; + margin-bottom: 10px; +} + +.pathContainer { + margin-left: 5px; +} + +.path { + margin-left: 5px; + color: $dangerColor; +} diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js new file mode 100644 index 000000000..285804022 --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContent.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './DeleteArtistModalContent.css'; + +class DeleteArtistModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + } + + onDeleteSeriesConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + + this.setState({ deleteFiles: false }); + this.props.onDeleteSelectedPress(deleteFiles); + } + + // + // Render + + render() { + const { + series, + onModalClose + } = this.props; + const deleteFiles = this.state.deleteFiles; + + return ( + + + Delete Selected Series + + + +
+ + {`Delete Series Folder${series.length > 1 ? 's' : ''}`} + + 1 ? 's' : ''} and all contents`} + kind={kinds.DANGER} + onChange={this.onDeleteFilesChange} + /> + +
+ +
+ {`Are you sure you want to delete ${series.length} selected series${deleteFiles ? ' and all contents' : ''}?`} +
+ +
    + { + series.map((s) => { + return ( +
  • + {s.title} + + { + deleteFiles && + + - + + {s.path} + + + } +
  • + ); + }) + } +
+
+ + + + + + +
+ ); + } +} + +DeleteArtistModalContent.propTypes = { + series: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteSelectedPress: PropTypes.func.isRequired +}; + +export default DeleteArtistModalContent; diff --git a/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js new file mode 100644 index 000000000..30064a4c3 --- /dev/null +++ b/frontend/src/Artist/Editor/Delete/DeleteArtistModalContentConnector.js @@ -0,0 +1,45 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import { bulkDeleteArtist } from 'Store/Actions/seriesEditorActions'; +import DeleteArtistModalContent from './DeleteArtistModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllSeriesSelector(), + (artistIds, allSeries) => { + const selectedSeries = _.intersectionWith(allSeries, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedSeries = _.orderBy(selectedSeries, 'sortTitle'); + const series = _.map(sortedSeries, (s) => { + return { + title: s.title, + path: s.path + }; + }); + + return { + series + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDeleteSelectedPress(deleteFiles) { + dispatch(bulkDeleteArtist({ + artistIds: props.artistIds, + deleteFiles + })); + + props.onModalClose(); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteArtistModalContent); diff --git a/frontend/src/Artist/Editor/Organize/OrganizeSeriesModal.js b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModal.js new file mode 100644 index 000000000..c970392ec --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import OrganizeSeriesModalContentConnector from './OrganizeSeriesModalContentConnector'; + +function OrganizeSeriesModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +OrganizeSeriesModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizeSeriesModal; diff --git a/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.css b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.css new file mode 100644 index 000000000..0b896f4ef --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.css @@ -0,0 +1,8 @@ +.renameIcon { + margin-left: 5px; +} + +.message { + margin-top: 20px; + margin-bottom: 10px; +} diff --git a/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.js b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.js new file mode 100644 index 000000000..10a459d52 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContent.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './OrganizeSeriesModalContent.css'; + +function OrganizeSeriesModalContent(props) { + const { + seriesTitles, + onModalClose, + onOrganizeSeriesPress + } = props; + + return ( + + + Organize Selected Series + + + + + Tip: To preview a rename... select "Cancel" then any series title and use the + + + +
+ Are you sure you want to organize all files in the {seriesTitles.length} selected series? +
+ +
    + { + seriesTitles.map((title) => { + return ( +
  • + {title} +
  • + ); + }) + } +
+
+ + + + + + +
+ ); +} + +OrganizeSeriesModalContent.propTypes = { + seriesTitles: PropTypes.arrayOf(PropTypes.string).isRequired, + onModalClose: PropTypes.func.isRequired, + onOrganizeSeriesPress: PropTypes.func.isRequired +}; + +export default OrganizeSeriesModalContent; diff --git a/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContentConnector.js b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContentConnector.js new file mode 100644 index 000000000..3cbee6003 --- /dev/null +++ b/frontend/src/Artist/Editor/Organize/OrganizeSeriesModalContentConnector.js @@ -0,0 +1,67 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import OrganizeSeriesModalContent from './OrganizeSeriesModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllSeriesSelector(), + (artistIds, allSeries) => { + const series = _.intersectionWith(allSeries, artistIds, (s, id) => { + return s.id === id; + }); + + const sortedSeries = _.orderBy(series, 'sortTitle'); + const seriesTitles = _.map(sortedSeries, 'title'); + + return { + seriesTitles + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class OrganizeSeriesModalContentConnector extends Component { + + // + // Listeners + + onOrganizeSeriesPress = () => { + this.props.executeCommand({ + name: commandNames.RENAME_SERIES, + artistIds: this.props.artistIds + }); + + this.props.onModalClose(true); + } + + // + // Render + + render(props) { + return ( + + ); + } +} + +OrganizeSeriesModalContentConnector.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeSeriesModalContentConnector); diff --git a/frontend/src/Artist/Editor/SeriesEditor.js b/frontend/src/Artist/Editor/SeriesEditor.js new file mode 100644 index 000000000..7039f4dde --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditor.js @@ -0,0 +1,328 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import NoArtist from 'Artist/NoArtist'; +import SeriesEditorRowConnector from './SeriesEditorRowConnector'; +import SeriesEditorFooter from './SeriesEditorFooter'; +import OrganizeSeriesModal from './Organize/OrganizeSeriesModal'; + +function getColumns(showLanguageProfile) { + return [ + { + name: 'status', + isVisible: true + }, + { + name: 'sortTitle', + label: 'Title', + isSortable: true, + isVisible: true + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + isSortable: true, + isVisible: true + }, + { + name: 'languageProfileId', + label: 'Language Profile', + isSortable: true, + isVisible: showLanguageProfile + }, + { + name: 'seriesType', + label: 'Series Type', + isSortable: false, + isVisible: true + }, + { + name: 'seasonFolder', + label: 'Season Folder', + isSortable: true, + isVisible: true + }, + { + name: 'path', + label: 'Path', + isSortable: true, + isVisible: true + }, + { + name: 'tags', + label: 'Tags', + isSortable: false, + isVisible: true + } + ]; +} + +class SeriesEditor extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isOrganizingSeriesModalOpen: false, + columns: getColumns(props.showLanguageProfile) + }; + } + + componentDidUpdate(prevProps) { + const { + isDeleting, + deleteError + } = this.props; + + const hasFinishedDeleting = prevProps.isDeleting && + !isDeleting && + !deleteError; + + if (hasFinishedDeleting) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSaveSelected = (changes) => { + this.props.onSaveSelected({ + artistIds: this.getSelectedIds(), + ...changes + }); + } + + onOrganizeSeriesPress = () => { + this.setState({ isOrganizingSeriesModalOpen: true }); + } + + onOrganizeSeriesModalClose = (organized) => { + this.setState({ isOrganizingSeriesModalOpen: false }); + + if (organized === true) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + isSaving, + saveError, + isDeleting, + deleteError, + isOrganizingSeries, + showLanguageProfile, + onSortPress, + onFilterSelect + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + columns + } = this.state; + + const selectedSeriesIds = this.getSelectedIds(); + + return ( + + + + + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Episodes + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + + + +
+ ); + } +} + +SeriesEditor.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + deleteError: PropTypes.object, + isOrganizingSeries: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onSaveSelected: PropTypes.func.isRequired +}; + +export default SeriesEditor; diff --git a/frontend/src/Artist/Editor/SeriesEditorConnector.js b/frontend/src/Artist/Editor/SeriesEditorConnector.js new file mode 100644 index 000000000..2e16cda74 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorConnector.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import { setSeriesEditorSort, setSeriesEditorFilter, saveArtistEditor } from 'Store/Actions/seriesEditorActions'; +import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import * as commandNames from 'Commands/commandNames'; +import SeriesEditor from './SeriesEditor'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.languageProfiles, + createClientSideCollectionSelector(), + createCommandSelector(commandNames.RENAME_SERIES), + (languageProfiles, series, isOrganizingSeries) => { + return { + isOrganizingSeries, + showLanguageProfile: languageProfiles.items.length > 1, + ...series + }; + } + ); +} + +const mapDispatchToProps = { + setSeriesEditorSort, + setSeriesEditorFilter, + saveArtistEditor, + fetchRootFolders +}; + +class SeriesEditorConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRootFolders(); + } + + // + // Listeners + + onSortPress = (sortKey) => { + this.props.setSeriesEditorSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setSeriesEditorFilter({ filterKey, filterValue, filterType }); + } + + onSaveSelected = (payload) => { + this.props.saveArtistEditor(payload); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SeriesEditorConnector.propTypes = { + setSeriesEditorSort: PropTypes.func.isRequired, + setSeriesEditorFilter: PropTypes.func.isRequired, + saveArtistEditor: PropTypes.func.isRequired, + fetchRootFolders: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'series', uiSection: 'seriesEditor' } + )(SeriesEditorConnector); diff --git a/frontend/src/Artist/Editor/SeriesEditorFooter.css b/frontend/src/Artist/Editor/SeriesEditorFooter.css new file mode 100644 index 000000000..5b509936b --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorFooter.css @@ -0,0 +1,57 @@ +.inputContainer { + margin-right: 20px; + min-width: 150px; +} + +.buttonContainer { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.buttonContainerContent { + flex-grow: 0; +} + +.buttons { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.organizeSelectedButton, +.tagsButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + margin-right: 10px; + height: 35px; +} + +.deleteSelectedButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + margin-left: 50px; + height: 35px; +} + +@media only screen and (max-width: $breakpointSmall) { + .inputContainer { + margin-right: 0; + } + + .buttonContainer { + justify-content: flex-start; + } + + .buttonContainerContent { + flex-grow: 1; + } + + .buttons { + justify-content: space-between; + } + + .selectedSeriesLabel { + text-align: left; + } +} diff --git a/frontend/src/Artist/Editor/SeriesEditorFooter.js b/frontend/src/Artist/Editor/SeriesEditorFooter.js new file mode 100644 index 000000000..db7f86363 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorFooter.js @@ -0,0 +1,314 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import SelectInput from 'Components/Form/SelectInput'; +import LanguageProfileSelectInputConnector from 'Components/Form/LanguageProfileSelectInputConnector'; +import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector'; +import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; +import SeriesTypeSelectInput from 'Components/Form/SeriesTypeSelectInput'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import TagsModal from './Tags/TagsModal'; +import DeleteArtistModal from './Delete/DeleteArtistModal'; +import SeriesEditorFooterLabel from './SeriesEditorFooterLabel'; +import styles from './SeriesEditorFooter.css'; + +const NO_CHANGE = 'noChange'; + +class SeriesEditorFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitored: NO_CHANGE, + qualityProfileId: NO_CHANGE, + languageProfileId: NO_CHANGE, + seriesType: NO_CHANGE, + seasonFolder: NO_CHANGE, + rootFolderPath: NO_CHANGE, + savingTags: false, + isDeleteArtistModalOpen: false, + isTagsModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + qualityProfileId: NO_CHANGE, + languageProfileId: NO_CHANGE, + seriesType: NO_CHANGE, + seasonFolder: NO_CHANGE, + rootFolderPath: NO_CHANGE, + savingTags: false + }); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + + if (value === NO_CHANGE) { + return; + } + + switch (name) { + case 'monitored': + this.props.onSaveSelected({ [name]: value === 'monitored' }); + break; + case 'seasonFolder': + this.props.onSaveSelected({ [name]: value === 'yes' }); + break; + default: + this.props.onSaveSelected({ [name]: value }); + } + } + + onApplyTagsPress = (tags, applyTags) => { + this.setState({ + savingTags: true, + isTagsModalOpen: false + }); + + this.props.onSaveSelected({ + tags, + applyTags + }); + } + + onDeleteSelectedPress = () => { + this.setState({ isDeleteArtistModalOpen: true }); + } + + onDeleteSeriesModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onTagsPress = () => { + this.setState({ isTagsModalOpen: true }); + } + + onTagsModalClose = () => { + this.setState({ isTagsModalOpen: false }); + } + + // + // Render + + render() { + const { + artistIds, + selectedCount, + isSaving, + isDeleting, + isOrganizingSeries, + showLanguageProfile, + onOrganizeSeriesPress + } = this.props; + + const { + monitored, + qualityProfileId, + languageProfileId, + seriesType, + seasonFolder, + rootFolderPath, + savingTags, + isTagsModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'monitored', value: 'Monitored', disabled: true }, + { key: 'unmonitored', value: 'Unmonitored' } + ]; + + const seasonFolderOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'yes', value: 'Yes' }, + { key: 'no', value: 'No' } + ]; + + return ( + +
+ + + +
+ +
+ + + +
+ + { + showLanguageProfile && +
+ + + +
+ } + +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+
+ + +
+
+ + Rename Files + + + + Set Tags + +
+ + + Delete + +
+
+
+ + + + +
+ ); + } +} + +SeriesEditorFooter.propTypes = { + artistIds: PropTypes.arrayOf(PropTypes.number).isRequired, + selectedCount: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + deleteError: PropTypes.object, + isOrganizingSeries: PropTypes.bool.isRequired, + showLanguageProfile: PropTypes.bool.isRequired, + onSaveSelected: PropTypes.func.isRequired, + onOrganizeSeriesPress: PropTypes.func.isRequired +}; + +export default SeriesEditorFooter; diff --git a/frontend/src/Artist/Editor/SeriesEditorFooterLabel.css b/frontend/src/Artist/Editor/SeriesEditorFooterLabel.css new file mode 100644 index 000000000..9b4b40be6 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorFooterLabel.css @@ -0,0 +1,8 @@ +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.savingIcon { + margin-left: 8px; +} diff --git a/frontend/src/Artist/Editor/SeriesEditorFooterLabel.js b/frontend/src/Artist/Editor/SeriesEditorFooterLabel.js new file mode 100644 index 000000000..fc77ece44 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorFooterLabel.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import styles from './SeriesEditorFooterLabel.css'; + +function SeriesEditorFooterLabel(props) { + const { + className, + label, + isSaving + } = props; + + return ( +
+ {label} + + { + isSaving && + + } +
+ ); +} + +SeriesEditorFooterLabel.propTypes = { + className: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isSaving: PropTypes.bool.isRequired +}; + +SeriesEditorFooterLabel.defaultProps = { + className: styles.label +}; + +export default SeriesEditorFooterLabel; diff --git a/frontend/src/Artist/Editor/SeriesEditorRow.css b/frontend/src/Artist/Editor/SeriesEditorRow.css new file mode 100644 index 000000000..d53a30f6d --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorRow.css @@ -0,0 +1,5 @@ +.seasonFolder { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} diff --git a/frontend/src/Artist/Editor/SeriesEditorRow.js b/frontend/src/Artist/Editor/SeriesEditorRow.js new file mode 100644 index 000000000..99530d292 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorRow.js @@ -0,0 +1,120 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import TagListConnector from 'Components/TagListConnector'; +import CheckInput from 'Components/Form/CheckInput'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell'; +import styles from './SeriesEditorRow.css'; + +class SeriesEditorRow extends Component { + + // + // Listeners + + onSeasonFolderChange = () => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + // + } + + // + // Render + + render() { + const { + id, + status, + titleSlug, + title, + monitored, + languageProfile, + qualityProfile, + seriesType, + seasonFolder, + path, + tags, + columns, + isSelected, + onSelectedChange + } = this.props; + + return ( + + + + + + + + + + + {qualityProfile.name} + + + { + _.find(columns, { name: 'languageProfileId' }).isVisible && + + {languageProfile.name} + + } + + + {titleCase(seriesType)} + + + + + + + + {path} + + + + + + + ); + } +} + +SeriesEditorRow.propTypes = { + id: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + languageProfile: PropTypes.object.isRequired, + qualityProfile: PropTypes.object.isRequired, + seriesType: PropTypes.string.isRequired, + seasonFolder: PropTypes.bool.isRequired, + path: PropTypes.string.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default SeriesEditorRow; diff --git a/frontend/src/Artist/Editor/SeriesEditorRowConnector.js b/frontend/src/Artist/Editor/SeriesEditorRowConnector.js new file mode 100644 index 000000000..3d1ee2e71 --- /dev/null +++ b/frontend/src/Artist/Editor/SeriesEditorRowConnector.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; +import SeriesEditorRow from './SeriesEditorRow'; + +function createMapStateToProps() { + return createSelector( + createLanguageProfileSelector(), + createQualityProfileSelector(), + (languageProfile, qualityProfile) => { + return { + languageProfile, + qualityProfile + }; + } + ); +} + +function SeriesEditorRowConnector(props) { + return ( + + ); +} + +SeriesEditorRowConnector.propTypes = { + qualityProfileId: PropTypes.number.isRequired +}; + +export default connect(createMapStateToProps)(SeriesEditorRowConnector); diff --git a/frontend/src/Artist/Editor/Tags/TagsModal.js b/frontend/src/Artist/Editor/Tags/TagsModal.js new file mode 100644 index 000000000..0f6c2d7ec --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import TagsModalContentConnector from './TagsModalContentConnector'; + +function TagsModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +TagsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default TagsModal; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContent.css b/frontend/src/Artist/Editor/Tags/TagsModalContent.css new file mode 100644 index 000000000..63be9aadd --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContent.css @@ -0,0 +1,12 @@ +.renameIcon { + margin-left: 5px; +} + +.message { + margin-top: 20px; + margin-bottom: 10px; +} + +.result { + padding-top: 4px; +} diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContent.js b/frontend/src/Artist/Editor/Tags/TagsModalContent.js new file mode 100644 index 000000000..067553977 --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContent.js @@ -0,0 +1,188 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './TagsModalContent.css'; + +class TagsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + tags: [], + applyTags: 'add' + }; + } + + // + // Lifecycle + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + onApplyTagsPress = () => { + const { + tags, + applyTags + } = this.state; + + this.props.onApplyTagsPress(tags, applyTags); + } + + // + // Render + + render() { + const { + seriesTags, + tagList, + onModalClose, + onApplyTagsPress + } = this.props; + + const { + tags, + applyTags + } = this.state; + + const applyTagsOptions = [ + { key: 'add', value: 'Add' }, + { key: 'remove', value: 'Remove' }, + { key: 'replace', value: 'Replace' } + ]; + + return ( + + + Tags + + + +
+ + Tags + + + + + + Apply Tags + + + + + + Result + +
+ { + seriesTags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + const removeTag = (applyTags === 'remove' && tags.indexOf(t) > -1) || + (applyTags === 'replace' && tags.indexOf(t) === -1); + + return ( + + ); + }) + } + + { + (applyTags === 'add' || applyTags === 'replace') && + tags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + if (seriesTags.indexOf(t) > -1) { + return null; + } + + return ( + + ); + }) + } +
+
+
+
+ + + + + + +
+ ); + } +} + +TagsModalContent.propTypes = { + seriesTags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired, + onApplyTagsPress: PropTypes.func.isRequired +}; + +export default TagsModalContent; diff --git a/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js new file mode 100644 index 000000000..7fc5d87a8 --- /dev/null +++ b/frontend/src/Artist/Editor/Tags/TagsModalContentConnector.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import TagsModalContent from './TagsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { artistIds }) => artistIds, + createAllSeriesSelector(), + createTagsSelector(), + (artistIds, allSeries, tagList) => { + const series = _.intersectionWith(allSeries, artistIds, (s, id) => { + return s.id === id; + }); + + const seriesTags = _.uniq(_.concat(..._.map(series, 'tags'))); + + return { + seriesTags, + tagList + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onAction() { + // Do something + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(TagsModalContent); diff --git a/frontend/src/Artist/Index/ArtistIndex.css b/frontend/src/Artist/Index/ArtistIndex.css new file mode 100644 index 000000000..443372a73 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndex.css @@ -0,0 +1,51 @@ +.pageContentBodyWrapper { + display: flex; + flex: 1 0 1px; + overflow: hidden; +} + +.contentBody { + composes: contentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; +} + +.postersInnerContentBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; + + /* 5px less padding than normal to handle poster's 5px margin */ + padding: calc($pageContentBodyPadding - 5px); +} + +.tableInnerContentBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.contentBodyContainer { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +@media only screen and (max-width: $breakpointSmall) { + .pageContentBodyWrapper { + flex-basis: auto; + } + + .contentBody { + flex-basis: 1px; + } + + .postersInnerContentBody { + padding: calc($pageContentBodyPaddingSmallScreen - 5px); + } +} diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js new file mode 100644 index 000000000..184671df5 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndex.js @@ -0,0 +1,326 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import { align, icons, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageJumpBar from 'Components/Page/PageJumpBar'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import NoArtist from 'Artist/NoArtist'; +import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector'; +import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal'; +import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector'; +import ArtistIndexFooter from './ArtistIndexFooter'; +import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu'; +import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu'; +import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu'; +import styles from './ArtistIndex.css'; + +function getViewComponent(view) { + if (view === 'posters') { + return ArtistIndexPostersConnector; + } + + return ArtistIndexTableConnector; +} + +class ArtistIndex extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._viewComponent = null; + + this.state = { + contentBody: null, + jumpBarItems: [], + isPosterOptionsModalOpen: false, + isRendered: false + }; + } + + componentDidMount() { + this.setJumpBarItems(); + } + + componentDidUpdate(prevProps) { + const { + items, + sortKey, + sortDirection + } = this.props; + + if ( + hasDifferentItems(prevProps.items, items) || + sortKey !== prevProps.sortKey || + sortDirection !== prevProps.sortDirection + ) { + this.setJumpBarItems(); + } + } + + // + // Control + + setContentBodyRef = (ref) => { + this.setState({ contentBody: ref }); + } + + setViewComponentRef = (ref) => { + this._viewComponent = ref; + } + + setJumpBarItems() { + const { + items, + sortKey, + sortDirection + } = this.props; + + // Reset if not sorting by sortTitle + if (sortKey !== 'sortName') { + this.setState({ jumpBarItems: [] }); + return; + } + + const characters = _.reduce(items, (acc, item) => { + const firstCharacter = item.sortName.charAt(0); + + if (isNaN(firstCharacter)) { + acc.push(firstCharacter); + } else { + acc.push('#'); + } + + return acc; + }, []).sort(); + + // Reverse if sorting descending + if (sortDirection === sortDirections.DESCENDING) { + characters.reverse(); + } + + this.setState({ jumpBarItems: _.sortedUniq(characters) }); + } + + // + // Listeners + + onPosterOptionsPress = () => { + this.setState({ isPosterOptionsModalOpen: true }); + } + + onPosterOptionsModalClose = () => { + this.setState({ isPosterOptionsModalOpen: false }); + } + + onJumpBarItemPress = (item) => { + const viewComponent = this._viewComponent.getWrappedInstance(); + viewComponent.scrollToFirstCharacter(item); + } + + onRender = () => { + this.setState({ isRendered: true }, () => { + const { + scrollTop, + isSmallScreen + } = this.props; + + if (isSmallScreen) { + // Seems to result in the view being off by 125px (distance to the top of the page) + // document.documentElement.scrollTop = document.body.scrollTop = scrollTop; + + // This works, but then jumps another 1px after scrolling + document.documentElement.scrollTop = scrollTop; + } + }); + } + + onScroll = ({ scrollTop }) => { + this.props.onScroll({ scrollTop }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + view, + isRefreshingSeries, + isRssSyncExecuting, + scrollTop, + onSortSelect, + onFilterSelect, + onViewSelect, + onRefreshSeriesPress, + onRssSyncPress, + ...otherProps + } = this.props; + + const { + contentBody, + jumpBarItems, + isPosterOptionsModalOpen, + isRendered + } = this.state; + + const ViewComponent = getViewComponent(view); + const isLoaded = !error && isPopulated && !!items.length && contentBody; + + return ( + + + + + + + + + + + + { + view === 'posters' && + + } + + { + view === 'posters' && + + } + + + + + + + + + +
+ + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load series
+ } + + { + isLoaded && +
+ + + +
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + { + isLoaded && !!jumpBarItems.length && + + } +
+ + +
+ ); + } +} + +ArtistIndex.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + view: PropTypes.string.isRequired, + isRefreshingSeries: PropTypes.bool.isRequired, + isRssSyncExecuting: PropTypes.bool.isRequired, + scrollTop: PropTypes.number.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortSelect: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onViewSelect: PropTypes.func.isRequired, + onRefreshSeriesPress: PropTypes.func.isRequired, + onRssSyncPress: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndex; diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js new file mode 100644 index 000000000..794dcaa41 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexConnector.js @@ -0,0 +1,160 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import dimensions from 'Styles/Variables/dimensions'; +import createCommandSelector from 'Store/Selectors/createCommandSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { fetchArtist } from 'Store/Actions/seriesActions'; +import scrollPositions from 'Store/scrollPositions'; +import { setArtistSort, setArtistFilter, setArtistView } from 'Store/Actions/artistIndexActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import withScrollPosition from 'Components/withScrollPosition'; +import ArtistIndex from './ArtistIndex'; + +const POSTERS_PADDING = 15; +const POSTERS_PADDING_SMALL_SCREEN = 5; +const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding); +const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen); + +// If the scrollTop is greater than zero it needs to be offset +// by the padding so when it is set initially so it is correct +// after React Virtualized takes the padding into account. + +function getScrollTop(view, scrollTop, isSmallScreen) { + if (scrollTop === 0) { + return 0; + } + + let padding = isSmallScreen ? TABLE_PADDING_SMALL_SCREEN : TABLE_PADDING; + + if (view === 'posters') { + padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING; + } + + return scrollTop + padding; +} + +function createMapStateToProps() { + return createSelector( + (state) => state.series, + (state) => state.seriesIndex, + createCommandSelector(commandNames.REFRESH_SERIES), + createCommandSelector(commandNames.RSS_SYNC), + createDimensionsSelector(), + (series, seriesIndex, isRefreshingSeries, isRssSyncExecuting, dimensionsState) => { + return { + isRefreshingSeries, + isRssSyncExecuting, + isSmallScreen: dimensionsState.isSmallScreen, + ...series, + ...seriesIndex + }; + } + ); +} + +const mapDispatchToProps = { + fetchArtist, + setArtistSort, + setArtistFilter, + setArtistView, + executeCommand +}; + +class ArtistIndexConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + view, + scrollTop, + isSmallScreen + } = props; + + this.state = { + scrollTop: getScrollTop(view, scrollTop, isSmallScreen) + }; + } + + componentDidMount() { + this.props.fetchArtist(); + } + + // + // Listeners + + onSortSelect = (sortKey) => { + this.props.setArtistSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setArtistFilter({ filterKey, filterValue, filterType }); + } + + onViewSelect = (view) => { + // Reset the scroll position before changing the view + this.setState({ scrollTop: 0 }, () => { + this.props.setArtistView({ view }); + }); + } + + onScroll = ({ scrollTop }) => { + this.setState({ + scrollTop + }, () => { + scrollPositions.seriesIndex = scrollTop; + }); + } + + onRefreshSeriesPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_SERIES + }); + } + + onRssSyncPress = () => { + this.props.executeCommand({ + name: commandNames.RSS_SYNC + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +ArtistIndexConnector.propTypes = { + view: PropTypes.string.isRequired, + scrollTop: PropTypes.number.isRequired, + fetchArtist: PropTypes.func.isRequired, + setArtistSort: PropTypes.func.isRequired, + setArtistFilter: PropTypes.func.isRequired, + setArtistView: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default withScrollPosition( + connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexConnector), + 'seriesIndex' +); diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.css b/frontend/src/Artist/Index/ArtistIndexFooter.css new file mode 100644 index 000000000..3aa369576 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooter.css @@ -0,0 +1,66 @@ +.footer { + display: flex; + flex-wrap: wrap; + margin-top: 20px; + font-size: $smallFontSize; +} + +.legendItem { + display: flex; + margin-bottom: 4px; + line-height: 16px; +} + +.legendItemColor { + margin-right: 8px; + width: 30px; + height: 16px; + border-radius: 4px; +} + +.continuing { + composes: legendItemColor; + + background-color: $primaryColor; +} + +.ended { + composes: legendItemColor; + + background-color: $successColor; +} + +.missingMonitored { + composes: legendItemColor; + + background-color: $dangerColor; +} + +.missingUnmonitored { + composes: legendItemColor; + + background-color: $warningColor; +} + +.statistics { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +@media (max-width: $breakpointLarge) { + .statistics { + display: block; + } +} + +@media (max-width: $breakpointSmall) { + .footer { + display: block; + } + + .statistics { + display: flex; + margin-top: 20px; + } +} diff --git a/frontend/src/Artist/Index/ArtistIndexFooter.js b/frontend/src/Artist/Index/ArtistIndexFooter.js new file mode 100644 index 000000000..70664e528 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexFooter.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; +import styles from './ArtistIndexFooter.css'; + +function ArtistIndexFooter({ series }) { + const count = series.length; + let episodes = 0; + let episodeFiles = 0; + let ended = 0; + let continuing = 0; + let monitored = 0; + + series.forEach((s) => { + episodes += s.trackCount || 0; + episodeFiles += s.trackFileCount || 0; + + if (s.status === 'ended') { + ended++; + } else { + continuing++; + } + + if (s.monitored) { + monitored++; + } + }); + + return ( +
+
+
+
+
Continuing (All tracks downloaded)
+
+ +
+
+
Ended (All tracks downloaded)
+
+ +
+
+
Missing Tracks (Artist monitored)
+
+ +
+
+
Missing Tracks (Artist not monitored)
+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ ); +} + +ArtistIndexFooter.propTypes = { + series: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default ArtistIndexFooter; diff --git a/frontend/src/Artist/Index/ArtistIndexItemConnector.js b/frontend/src/Artist/Index/ArtistIndexItemConnector.js new file mode 100644 index 000000000..2f9e64919 --- /dev/null +++ b/frontend/src/Artist/Index/ArtistIndexItemConnector.js @@ -0,0 +1,77 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state, { seasons }) => seasons, + createQualityProfileSelector(), + createLanguageProfileSelector(), + createCommandsSelector(), + (artistId, seasons, qualityProfile, languageProfile, commands) => { + const isRefreshingSeries = _.some(commands, (command) => { + return command.name === commandNames.REFRESH_SERIES && + command.body.artistId === artistId; + }); + + const latestSeason = _.maxBy(seasons, (season) => season.seasonNumber); + + return { + qualityProfile, + languageProfile, + latestSeason, + isRefreshingSeries + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class ArtistIndexItemConnector extends Component { + + // + // Listeners + + onRefreshSeriesPress = () => { + this.props.executeCommand({ + name: commandNames.REFRESH_SERIES, + artistId: this.props.id + }); + } + + // + // Render + + render() { + const { + component: ItemComponent, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +ArtistIndexItemConnector.propTypes = { + id: PropTypes.number.isRequired, + component: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector); diff --git a/frontend/src/Artist/Index/ArtistIndexPage.js b/frontend/src/Artist/Index/ArtistIndexPage.js new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js new file mode 100644 index 000000000..2fab66a36 --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexFilterMenu.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; + +function ArtistIndexFilterMenu(props) { + const { + filterKey, + filterValue, + onFilterSelect + } = props; + + return ( + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Episodes + + + + ); +} + +ArtistIndexFilterMenu.propTypes = { + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexFilterMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js new file mode 100644 index 000000000..6941e0536 --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexSortMenu.js @@ -0,0 +1,145 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align, sortDirections } from 'Helpers/Props'; +import SortMenu from 'Components/Menu/SortMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import SortMenuItem from 'Components/Menu/SortMenuItem'; + +function ArtistIndexSortMenu(props) { + const { + sortKey, + sortDirection, + onSortSelect + } = props; + + return ( + + + + Name + + + + Network + + + + Quality Profile + + + + Language Profile + + + + Next Airing + + + + Previous Airing + + + + Added + + + + Albums + + + + Tracks + + + + Track Count + + + + Latest Season + + + + Path + + + + Size on Disk + + + + ); +} + +ArtistIndexSortMenu.propTypes = { + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + onSortSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexSortMenu; diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js new file mode 100644 index 000000000..7e3e35764 --- /dev/null +++ b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js @@ -0,0 +1,42 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align } from 'Helpers/Props'; +import ViewMenu from 'Components/Menu/ViewMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; + +function ArtistIndexViewMenu(props) { + const { + view, + onViewSelect + } = props; + + return ( + + + + Table + + + + Posters + + + + ); +} + +ArtistIndexViewMenu.propTypes = { + view: PropTypes.string.isRequired, + onViewSelect: PropTypes.func.isRequired +}; + +export default ArtistIndexViewMenu; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css new file mode 100644 index 000000000..eb1fd8be0 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.css @@ -0,0 +1,88 @@ +$hoverScale: 1.05; + +.container { + padding: 10px; +} + +.content { + transition: all 200ms ease-in; + + &:hover { + z-index: 2; + box-shadow: 0 0 12px $black; + transition: all 200ms ease-in; + + // Transforming causes the content to shift slightly + // transform: scale($hoverScale); + + .controls { + opacity: 0.9; + transition: opacity 200ms linear 150ms; + } + } +} + +.posterContainer { + position: relative; +} + +.link { + composes: link from 'Components/Link/Link.css'; + + display: block; + background-color: $defaultColor; +} + +.nextAiring { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.title { + composes: truncate from 'Styles/mixins/truncate.css'; + + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} + +.ended { + position: absolute; + top: 0; + right: 0; + width: 0; + height: 0; + border-width: 0 25px 25px 0; + border-style: solid; + border-color: transparent $dangerColor transparent transparent; + color: $white; +} + +.controls { + position: absolute; + bottom: 10px; + left: 10px; + border-radius: 4px; + background-color: #216044; + color: $white; + font-size: $smallFontSize; + opacity: 0; + transition: opacity 0; +} + +.action { + composes: button from 'Components/Link/IconButton.css'; + + &:hover { + color: #ccc; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .container { + padding: 5px; + } +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js new file mode 100644 index 000000000..2d5fc45b9 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPoster.js @@ -0,0 +1,230 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Label from 'Components/Label'; +import Link from 'Components/Link/Link'; +import ArtistPoster from 'Artist/ArtistPoster'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistIndexPosterProgressBar from './ArtistIndexPosterProgressBar'; +import ArtistIndexPosterInfo from './ArtistIndexPosterInfo'; +import styles from './ArtistIndexPoster.css'; + +class ArtistIndexPoster extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditSeriesPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditSeriesModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteSeriesPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteSeriesModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + style, + id, + artistName, + monitored, + status, + nameSlug, + nextAiring, + trackCount, + trackFileCount, + images, + posterWidth, + posterHeight, + detailedProgressBar, + showTitle, + showQualityProfile, + qualityProfile, + showRelativeDates, + shortDateFormat, + timeFormat, + isRefreshingSeries, + onRefreshSeriesPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + const link = `/series/${nameSlug}`; + + const elementStyle = { + width: `${posterWidth}px`, + height: `${posterHeight}px` + }; + + return ( +
+
+
+ + + { + status === 'ended' && +
+ } + + + + +
+ + + + { + showTitle && +
+ {artistName} +
+ } + + { + showQualityProfile && +
+ {qualityProfile.name} +
+ } + +
+ { + getRelativeDate( + nextAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ + + + + + +
+
+ ); + } +} + +ArtistIndexPoster.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + nextAiring: PropTypes.string, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + images: PropTypes.arrayOf(PropTypes.object).isRequired, + posterWidth: PropTypes.number.isRequired, + posterHeight: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + qualityProfile: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + isRefreshingSeries: PropTypes.bool.isRequired, + onRefreshSeriesPress: PropTypes.func.isRequired +}; + +ArtistIndexPoster.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexPoster; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css new file mode 100644 index 000000000..cab3dec61 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.css @@ -0,0 +1,6 @@ +.info { + background-color: $defaultColor; + color: $white; + text-align: center; + font-size: $smallFontSize; +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js new file mode 100644 index 000000000..5a738cc0e --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterInfo.js @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import formatBytes from 'Utilities/Number/formatBytes'; +import styles from './ArtistIndexPosterInfo.css'; + +function ArtistIndexPosterInfo(props) { + const { + network, + qualityProfile, + previousAiring, + added, + albumCount, + path, + sizeOnDisk, + sortKey, + showRelativeDates, + shortDateFormat, + timeFormat + } = props; + + if (sortKey === 'network' && network) { + return ( +
+ {network} +
+ ); + } + + if (sortKey === 'qualityProfileId') { + return ( +
+ {qualityProfile.name} +
+ ); + } + + if (sortKey === 'previousAiring' && previousAiring) { + return ( +
+ { + getRelativeDate( + previousAiring, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: true + } + ) + } +
+ ); + } + + if (sortKey === 'added' && added) { + const addedDate = getRelativeDate( + added, + shortDateFormat, + showRelativeDates, + { + timeFormat, + timeForToday: false + } + ); + + return ( +
+ {`Added ${addedDate}`} +
+ ); + } + + if (sortKey === 'albumCount') { + let seasons = '1 season'; + + if (albumCount === 0) { + seasons = 'No seasons'; + } else if (albumCount > 1) { + seasons = `${albumCount} seasons`; + } + + return ( +
+ {seasons} +
+ ); + } + + if (sortKey === 'path') { + return ( +
+ {path} +
+ ); + } + + if (sortKey === 'sizeOnDisk') { + return ( +
+ {formatBytes(sizeOnDisk)} +
+ ); + } + + return null; +} + +ArtistIndexPosterInfo.propTypes = { + network: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + sortKey: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default ArtistIndexPosterInfo; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.css b/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.css new file mode 100644 index 000000000..dbf3499ab --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.css @@ -0,0 +1,14 @@ +.progress { + composes: container from 'Components/ProgressBar.css'; + + border-radius: 0; + background-color: #5b5b5b; + color: $white; + transition: width 200ms ease; +} + +.progressBar { + composes: progressBar from 'Components/ProgressBar.css'; + + transition: width 200ms ease; +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.js new file mode 100644 index 000000000..1ab803251 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosterProgressBar.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import { sizes } from 'Helpers/Props'; +import ProgressBar from 'Components/ProgressBar'; +import styles from './ArtistIndexPosterProgressBar.css'; + +function ArtistIndexPosterProgressBar(props) { + const { + monitored, + status, + trackCount, + trackFileCount, + posterWidth, + detailedProgressBar + } = props; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + const text = `${trackFileCount} / ${trackCount}`; + + return ( + + ); +} + +ArtistIndexPosterProgressBar.propTypes = { + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + trackCount: PropTypes.number.isRequired, + trackFileCount: PropTypes.number.isRequired, + posterWidth: PropTypes.number.isRequired, + detailedProgressBar: PropTypes.bool.isRequired +}; + +export default ArtistIndexPosterProgressBar; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css new file mode 100644 index 000000000..9c6520fb5 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.css @@ -0,0 +1,3 @@ +.grid { + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js new file mode 100644 index 000000000..b815d5708 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPosters.js @@ -0,0 +1,326 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { Grid, WindowScroller } from 'react-virtualized'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import dimensions from 'Styles/Variables/dimensions'; +import { sortDirections } from 'Helpers/Props'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexPoster from './ArtistIndexPoster'; +import styles from './ArtistIndexPosters.css'; + +// Poster container dimensions +const columnPadding = 20; +const columnPaddingSmallScreen = 10; +const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); +const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); + +const additionalColumnCount = { + small: 3, + medium: 2, + large: 1 +}; + +function calculateColumnWidth(width, posterSize, isSmallScreen) { + const maxiumColumnWidth = isSmallScreen ? 172 : 182; + const columns = Math.floor(width / maxiumColumnWidth); + const remainder = width % maxiumColumnWidth; + + if (remainder === 0 && posterSize === 'large') { + return maxiumColumnWidth; + } + + return Math.floor(width / (columns + additionalColumnCount[posterSize])); +} + +function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) { + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = posterOptions; + + const nextAiringHeight = 19; + + const heights = [ + posterHeight, + detailedProgressBar ? detailedProgressBarHeight : progressBarHeight, + nextAiringHeight, + isSmallScreen ? columnPaddingSmallScreen : columnPadding + ]; + + if (showTitle) { + heights.push(19); + } + + if (showQualityProfile) { + heights.push(19); + } + + switch (sortKey) { + case 'network': + case 'seasons': + case 'previousAiring': + case 'added': + case 'path': + case 'sizeOnDisk': + heights.push(19); + break; + case 'qualityProfileId': + if (!showQualityProfile) { + heights.push(19); + } + break; + } + + return heights.reduce((acc, height) => acc + height, 0); +} + +function calculatePosterHeight(posterWidth) { + return Math.ceil((250 / 170) * posterWidth); +} + +class ArtistIndexPosters extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0, + columnWidth: 182, + columnCount: 1, + posterWidth: 162, + posterHeight: 238, + rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}) + }; + + this._isInitialized = false; + this._grid = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + componentDidUpdate(prevProps) { + const { + items, + filterKey, + filterValue, + sortKey, + sortDirection, + posterOptions + } = this.props; + + const itemsChanged = hasDifferentItems(prevProps.items, items); + + if ( + prevProps.sortKey !== sortKey || + prevProps.posterOptions !== posterOptions || + itemsChanged + ) { + this.calculateGrid(); + } + + if ( + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection || + itemsChanged + ) { + this._grid.recomputeGridSize(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + const { + columnCount, + rowHeight + } = this.state; + + const index = _.findIndex(items, (item) => { + const firstCharacter = item.sortTitle.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (index != null) { + const row = Math.floor(index / columnCount); + const scrollTop = rowHeight * row; + + this.props.onScroll({ scrollTop }); + } + } + + setGridRef = (ref) => { + this._grid = ref; + } + + calculateGrid = (width = this.state.width, isSmallScreen) => { + const { + sortKey, + posterOptions + } = this.props; + + const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; + const columnWidth = calculateColumnWidth(width, this.props.posterOptions.size); + const columnCount = Math.max(Math.floor(width / columnWidth), 1); + const posterWidth = columnWidth - padding; + const posterHeight = calculatePosterHeight(posterWidth); + const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions); + + this.setState({ + width, + columnWidth, + columnCount, + posterWidth, + posterHeight, + rowHeight + }); + } + + cellRenderer = ({ key, rowIndex, columnIndex, style }) => { + const { + items, + sortKey, + posterOptions, + showRelativeDates, + shortDateFormat, + timeFormat + } = this.props; + + const { + posterWidth, + posterHeight, + columnCount + } = this.state; + + const { + detailedProgressBar, + showTitle, + showQualityProfile + } = posterOptions; + + const series = items[rowIndex * columnCount + columnIndex]; + + if (!series) { + return null; + } + + return ( + + ); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.calculateGrid(width, this.props.isSmallScreen); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + items, + scrollTop, + isSmallScreen, + onScroll + } = this.props; + + const { + width, + columnWidth, + columnCount, + rowHeight + } = this.state; + + const rowCount = Math.ceil(items.length / columnCount); + + return ( + + + {({ height, isScrolling }) => { + return ( + + ); + } + } + + + ); + } +} + +ArtistIndexPosters.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + posterOptions: PropTypes.object.isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexPosters; diff --git a/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js new file mode 100644 index 000000000..9e200cfa6 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/ArtistIndexPostersConnector.js @@ -0,0 +1,33 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import ArtistIndexPosters from './ArtistIndexPosters'; + +function createMapStateToProps() { + return createSelector( + (state) => state.seriesIndex.posterOptions, + createClientSideCollectionSelector(), + createUISettingsSelector(), + createDimensionsSelector(), + (posterOptions, series, uiSettings, dimensions) => { + return { + posterOptions, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + timeFormat: uiSettings.timeFormat, + isSmallScreen: dimensions.isSmallScreen, + ...series + }; + } + ); +} + +export default connectSection( + createMapStateToProps, + undefined, + undefined, + { withRef: true }, + { section: 'series', uiSection: 'seriesIndex' } + )(ArtistIndexPosters); diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js new file mode 100644 index 000000000..e1b0a257a --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ArtistIndexPosterOptionsModalContentConnector from './ArtistIndexPosterOptionsModalContentConnector'; + +function ArtistIndexPosterOptionsModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +ArtistIndexPosterOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModal; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js new file mode 100644 index 000000000..275af0528 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContent.js @@ -0,0 +1,173 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +const posterSizeOptions = [ + { key: 'small', value: 'Small' }, + { key: 'medium', value: 'Medium' }, + { key: 'large', value: 'Large' } +]; + +class ArtistIndexPosterOptionsModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + detailedProgressBar: props.detailedProgressBar, + size: props.size, + showTitle: props.showTitle, + showQualityProfile: props.showQualityProfile + }; + } + + componentDidUpdate(prevProps) { + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.props; + + const state = {}; + + if (detailedProgressBar !== prevProps.detailedProgressBar) { + state.detailedProgressBar = detailedProgressBar; + } + + if (size !== prevProps.size) { + state.size = size; + } + + if (showTitle !== prevProps.showTitle) { + state.showTitle = showTitle; + } + + if (showQualityProfile !== prevProps.showQualityProfile) { + state.showQualityProfile = showQualityProfile; + } + + if (!_.isEmpty(state)) { + this.setState(state); + } + } + + // + // Listeners + + onChangePosterOption = ({ name, value }) => { + this.setState({ + [name]: value + }, () => { + this.props.onChangePosterOption({ [name]: value }); + }); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + detailedProgressBar, + size, + showTitle, + showQualityProfile + } = this.state; + + return ( + + + Poster Options + + + +
+ + Poster Size + + + + + + Detailed Progress Bar + + + + + + Show Title + + + + + + Show Quality Profile + + + +
+
+ + + + +
+ ); + } +} + +ArtistIndexPosterOptionsModalContent.propTypes = { + size: PropTypes.string.isRequired, + showTitle: PropTypes.bool.isRequired, + showQualityProfile: PropTypes.bool.isRequired, + detailedProgressBar: PropTypes.bool.isRequired, + onChangePosterOption: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default ArtistIndexPosterOptionsModalContent; diff --git a/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js new file mode 100644 index 000000000..878518647 --- /dev/null +++ b/frontend/src/Artist/Index/Posters/Options/ArtistIndexPosterOptionsModalContentConnector.js @@ -0,0 +1,23 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setArtistPosterOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.seriesIndex, + (seriesIndex) => { + return seriesIndex.posterOptions; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onChangePosterOption(payload) { + dispatch(setArtistPosterOption(payload)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexPosterOptionsModalContent); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js new file mode 100644 index 000000000..106f07327 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexActionsCell.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; + +class ArtistIndexActionsCell extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + // + // Listeners + + onEditSeriesPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditSeriesModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteSeriesPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteSeriesModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + isRefreshingSeries, + onRefreshSeriesPress, + ...otherProps + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + + + + + + + + + + ); + } +} + +ArtistIndexActionsCell.propTypes = { + id: PropTypes.number.isRequired, + isRefreshingSeries: PropTypes.bool.isRequired, + onRefreshSeriesPress: PropTypes.func.isRequired +}; + +export default ArtistIndexActionsCell; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css new file mode 100644 index 000000000..f5095d9dd --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css @@ -0,0 +1,81 @@ +.status { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 60px; +} + +.sortName { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 4 0 110px; +} + +.network { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 2 0 90px; +} + +.qualityProfileId, +.languageProfileId { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 125px; +} + +.nextAiring, +.previousAiring, +.added { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 180px; +} + +.albumCount { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 100px; +} + +.trackProgress, +.latestSeason { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 150px; +} + +.trackCount { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 120px; +} + +.path { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 150px; +} + +.sizeOnDisk { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 110px; +} + +.tags { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 1 0 60px; +} + +.useSceneNumbering { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 145px; +} + +.actions { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + flex: 0 0 70px; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.js b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js new file mode 100644 index 000000000..8c1bd8682 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; +import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; +import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; +import styles from './ArtistIndexHeader.css'; + +class ArtistIndexHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isTableOptionsModalOpen: false + }; + } + + // + // Listeners + + onTableOptionsPress = () => { + this.setState({ isTableOptionsModalOpen: true }); + } + + onTableOptionsModalClose = () => { + this.setState({ isTableOptionsModalOpen: false }); + } + + // + // Render + + render() { + const { + columns, + onTableOptionChange, + ...otherProps + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + label, + isSortable, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'actions') { + return ( + + + + ); + } + + return ( + + {label} + + ); + }) + } + + + + ); + } +} + +ArtistIndexHeader.propTypes = { + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onTableOptionChange: PropTypes.func.isRequired +}; + +export default ArtistIndexHeader; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js new file mode 100644 index 000000000..37ddd9ef3 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexHeaderConnector.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { setArtistTableOption } from 'Store/Actions/artistIndexActions'; +import ArtistIndexHeader from './ArtistIndexHeader'; + +function createMapDispatchToProps(dispatch, props) { + return { + onTableOptionChange(payload) { + dispatch(setArtistTableOption(payload)); + } + }; +} + +export default connect(undefined, createMapDispatchToProps)(ArtistIndexHeader); diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css new file mode 100644 index 000000000..23f061f52 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css @@ -0,0 +1,90 @@ +.status { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 60px; +} + +.sortName { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 4 0 110px; +} + +.network { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 2 0 90px; +} + +.qualityProfileId, +.languageProfileId { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 125px; +} + +.nextAiring, +.previousAiring, +.added { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 180px; +} + +.albumCount { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 100px; +} + +.trackProgress, +.latestSeason { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + display: flex; + justify-content: center; + flex: 0 0 150px; + flex-direction: column; +} + +.trackCount { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 120px; +} + +.path { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 150px; +} + +.sizeOnDisk { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 110px; +} + +.tags { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 1 0 60px; +} + +.useSceneNumbering { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 145px; +} + +.actions { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 70px; +} + +.checkInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin-top: 0; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js new file mode 100644 index 000000000..44df146f4 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js @@ -0,0 +1,389 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import ProgressBar from 'Components/ProgressBar'; +import TagListConnector from 'Components/TagListConnector'; +// import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableRow from 'Components/Table/VirtualTableRow'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; +import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; +import ArtistStatusCell from './ArtistStatusCell'; +import styles from './ArtistIndexRow.css'; + +class ArtistIndexRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: false + }; + } + + onEditSeriesPress = () => { + this.setState({ isEditArtistModalOpen: true }); + } + + onEditSeriesModalClose = () => { + this.setState({ isEditArtistModalOpen: false }); + } + + onDeleteSeriesPress = () => { + this.setState({ + isEditArtistModalOpen: false, + isDeleteArtistModalOpen: true + }); + } + + onDeleteSeriesModalClose = () => { + this.setState({ isDeleteArtistModalOpen: false }); + } + + onUseSceneNumberingChange = () => { + // Mock handler to satisfy `onChange` being required for `CheckInput`. + // + } + + // + // Render + + render() { + const { + style, + id, + monitored, + status, + artistName, + nameSlug, + network, + qualityProfile, + languageProfile, + nextAiring, + previousAiring, + added, + albumCount, + trackCount, + trackFileCount, + totalTrackCount, + latestSeason, + path, + sizeOnDisk, + tags, + // useSceneNumbering, + columns, + isRefreshingSeries, + onRefreshSeriesPress + } = this.props; + + const { + isEditArtistModalOpen, + isDeleteArtistModalOpen + } = this.state; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'status') { + return ( + + ); + } + + if (name === 'sortName') { + return ( + + + + ); + } + + if (name === 'network') { + return ( + + {network} + + ); + } + + if (name === 'qualityProfileId') { + return ( + + {qualityProfile.name} + + ); + } + + if (name === 'languageProfileId') { + return ( + + {languageProfile.name} + + ); + } + + if (name === 'nextAiring') { + return ( + + ); + } + + if (name === 'previousAiring') { + return ( + + ); + } + + if (name === 'added') { + return ( + + ); + } + + if (name === 'albumCount') { + return ( + + {albumCount} + + ); + } + + if (name === 'trackProgress') { + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + return ( + + + + ); + } + + if (name === 'latestSeason') { + const seasonStatistics = latestSeason.statistics; + const progress = seasonStatistics.episodeCount ? seasonStatistics.episodeFileCount / seasonStatistics.episodeCount * 100 : 100; + + return ( + + + + ); + } + + if (name === 'trackCount') { + return ( + + {totalTrackCount} + + ); + } + + if (name === 'path') { + return ( + + {path} + + ); + } + + if (name === 'sizeOnDisk') { + return ( + + {formatBytes(sizeOnDisk)} + + ); + } + + if (name === 'tags') { + return ( + + + + ); + } + + // if (name === 'useSceneNumbering') { + // return ( + // + // + // + // ); + // } + + if (name === 'actions') { + return ( + + + + + + ); + } + + return null; + }) + } + + + + + + ); + } +} + +ArtistIndexRow.propTypes = { + style: PropTypes.object.isRequired, + id: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + artistName: PropTypes.string.isRequired, + nameSlug: PropTypes.string.isRequired, + network: PropTypes.string, + qualityProfile: PropTypes.object.isRequired, + languageProfile: PropTypes.object.isRequired, + nextAiring: PropTypes.string, + previousAiring: PropTypes.string, + added: PropTypes.string, + albumCount: PropTypes.number.isRequired, + trackCount: PropTypes.number, + trackFileCount: PropTypes.number, + totalTrackCount: PropTypes.number, + latestSeason: PropTypes.object, + path: PropTypes.string.isRequired, + sizeOnDisk: PropTypes.number, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + // useSceneNumbering: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + isRefreshingSeries: PropTypes.bool.isRequired, + onRefreshSeriesPress: PropTypes.func.isRequired +}; + +ArtistIndexRow.defaultProps = { + trackCount: 0, + trackFileCount: 0 +}; + +export default ArtistIndexRow; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.css b/frontend/src/Artist/Index/Table/ArtistIndexTable.css new file mode 100644 index 000000000..e46160a96 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.css @@ -0,0 +1,5 @@ +.tableContainer { + composes: tableContainer from 'Components/Table/VirtualTable.css'; + + flex: 1 0 auto; +} diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js new file mode 100644 index 000000000..59b4c23be --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js @@ -0,0 +1,143 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sortDirections } from 'Helpers/Props'; +import VirtualTable from 'Components/Table/VirtualTable'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexHeaderConnector from './ArtistIndexHeaderConnector'; +import ArtistIndexRow from './ArtistIndexRow'; +import styles from './ArtistIndexTable.css'; + +class ArtistIndexTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._table = null; + } + + componentDidUpdate(prevProps) { + const { + columns, + filterKey, + filterValue, + sortKey, + sortDirection + } = this.props; + + if (prevProps.columns !== columns || + prevProps.filterKey !== filterKey || + prevProps.filterValue !== filterValue || + prevProps.sortKey !== sortKey || + prevProps.sortDirection !== sortDirection + ) { + this._table.forceUpdateGrid(); + } + } + + // + // Control + + scrollToFirstCharacter(character) { + const items = this.props.items; + + const row = _.findIndex(items, (item) => { + const firstCharacter = item.sortTitle.charAt(0); + + if (character === '#') { + return !isNaN(firstCharacter); + } + + return firstCharacter === character; + }); + + if (row != null) { + this._table.scrollToRow(row); + } + } + + setTableRef = (ref) => { + this._table = ref; + } + + rowRenderer = ({ key, rowIndex, style }) => { + const { + items, + columns + } = this.props; + + const series = items[rowIndex]; + + return ( + + ); + } + + // + // Render + + render() { + const { + items, + columns, + sortKey, + sortDirection, + isSmallScreen, + scrollTop, + contentBody, + onSortPress, + onRender, + onScroll + } = this.props; + + return ( + + } + onRender={onRender} + onScroll={onScroll} + /> + ); + } +} + +ArtistIndexTable.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + onSortPress: PropTypes.func.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +export default ArtistIndexTable; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js new file mode 100644 index 000000000..f94950b87 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistIndexTableConnector.js @@ -0,0 +1,34 @@ +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { setArtistSort } from 'Store/Actions/artistIndexActions'; +import ArtistIndexTable from './ArtistIndexTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.dimensions, + createClientSideCollectionSelector(), + (dimensions, series) => { + return { + isSmallScreen: dimensions.isSmallScreen, + ...series + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onSortPress(sortKey) { + dispatch(setArtistSort({ sortKey })); + } + }; +} + +export default connectSection( + createMapStateToProps, + createMapDispatchToProps, + undefined, + { withRef: true }, + { section: 'series', uiSection: 'seriesIndex' } + )(ArtistIndexTable); diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.css b/frontend/src/Artist/Index/Table/ArtistStatusCell.css new file mode 100644 index 000000000..d19ddf05b --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.css @@ -0,0 +1,9 @@ +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 60px; +} + +.statusIcon { + width: 20px; +} diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.js b/frontend/src/Artist/Index/Table/ArtistStatusCell.js new file mode 100644 index 000000000..734e50722 --- /dev/null +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './ArtistStatusCell.css'; + +function ArtistStatusCell(props) { + const { + className, + monitored, + status, + component: Component, + ...otherProps + } = props; + + return ( + + + + + + ); +} + +ArtistStatusCell.propTypes = { + className: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + status: PropTypes.string.isRequired, + component: PropTypes.func +}; + +ArtistStatusCell.defaultProps = { + className: styles.status, + component: VirtualTableRowCell +}; + +export default ArtistStatusCell; diff --git a/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js b/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js new file mode 100644 index 000000000..b15554482 --- /dev/null +++ b/frontend/src/Artist/Index/Table/artistIndexCellRenderers.js @@ -0,0 +1,138 @@ +import React from 'react'; +import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; +import ProgressBar from 'Components/ProgressBar'; +import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; +import ArtistIndexActionsCell from './ArtistIndexActionsCell'; +import ArtistStatusCell from './ArtistStatusCell'; + +export default function artistIndexCellRenderers(cellProps) { + const { + cellKey, + dataKey, + rowData, + ...otherProps + } = cellProps; + + const { + id, + monitored, + status, + name, + nameSlug, + network, + qualityProfileId, + nextAiring, + previousAiring, + albumCount, + trackCount, + trackFileCount + } = rowData; + + const progress = trackCount ? trackFileCount / trackCount * 100 : 100; + + if (dataKey === 'status') { + return ( + + ); + } + + if (dataKey === 'sortTitle') { + return ( + + + + + ); + } + + if (dataKey === 'network') { + return ( + + {network} + + + ); + } + + if (dataKey === 'qualityProfileId') { + return ( + + + + ); + } + + if (dataKey === 'nextAiring') { + return ( + + ); + } + + if (dataKey === 'seasonCount') { + return ( + + {albumCount} + + ); + } + + if (dataKey === 'episodeProgress') { + return ( + + + + ); + } + + if (dataKey === 'actions') { + return ( + + ); + } +} diff --git a/frontend/src/Artist/NoArtist.css b/frontend/src/Artist/NoArtist.css new file mode 100644 index 000000000..38a01f391 --- /dev/null +++ b/frontend/src/Artist/NoArtist.css @@ -0,0 +1,11 @@ +.message { + margin-top: 10px; + margin-bottom: 30px; + text-align: center; + font-size: 20px; +} + +.buttonContainer { + margin-top: 20px; + text-align: center; +} diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js new file mode 100644 index 000000000..b6e90cf63 --- /dev/null +++ b/frontend/src/Artist/NoArtist.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import styles from './NoArtist.css'; + +function NoArtist() { + return ( +
+
+ No artist found, to get started you'll want to add a new artist or import some existing ones. +
+ +
+ +
+ +
+ +
+
+ ); +} + +export default NoArtist; diff --git a/frontend/src/Calendar/Agenda/Agenda.css b/frontend/src/Calendar/Agenda/Agenda.css new file mode 100644 index 000000000..0304d9db5 --- /dev/null +++ b/frontend/src/Calendar/Agenda/Agenda.css @@ -0,0 +1,3 @@ +.agenda { + margin-top: 10px; +} diff --git a/frontend/src/Calendar/Agenda/Agenda.js b/frontend/src/Calendar/Agenda/Agenda.js new file mode 100644 index 000000000..89472301d --- /dev/null +++ b/frontend/src/Calendar/Agenda/Agenda.js @@ -0,0 +1,38 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import AgendaEventConnector from './AgendaEventConnector'; +import styles from './Agenda.css'; + +function Agenda(props) { + const { + items + } = props; + + return ( +
+ { + items.map((item, index) => { + const momentDate = moment(item.airDateUtc); + const showDate = index === 0 || + !moment(items[index - 1].airDateUtc).isSame(momentDate, 'day'); + + return ( + + ); + }) + } +
+ ); +} + +Agenda.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Agenda; diff --git a/frontend/src/Calendar/Agenda/AgendaConnector.js b/frontend/src/Calendar/Agenda/AgendaConnector.js new file mode 100644 index 000000000..b6f238873 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaConnector.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import Agenda from './Agenda'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (calendar) => { + return calendar; + } + ); +} + +export default connect(createMapStateToProps)(Agenda); diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css b/frontend/src/Calendar/Agenda/AgendaEvent.css new file mode 100644 index 000000000..94d36a125 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEvent.css @@ -0,0 +1,113 @@ +.event { + display: flex; + overflow-x: hidden; + padding: 5px; + border-bottom: 1px solid $borderColor; + font-size: 14px; + + &:hover { + background-color: $tableRowHoverBackgroundColor; + } +} + +.status { + width: 10px; + border-left-width: 4px; + border-left-style: solid; +} + +.date { + flex: 0 0 250px; + font-weight: bold; +} + +.time { + flex: 0 0 120px; + margin-right: 10px; +} + +.seriesTitle, +.episodeTitle { + composes: truncate from 'Styles/Mixins/truncate.css'; + + flex: 0 1 300px; + margin-right: 10px; +} + +.episodeTitle { + flex: 1 1 1px; +} + +.seasonEpisodeNumber { + flex: 0 0 100px; +} + +.episodeSeparator { + display: none; +} + +.absoluteEpisodeNumber { + margin-left: 3px; +} + +/* + * Status + */ + +.downloaded { + composes: downloaded from 'Calendar/Events/CalendarEvent.css'; +} + +.downloading { + composes: downloading from 'Calendar/Events/CalendarEvent.css'; +} + +.unmonitored { + composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; +} + +.onAir { + composes: onAir from 'Calendar/Events/CalendarEvent.css'; +} + +.missing { + composes: missing from 'Calendar/Events/CalendarEvent.css'; +} + +.premiere { + composes: premiere from 'Calendar/Events/CalendarEvent.css'; +} + +.unaired { + composes: unaired from 'Calendar/Events/CalendarEvent.css'; +} + +@media only screen and (max-width: $breakpointSmall) { + .event { + position: relative; + flex-wrap: wrap; + padding-left: 10px; + } + + .status { + position: absolute; + top: 7%; + left: 0; + height: 86%; + } + + .date, + .time, + .seriesTitle { + flex: 0 0 100%; + } + + .seasonEpisodeNumber { + flex: 0 0 auto; + } + + .episodeSeparator { + display: inline-block; + margin: 0 5px; + } +} diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js new file mode 100644 index 000000000..028f969dd --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEvent.js @@ -0,0 +1,160 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import { icons } from 'Helpers/Props'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import episodeEntities from 'Episode/episodeEntities'; +import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; +import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails'; +import styles from './AgendaEvent.css'; + +class AgendaEvent extends Component { + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + series, + title, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDateUtc, + monitored, + hasFile, + grabbed, + queueItem, + showDate, + timeFormat, + longDateFormat + } = this.props; + + const startTime = moment(airDateUtc); + const endTime = startTime.add(series.runtime, 'minutes'); + const downloading = !!(queueItem || grabbed); + const isMonitored = series.monitored && monitored; + const statusStyle = getStatusStyle(episodeNumber, hasFile, downloading, startTime, endTime, isMonitored); + + return ( +
+ +
+ { + showDate && + startTime.format(longDateFormat) + } +
+ +
+ +
+ {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} +
+ +
+ {series.title} +
+ +
+ {seasonNumber}x{padNumber(episodeNumber, 2)} + + { + series.seriesType === 'anime' && absoluteEpisodeNumber && + ({absoluteEpisodeNumber}) + } + +
-
+
+ +
+ {title} +
+ + { + !!queueItem && + + } + + { + !queueItem && grabbed && + + } + + + +
+ ); + } +} + +AgendaEvent.propTypes = { + id: PropTypes.number.isRequired, + series: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + airDateUtc: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + hasFile: PropTypes.bool.isRequired, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + showDate: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired +}; + +export default AgendaEvent; diff --git a/frontend/src/Calendar/Agenda/AgendaEventConnector.js b/frontend/src/Calendar/Agenda/AgendaEventConnector.js new file mode 100644 index 000000000..d6d4c8cc5 --- /dev/null +++ b/frontend/src/Calendar/Agenda/AgendaEventConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import AgendaEvent from './AgendaEvent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createQueueItemSelector(), + createUISettingsSelector(), + (series, queueItem, uiSettings) => { + return { + series, + queueItem, + timeFormat: uiSettings.timeFormat, + longDateFormat: uiSettings.longDateFormat + }; + } + ); +} + +export default connect(createMapStateToProps)(AgendaEvent); diff --git a/frontend/src/Calendar/Calendar.css b/frontend/src/Calendar/Calendar.css new file mode 100644 index 000000000..37e6ff618 --- /dev/null +++ b/frontend/src/Calendar/Calendar.css @@ -0,0 +1,8 @@ +.calendar { + flex-grow: 1; + width: 100%; +} + +.calendarContent { + width: 100%; +} diff --git a/frontend/src/Calendar/Calendar.js b/frontend/src/Calendar/Calendar.js new file mode 100644 index 000000000..6ceb1f3bb --- /dev/null +++ b/frontend/src/Calendar/Calendar.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import * as calendarViews from './calendarViews'; +import CalendarHeaderConnector from './Header/CalendarHeaderConnector'; +import DaysOfWeekConnector from './Day/DaysOfWeekConnector'; +import CalendarDaysConnector from './Day/CalendarDaysConnector'; +import AgendaConnector from './Agenda/AgendaConnector'; +import styles from './Calendar.css'; + +class Calendar extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + view + } = this.props; + + return ( +
+ { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && view === calendarViews.AGENDA && +
+ + +
+ } + + { + !error && isPopulated && view !== calendarViews.AGENDA && +
+ + + +
+ } +
+ ); + } +} + +Calendar.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + view: PropTypes.string.isRequired +}; + +export default Calendar; diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js new file mode 100644 index 000000000..993aeef73 --- /dev/null +++ b/frontend/src/Calendar/CalendarConnector.js @@ -0,0 +1,145 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import * as calendarActions from 'Store/Actions/calendarActions'; +import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import Calendar from './Calendar'; + +const UPDATE_DELAY = 3600000; // 1 hour + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (calendar) => { + return calendar; + } + ); +} + +const mapDispatchToProps = { + ...calendarActions, + fetchEpisodeFiles, + clearEpisodeFiles, + fetchQueueDetails, + clearQueueDetails +}; + +class CalendarConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.updateTimeoutId = null; + } + + componentDidMount() { + this.props.gotoCalendarToday(); + this.scheduleUpdate(); + } + + componentDidUpdate(prevProps) { + const { + items, + time + } = this.props; + + if (hasDifferentItems(prevProps.items, items)) { + const episodeIds = selectUniqueIds(items, 'id'); + const episodeFileIds = selectUniqueIds(items, 'episodeFileId'); + + this.props.fetchQueueDetails({ episodeIds }); + + if (episodeFileIds.length) { + this.props.fetchEpisodeFiles({ episodeFileIds }); + } + } + + if (prevProps.time !== time) { + this.scheduleUpdate(); + } + } + + componentWillUnmount() { + this.props.clearCalendar(); + this.props.clearQueueDetails(); + this.props.clearEpisodeFiles(); + this.clearUpdateTimeout(); + } + + // + // Control + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + + this.updateTimeoutId = setTimeout(this.scheduleUpdate, UPDATE_DELAY); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + updateCalendar = () => { + this.props.gotoCalendarToday(); + this.scheduleUpdate(); + } + + // + // Listeners + + onCalendarViewChange = (view) => { + this.props.setCalendarView({ view }); + } + + onTodayPress = () => { + this.props.gotoCalendarToday(); + } + + onPreviousPress = () => { + this.props.gotoCalendarPreviousRange(); + } + + onNextPress = () => { + this.props.gotoCalendarNextRange(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarConnector.propTypes = { + time: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + setCalendarView: PropTypes.func.isRequired, + gotoCalendarToday: PropTypes.func.isRequired, + gotoCalendarPreviousRange: PropTypes.func.isRequired, + gotoCalendarNextRange: PropTypes.func.isRequired, + clearCalendar: PropTypes.func.isRequired, + fetchEpisodeFiles: PropTypes.func.isRequired, + clearEpisodeFiles: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector); diff --git a/frontend/src/Calendar/CalendarPage.css b/frontend/src/Calendar/CalendarPage.css new file mode 100644 index 000000000..776f3100f --- /dev/null +++ b/frontend/src/Calendar/CalendarPage.css @@ -0,0 +1,14 @@ +.calendarPageBody { + composes: contentBody from 'Components/Page/PageContentBody.css'; + + display: flex; +} + +.calendarInnerPageBody { + composes: innerContentBody from 'Components/Page/PageContentBody.css'; + + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; +} diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js new file mode 100644 index 000000000..b54900b17 --- /dev/null +++ b/frontend/src/Calendar/CalendarPage.js @@ -0,0 +1,134 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import { align, icons } from 'Helpers/Props'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import CalendarLinkModal from './iCal/CalendarLinkModal'; +import Legend from './Legend/Legend'; +import CalendarConnector from './CalendarConnector'; +import styles from './CalendarPage.css'; + +const MINIMUM_DAY_WIDTH = 120; + +class CalendarPage extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isCalendarLinkModalOpen: false, + width: 0 + }; + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ width }, () => { + const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))); + console.log(`${width} || ${days}`); + this.props.onDaysCountChange(days); + }); + } + + onFilterMenuItemPress = (filterKey, unmonitored) => { + this.props.onUnmonitoredChange(unmonitored); + } + + onGetCalendarLinkPress = () => { + this.setState({ isCalendarLinkModalOpen: true }); + } + + onGetCalendarLinkModalClose = () => { + this.setState({ isCalendarLinkModalOpen: false }); + } + + // + // Render + + render() { + const { + unmonitored, + colorImpairedMode + } = this.props; + + return ( + + + + + + + + + + + All + + + + Monitored Only + + + + + + + + + + + + + + + + + ); + } +} + +CalendarPage.propTypes = { + unmonitored: PropTypes.bool.isRequired, + colorImpairedMode: PropTypes.bool.isRequired, + onDaysCountChange: PropTypes.func.isRequired, + onUnmonitoredChange: PropTypes.func.isRequired +}; + +export default CalendarPage; diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js new file mode 100644 index 000000000..59de66f74 --- /dev/null +++ b/frontend/src/Calendar/CalendarPageConnector.js @@ -0,0 +1,33 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { setCalendarDaysCount, setCalendarIncludeUnmonitored } from 'Store/Actions/calendarActions'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CalendarPage from './CalendarPage'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createUISettingsSelector(), + (calendar, uiSettings) => { + return { + unmonitored: calendar.unmonitored, + showUpcoming: calendar.showUpcoming, + colorImpairedMode: uiSettings.enableColorImpairedMode + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDaysCountChange(dayCount) { + dispatch(setCalendarDaysCount({ dayCount })); + }, + + onUnmonitoredChange(unmonitored) { + dispatch(setCalendarIncludeUnmonitored({ unmonitored })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage); diff --git a/frontend/src/Calendar/Day/CalendarDay.css b/frontend/src/Calendar/Day/CalendarDay.css new file mode 100644 index 000000000..1c7694f0b --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDay.css @@ -0,0 +1,25 @@ +.day { + flex: 1 0 14.28%; + overflow: hidden; + min-height: 70px; + border-bottom: 1px solid $borderColor; + border-left: 1px solid $borderColor; +} + +.isSingleDay { + width: 100%; +} + +.dayOfMonth { + padding-right: 5px; + border-bottom: 1px solid $borderColor; + text-align: right; +} + +.isToday { + background-color: $calendarTodayBackgroundColor; +} + +.isDifferentMonth { + color: $disabledColor; +} diff --git a/frontend/src/Calendar/Day/CalendarDay.js b/frontend/src/Calendar/Day/CalendarDay.js new file mode 100644 index 000000000..5e61e7364 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDay.js @@ -0,0 +1,61 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector'; +import styles from './CalendarDay.css'; + +function CalendarDay(props) { + const { + date, + time, + isTodaysDate, + events, + view, + onEventModalOpenToggle + } = props; + + return ( +
+ { + view === calendarViews.MONTH && +
+ {moment(date).date()} +
+ } +
+ { + events.map((event) => { + return ( + + ); + }) + } +
+
+ ); +} + +CalendarDay.propTypes = { + date: PropTypes.string.isRequired, + time: PropTypes.string.isRequired, + isTodaysDate: PropTypes.bool.isRequired, + events: PropTypes.arrayOf(PropTypes.object).isRequired, + view: PropTypes.string.isRequired, + onEventModalOpenToggle: PropTypes.func.isRequired +}; + +export default CalendarDay; diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js new file mode 100644 index 000000000..c825c91fb --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDayConnector.js @@ -0,0 +1,55 @@ +import _ from 'lodash'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import CalendarDay from './CalendarDay'; + +function createCalendarEventsConnector() { + return createSelector( + (state, { date }) => date, + (state) => state.calendar, + (date, calendar) => { + const filtered = _.filter(calendar.items, (item) => { + return moment(date).isSame(moment(item.airDateUtc), 'day'); + }); + + return _.sortBy(filtered, (item) => moment(item.airDateUtc).unix()); + } + ); +} + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createCalendarEventsConnector(), + (calendar, events) => { + return { + time: calendar.time, + view: calendar.view, + events + }; + } + ); +} + +class CalendarDayConnector extends Component { + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarDayConnector.propTypes = { + date: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(CalendarDayConnector); diff --git a/frontend/src/Calendar/Day/CalendarDays.css b/frontend/src/Calendar/Day/CalendarDays.css new file mode 100644 index 000000000..22005e3e6 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDays.css @@ -0,0 +1,14 @@ +.days { + display: flex; + border-right: 1px solid $borderColor; +} + +.day, +.week, +.forecast { + flex-wrap: nowrap; +} + +.month { + flex-wrap: wrap; +} diff --git a/frontend/src/Calendar/Day/CalendarDays.js b/frontend/src/Calendar/Day/CalendarDays.js new file mode 100644 index 000000000..65a894081 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDays.js @@ -0,0 +1,163 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import isToday from 'Utilities/Date/isToday'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarDayConnector from './CalendarDayConnector'; +import styles from './CalendarDays.css'; + +class CalendarDays extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._touchStart = null; + + this.state = { + todaysDate: moment().startOf('day').toISOString(), + isEventModalOpen: false + }; + + this.updateTimeoutId = null; + } + + // Lifecycle + + componentDidMount() { + const view = this.props.view; + + if (view === calendarViews.MONTH) { + this.scheduleUpdate(); + } + + window.addEventListener('touchstart', this.onTouchStart); + window.addEventListener('touchend', this.onTouchEnd); + window.addEventListener('touchcancel', this.onTouchCancel); + window.addEventListener('touchmove', this.onTouchMove); + } + + componentWillUnmount() { + this.clearUpdateTimeout(); + + window.removeEventListener('touchstart', this.onTouchStart); + window.removeEventListener('touchend', this.onTouchEnd); + window.removeEventListener('touchcancel', this.onTouchCancel); + window.removeEventListener('touchmove', this.onTouchMove); + } + + // + // Control + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + const todaysDate = moment().startOf('day'); + const diff = moment().diff(todaysDate.clone().add(1, 'day')); + + this.setState({ todaysDate: todaysDate.toISOString() }); + + this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + // + // Listeners + + onEventModalOpenToggle = (isEventModalOpen) => { + this.setState({ isEventModalOpen }); + } + + onTouchStart = (event) => { + const touches = event.touches; + const touchStart = touches[0].pageX; + + if (touches.length !== 1) { + return; + } + + if ( + touchStart < 50 || + this.props.isSidebarVisible || + this.state.isEventModalOpen + ) { + return; + } + + this._touchStart = touchStart; + } + + onTouchEnd = (event) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; + + if (!this._touchStart) { + return; + } + + if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) { + this.props.onNavigatePrevious(); + } else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) { + this.props.onNavigateNext(); + } + + this._touchStart = null; + } + + onTouchCancel = (event) => { + this._touchStart = null; + } + + onTouchMove = (event) => { + if (!this._touchStart) { + return; + } + } + + // + // Render + + render() { + const { + dates, + view + } = this.props; + + return ( +
+ { + dates.map((date) => { + return ( + + ); + }) + } +
+ ); + } +} + +CalendarDays.propTypes = { + dates: PropTypes.arrayOf(PropTypes.string).isRequired, + view: PropTypes.string.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + onNavigatePrevious: PropTypes.func.isRequired, + onNavigateNext: PropTypes.func.isRequired +}; + +export default CalendarDays; diff --git a/frontend/src/Calendar/Day/CalendarDaysConnector.js b/frontend/src/Calendar/Day/CalendarDaysConnector.js new file mode 100644 index 000000000..9dd965146 --- /dev/null +++ b/frontend/src/Calendar/Day/CalendarDaysConnector.js @@ -0,0 +1,32 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { gotoCalendarPreviousRange, gotoCalendarNextRange } from 'Store/Actions/calendarActions'; +import CalendarDays from './CalendarDays'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + (state) => state.app.isSidebarVisible, + (calendar, isSidebarVisible) => { + return { + dates: calendar.dates, + view: calendar.view, + isSidebarVisible + }; + } + ); +} + +function createMapDispatchToProps(dispatch) { + return { + onNavigatePrevious() { + dispatch(gotoCalendarPreviousRange()); + }, + + onNavigateNext() { + dispatch(gotoCalendarNextRange()); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarDays); diff --git a/frontend/src/Calendar/Day/DayOfWeek.css b/frontend/src/Calendar/Day/DayOfWeek.css new file mode 100644 index 000000000..8c3552e55 --- /dev/null +++ b/frontend/src/Calendar/Day/DayOfWeek.css @@ -0,0 +1,13 @@ +.dayOfWeek { + flex: 1 0 14.28%; + background-color: #e4eaec; + text-align: center; +} + +.isSingleDay { + width: 100%; +} + +.isToday { + background-color: $calendarTodayBackgroundColor; +} diff --git a/frontend/src/Calendar/Day/DayOfWeek.js b/frontend/src/Calendar/Day/DayOfWeek.js new file mode 100644 index 000000000..b8ab0a43b --- /dev/null +++ b/frontend/src/Calendar/Day/DayOfWeek.js @@ -0,0 +1,55 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import * as calendarViews from 'Calendar/calendarViews'; +import styles from './DayOfWeek.css'; + +class DayOfWeek extends Component { + + // + // Render + + render() { + const { + date, + view, + isTodaysDate, + calendarWeekColumnHeader, + shortDateFormat, + showRelativeDates + } = this.props; + + const highlightToday = view !== calendarViews.MONTH && isTodaysDate; + const momentDate = moment(date); + let formatedDate = momentDate.format('dddd'); + + if (view === calendarViews.WEEK) { + formatedDate = momentDate.format(calendarWeekColumnHeader); + } else if (view === calendarViews.FORECAST) { + formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates); + } + + return ( +
+ {formatedDate} +
+ ); + } +} + +DayOfWeek.propTypes = { + date: PropTypes.string.isRequired, + view: PropTypes.string.isRequired, + isTodaysDate: PropTypes.bool.isRequired, + calendarWeekColumnHeader: PropTypes.string.isRequired, + shortDateFormat: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired +}; + +export default DayOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeek.css b/frontend/src/Calendar/Day/DaysOfWeek.css new file mode 100644 index 000000000..518664633 --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeek.css @@ -0,0 +1,4 @@ +.daysOfWeek { + display: flex; + margin-top: 10px; +} diff --git a/frontend/src/Calendar/Day/DaysOfWeek.js b/frontend/src/Calendar/Day/DaysOfWeek.js new file mode 100644 index 000000000..a67777f7c --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeek.js @@ -0,0 +1,97 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import DayOfWeek from './DayOfWeek'; +import * as calendarViews from 'Calendar/calendarViews'; +import styles from './DaysOfWeek.css'; + +class DaysOfWeek extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + todaysDate: moment().startOf('day').toISOString() + }; + + this.updateTimeoutId = null; + } + + // Lifecycle + + componentDidMount() { + const view = this.props.view; + + if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) { + this.scheduleUpdate(); + } + } + + componentWillUnmount() { + this.clearUpdateTimeout(); + } + + // + // Control + + scheduleUpdate = () => { + this.clearUpdateTimeout(); + const todaysDate = moment().startOf('day'); + const diff = todaysDate.clone().add(1, 'day').diff(moment()); + + this.setState({ + todaysDate: todaysDate.toISOString() + }); + + this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff); + } + + clearUpdateTimeout = () => { + if (this.updateTimeoutId) { + clearTimeout(this.updateTimeoutId); + } + } + + // + // Render + + render() { + const { + dates, + view, + ...otherProps + } = this.props; + + if (view === calendarViews.AGENDA) { + return null; + } + + return ( +
+ { + dates.map((date) => { + return ( + + ); + }) + } +
+ ); + } +} + +DaysOfWeek.propTypes = { + dates: PropTypes.arrayOf(PropTypes.string), + view: PropTypes.string.isRequired +}; + +export default DaysOfWeek; diff --git a/frontend/src/Calendar/Day/DaysOfWeekConnector.js b/frontend/src/Calendar/Day/DaysOfWeekConnector.js new file mode 100644 index 000000000..7f5cdef19 --- /dev/null +++ b/frontend/src/Calendar/Day/DaysOfWeekConnector.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import DaysOfWeek from './DaysOfWeek'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createUISettingsSelector(), + (calendar, UiSettings) => { + return { + dates: calendar.dates.slice(0, 7), + view: calendar.view, + calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader, + shortDateFormat: UiSettings.shortDateFormat, + showRelativeDates: UiSettings.showRelativeDates + }; + } + ); +} + +export default connect(createMapStateToProps)(DaysOfWeek); diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css new file mode 100644 index 000000000..8b4a5372b --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEvent.css @@ -0,0 +1,86 @@ +.event { + overflow-x: hidden; + margin: 4px 2px; + padding: 5px; + border-bottom: 1px solid $borderColor; + border-left: 4px solid $borderColor; + font-size: 12px; +} + +.info, +.episodeInfo { + display: flex; +} + +.seriesTitle, +.episodeTitle { + composes: truncate from 'Styles/Mixins/truncate.css'; + + flex: 1 0 1px; + margin-right: 10px; +} + +.seriesTitle { + color: #3a3f51; + font-size: 14px; +} + +.absoluteEpisodeNumber { + margin-left: 3px; +} + +.statusIcon { + margin-left: 3px; +} + +/* + * Status + */ + +.downloaded { + border-left-color: $successColor; +} + +.downloading { + border-left-color: $purple; +} + +.unmonitored { + border-left-color: $gray; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(45deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.onAir { + border-left-color: $warningColor; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.missing { + border-left-color: $dangerColor; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.premiere { + border-left-color: $sonarrBlue; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} + +.unaired { + border-left-color: $primaryColor; + + &:global(.colorImpaired) { + background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px); + } +} diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js new file mode 100644 index 000000000..57c2a4722 --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEvent.js @@ -0,0 +1,165 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import formatTime from 'Utilities/Date/formatTime'; +import padNumber from 'Utilities/Number/padNumber'; +import getStatusStyle from 'Calendar/getStatusStyle'; +import episodeEntities from 'Episode/episodeEntities'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal'; +import CalendarEventQueueDetails from './CalendarEventQueueDetails'; +import styles from './CalendarEvent.css'; + +class CalendarEvent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }, () => { + this.props.onEventModalOpenToggle(true); + }); + } + + onDetailsModalClose = () => { + this.setState({ isDetailsModalOpen: false }, () => { + this.props.onEventModalOpenToggle(false); + }); + } + + // + // Render + + render() { + const { + id, + series, + title, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDateUtc, + monitored, + hasFile, + grabbed, + queueItem, + timeFormat, + colorImpairedMode + } = this.props; + + const startTime = moment(airDateUtc); + const endTime = startTime.add(series.runtime, 'minutes'); + const downloading = !!(queueItem || grabbed); + const isMonitored = series.monitored && monitored; + const statusStyle = getStatusStyle(episodeNumber, hasFile, downloading, startTime, endTime, isMonitored); + const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber; + + return ( +
+ +
+
+ {series.title} +
+ + { + missingAbsoluteNumber && + + } + + { + !!queueItem && + + + + } + + { + !queueItem && grabbed && + + } +
+ +
+
+ {title} +
+ +
+ {seasonNumber}x{padNumber(episodeNumber, 2)} + + { + series.seriesType === 'anime' && absoluteEpisodeNumber && + ({absoluteEpisodeNumber}) + } +
+
+ +
+ {formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })} +
+ + + +
+ ); + } +} + +CalendarEvent.propTypes = { + id: PropTypes.number.isRequired, + series: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + airDateUtc: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + hasFile: PropTypes.bool.isRequired, + grabbed: PropTypes.bool, + queueItem: PropTypes.object, + timeFormat: PropTypes.string.isRequired, + colorImpairedMode: PropTypes.bool.isRequired, + onEventModalOpenToggle: PropTypes.func.isRequired +}; + +export default CalendarEvent; diff --git a/frontend/src/Calendar/Events/CalendarEventConnector.js b/frontend/src/Calendar/Events/CalendarEventConnector.js new file mode 100644 index 000000000..08d88813d --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventConnector.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import CalendarEvent from './CalendarEvent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createQueueItemSelector(), + createUISettingsSelector(), + (series, queueItem, uiSettings) => { + return { + series, + queueItem, + timeFormat: uiSettings.timeFormat, + colorImpairedMode: uiSettings.enableColorImpairedMode + }; + } + ); +} + +export default connect(createMapStateToProps)(CalendarEvent); diff --git a/frontend/src/Calendar/Events/CalendarEventQueueDetails.js b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js new file mode 100644 index 000000000..81d81465c --- /dev/null +++ b/frontend/src/Calendar/Events/CalendarEventQueueDetails.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import colors from 'Styles/Variables/colors'; +import CircularProgressBar from 'Components/CircularProgressBar'; +import QueueDetails from 'Activity/Queue/QueueDetails'; + +function CalendarEventQueueDetails(props) { + const { + title, + size, + sizeleft, + estimatedCompletionTime, + status, + errorMessage + } = props; + + const progress = (100 - sizeleft / size * 100); + + return ( + + +
+ } + /> + ); +} + +CalendarEventQueueDetails.propTypes = { + title: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + sizeleft: PropTypes.number.isRequired, + estimatedCompletionTime: PropTypes.string, + status: PropTypes.string.isRequired, + errorMessage: PropTypes.string +}; + +export default CalendarEventQueueDetails; diff --git a/frontend/src/Calendar/Header/CalendarHeader.css b/frontend/src/Calendar/Header/CalendarHeader.css new file mode 100644 index 000000000..1127bb3c3 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeader.css @@ -0,0 +1,53 @@ +.header { + display: flex; +} + +.navigationButtons { + flex: 1 1 33%; + text-align: left; +} + +.todayButton { + composes: button from 'Components/Link/Button.css'; + + margin-left: 5px; +} + +.titleDesktop, +.titleMobile { + text-align: center; + font-size: 18px; +} + +.titleMobile { + margin-bottom: 5px; +} + +.viewButtonsContainer { + display: flex; + justify-content: flex-end; + flex: 1 1 33%; +} + +.viewMenu { + composes: menu from 'Components/Menu/Menu.css'; + + line-height: 31px; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin-top: 5px; + margin-right: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .navigationButtons { + flex: 1 0 50%; + } + + .viewButtonsContainer { + flex: 0 0 100px; + } +} diff --git a/frontend/src/Calendar/Header/CalendarHeader.js b/frontend/src/Calendar/Header/CalendarHeader.js new file mode 100644 index 000000000..4fea8356d --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeader.js @@ -0,0 +1,253 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import ViewMenuItem from 'Components/Menu/ViewMenuItem'; +import * as calendarViews from 'Calendar/calendarViews'; +import CalendarHeaderViewButton from './CalendarHeaderViewButton'; +import styles from './CalendarHeader.css'; + +function getTitle(time, start, end, view, longDateFormat) { + const timeMoment = moment(time); + const startMoment = moment(start); + const endMoment = moment(end); + + if (view === 'day') { + return timeMoment.format(longDateFormat); + } else if (view === 'month') { + return timeMoment.format('MMMM YYYY'); + } else if (view === 'agenda') { + return 'Agenda'; + } + + let startFormat = 'MMM D YYYY'; + let endFormat = 'MMM D YYYY'; + + if (startMoment.isSame(endMoment, 'month')) { + startFormat = 'MMM D'; + endFormat = 'D YYYY'; + } else if (startMoment.isSame(endMoment, 'year')) { + startFormat = 'MMM D'; + endFormat = 'MMM D YYYY'; + } + + return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`; +} + +// TODO Convert to a stateful Component so we can track view internally when changed + +class CalendarHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + view: props.view + }; + } + + componentDidUpdate(prevProps) { + const view = this.props.view; + + if (prevProps.view !== view) { + this.setState({ view }); + } + } + + // + // Listeners + + onViewChange = (view) => { + this.setState({ view }, () => { + this.props.onViewChange(view); + }); + } + + // + // Render + + render() { + const { + isFetching, + time, + start, + end, + longDateFormat, + isSmallScreen, + onTodayPress, + onPreviousPress, + onNextPress + } = this.props; + + const view = this.state.view; + + const title = getTitle(time, start, end, view, longDateFormat); + + return ( +
+ { + isSmallScreen && +
+ {title} +
+ } + +
+
+ + + + + +
+ + { + !isSmallScreen && +
+ {title} +
+ } + +
+ { + isFetching && + + } + + { + isSmallScreen ? + + + + + + + + Week + + + + Forecast + + + + Day + + + + Agenda + + + : + +
+ + + + + + + + + +
+ } +
+
+
+ ); + } +} + +CalendarHeader.propTypes = { + isFetching: PropTypes.bool.isRequired, + time: PropTypes.string.isRequired, + start: PropTypes.string.isRequired, + end: PropTypes.string.isRequired, + view: PropTypes.oneOf(calendarViews.all).isRequired, + isSmallScreen: PropTypes.bool.isRequired, + longDateFormat: PropTypes.string.isRequired, + onViewChange: PropTypes.func.isRequired, + onTodayPress: PropTypes.func.isRequired, + onPreviousPress: PropTypes.func.isRequired, + onNextPress: PropTypes.func.isRequired +}; + +export default CalendarHeader; diff --git a/frontend/src/Calendar/Header/CalendarHeaderConnector.js b/frontend/src/Calendar/Header/CalendarHeaderConnector.js new file mode 100644 index 000000000..c96cf2869 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeaderConnector.js @@ -0,0 +1,84 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import { setCalendarView, gotoCalendarToday, gotoCalendarPreviousRange, gotoCalendarNextRange } from 'Store/Actions/calendarActions'; +import CalendarHeader from './CalendarHeader'; + +function createMapStateToProps() { + return createSelector( + (state) => state.calendar, + createDimensionsSelector(), + createUISettingsSelector(), + (calendar, dimensions, uiSettings) => { + const result = _.pick(calendar, [ + 'isFetching', + 'view', + 'time', + 'start', + 'end' + ]); + + result.isSmallScreen = dimensions.isSmallScreen; + result.longDateFormat = uiSettings.longDateFormat; + + return result; + } + ); +} + +const mapDispatchToProps = { + setCalendarView, + gotoCalendarToday, + gotoCalendarPreviousRange, + gotoCalendarNextRange +}; + +class CalendarHeaderConnector extends Component { + + // + // Listeners + + onViewChange = (view) => { + this.props.setCalendarView({ view }); + } + + onTodayPress = () => { + this.props.gotoCalendarToday(); + } + + onPreviousPress = () => { + this.props.gotoCalendarPreviousRange(); + } + + onNextPress = () => { + this.props.gotoCalendarNextRange(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CalendarHeaderConnector.propTypes = { + setCalendarView: PropTypes.func.isRequired, + gotoCalendarToday: PropTypes.func.isRequired, + gotoCalendarPreviousRange: PropTypes.func.isRequired, + gotoCalendarNextRange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector); diff --git a/frontend/src/Calendar/Header/CalendarHeaderViewButton.js b/frontend/src/Calendar/Header/CalendarHeaderViewButton.js new file mode 100644 index 000000000..8dd5ae9f0 --- /dev/null +++ b/frontend/src/Calendar/Header/CalendarHeaderViewButton.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import Button from 'Components/Link/Button'; +import * as calendarViews from 'Calendar/calendarViews'; +// import styles from './CalendarHeaderViewButton.css'; + +class CalendarHeaderViewButton extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.view); + } + + // + // Render + + render() { + const { + view, + selectedView, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +CalendarHeaderViewButton.propTypes = { + view: PropTypes.oneOf(calendarViews.all).isRequired, + selectedView: PropTypes.oneOf(calendarViews.all).isRequired, + onPress: PropTypes.func.isRequired +}; + +export default CalendarHeaderViewButton; diff --git a/frontend/src/Calendar/Legend/Legend.css b/frontend/src/Calendar/Legend/Legend.css new file mode 100644 index 000000000..296cbd9d5 --- /dev/null +++ b/frontend/src/Calendar/Legend/Legend.css @@ -0,0 +1,6 @@ +.legend { + display: flex; + flex-wrap: wrap; + margin-top: 10px; + padding: 3px 0; +} diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js new file mode 100644 index 000000000..5214954b6 --- /dev/null +++ b/frontend/src/Calendar/Legend/Legend.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import LegendItem from './LegendItem'; +import styles from './Legend.css'; + +function Legend({ colorImpairedMode }) { + return ( +
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ +
+
+ ); +} + +Legend.propTypes = { + colorImpairedMode: PropTypes.bool.isRequired +}; + +export default Legend; diff --git a/frontend/src/Calendar/Legend/LegendItem.css b/frontend/src/Calendar/Legend/LegendItem.css new file mode 100644 index 000000000..d146e9d68 --- /dev/null +++ b/frontend/src/Calendar/Legend/LegendItem.css @@ -0,0 +1,41 @@ +.legendItem { + margin: 3px 0; + margin-right: 6px; + padding-left: 5px; + width: 150px; + border-left-width: 4px; + border-left-style: solid; + cursor: default; +} + +/* + * Status + */ + +.downloaded { + composes: downloaded from 'Calendar/Events/CalendarEvent.css'; +} + +.downloading { + composes: downloading from 'Calendar/Events/CalendarEvent.css'; +} + +.unmonitored { + composes: unmonitored from 'Calendar/Events/CalendarEvent.css'; +} + +.onAir { + composes: onAir from 'Calendar/Events/CalendarEvent.css'; +} + +.missing { + composes: missing from 'Calendar/Events/CalendarEvent.css'; +} + +.premiere { + composes: premiere from 'Calendar/Events/CalendarEvent.css'; +} + +.unaired { + composes: unaired from 'Calendar/Events/CalendarEvent.css'; +} diff --git a/frontend/src/Calendar/Legend/LegendItem.js b/frontend/src/Calendar/Legend/LegendItem.js new file mode 100644 index 000000000..961f48b86 --- /dev/null +++ b/frontend/src/Calendar/Legend/LegendItem.js @@ -0,0 +1,36 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import titleCase from 'Utilities/String/titleCase'; +import styles from './LegendItem.css'; + +function LegendItem(props) { + const { + name, + status, + tooltip, + colorImpairedMode + } = props; + + return ( +
+ {name ? name : titleCase(status)} +
+ ); +} + +LegendItem.propTypes = { + name: PropTypes.string, + status: PropTypes.string.isRequired, + tooltip: PropTypes.string.isRequired, + colorImpairedMode: PropTypes.bool.isRequired +}; + +export default LegendItem; diff --git a/frontend/src/Calendar/calendarViews.js b/frontend/src/Calendar/calendarViews.js new file mode 100644 index 000000000..929958b66 --- /dev/null +++ b/frontend/src/Calendar/calendarViews.js @@ -0,0 +1,7 @@ +export const DAY = 'day'; +export const WEEK = 'week'; +export const MONTH = 'month'; +export const FORECAST = 'forecast'; +export const AGENDA = 'agenda'; + +export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA]; diff --git a/frontend/src/Calendar/getStatusStyle.js b/frontend/src/Calendar/getStatusStyle.js new file mode 100644 index 000000000..ca4a02e0e --- /dev/null +++ b/frontend/src/Calendar/getStatusStyle.js @@ -0,0 +1,33 @@ +import moment from 'moment'; + +function getStatusStyle(episodeNumber, hasFile, downloading, startTime, endTime, isMonitored) { + const currentTime = moment(); + + if (hasFile) { + return 'downloaded'; + } + + if (downloading) { + return 'downloading'; + } + + if (!isMonitored) { + return 'unmonitored'; + } + + if (currentTime.isAfter(startTime) && currentTime.isBefore(endTime)) { + return 'onAir'; + } + + if (endTime.isBefore(currentTime) && !hasFile) { + return 'missing'; + } + + if (episodeNumber === 1) { + return 'premiere'; + } + + return 'unaired'; +} + +export default getStatusStyle; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModal.js b/frontend/src/Calendar/iCal/CalendarLinkModal.js new file mode 100644 index 000000000..8cc487c16 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModal.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector'; + +function CalendarLinkModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +CalendarLinkModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CalendarLinkModal; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContent.js b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js new file mode 100644 index 000000000..e965b862d --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContent.js @@ -0,0 +1,221 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function getUrls(state) { + const { + unmonitored, + premieresOnly, + asAllDay, + tags + } = state; + + let icalUrl = `${window.location.host}${window.Sonarr.urlBase}/feed/calendar/Sonarr.ics?`; + + if (unmonitored) { + icalUrl += 'unmonitored=true&'; + } + + if (premieresOnly) { + icalUrl += 'premieresOnly=true&'; + } + + if (asAllDay) { + icalUrl += 'asAllDay=true&'; + } + + if (tags.length) { + icalUrl += `tags=${tags.toString()}&`; + } + + icalUrl += `apikey=${window.Sonarr.apiKey}`; + + const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`; + const iCalWebCalUrl = `webcal://${icalUrl}`; + + return { + iCalHttpUrl, + iCalWebCalUrl + }; +} + +class CalendarLinkModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const defaultState = { + unmonitored: false, + premieresOnly: false, + asAllDay: false, + tags: [] + }; + + const urls = getUrls(defaultState); + + this.state = { + ...defaultState, + ...urls + }; + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + const state = { + ...this.state, + [name]: value + }; + + const urls = getUrls(state); + + this.setState({ + [name]: value, + ...urls + }); + } + + onLinkFocus = (event) => { + event.target.select(); + } + + // + // Render + + render() { + const { + onModalClose + } = this.props; + + const { + unmonitored, + premieresOnly, + asAllDay, + tags, + iCalHttpUrl, + iCalWebCalUrl + } = this.state; + + return ( + + + Sonarr Calendar Feed + + + +
+ + Include Unmonitored + + + + + + Season Premieres Only + + + + + + Show as All-Day Events + + + + + + Tags + + + + + + iCal Feed + + , + + + + + ]} + onChange={this.onInputChange} + onFocus={this.onLinkFocus} + /> + +
+
+ + + + +
+ ); + } +} + +CalendarLinkModalContent.propTypes = { + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default CalendarLinkModalContent; diff --git a/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js b/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js new file mode 100644 index 000000000..e10c5c3f9 --- /dev/null +++ b/frontend/src/Calendar/iCal/CalendarLinkModalContentConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import CalendarLinkModalContent from './CalendarLinkModalContent'; + +function createMapStateToProps() { + return createSelector( + createTagsSelector(), + (tagList) => { + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(CalendarLinkModalContent); diff --git a/frontend/src/Commands/commandNames.js b/frontend/src/Commands/commandNames.js new file mode 100644 index 000000000..4942258da --- /dev/null +++ b/frontend/src/Commands/commandNames.js @@ -0,0 +1,19 @@ +export const APPLICATION_UPDATE = 'ApplicationUpdate'; +export const BACKUP = 'Backup'; +export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload'; +export const CLEAR_BLACKLIST = 'ClearBlacklist'; +export const CLEAR_LOGS = 'ClearLog'; +export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch'; +export const DELETE_LOG_FILES = 'DeleteLogFiles'; +export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles'; +export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan'; +export const EPISODE_SEARCH = 'EpisodeSearch'; +export const INTERACTIVE_IMPORT = 'ManualImport'; +export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch'; +export const REFRESH_SERIES = 'RefreshSeries'; +export const RENAME_FILES = 'RenameFiles'; +export const RENAME_SERIES = 'RenameSeries'; +export const RESET_API_KEY = 'ResetApiKey'; +export const RSS_SYNC = 'RssSync'; +export const SEASON_SEARCH = 'SeasonSearch'; +export const SERIES_SEARCH = 'SeriesSearch'; diff --git a/frontend/src/Components/Alert.css b/frontend/src/Components/Alert.css new file mode 100644 index 000000000..312fbb4f2 --- /dev/null +++ b/frontend/src/Components/Alert.css @@ -0,0 +1,31 @@ +.alert { + display: block; + margin: 5px; + padding: 15px; + border: 1px solid transparent; + border-radius: 4px; +} + +.danger { + border-color: $alertDangerBorderColor; + background-color: $alertDangerBackgroundColor; + color: $alertDangerColor; +} + +.info { + border-color: $alertInfoBorderColor; + background-color: $alertInfoBackgroundColor; + color: $alertInfoColor; +} + +.success { + border-color: $alertSuccessBorderColor; + background-color: $alertSuccessBackgroundColor; + color: $alertSuccessColor; +} + +.warning { + border-color: $alertWarningBorderColor; + background-color: $alertWarningBackgroundColor; + color: $alertWarningColor; +} diff --git a/frontend/src/Components/Alert.js b/frontend/src/Components/Alert.js new file mode 100644 index 000000000..dc19a418c --- /dev/null +++ b/frontend/src/Components/Alert.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { kinds } from 'Helpers/Props'; +import styles from './Alert.css'; + +function Alert({ className, kind, children, ...otherProps }) { + return ( +
+ {children} +
+ ); +} + +Alert.propTypes = { + className: PropTypes.string.isRequired, + kind: PropTypes.oneOf(kinds.all).isRequired, + children: PropTypes.node.isRequired +}; + +Alert.defaultProps = { + className: styles.alert, + kind: kinds.INFO +}; + +export default Alert; diff --git a/frontend/src/Components/Card.css b/frontend/src/Components/Card.css new file mode 100644 index 000000000..e500ca154 --- /dev/null +++ b/frontend/src/Components/Card.css @@ -0,0 +1,8 @@ +.card { + margin: 10px; + padding: 10px; + border-radius: 3px; + background-color: $white; + box-shadow: 0 0 10px 1px $cardShadowColor; + color: $defaultColor; +} diff --git a/frontend/src/Components/Card.js b/frontend/src/Components/Card.js new file mode 100644 index 000000000..cc45edba3 --- /dev/null +++ b/frontend/src/Components/Card.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './Card.css'; + +class Card extends Component { + + // + // Render + + render() { + const { + className, + children, + onPress + } = this.props; + + return ( + + {children} + + ); + } +} + +Card.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + onPress: PropTypes.func.isRequired +}; + +Card.defaultProps = { + className: styles.card +}; + +export default Card; diff --git a/frontend/src/Components/CircularProgressBar.css b/frontend/src/Components/CircularProgressBar.css new file mode 100644 index 000000000..32b349404 --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.css @@ -0,0 +1,21 @@ +.circularProgressBarContainer { + position: relative; + display: inline-block; + vertical-align: top; + text-align: center; +} + +.circularProgressBar { + position: absolute; + top: 0; + left: 0; + transform: rotate(-90deg); + transform-origin: center center; +} + +.circularProgressBarText { + position: absolute; + width: 100%; + height: 100%; + font-weight: bold; +} diff --git a/frontend/src/Components/CircularProgressBar.js b/frontend/src/Components/CircularProgressBar.js new file mode 100644 index 000000000..d340af170 --- /dev/null +++ b/frontend/src/Components/CircularProgressBar.js @@ -0,0 +1,140 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import colors from 'Styles/Variables/colors'; +import styles from './CircularProgressBar.css'; + +class CircularProgressBar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + progress: 0 + }; + } + + componentDidMount() { + this._progressStep(); + } + + componentDidUpdate(prevProps) { + const progress = this.props.progress; + + if (prevProps.progress !== progress) { + this._cancelProgressStep(); + this._progressStep(); + } + } + + componentWillUnmount() { + this._cancelProgressStep(); + } + + // + // Control + + _progressStep() { + this.requestAnimationFrame = window.requestAnimationFrame(() => { + this.setState({ + progress: this.state.progress + 1 + }, () => { + if (this.state.progress < this.props.progress) { + this._progressStep(); + } + }); + }); + } + + _cancelProgressStep() { + if (this.requestAnimationFrame) { + window.cancelAnimationFrame(this.requestAnimationFrame); + } + } + + // + // Render + + render() { + const { + className, + containerClassName, + size, + strokeWidth, + strokeColor, + showProgressText + } = this.props; + + const progress = this.state.progress; + + const center = size / 2; + const radius = center - strokeWidth; + const circumference = Math.PI * (radius * 2); + const sizeInPixels = `${size}px`; + const strokeDashoffset = ((100 - progress) / 100) * circumference; + const progressText = `${Math.round(progress)}%`; + + return ( +
+ + + + + + { + showProgressText && +
+ {progressText} +
+ } +
+ ); + } +} + +CircularProgressBar.propTypes = { + className: PropTypes.string, + containerClassName: PropTypes.string, + size: PropTypes.number, + progress: PropTypes.number.isRequired, + strokeWidth: PropTypes.number, + strokeColor: PropTypes.string, + showProgressText: PropTypes.bool +}; + +CircularProgressBar.defaultProps = { + className: styles.circularProgressBar, + containerClassName: styles.circularProgressBarContainer, + size: 60, + strokeWidth: 5, + strokeColor: colors.sonarrBlue, + showProgressText: false +}; + +export default CircularProgressBar; diff --git a/frontend/src/Components/DescriptionList/DescriptionList.css b/frontend/src/Components/DescriptionList/DescriptionList.css new file mode 100644 index 000000000..94cd75ba9 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.css @@ -0,0 +1,4 @@ +.descriptionList { + margin-top: 0; + margin-bottom: 20px; +} diff --git a/frontend/src/Components/DescriptionList/DescriptionList.js b/frontend/src/Components/DescriptionList/DescriptionList.js new file mode 100644 index 000000000..b7a1d1634 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionList.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './DescriptionList.css'; + +class DescriptionList extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +DescriptionList.propTypes = { + children: PropTypes.node +}; + +export default DescriptionList; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItem.js b/frontend/src/Components/DescriptionList/DescriptionListItem.js new file mode 100644 index 000000000..4ba70bf33 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItem.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import DescriptionListItemTitle from './DescriptionListItemTitle'; +import DescriptionListItemDescription from './DescriptionListItemDescription'; + +class DescriptionListItem extends Component { + + // + // Render + + render() { + const { + titleClassName, + descriptionClassName, + title, + data + } = this.props; + + return ( + + + {title} + + + + {data} + + + ); + } +} + +DescriptionListItem.propTypes = { + titleClassName: PropTypes.string, + descriptionClassName: PropTypes.string, + title: PropTypes.string, + data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) +}; + +export default DescriptionListItem; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css new file mode 100644 index 000000000..582eaff24 --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.css @@ -0,0 +1,13 @@ +.description { + line-height: 1.528571429; +} + +.description { + margin-left: 0; +} + +@media (min-width: 768px) { + .description { + margin-left: 180px; + } +} diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js new file mode 100644 index 000000000..4ef3c015e --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemDescription.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DescriptionListItemDescription.css'; + +function DescriptionListItemDescription(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +DescriptionListItemDescription.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]) +}; + +DescriptionListItemDescription.defaultProps = { + className: styles.description +}; + +export default DescriptionListItemDescription; diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css new file mode 100644 index 000000000..a1eb377cb --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.css @@ -0,0 +1,18 @@ +.title { + line-height: 1.528571429; +} + +.title { + font-weight: bold; +} + +@media (min-width: 768px) { + .title { + composes: truncate from 'Styles/Mixins/truncate.css'; + + float: left; + clear: left; + width: 160px; + text-align: right; + } +} diff --git a/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js new file mode 100644 index 000000000..e1632c1cf --- /dev/null +++ b/frontend/src/Components/DescriptionList/DescriptionListItemTitle.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DescriptionListItemTitle.css'; + +function DescriptionListItemTitle(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +DescriptionListItemTitle.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.string +}; + +DescriptionListItemTitle.defaultProps = { + className: styles.title +}; + +export default DescriptionListItemTitle; diff --git a/frontend/src/Components/DragPreviewLayer.css b/frontend/src/Components/DragPreviewLayer.css new file mode 100644 index 000000000..46f721fef --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.css @@ -0,0 +1,9 @@ +.dragLayer { + position: fixed; + top: 0; + left: 0; + z-index: 9999; + width: 100%; + height: 100%; + pointer-events: none; +} diff --git a/frontend/src/Components/DragPreviewLayer.js b/frontend/src/Components/DragPreviewLayer.js new file mode 100644 index 000000000..a111df70e --- /dev/null +++ b/frontend/src/Components/DragPreviewLayer.js @@ -0,0 +1,22 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './DragPreviewLayer.css'; + +function DragPreviewLayer({ children, ...otherProps }) { + return ( +
+ {children} +
+ ); +} + +DragPreviewLayer.propTypes = { + children: PropTypes.node, + className: PropTypes.string +}; + +DragPreviewLayer.defaultProps = { + className: styles.dragLayer +}; + +export default DragPreviewLayer; diff --git a/frontend/src/Components/FieldSet.css b/frontend/src/Components/FieldSet.css new file mode 100644 index 000000000..daf3bdf2e --- /dev/null +++ b/frontend/src/Components/FieldSet.css @@ -0,0 +1,19 @@ +.fieldSet { + margin: 0; + margin-bottom: 20px; + padding: 0; + min-width: 0; + border: 0; +} + +.legend { + display: block; + margin-bottom: 21px; + padding: 0; + width: 100%; + border: 0; + border-bottom: 1px solid #e5e5e5; + color: #3a3f51; + font-size: 21px; + line-height: inherit; +} diff --git a/frontend/src/Components/FieldSet.js b/frontend/src/Components/FieldSet.js new file mode 100644 index 000000000..76e68a934 --- /dev/null +++ b/frontend/src/Components/FieldSet.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './FieldSet.css'; + +class FieldSet extends Component { + + // + // Render + + render() { + const { + legend, + children + } = this.props; + + return ( +
+ + {legend} + + {children} +
+ ); + } + +} + +FieldSet.propTypes = { + legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]), + children: PropTypes.node +}; + +export default FieldSet; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.css b/frontend/src/Components/FileBrowser/FileBrowserModal.css new file mode 100644 index 000000000..30b936800 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.css @@ -0,0 +1,5 @@ +.modal { + composes: modal from 'Components/Modal/Modal.css'; + + height: 600px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserModal.js b/frontend/src/Components/FileBrowser/FileBrowserModal.js new file mode 100644 index 000000000..6b58dbb8c --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModal.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import FileBrowserModalContentConnector from './FileBrowserModalContentConnector'; +import styles from './FileBrowserModal.css'; + +class FileBrowserModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +FileBrowserModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileBrowserModal; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.css b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css new file mode 100644 index 000000000..7da2ea225 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.css @@ -0,0 +1,16 @@ +.modalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + display: flex; + flex-direction: column; +} + +.pathInput { + composes: pathInputWrapper from 'Components/Form/PathInput.css'; + + flex: 0 0 auto; +} + +.scroller { + margin-top: 20px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContent.js b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js new file mode 100644 index 000000000..f81019e1c --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContent.js @@ -0,0 +1,213 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import PathInput from 'Components/Form/PathInput'; +import FileBrowserRow from './FileBrowserRow'; +import styles from './FileBrowserModalContent.css'; + +const columns = [ + { + name: 'type', + label: 'Type', + isVisible: true + }, + { + name: 'name', + label: 'Name', + isVisible: true + } +]; + +class FileBrowserModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scrollerNode = null; + + this.state = { + isFileBrowserModalOpen: false, + currentPath: props.value + }; + } + + componentDidUpdate(prevProps) { + const { + currentPath + } = this.props; + + if (currentPath !== this.state.currentPath) { + this.setState({ currentPath }); + this._scrollerNode.scrollTop = 0; + } + } + + // + // Control + + setScrollerRef = (ref) => { + if (ref) { + this._scrollerNode = ReactDOM.findDOMNode(ref); + } else { + this._scrollerNode = null; + } + } + + // + // Listeners + + onPathInputChange = ({ value }) => { + this.setState({ currentPath: value }); + } + + onRowPress = (path) => { + this.props.onFetchPaths(path); + } + + onOkPress = () => { + this.props.onChange({ + name: this.props.name, + value: this.state.currentPath + }); + + this.props.onClearPaths(); + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + parent, + directories, + files, + onModalClose, + ...otherProps + } = this.props; + + const emptyParent = parent === ''; + + return ( + + + File Browser + + + + + + + + + { + emptyParent && + + } + + { + !emptyParent && parent && + + } + + { + directories.map((directory) => { + return ( + + ); + }) + } + + { + files.map((file) => { + return ( + + ); + }) + } + +
+
+
+ + + + + + +
+ ); + } +} + +FileBrowserModalContent.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + parent: PropTypes.string, + currentPath: PropTypes.string.isRequired, + directories: PropTypes.arrayOf(PropTypes.object).isRequired, + files: PropTypes.arrayOf(PropTypes.object).isRequired, + onFetchPaths: PropTypes.func.isRequired, + onClearPaths: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default FileBrowserModalContent; diff --git a/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js new file mode 100644 index 000000000..adf52fbcd --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserModalContentConnector.js @@ -0,0 +1,86 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchPaths, clearPaths } from 'Store/Actions/pathActions'; +import FileBrowserModalContent from './FileBrowserModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.paths, + (paths) => { + const { + parent, + currentPath, + directories, + files + } = paths; + + const filteredPaths = _.filter([...directories, ...files], ({ path }) => { + return path.toLowerCase().startsWith(currentPath.toLowerCase()); + }); + + return { + parent, + currentPath, + directories, + files, + paths: filteredPaths + }; + } + ); +} + +const mapDispatchToProps = { + fetchPaths, + clearPaths +}; + +class FileBrowserModalContentConnector extends Component { + + // Lifecycle + + componentDidMount() { + this.props.fetchPaths({ path: this.props.value }); + } + + // + // Listeners + + onFetchPaths = (path) => { + this.props.fetchPaths({ path }); + } + + onClearPaths = () => { + // this.props.clearPaths(); + } + + onModalClose = () => { + this.props.clearPaths(); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +FileBrowserModalContentConnector.propTypes = { + value: PropTypes.string, + fetchPaths: PropTypes.func.isRequired, + clearPaths: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector); diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.css b/frontend/src/Components/FileBrowser/FileBrowserRow.css new file mode 100644 index 000000000..a9c34be6a --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.css @@ -0,0 +1,5 @@ +.type { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 32px; +} diff --git a/frontend/src/Components/FileBrowser/FileBrowserRow.js b/frontend/src/Components/FileBrowser/FileBrowserRow.js new file mode 100644 index 000000000..42ac30405 --- /dev/null +++ b/frontend/src/Components/FileBrowser/FileBrowserRow.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './FileBrowserRow.css'; + +function getIconName(type) { + switch (type) { + case 'computer': + return icons.COMPUTER; + case 'drive': + return icons.DRIVE; + case 'file': + return icons.FILE; + case 'parent': + return icons.PARENT; + default: + return icons.FOLDER; + } +} + +class FileBrowserRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.path); + } + + // + // Render + + render() { + const { + type, + name + } = this.props; + + return ( + + + + + + {name} + + ); + } + +} + +FileBrowserRow.propTypes = { + type: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default FileBrowserRow; diff --git a/frontend/src/Components/Form/CaptchaInput.css b/frontend/src/Components/Form/CaptchaInput.css new file mode 100644 index 000000000..e7cd1dc4e --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInput.css @@ -0,0 +1,23 @@ +.captchaInputWrapper { + display: flex; +} + +.input { + composes: input from 'Components/Form/Input.css'; +} + +.hasError { + composes: hasError from 'Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from 'Components/Form/Input.css'; +} + +.hasButton { + composes: hasButton from 'Components/Form/Input.css'; +} + +.recaptchaWrapper { + margin-top: 10px; +} diff --git a/frontend/src/Components/Form/CaptchaInput.js b/frontend/src/Components/Form/CaptchaInput.js new file mode 100644 index 000000000..a8600255a --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInput.js @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ReCAPTCHA from 'react-google-recaptcha'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import FormInputButton from './FormInputButton'; +import TextInput from './TextInput'; +import styles from './CaptchaInput.css'; + +function CaptchaInput(props) { + const { + className, + name, + value, + hasError, + hasWarning, + refreshing, + siteKey, + secretToken, + onChange, + onRefreshPress, + onCaptchaChange + } = props; + + return ( +
+
+ + + + + +
+ + { + !!siteKey && !!secretToken && +
+ +
+ } +
+ ); +} + +CaptchaInput.propTypes = { + className: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + refreshing: PropTypes.bool.isRequired, + siteKey: PropTypes.string, + secretToken: PropTypes.string, + onChange: PropTypes.func.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onCaptchaChange: PropTypes.func.isRequired +}; + +CaptchaInput.defaultProps = { + className: styles.input, + value: '' +}; + +export default CaptchaInput; diff --git a/frontend/src/Components/Form/CaptchaInputConnector.js b/frontend/src/Components/Form/CaptchaInputConnector.js new file mode 100644 index 000000000..17b875c88 --- /dev/null +++ b/frontend/src/Components/Form/CaptchaInputConnector.js @@ -0,0 +1,98 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { refreshCaptcha, getCaptchaCookie, resetCaptcha } from 'Store/Actions/captchaActions'; +import CaptchaInput from './CaptchaInput'; + +function createMapStateToProps() { + return createSelector( + (state) => state.captcha, + (captcha) => { + return captcha; + } + ); +} + +const mapDispatchToProps = { + refreshCaptcha, + getCaptchaCookie, + resetCaptcha +}; + +class CaptchaInputConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + const { + name, + token, + onChange + } = this.props; + + if (token && token !== prevProps.token) { + onChange({ name, value: token }); + } + } + + componentWillUnmount = () => { + this.props.resetCaptcha(); + } + + // + // Listeners + + onRefreshPress = () => { + const { + provider, + providerData + } = this.props; + + this.props.refreshCaptcha({ provider, providerData }); + } + + onCaptchaChange = (captchaResponse) => { + // If the captcha has expired `captchaResponse` will be null. + // In the event it's null don't try to get the captchaCookie. + // TODO: Should we clear the cookie? or reset the captcha? + + if (!captchaResponse) { + return; + } + + const { + provider, + providerData + } = this.props; + + this.props.getCaptchaCookie({ provider, providerData, captchaResponse }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CaptchaInputConnector.propTypes = { + provider: PropTypes.string.isRequired, + providerData: PropTypes.object.isRequired, + name: PropTypes.string.isRequired, + token: PropTypes.string, + onChange: PropTypes.func.isRequired, + refreshCaptcha: PropTypes.func.isRequired, + getCaptchaCookie: PropTypes.func.isRequired, + resetCaptcha: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CaptchaInputConnector); diff --git a/frontend/src/Components/Form/CheckInput.css b/frontend/src/Components/Form/CheckInput.css new file mode 100644 index 000000000..5c35e5d2f --- /dev/null +++ b/frontend/src/Components/Form/CheckInput.css @@ -0,0 +1,105 @@ +.container { + position: relative; + display: flex; + flex: 1 1 65%; + user-select: none; +} + +.label { + display: flex; + margin-bottom: 0; + min-height: 21px; + font-weight: normal; + cursor: pointer; +} + +.checkbox { + position: absolute; + opacity: 0; + cursor: pointer; + pointer-events: none; + + &:global(.isDisabled) { + cursor: not-allowed; + } +} + +.input { + flex: 1 0 auto; + margin-top: 7px; + margin-right: 5px; + width: 20px; + height: 20px; + border: 1px solid #ccc; + border-radius: 2px; + background-color: $white; + color: $white; + text-align: center; + line-height: 20px; +} + +.checkbox:focus + .input { + outline: 0; + border-color: $inputFocusBorderColor; + box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor; +} + +.dangerIsChecked { + border-color: $dangerColor; + background-color: $dangerColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.primaryIsChecked { + border-color: $primaryColor; + background-color: $primaryColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.successIsChecked { + border-color: $successColor; + background-color: $successColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.warningIsChecked { + border-color: $warningColor; + background-color: $warningColor; + + &.isDisabled { + opacity: 0.7; + } +} + +.isNotChecked { + &.isDisabled { + border-color: $disabledCheckInputColor; + background-color: $disabledCheckInputColor; + opacity: 0.7; + } +} + +.isIndeterminate { + border-color: $gray; + background-color: $gray; +} + +.helpText { + composes: helpText from 'Components/Form/FormInputHelpText.css'; + + margin-top: 8px; + margin-left: 5px; +} + +.isDisabled { + cursor: not-allowed; +} diff --git a/frontend/src/Components/Form/CheckInput.js b/frontend/src/Components/Form/CheckInput.js new file mode 100644 index 000000000..0fc0be9cd --- /dev/null +++ b/frontend/src/Components/Form/CheckInput.js @@ -0,0 +1,187 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import FormInputHelpText from './FormInputHelpText'; +import styles from './CheckInput.css'; + +class CheckInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._checkbox = null; + } + + componentDidMount() { + this.setIndeterminate(); + } + + componentDidUpdate() { + this.setIndeterminate(); + } + + // + // Control + + setIndeterminate() { + if (!this._checkbox) { + return; + } + + const { + value, + uncheckedValue, + checkedValue + } = this.props; + + this._checkbox.indeterminate = value !== uncheckedValue && value !== checkedValue; + } + + toggleChecked = (checked, shiftKey) => { + const { + name, + value, + checkedValue, + uncheckedValue + } = this.props; + + const newValue = checked ? checkedValue : uncheckedValue; + + if (value !== newValue) { + this.props.onChange({ + name, + value: newValue, + shiftKey + }); + } + } + + // + // Listeners + + setRef = (ref) => { + this._checkbox = ref; + } + + onClick = (event) => { + const shiftKey = event.nativeEvent.shiftKey; + const checked = !this._checkbox.checked; + + event.preventDefault(); + this.toggleChecked(checked, shiftKey); + } + + onChange = (event) => { + const checked = event.target.checked; + const shiftKey = event.nativeEvent.shiftKey; + + this.toggleChecked(checked, shiftKey); + } + + // + // Render + + render() { + const { + className, + containerClassName, + name, + value, + checkedValue, + uncheckedValue, + helpText, + helpTextWarning, + isDisabled, + kind + } = this.props; + + const isChecked = value === checkedValue; + const isUnchecked = value === uncheckedValue; + const isIndeterminate = !isChecked && !isUnchecked; + const isCheckClass = `${kind}IsChecked`; + + return ( +
+ +
+ ); + } +} + +CheckInput.propTypes = { + className: PropTypes.string.isRequired, + containerClassName: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + checkedValue: PropTypes.bool, + uncheckedValue: PropTypes.bool, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + helpText: PropTypes.string, + helpTextWarning: PropTypes.string, + isDisabled: PropTypes.bool, + kind: PropTypes.oneOf(kinds.all).isRequired, + onChange: PropTypes.func.isRequired +}; + +CheckInput.defaultProps = { + className: styles.input, + containerClassName: styles.container, + checkedValue: true, + uncheckedValue: false, + kind: kinds.PRIMARY +}; + +export default CheckInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInput.css b/frontend/src/Components/Form/EnhancedSelectInput.css new file mode 100644 index 000000000..4662cc581 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInput.css @@ -0,0 +1,66 @@ +.tether { + z-index: 2000; +} + +.enhancedSelect { + composes: input from 'Components/Form/Input.css'; + composes: link from 'Components/Link/Link.css'; + + position: relative; + display: flex; + align-items: center; + padding: 6px 16px; + width: 100%; + height: 35px; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; + color: $black; + cursor: default; +} + +.hasError { + composes: hasError from 'Components/Form/Input.css'; +} + +.hasWarning { + composes: hasWarning from 'Components/Form/Input.css'; +} + +.isDisabled { + opacity: 0.7; + cursor: not-allowed; +} + +.dropdownArrowContainer { + margin-left: 12px; +} + +.optionsContainer { + width: auto; +} + +.options { + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} + +.optionsModal { + display: flex; + justify-content: center; + max-width: 90%; + width: 350px !important; + height: auto !important; +} + +.optionsInnerModalBody { + composes: innerModalBody from 'Components/Modal/ModalBody.css'; + + padding: 0; + width: 100%; + border: 1px solid $inputBorderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Components/Form/EnhancedSelectInput.js b/frontend/src/Components/Form/EnhancedSelectInput.js new file mode 100644 index 000000000..76383f749 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInput.js @@ -0,0 +1,399 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import isMobileUtil from 'Utilities/isMobile'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import Modal from 'Components/Modal/Modal'; +import ModalBody from 'Components/Modal/ModalBody'; +import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue'; +import EnhancedSelectInputOption from './EnhancedSelectInputOption'; +import styles from './EnhancedSelectInput.css'; + +const tetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ], + attachment: 'top left', + targetAttachment: 'bottom left' +}; + +function isArrowKey(keyCode) { + return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; +} + +function getSelectedOption(selectedIndex, values) { + return values[selectedIndex]; +} + +function findIndex(startingIndex, direction, values) { + let indexToTest = startingIndex + direction; + + while (indexToTest !== startingIndex) { + if (indexToTest < 0) { + indexToTest = values.length - 1; + } else if (indexToTest >= values.length) { + indexToTest = 0; + } + + if (getSelectedOption(indexToTest, values).isDisabled) { + indexToTest = indexToTest + direction; + } else { + return indexToTest; + } + } +} + +function previousIndex(selectedIndex, values) { + return findIndex(selectedIndex, -1, values); +} + +function nextIndex(selectedIndex, values) { + return findIndex(selectedIndex, 1, values); +} + +function getSelectedIndex(props) { + const { + value, + values + } = props; + + return values.findIndex((v) => { + return v.key === value; + }); +} + +function getKey(selectedIndex, values) { + return values[selectedIndex].key; +} + +class EnhancedSelectInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOpen: false, + selectedIndex: getSelectedIndex(props), + width: 0, + isMobile: isMobileUtil() + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value) { + this.setState({ + selectedIndex: getSelectedIndex(this.props) + }); + } + } + + // + // Control + + _setButtonRef = (ref) => { + this._buttonRef = ref; + } + + _setOptionsRef = (ref) => { + this._optionsRef = ref; + } + + _addListener() { + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const button = ReactDOM.findDOMNode(this._buttonRef); + const options = ReactDOM.findDOMNode(this._optionsRef); + + if (!button || this.state.isMobile) { + return; + } + + if ( + !button.contains(event.target) && + options && + !options.contains(event.target) && + this.state.isOpen + ) { + this.setState({ isOpen: false }); + this._removeListener(); + } + } + + onBlur = () => { + this.setState({ + selectedIndex: getSelectedIndex(this.props) + }); + } + + onKeyDown = (event) => { + const { + values + } = this.props; + + const { + isOpen, + selectedIndex + } = this.state; + + const keyCode = event.keyCode; + const newState = {}; + + if (!isOpen) { + if (isArrowKey(keyCode)) { + event.preventDefault(); + newState.isOpen = true; + } + + if ( + selectedIndex == null || + getSelectedOption(selectedIndex, values).isDisabled + ) { + if (keyCode === keyCodes.UP_ARROW) { + newState.selectedIndex = previousIndex(0, values); + } else if (keyCode === keyCodes.DOWN_ARROW) { + newState.selectedIndex = nextIndex(values.length - 1, values); + } + } + + this.setState(newState); + return; + } + + if (keyCode === keyCodes.UP_ARROW) { + event.preventDefault(); + newState.selectedIndex = previousIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.DOWN_ARROW) { + event.preventDefault(); + newState.selectedIndex = nextIndex(selectedIndex, values); + } + + if (keyCode === keyCodes.ENTER) { + event.preventDefault(); + newState.isOpen = false; + this.onSelect(getKey(selectedIndex, values)); + } + + if (keyCode === keyCodes.TAB) { + newState.isOpen = false; + this.onSelect(getKey(selectedIndex, values)); + } + + if (keyCode === keyCodes.ESCAPE) { + event.preventDefault(); + event.stopPropagation(); + newState.isOpen = false; + newState.selectedIndex = getSelectedIndex(this.props); + } + + if (!_.isEmpty(newState)) { + this.setState(newState); + } + } + + onPress = () => { + if (this.state.isOpen) { + this._removeListener(); + } else { + this._addListener(); + } + + this.setState({ isOpen: !this.state.isOpen }); + } + + onSelect = (value) => { + this.setState({ isOpen: false }); + + this.props.onChange({ + name: this.props.name, + value + }); + } + + onMeasure = ({ width }) => { + this.setState({ width }); + } + + onOptionsModalClose = () => { + this.setState({ isOpen: false }); + } + + // + // Render + + render() { + const { + className, + disabledClassName, + values, + isDisabled, + hasError, + hasWarning, + selectedValueOptions, + selectedValueComponent: SelectedValueComponent, + optionComponent: OptionComponent + } = this.props; + + const { + selectedIndex, + width, + isOpen, + isMobile + } = this.state; + + const selectedOption = getSelectedOption(selectedIndex, values); + + return ( +
+ + + + + {selectedOption ? selectedOption.value : null} + + +
+ +
+ +
+ + { + isOpen && !isMobile && +
+
+ { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } +
+
+ } +
+ + { + isMobile && + + + { + values.map((v, index) => { + return ( + + {v.value} + + ); + }) + } + + + } +
+ ); + } +} + +EnhancedSelectInput.propTypes = { + className: PropTypes.string, + disabledClassName: PropTypes.string, + name: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + values: PropTypes.arrayOf(PropTypes.object).isRequired, + isDisabled: PropTypes.bool, + hasError: PropTypes.bool, + hasWarning: PropTypes.bool, + selectedValueOptions: PropTypes.object.isRequired, + selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + optionComponent: PropTypes.func, + onChange: PropTypes.func.isRequired +}; + +EnhancedSelectInput.defaultProps = { + className: styles.enhancedSelect, + disabledClassName: styles.isDisabled, + isDisabled: false, + selectedValueOptions: {}, + selectedValueComponent: EnhancedSelectInputSelectedValue, + optionComponent: EnhancedSelectInputOption +}; + +export default EnhancedSelectInput; diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.css b/frontend/src/Components/Form/EnhancedSelectInputOption.css new file mode 100644 index 000000000..dedf7beaa --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputOption.css @@ -0,0 +1,37 @@ +.option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 10px; + width: 100%; + cursor: default; + + &:hover { + background-color: #f9f9f9; + } +} + +.isSelected { + background-color: #e2e2e2; + + &.isMobile { + background-color: inherit; + + .iconContainer { + color: $primaryColor; + } + } +} + +.isDisabled { + background-color: #aaa; +} + +.isMobile { + height: 50px; + border-bottom: 1px solid $borderColor; + + &:last-child { + border: none; + } +} diff --git a/frontend/src/Components/Form/EnhancedSelectInputOption.js b/frontend/src/Components/Form/EnhancedSelectInputOption.js new file mode 100644 index 000000000..a1a161c79 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputOption.js @@ -0,0 +1,77 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './EnhancedSelectInputOption.css'; + +class EnhancedSelectInputOption extends Component { + + // + // Listeners + + onPress = () => { + const { + id, + onSelect + } = this.props; + + onSelect(id); + } + + // + // Render + + render() { + const { + className, + isSelected, + isDisabled, + isMobile, + children + } = this.props; + + return ( + + {children} + + { + isMobile && +
+ +
+ } + + ); + } +} + +EnhancedSelectInputOption.propTypes = { + className: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool.isRequired, + isMobile: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + onSelect: PropTypes.func.isRequired +}; + +EnhancedSelectInputOption.defaultProps = { + className: styles.option, + isDisabled: false +}; + +export default EnhancedSelectInputOption; diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css new file mode 100644 index 000000000..aab9f1b7d --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.css @@ -0,0 +1,3 @@ +.selectedValue { + flex: 1 1 auto; +} diff --git a/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js new file mode 100644 index 000000000..2343fedc2 --- /dev/null +++ b/frontend/src/Components/Form/EnhancedSelectInputSelectedValue.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './EnhancedSelectInputSelectedValue.css'; + +function EnhancedSelectInputSelectedValue(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +EnhancedSelectInputSelectedValue.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node +}; + +EnhancedSelectInputSelectedValue.defaultProps = { + className: styles.selectedValue +}; + +export default EnhancedSelectInputSelectedValue; diff --git a/frontend/src/Components/Form/Form.css b/frontend/src/Components/Form/Form.css new file mode 100644 index 000000000..987b1a0a1 --- /dev/null +++ b/frontend/src/Components/Form/Form.css @@ -0,0 +1,11 @@ +.form { + +} + +.error { + color: $dangerColor; +} + +.warning { + color: $warningColor; +} diff --git a/frontend/src/Components/Form/Form.js b/frontend/src/Components/Form/Form.js new file mode 100644 index 000000000..9a3579b45 --- /dev/null +++ b/frontend/src/Components/Form/Form.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './Form.css'; + +function Form({ children, validationErrors, validationWarnings, ...otherProps }) { + return ( +
+
+ { + validationErrors.map((error, index) => { + return ( +
+ {error.errorMessage} +
+ ); + }) + } + + { + validationWarnings.map((warning, index) => { + return ( +
+ {warning.errorMessage} +
+ ); + }) + } +
+ + {children} +
+ ); +} + +Form.propTypes = { + children: PropTypes.node.isRequired, + validationErrors: PropTypes.arrayOf(PropTypes.object).isRequired, + validationWarnings: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +Form.defaultProps = { + validationErrors: [], + validationWarnings: [] +}; + +export default Form; diff --git a/frontend/src/Components/Form/FormGroup.css b/frontend/src/Components/Form/FormGroup.css new file mode 100644 index 000000000..41a56eff0 --- /dev/null +++ b/frontend/src/Components/Form/FormGroup.css @@ -0,0 +1,24 @@ +.group { + display: flex; + margin-bottom: 20px; +} + +/* Sizes */ + +.small { + max-width: $formGroupSmallWidth; +} + +.medium { + max-width: $formGroupMediumWidth; +} + +.large { + max-width: $formGroupLargeWidth; +} + +@media only screen and (max-width: $breakpointLarge) { + .group { + display: block; + } +} diff --git a/frontend/src/Components/Form/FormGroup.js b/frontend/src/Components/Form/FormGroup.js new file mode 100644 index 000000000..edec4b86d --- /dev/null +++ b/frontend/src/Components/Form/FormGroup.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { map } from 'Helpers/elementChildren'; +import { sizes } from 'Helpers/Props'; +import styles from './FormGroup.css'; + +function FormGroup(props) { + const { + className, + children, + size, + advancedSettings, + isAdvanced, + ...otherProps + } = props; + + if (!advancedSettings && isAdvanced) { + return null; + } + + const childProps = isAdvanced ? { isAdvanced } : {}; + + return ( +
+ { + map(children, (child) => { + return React.cloneElement(child, childProps); + }) + } +
+ ); +} + +FormGroup.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + size: PropTypes.string.isRequired, + advancedSettings: PropTypes.bool.isRequired, + isAdvanced: PropTypes.bool.isRequired +}; + +FormGroup.defaultProps = { + className: styles.group, + size: sizes.SMALL, + advancedSettings: false, + isAdvanced: false +}; + +export default FormGroup; diff --git a/frontend/src/Components/Form/FormInputButton.css b/frontend/src/Components/Form/FormInputButton.css new file mode 100644 index 000000000..27a1923be --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.css @@ -0,0 +1,12 @@ +.button { + composes: button from 'Components/Link/Button.css'; + + border-left: none; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.middleButton { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} diff --git a/frontend/src/Components/Form/FormInputButton.js b/frontend/src/Components/Form/FormInputButton.js new file mode 100644 index 000000000..4b6491663 --- /dev/null +++ b/frontend/src/Components/Form/FormInputButton.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import { kinds } from 'Helpers/Props'; +import styles from './FormInputButton.css'; + +function FormInputButton(props) { + const { + className, + canSpin, + isLastButton, + ...otherProps + } = props; + + if (canSpin) { + return ( + + ); + } + + return ( + + ); +} + +SpinnerButton.propTypes = { + className: PropTypes.string.isRequired, + isSpinning: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool, + spinnerIcon: PropTypes.string.isRequired, + children: PropTypes.node +}; + +SpinnerButton.defaultProps = { + className: styles.button, + spinnerIcon: icons.SPINNER +}; + +export default SpinnerButton; diff --git a/frontend/src/Components/Link/SpinnerErrorButton.css b/frontend/src/Components/Link/SpinnerErrorButton.css new file mode 100644 index 000000000..5f4e68545 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerErrorButton.css @@ -0,0 +1,23 @@ +.iconContainer { + composes: spinnerContainer from 'Components/Link/SpinnerButton.css'; +} + +.icon { + z-index: 1; +} + +.label { + composes: label from 'Components/Link/SpinnerButton.css'; +} + +.showIcon { + .iconContainer { + left: 50%; + visibility: visible; + } + + .label { + left: 100%; + opacity: 0; + } +} diff --git a/frontend/src/Components/Link/SpinnerErrorButton.js b/frontend/src/Components/Link/SpinnerErrorButton.js new file mode 100644 index 000000000..87cf55d95 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerErrorButton.js @@ -0,0 +1,162 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import styles from './SpinnerErrorButton.css'; + +function getTestResult(error) { + if (!error) { + return { + wasSuccessful: true, + hasWarning: false, + hasError: false + }; + } + + if (error.status !== 400) { + return { + wasSuccessful: false, + hasWarning: false, + hasError: true + }; + } + + const failures = error.responseJSON; + + const hasWarning = _.some(failures, { isWarning: true }); + const hasError = _.some(failures, (failure) => !failure.isWarning); + + return { + wasSuccessful: false, + hasWarning, + hasError + }; +} + +class SpinnerErrorButton extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._testResultTimeout = null; + + this.state = { + wasSuccessful: false, + hasWarning: false, + hasError: false + }; + } + + componentDidUpdate(prevProps) { + const { + isSpinning, + error + } = this.props; + + if (prevProps.isSpinning && !isSpinning) { + const testResult = getTestResult(error); + + this.setState(testResult, () => { + const { + wasSuccessful, + hasWarning, + hasError + } = testResult; + + if (wasSuccessful || hasWarning || hasError) { + this._testResultTimeout = setTimeout(this.resetState, 3000); + } + }); + } + } + + componentWillUnmount() { + if (this._testResultTimeout) { + clearTimeout(this._testResultTimeout); + } + } + + // + // Control + + resetState = () => { + this.setState({ + wasSuccessful: false, + hasWarning: false, + hasError: false + }); + } + + // + // Render + + render() { + const { + isSpinning, + error, + children, + ...otherProps + } = this.props; + + const { + wasSuccessful, + hasWarning, + hasError + } = this.state; + + const showIcon = wasSuccessful || hasWarning || hasError; + + let iconName = icons.CHECK; + let iconKind = kinds.SUCCESS; + + if (hasWarning) { + iconName = icons.WARNING; + iconKind = kinds.WARNING; + } + + if (hasError) { + iconName = icons.DANGER; + iconKind = kinds.DANGER; + } + + return ( + + + { + showIcon && + + + + } + + { + + { + children + } + + } + + + ); + } +} + +SpinnerErrorButton.propTypes = { + isSpinning: PropTypes.bool.isRequired, + error: PropTypes.object, + children: PropTypes.node.isRequired +}; + +export default SpinnerErrorButton; diff --git a/frontend/src/Components/Link/SpinnerIconButton.js b/frontend/src/Components/Link/SpinnerIconButton.js new file mode 100644 index 000000000..8f62d6031 --- /dev/null +++ b/frontend/src/Components/Link/SpinnerIconButton.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import IconButton from './IconButton'; + +function SpinnerIconButton(props) { + const { + name, + spinningName, + isDisabled, + isSpinning, + ...otherProps + } = props; + + return ( + + ); +} + +SpinnerIconButton.propTypes = { + name: PropTypes.string.isRequired, + spinningName: PropTypes.string.isRequired, + isDisabled: PropTypes.bool.isRequired, + isSpinning: PropTypes.bool.isRequired +}; + +SpinnerIconButton.defaultProps = { + spinningName: icons.SPINNER, + isDisabled: false, + isSpinning: false +}; + +export default SpinnerIconButton; diff --git a/frontend/src/Components/Loading/LoadingIndicator.css b/frontend/src/Components/Loading/LoadingIndicator.css new file mode 100644 index 000000000..b8288f7f9 --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.css @@ -0,0 +1,65 @@ +.loading { + margin-top: 20px; + text-align: center; +} + +.rippleContainer { + position: relative; + display: inline-block; +} + +.ripple:nth-child(0) { + animation-delay: -0.8s; +} + +.ripple:nth-child(1) { + animation-delay: -0.6s; +} + +.ripple:nth-child(2) { + animation-delay: -0.4s; +} + +.ripple:nth-child(3) { + animation-delay: -0.2s; +} + +.ripple { + position: absolute; + border: 2px solid #3a3f51; + border-radius: 100%; + animation-fill-mode: both; + animation: rippleContainer 1.25s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8); +} + +@-webkit-keyframes rippleContainer { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 70% { + opacity: 0.7; + transform: scale(1); + } + + 100% { + opacity: 0; + } +} + +@keyframes rippleContainer { + 0% { + opacity: 1; + transform: scale(0.1); + } + + 70% { + opacity: 0.7; + transform: scale(1); + } + + 100% { + opacity: 0; + } +} diff --git a/frontend/src/Components/Loading/LoadingIndicator.js b/frontend/src/Components/Loading/LoadingIndicator.js new file mode 100644 index 000000000..5f9a15b1a --- /dev/null +++ b/frontend/src/Components/Loading/LoadingIndicator.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './LoadingIndicator.css'; + +function LoadingIndicator({ className, size }) { + const sizeInPx = `${size}px`; + const width = sizeInPx; + const height = sizeInPx; + + return ( +
+
+
+ +
+ +
+
+
+ ); +} + +LoadingIndicator.propTypes = { + className: PropTypes.string, + size: PropTypes.number +}; + +LoadingIndicator.defaultProps = { + className: styles.loading, + size: 50 +}; + +export default LoadingIndicator; diff --git a/frontend/src/Components/Loading/LoadingMessage.css b/frontend/src/Components/Loading/LoadingMessage.css new file mode 100644 index 000000000..a7b39e76f --- /dev/null +++ b/frontend/src/Components/Loading/LoadingMessage.css @@ -0,0 +1,6 @@ +.loadingMessage { + margin: 50px 10px 0; + text-align: center; + font-weight: 300; + font-size: 36px; +} diff --git a/frontend/src/Components/Loading/LoadingMessage.js b/frontend/src/Components/Loading/LoadingMessage.js new file mode 100644 index 000000000..ab7b2592f --- /dev/null +++ b/frontend/src/Components/Loading/LoadingMessage.js @@ -0,0 +1,36 @@ +import React from 'react'; +import styles from './LoadingMessage.css'; + +const messages = [ + 'Downloading more RAM', + 'Now in Technicolor', + 'Previously on Sonarr...', + 'Bleep Bloop.', + 'Locating the required gigapixels to render...', + 'Spinning up the hamster wheel...', + 'At least you\'re not on hold', + 'Hum something loud while others stare', + 'Loading humorous message... Please Wait', + 'I could\'ve been faster in Python', + 'Don\'t forget to rewind your episodes', + 'Congratulations! you are the 1000th visitor.', + 'HELP!, I\'m being held hostage and forced to write these stupid lines!', + 'RE-calibrating the internet...', + 'I\'ll be here all week', + 'Don\'t forget to tip your waitress', + 'Apply directly to the forehead', + 'Loading Battlestation' +]; + +function LoadingMessage() { + const index = Math.floor(Math.random() * messages.length); + const message = messages[index]; + + return ( +
+ {message} +
+ ); +} + +export default LoadingMessage; diff --git a/frontend/src/Components/Menu/FilterMenu.css b/frontend/src/Components/Menu/FilterMenu.css new file mode 100644 index 000000000..34991aed9 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenu.css @@ -0,0 +1,9 @@ +.filterMenu { + composes: menu from './Menu.css'; +} + +@media only screen and (max-width: $breakpointSmall) { + .filterMenu { + margin-right: 10px; + } +} diff --git a/frontend/src/Components/Menu/FilterMenu.js b/frontend/src/Components/Menu/FilterMenu.js new file mode 100644 index 000000000..815176e13 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenu.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import styles from './FilterMenu.css'; + +class FilterMenu extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + + {children} + + ); + } +} + +FilterMenu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +FilterMenu.defaultProps = { + className: styles.filterMenu +}; + +export default FilterMenu; diff --git a/frontend/src/Components/Menu/FilterMenuItem.js b/frontend/src/Components/Menu/FilterMenuItem.js new file mode 100644 index 000000000..54c293c49 --- /dev/null +++ b/frontend/src/Components/Menu/FilterMenuItem.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import SelectedMenuItem from './SelectedMenuItem'; + +class FilterMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + value, + onPress + } = this.props; + + onPress(name, value); + } + + // + // Render + + render() { + const { + name, + value, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const isSelected = name === filterKey && value === filterValue; + + return ( + + ); + } +} + +FilterMenuItem.propTypes = { + name: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + onPress: PropTypes.func.isRequired +}; + +FilterMenuItem.defaultProps = { + name: null, + value: null +}; + +export default FilterMenuItem; diff --git a/frontend/src/Components/Menu/Menu.css b/frontend/src/Components/Menu/Menu.css new file mode 100644 index 000000000..9cce48fee --- /dev/null +++ b/frontend/src/Components/Menu/Menu.css @@ -0,0 +1,7 @@ +.tether { + z-index: 2000; +} + +.menu { + position: relative; +} diff --git a/frontend/src/Components/Menu/Menu.js b/frontend/src/Components/Menu/Menu.js new file mode 100644 index 000000000..06e38dcf7 --- /dev/null +++ b/frontend/src/Components/Menu/Menu.js @@ -0,0 +1,203 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import TetherComponent from 'react-tether'; +import { align } from 'Helpers/Props'; +import styles from './Menu.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [align.RIGHT]: { + ...baseTetherOptions, + attachment: 'top right', + targetAttachment: 'bottom right' + }, + + [align.LEFT]: { + ...baseTetherOptions, + attachment: 'top left', + targetAttachment: 'bottom left' + } +}; + +class Menu extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMenuOpen: false, + maxHeight: 0 + }; + } + + componentDidMount() { + this.setMaxHeight(); + } + + // + // Control + + getMaxHeight() { + if (!this.props.enforceMaxHeight) { + return; + } + + const menu = ReactDOM.findDOMNode(this.refs.menu); + + if (!menu) { + return; + } + + const { bottom } = menu.getBoundingClientRect(); + const maxHeight = window.innerHeight - bottom; + + return maxHeight; + } + + setMaxHeight() { + this.setState({ + maxHeight: this.getMaxHeight() + }); + } + + _addListener() { + // Listen to resize events on the window and scroll events + // on all elements to ensure the menu is the best size possible. + // Listen for click events on the window to support closing the + // menu on clicks outside. + + window.addEventListener('resize', this.onWindowResize); + window.addEventListener('scroll', this.onWindowScroll, { capture: true }); + window.addEventListener('click', this.onWindowClick); + } + + _removeListener() { + window.removeEventListener('resize', this.onWindowResize); + window.removeEventListener('scroll', this.onWindowScroll, { capture: true }); + window.removeEventListener('click', this.onWindowClick); + } + + // + // Listeners + + onWindowClick = (event) => { + const menu = ReactDOM.findDOMNode(this.refs.menu); + const menuContent = ReactDOM.findDOMNode(this.refs.menuContent); + + if (!menu) { + return; + } + + if ((!menu.contains(event.target) || menuContent.contains(event.target)) && this.state.isMenuOpen) { + this.setState({ isMenuOpen: false }); + this._removeListener(); + } + } + + onWindowResize = () => { + this.setMaxHeight(); + } + + onWindowScroll = () => { + this.setMaxHeight(); + } + + onMenuButtonPress = () => { + const state = { + isMenuOpen: !this.state.isMenuOpen + }; + + if (this.state.isMenuOpen) { + this._removeListener(); + } else { + state.maxHeight = this.getMaxHeight(); + this._addListener(); + } + + this.setState(state); + } + + // + // Render + + render() { + const { + className, + children, + alignMenu + } = this.props; + + const { + maxHeight, + isMenuOpen + } = this.state; + + const childrenArray = React.Children.toArray(children); + const button = React.cloneElement( + childrenArray[0], + { + onPress: this.onMenuButtonPress + } + ); + + const content = React.cloneElement( + childrenArray[1], + { + ref: 'menuContent', + alignMenu, + maxHeight, + isOpen: isMenuOpen + } + ); + + return ( + +
+ {button} +
+ + { + isMenuOpen && + content + } +
+ ); + } +} + +Menu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + alignMenu: PropTypes.oneOf([align.LEFT, align.RIGHT]), + enforceMaxHeight: PropTypes.bool.isRequired +}; + +Menu.defaultProps = { + className: styles.menu, + alignMenu: align.LEFT, + enforceMaxHeight: true +}; + +export default Menu; diff --git a/frontend/src/Components/Menu/MenuButton.css b/frontend/src/Components/Menu/MenuButton.css new file mode 100644 index 000000000..04a7439cd --- /dev/null +++ b/frontend/src/Components/Menu/MenuButton.css @@ -0,0 +1,15 @@ +.menuButton { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + + &::after { + margin-left: 5px; + content: '\25BE'; + } + + &:hover { + color: $toobarButtonHoverColor; + } +} diff --git a/frontend/src/Components/Menu/MenuButton.js b/frontend/src/Components/Menu/MenuButton.js new file mode 100644 index 000000000..d89a52d1d --- /dev/null +++ b/frontend/src/Components/Menu/MenuButton.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './MenuButton.css'; + +class MenuButton extends Component { + + // + // Render + + render() { + const { + className, + children, + onPress, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +MenuButton.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + onPress: PropTypes.func +}; + +MenuButton.defaultProps = { + className: styles.menuButton +}; + +export default MenuButton; diff --git a/frontend/src/Components/Menu/MenuContent.css b/frontend/src/Components/Menu/MenuContent.css new file mode 100644 index 000000000..0acc07390 --- /dev/null +++ b/frontend/src/Components/Menu/MenuContent.css @@ -0,0 +1,11 @@ +.menuContent { + display: flex; + flex-direction: column; + background-color: $toolbarMenuItemBackgroundColor; + line-height: 20px; +} + +.scroller { + display: flex; + flex-direction: column; +} diff --git a/frontend/src/Components/Menu/MenuContent.js b/frontend/src/Components/Menu/MenuContent.js new file mode 100644 index 000000000..1acacf80f --- /dev/null +++ b/frontend/src/Components/Menu/MenuContent.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './MenuContent.css'; + +class MenuContent extends Component { + + // + // Render + + render() { + const { + className, + children, + maxHeight + } = this.props; + + return ( +
+ + {children} + +
+ ); + } +} + +MenuContent.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired, + maxHeight: PropTypes.number +}; + +MenuContent.defaultProps = { + className: styles.menuContent +}; + +export default MenuContent; diff --git a/frontend/src/Components/Menu/MenuItem.css b/frontend/src/Components/Menu/MenuItem.css new file mode 100644 index 000000000..b2ad5d312 --- /dev/null +++ b/frontend/src/Components/Menu/MenuItem.css @@ -0,0 +1,19 @@ +.menuItem { + composes: truncate from 'Styles/mixins/truncate.css'; + + display: block; + flex-shrink: 0; + padding: 10px 20px; + min-width: 150px; + max-width: 250px; + background-color: $toolbarMenuItemBackgroundColor; + color: $menuItemColor; + line-height: 20px; + + &:hover, + &:focus { + background-color: $toolbarMenuItemHoverBackgroundColor; + color: $menuItemHoverColor; + text-decoration: none; + } +} diff --git a/frontend/src/Components/Menu/MenuItem.js b/frontend/src/Components/Menu/MenuItem.js new file mode 100644 index 000000000..ff083450b --- /dev/null +++ b/frontend/src/Components/Menu/MenuItem.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './MenuItem.css'; + +class MenuItem extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +MenuItem.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +MenuItem.defaultProps = { + className: styles.menuItem +}; + +export default MenuItem; diff --git a/frontend/src/Components/Menu/SelectedMenuItem.css b/frontend/src/Components/Menu/SelectedMenuItem.css new file mode 100644 index 000000000..739419d69 --- /dev/null +++ b/frontend/src/Components/Menu/SelectedMenuItem.css @@ -0,0 +1,15 @@ +.item { + display: flex; + justify-content: space-between; + white-space: nowrap; +} + +.isSelected { + visibility: visible; + margin-left: 20px; +} + +.isNotSelected { + visibility: hidden; + margin-left: 20px; +} diff --git a/frontend/src/Components/Menu/SelectedMenuItem.js b/frontend/src/Components/Menu/SelectedMenuItem.js new file mode 100644 index 000000000..bc8f41f5a --- /dev/null +++ b/frontend/src/Components/Menu/SelectedMenuItem.js @@ -0,0 +1,63 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import MenuItem from './MenuItem'; +import styles from './SelectedMenuItem.css'; + +class SelectedMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + onPress + } = this.props; + + onPress(name); + } + + // + // Render + + render() { + const { + children, + selectedIconName, + isSelected, + ...otherProps + } = this.props; + + return ( + +
+ {children} + + +
+
+ ); + } +} + +SelectedMenuItem.propTypes = { + name: PropTypes.string, + children: PropTypes.node.isRequired, + selectedIconName: PropTypes.string.isRequired, + isSelected: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +SelectedMenuItem.defaultProps = { + selectedIconName: icons.CHECK +}; + +export default SelectedMenuItem; diff --git a/frontend/src/Components/Menu/SortMenu.js b/frontend/src/Components/Menu/SortMenu.js new file mode 100644 index 000000000..2977f661b --- /dev/null +++ b/frontend/src/Components/Menu/SortMenu.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; + +class SortMenu extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + + {children} + + ); + } +} + +SortMenu.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +export default SortMenu; diff --git a/frontend/src/Components/Menu/SortMenuItem.js b/frontend/src/Components/Menu/SortMenuItem.js new file mode 100644 index 000000000..e35864ae6 --- /dev/null +++ b/frontend/src/Components/Menu/SortMenuItem.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import SelectedMenuItem from './SelectedMenuItem'; + +function SortMenuItem(props) { + const { + name, + sortKey, + sortDirection, + ...otherProps + } = props; + + const isSelected = name === sortKey; + + return ( + + ); +} + +SortMenuItem.propTypes = { + name: PropTypes.string, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + onPress: PropTypes.func.isRequired +}; + +SortMenuItem.defaultProps = { + name: null +}; + +export default SortMenuItem; diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.css b/frontend/src/Components/Menu/ToolbarMenuButton.css new file mode 100644 index 000000000..ef08e2843 --- /dev/null +++ b/frontend/src/Components/Menu/ToolbarMenuButton.css @@ -0,0 +1,13 @@ +.menuButton { + composes: menuButton from './MenuButton.css'; + + width: $toolbarButtonWidth; + height: $toolbarHeight; + text-align: center; +} + +.label { + height: 14px; + color: $toolbarLabelColor; + font-size: $extraSmallFontSize; +} diff --git a/frontend/src/Components/Menu/ToolbarMenuButton.js b/frontend/src/Components/Menu/ToolbarMenuButton.js new file mode 100644 index 000000000..33f7802de --- /dev/null +++ b/frontend/src/Components/Menu/ToolbarMenuButton.js @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Icon from 'Components/Icon'; +import MenuButton from 'Components/Menu/MenuButton'; +import styles from './ToolbarMenuButton.css'; + +function ToolbarMenuButton(props) { + const { + iconName, + text, + ...otherProps + } = props; + + return ( + +
+ + +
+ {text} +
+
+
+ ); +} + +ToolbarMenuButton.propTypes = { + iconName: PropTypes.string.isRequired, + text: PropTypes.string +}; + +export default ToolbarMenuButton; diff --git a/frontend/src/Components/Menu/ViewMenu.js b/frontend/src/Components/Menu/ViewMenu.js new file mode 100644 index 000000000..61be4f952 --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenu.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Menu from 'Components/Menu/Menu'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; + +function ViewMenu(props) { + const { + children, + ...otherProps + } = props; + + return ( + + + {children} + + ); +} + +ViewMenu.propTypes = { + children: PropTypes.node.isRequired +}; + +export default ViewMenu; diff --git a/frontend/src/Components/Menu/ViewMenuItem.js b/frontend/src/Components/Menu/ViewMenuItem.js new file mode 100644 index 000000000..d355d6e94 --- /dev/null +++ b/frontend/src/Components/Menu/ViewMenuItem.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import SelectedMenuItem from './SelectedMenuItem'; + +function ViewMenuItem(props) { + const { + name, + selectedView, + ...otherProps + } = props; + + const isSelected = name === selectedView; + + return ( + + ); +} + +ViewMenuItem.propTypes = { + name: PropTypes.string, + selectedView: PropTypes.string.isRequired +}; + +export default ViewMenuItem; diff --git a/frontend/src/Components/Modal/ConfirmModal.js b/frontend/src/Components/Modal/ConfirmModal.js new file mode 100644 index 000000000..5bb783d43 --- /dev/null +++ b/frontend/src/Components/Modal/ConfirmModal.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function ConfirmModal(props) { + const { + isOpen, + kind, + size, + title, + message, + confirmLabel, + cancelLabel, + hideCancelButton, + isSpinning, + onConfirm, + onCancel + } = props; + + return ( + + + {title} + + + {message} + + + + { + !hideCancelButton && + + } + + + {confirmLabel} + + + + + ); +} + +ConfirmModal.propTypes = { + className: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + kind: PropTypes.oneOf(kinds.all), + size: PropTypes.oneOf(sizes.all), + title: PropTypes.string.isRequired, + message: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + confirmLabel: PropTypes.string, + cancelLabel: PropTypes.string, + hideCancelButton: PropTypes.bool, + isSpinning: PropTypes.bool.isRequired, + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +ConfirmModal.defaultProps = { + kind: kinds.PRIMARY, + size: sizes.MEDIUM, + confirmLabel: 'OK', + cancelLabel: 'Cancel', + isSpinning: false +}; + +export default ConfirmModal; diff --git a/frontend/src/Components/Modal/Modal.css b/frontend/src/Components/Modal/Modal.css new file mode 100644 index 000000000..4872a011e --- /dev/null +++ b/frontend/src/Components/Modal/Modal.css @@ -0,0 +1,79 @@ +.modalContainer { + position: absolute; + top: 0; + z-index: 1000; + width: 100%; + height: 100%; +} + +.modalBackdrop { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + background-color: $modalBackdropBackgroundColor; + opacity: 1; +} + +.modal { + position: relative; + display: flex; + max-height: 90%; + border-radius: 6px; + opacity: 1; +} + +.modalOpen { + /* Prevent the body from scrolling when the modal is open */ + overflow: hidden !important; +} + +/* + * Sizes + */ + +.small { + composes: modal; + + width: 480px; +} + +.medium { + composes: modal; + + width: 720px; +} + +.large { + composes: modal; + + width: 1080px; +} + +@media only screen and (max-width: $breakpointLarge) { + .modal.large { + width: 90%; + } +} + +@media only screen and (max-width: $breakpointMedium) { + .modal.small, + .modal.medium { + width: 90%; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .modalContainer { + position: fixed; + } + + .modal.small, + .modal.medium, + .modal.large { + max-height: 100%; + width: 100%; + height: 100%; + } +} diff --git a/frontend/src/Components/Modal/Modal.js b/frontend/src/Components/Modal/Modal.js new file mode 100644 index 000000000..1696da829 --- /dev/null +++ b/frontend/src/Components/Modal/Modal.js @@ -0,0 +1,196 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Portal from 'react-portal'; +import classNames from 'classnames'; +import elementClass from 'element-class'; +import getUniqueElememtId from 'Utilities/getUniqueElementId'; +import * as keyCodes from 'Utilities/Constants/keyCodes'; +import { sizes } from 'Helpers/Props'; +import styles from './Modal.css'; + +const openModals = []; + +function removeFromOpenModals(id) { + const index = openModals.indexOf(id); + + if (index >= 0) { + openModals.splice(index, 1); + } +} + +class Modal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._modalId = getUniqueElememtId(); + } + + componentDidMount() { + if (this.props.isOpen) { + this._openModal(); + } + } + + componentDidUpdate(prevProps) { + const { + isOpen + } = this.props; + + if (!prevProps.isOpen && isOpen) { + this._openModal(); + } else if (prevProps.isOpen && !isOpen) { + this._closeModal(); + } + } + + componentWillUnmount() { + if (this.props.isOpen) { + this._closeModal(); + } + } + + // + // Control + + _openModal() { + openModals.push(this._modalId); + window.addEventListener('keydown', this.onKeyDown); + + if (openModals.length === 1) { + elementClass(document.body).add(styles.modalOpen); + } + } + + _closeModal() { + removeFromOpenModals(this._modalId); + window.removeEventListener('keydown', this.onKeyDown); + + if (openModals.length === 0) { + elementClass(document.body).remove(styles.modalOpen); + } + } + + _isBackdropTarget(event) { + const targetElement = this._findEventTarget(event); + + if (targetElement) { + const modalElement = ReactDOM.findDOMNode(this.refs.modal); + + return !modalElement || !modalElement.contains(targetElement); + } + + return false; + } + + _findEventTarget(event) { + const changedTouches = event.changedTouches; + + if (!changedTouches) { + return event.target; + } + + if (changedTouches.length === 1) { + const touch = changedTouches[0]; + + return document.elementFromPoint(touch.clientX, touch.clientY); + } + } + + // + // Listeners + + onBackdropBeginPress = (event) => { + this._isBackdropPressed = this._isBackdropTarget(event); + } + + onBackdropEndPress = (event) => { + if (this._isBackdropPressed && this._isBackdropTarget(event)) { + this.props.onModalClose(); + } + + this._isBackdropPressed = false; + } + + onKeyDown = (event) => { + const keyCode = event.keyCode; + + if (keyCode === keyCodes.ESCAPE) { + if (openModals.indexOf(this._modalId) === openModals.length - 1) { + event.preventDefault(); + event.stopPropagation(); + + this.props.onModalClose(); + } + } + } + + onClosePress = (event) => { + this.props.onModalClose(); + } + + // + // Render + + render() { + const { + className, + backdropClassName, + size, + children, + isOpen + } = this.props; + + return ( + +
+ { + isOpen && +
+
+
+ {children} +
+
+
+ } +
+
+ ); + } +} + +Modal.propTypes = { + className: PropTypes.string, + backdropClassName: PropTypes.string, + size: PropTypes.oneOf(sizes.all), + children: PropTypes.node, + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +Modal.defaultProps = { + className: styles.modal, + backdropClassName: styles.modalBackdrop, + size: sizes.LARGE +}; + +export default Modal; diff --git a/frontend/src/Components/Modal/ModalBody.css b/frontend/src/Components/Modal/ModalBody.css new file mode 100644 index 000000000..2e55a91cb --- /dev/null +++ b/frontend/src/Components/Modal/ModalBody.css @@ -0,0 +1,14 @@ +$modalBodyPadding: 30px; + +.modalBody { + flex: 1 0 1px; + padding: $modalBodyPadding; +} + +.modalScroller { + flex-grow: 1; +} + +.innerModalBody { + padding: $modalBodyPadding; +} diff --git a/frontend/src/Components/Modal/ModalBody.js b/frontend/src/Components/Modal/ModalBody.js new file mode 100644 index 000000000..a35f2ecf5 --- /dev/null +++ b/frontend/src/Components/Modal/ModalBody.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './ModalBody.css'; + +class ModalBody extends Component { + + // + // Render + + render() { + const { + innerClassName, + scrollDirection, + children, + ...otherProps + } = this.props; + + let className = this.props.className; + const hasScroller = scrollDirection !== scrollDirections.NONE; + + if (!className) { + className = hasScroller ? styles.modalScroller : styles.modalBody; + } + + return ( + + { + hasScroller ? +
+ {children} +
: + children + } +
+ ); + } + +} + +ModalBody.propTypes = { + className: PropTypes.string, + innerClassName: PropTypes.string, + children: PropTypes.node, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]) +}; + +ModalBody.defaultProps = { + innerClassName: styles.innerModalBody, + scrollDirection: scrollDirections.VERTICAL +}; + +export default ModalBody; diff --git a/frontend/src/Components/Modal/ModalContent.css b/frontend/src/Components/Modal/ModalContent.css new file mode 100644 index 000000000..afd798dfa --- /dev/null +++ b/frontend/src/Components/Modal/ModalContent.css @@ -0,0 +1,23 @@ +.modalContent { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + width: 100%; + background-color: $modalBackgroundColor; +} + +.closeButton { + position: absolute; + top: 0; + right: 0; + z-index: 1; + width: 60px; + height: 60px; + text-align: center; + line-height: 60px; + + &:hover { + color: $modalCloseButtonHoverColor; + } +} diff --git a/frontend/src/Components/Modal/ModalContent.js b/frontend/src/Components/Modal/ModalContent.js new file mode 100644 index 000000000..cc165dda2 --- /dev/null +++ b/frontend/src/Components/Modal/ModalContent.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './ModalContent.css'; + +function ModalContent(props) { + const { + className, + children, + onModalClose, + ...otherProps + } = props; + + return ( +
+ + + + + {children} +
+ ); +} + +ModalContent.propTypes = { + className: PropTypes.string, + children: PropTypes.node, + onModalClose: PropTypes.func.isRequired +}; + +ModalContent.defaultProps = { + className: styles.modalContent +}; + +export default ModalContent; diff --git a/frontend/src/Components/Modal/ModalFooter.css b/frontend/src/Components/Modal/ModalFooter.css new file mode 100644 index 000000000..3b817d2bf --- /dev/null +++ b/frontend/src/Components/Modal/ModalFooter.css @@ -0,0 +1,23 @@ +.modalFooter { + display: flex; + align-items: center; + justify-content: flex-end; + flex-shrink: 0; + padding: 15px 30px; + border-top: 1px solid $borderColor; + + a, + button { + margin-left: 10px; + + &:first-child { + margin-left: 0; + } + } +} + +@media only screen and (max-width: $breakpointSmall) { + .modalFooter { + padding: 15px; + } +} diff --git a/frontend/src/Components/Modal/ModalFooter.js b/frontend/src/Components/Modal/ModalFooter.js new file mode 100644 index 000000000..0cf8811d3 --- /dev/null +++ b/frontend/src/Components/Modal/ModalFooter.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './ModalFooter.css'; + +class ModalFooter extends Component { + + // + // Render + + render() { + const { + children, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +ModalFooter.propTypes = { + children: PropTypes.node +}; + +export default ModalFooter; diff --git a/frontend/src/Components/Modal/ModalHeader.css b/frontend/src/Components/Modal/ModalHeader.css new file mode 100644 index 000000000..9ebaad61c --- /dev/null +++ b/frontend/src/Components/Modal/ModalHeader.css @@ -0,0 +1,8 @@ +.modalHeader { + composes: truncate from 'Styles/Mixins/truncate.css'; + + flex-shrink: 0; + padding: 15px 50px 15px 30px; + border-bottom: 1px solid $borderColor; + font-size: 18px; +} diff --git a/frontend/src/Components/Modal/ModalHeader.js b/frontend/src/Components/Modal/ModalHeader.js new file mode 100644 index 000000000..52879b57d --- /dev/null +++ b/frontend/src/Components/Modal/ModalHeader.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './ModalHeader.css'; + +class ModalHeader extends Component { + + // + // Render + + render() { + const { + children, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +ModalHeader.propTypes = { + children: PropTypes.node +}; + +export default ModalHeader; diff --git a/frontend/src/Components/MonitorToggleButton.css b/frontend/src/Components/MonitorToggleButton.css new file mode 100644 index 000000000..e2c68bed1 --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.css @@ -0,0 +1,13 @@ +.toggleButton { + composes: button from 'Components/Link/IconButton.css'; + + padding: 0; + font-size: inherit; +} + +.disabledButton { + composes: button from 'Components/Link/IconButton.css'; + + color: $disabledColor; + cursor: not-allowed; +} diff --git a/frontend/src/Components/MonitorToggleButton.js b/frontend/src/Components/MonitorToggleButton.js new file mode 100644 index 000000000..1190c03a3 --- /dev/null +++ b/frontend/src/Components/MonitorToggleButton.js @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import styles from './MonitorToggleButton.css'; + +class MonitorToggleButton extends Component { + + // + // Listeners + + onPress = (event) => { + const shiftKey = event.nativeEvent.shiftKey; + + this.props.onPress(!this.props.monitored, { shiftKey }); + } + + // + // Render + + render() { + const { + className, + monitored, + isDisabled, + isSaving, + size, + ...otherProps + } = this.props; + + const monitoredMessage = 'Monitored, click to unmonitor'; + const unmonitoredMessage = 'Unmonitored, click to monitor'; + const iconName = monitored ? icons.MONITORED : icons.UNMONITORED; + + if (isDisabled) { + return ( + + ); + } + + return ( + + ); + } +} + +MonitorToggleButton.propTypes = { + className: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + size: PropTypes.number, + isDisabled: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + onPress: PropTypes.func.isRequired +}; + +MonitorToggleButton.defaultProps = { + className: styles.toggleButton, + isDisabled: false, + isSaving: false +}; + +export default MonitorToggleButton; diff --git a/frontend/src/Components/NotFound.css b/frontend/src/Components/NotFound.css new file mode 100644 index 000000000..9aaf1114f --- /dev/null +++ b/frontend/src/Components/NotFound.css @@ -0,0 +1,14 @@ +.container { + text-align: center; +} + +.message { + margin: 50px 0; + text-align: center; + font-weight: 300; + font-size: 36px; +} + +.image { + height: 350px; +} diff --git a/frontend/src/Components/NotFound.js b/frontend/src/Components/NotFound.js new file mode 100644 index 000000000..7043da46f --- /dev/null +++ b/frontend/src/Components/NotFound.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import styles from './NotFound.css'; + +function NotFound({ message }) { + return ( + +
+
+ {message} +
+ + +
+
+ ); +} + +NotFound.propTypes = { + message: PropTypes.string.isRequired +}; + +NotFound.defaultProps = { + message: 'You must be lost, nothing to see here.' +}; + +export default NotFound; diff --git a/frontend/src/Components/Page/ErrorPage.css b/frontend/src/Components/Page/ErrorPage.css new file mode 100644 index 000000000..e62a82a6b --- /dev/null +++ b/frontend/src/Components/Page/ErrorPage.css @@ -0,0 +1,12 @@ +.page { + composes: page from './Page.css'; + + margin-top: 20px; + text-align: center; + font-size: 20px; +} + +.version { + margin-top: 20px; + font-size: 16px; +} diff --git a/frontend/src/Components/Page/ErrorPage.js b/frontend/src/Components/Page/ErrorPage.js new file mode 100644 index 000000000..8ef61fecd --- /dev/null +++ b/frontend/src/Components/Page/ErrorPage.js @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import getErrorMessage from 'Utilities/Object/getErrorMessage'; +import styles from './ErrorPage.css'; + +function ErrorPage(props) { + const { + version, + isLocalStorageSupported, + seriesError, + tagsError, + qualityProfilesError, + uiSettingsError + } = props; + + let errorMessage = 'Failed to load Sonarr'; + + if (!isLocalStorageSupported) { + errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; + } else if (seriesError) { + errorMessage = getErrorMessage(seriesError, 'Failed to load series from API'); + } else if (tagsError) { + errorMessage = getErrorMessage(seriesError, 'Failed to load series from API'); + } else if (qualityProfilesError) { + errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API'); + } else if (uiSettingsError) { + errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API'); + } + + return ( +
+
+ {errorMessage} +
+ +
+ Version {version} +
+
+ ); +} + +ErrorPage.propTypes = { + version: PropTypes.string.isRequired, + isLocalStorageSupported: PropTypes.bool.isRequired, + seriesError: PropTypes.object, + tagsError: PropTypes.object, + qualityProfilesError: PropTypes.object, + uiSettingsError: PropTypes.object +}; + +export default ErrorPage; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js new file mode 100644 index 000000000..a1d106b58 --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModal.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import KeyboardShortcutsModalContentConnector from './KeyboardShortcutsModalContentConnector'; + +function KeyboardShortcutsModal(props) { + const { + isOpen, + onModalClose + } = props; + + return ( + + + + ); +} + +KeyboardShortcutsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default KeyboardShortcutsModal; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css new file mode 100644 index 000000000..4425e0e0d --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.css @@ -0,0 +1,15 @@ +.shortcut { + display: flex; + justify-content: space-between; + padding: 5px 20px; + font-size: 18px; +} + +.key { + padding: 2px 4px; + border-radius: 3px; + background-color: $defaultColor; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); + color: $white; + font-size: 16px; +} diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js new file mode 100644 index 000000000..9c07e047c --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContent.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { shortcuts } from 'Components/keyboardShortcuts'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './KeyboardShortcutsModalContent.css'; + +function getShortcuts() { + const allShortcuts = []; + + Object.keys(shortcuts).forEach((key) => { + allShortcuts.push(shortcuts[key]); + }); + + return allShortcuts; +} + +function getShortcutKey(combo, isOsx) { + const comboMatch = combo.match(/(.+?)\+(.)/); + + if (!comboMatch) { + return combo; + } + + const modifier = comboMatch[1]; + const key = comboMatch[2]; + let osModifier = modifier; + + if (modifier === 'mod') { + osModifier = isOsx ? 'cmd' : 'ctrl'; + } + + return `${osModifier} + ${key}`; +} + +function KeyboardShortcutsModalContent(props) { + const { + isOsx, + onModalClose + } = props; + + const allShortcuts = getShortcuts(); + + return ( + + + Keyboard Shortcuts + + + + { + allShortcuts.map((shortcut) => { + return ( +
+
+ {getShortcutKey(shortcut.key, isOsx)} +
+ +
+ {shortcut.name} +
+
+ ); + }) + } +
+ + + + +
+ ); +} + +KeyboardShortcutsModalContent.propTypes = { + isOsx: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default KeyboardShortcutsModalContent; diff --git a/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js new file mode 100644 index 000000000..d80877153 --- /dev/null +++ b/frontend/src/Components/Page/Header/KeyboardShortcutsModalContentConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import KeyboardShortcutsModalContent from './KeyboardShortcutsModalContent'; + +function createMapStateToProps() { + return createSelector( + createSystemStatusSelector(), + (systemStatus) => { + return { + isOsx: systemStatus.isOsx + }; + } + ); +} + +export default connect(createMapStateToProps)(KeyboardShortcutsModalContent); diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css new file mode 100644 index 000000000..1c8de74b6 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeader.css @@ -0,0 +1,60 @@ +.header { + z-index: 2; + display: flex; + align-items: center; + flex: 0 0 auto; + height: $headerHeight; + background-color: #00a65b; + color: $white; +} + +.logoContainer { + display: flex; + justify-content: center; + flex: 0 0 $sidebarWidth; +} + +.logo { + width: 32px; + height: 32px; +} + +.sidebarToggleContainer { + display: none; + justify-content: center; + flex: 0 0 45px; + margin-right: 14px; +} + +.right { + display: flex; + justify-content: flex-end; + flex-grow: 1; +} + +.donate { + composes: link from 'Components/Link/Link.css'; + + width: 30px; + color: $themeRed; + text-align: center; + line-height: 60px; + + &:hover { + color: #9c1f30; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .logoContainer { + flex: 0 0 60px; + } + + .sidebarToggleContainer { + display: flex; + } + + .donate { + display: none; + } +} diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js new file mode 100644 index 000000000..9450a43c5 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeader.js @@ -0,0 +1,96 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import IconButton from 'Components/Link/IconButton'; +import Link from 'Components/Link/Link'; +import SeriesSearchInputConnector from './SeriesSearchInputConnector'; +import PageHeaderActionsMenuConnector from './PageHeaderActionsMenuConnector'; +import KeyboardShortcutsModal from './KeyboardShortcutsModal'; +import styles from './PageHeader.css'; + +class PageHeader extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props); + + this.state = { + isKeyboardShortcutsModalOpen: false + }; + } + + componentDidMount() { + this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.openKeyboardShortcutsModal); + } + + // + // Control + + openKeyboardShortcutsModal = () => { + this.setState({ isKeyboardShortcutsModalOpen: true }); + } + + // + // Listeners + + onKeyboardShortcutsModalClose = () => { + this.setState({ isKeyboardShortcutsModalOpen: false }); + } + + // + // Render + + render() { + const { + onSidebarToggle + } = this.props; + + return ( +
+
+ + + +
+ + +
+ +
+ + + +
+ + +
+ + +
+ ); + } +} + +PageHeader.propTypes = { + onSidebarToggle: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +export default keyboardShortcuts(PageHeader); diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css new file mode 100644 index 000000000..44aa20453 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.css @@ -0,0 +1,27 @@ +.menuButton { + margin-right: 15px; + width: 30px; + height: 60px; + text-align: center; + + &:hover { + color: $themeDarkColor; + } +} + +.itemIcon { + margin-right: 8px; +} + +.separator { + overflow: hidden; + height: 1px; + background-color: $themeDarkColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .menuButton { + margin-right: 5px; + } +} + diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js new file mode 100644 index 000000000..a261c1b01 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenu.js @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { align, icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import styles from './PageHeaderActionsMenu.css'; + +function PageHeaderActionsMenu(props) { + const { + formsAuth, + onRestartPress, + onShutdownPress + } = props; + + return ( +
+ + + + + + + + + Restart + + + + + Shutdown + + + { + formsAuth && +
+ } + + { + formsAuth && + + + Logout + + } + +
+
+ ); +} + +PageHeaderActionsMenu.propTypes = { + formsAuth: PropTypes.bool.isRequired, + onRestartPress: PropTypes.func.isRequired, + onShutdownPress: PropTypes.func.isRequired +}; + +export default PageHeaderActionsMenu; diff --git a/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js new file mode 100644 index 000000000..66d131521 --- /dev/null +++ b/frontend/src/Components/Page/Header/PageHeaderActionsMenuConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { restart, shutdown } from 'Store/Actions/systemActions'; +import PageHeaderActionsMenu from './PageHeaderActionsMenu'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + formsAuth: status.item.authentication === 'forms' + }; + } + ); +} + +const mapDispatchToProps = { + restart, + shutdown +}; + +class PageHeaderActionsMenuConnector extends Component { + + // + // Listeners + + onRestartPress = () => { + this.props.restart(); + } + + onShutdownPress = () => { + this.props.shutdown(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +PageHeaderActionsMenuConnector.propTypes = { + restart: PropTypes.func.isRequired, + shutdown: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(PageHeaderActionsMenuConnector); diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.css b/frontend/src/Components/Page/Header/SeriesSearchInput.css new file mode 100644 index 000000000..aaf8a5be5 --- /dev/null +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.css @@ -0,0 +1,98 @@ +.wrapper { + display: flex; +} + +.icon { + line-height: 24px !important; +} + +.input { + margin-left: 8px; + width: 200px; + border: none; + border-bottom: solid 1px $white; + background-color: transparent; + box-shadow: none; + color: $white; + transition: border 0.3s ease-out; + + &::placeholder { + color: $white; + transition: color 0.3s ease-out; + } + + &:focus { + outline: 0; + border-bottom-color: transparent; + + &::placeholder { + color: transparent; + } + } +} + +.container { + position: relative; + flex-grow: 1; +} + +.seriesContainer { + composes: scrollbar from 'Styles/Mixins/scroller.css'; + composes: scrollbarTrack from 'Styles/Mixins/scroller.css'; + composes: scrollbarThumb from 'Styles/Mixins/scroller.css'; +} + +.containerOpen { + .seriesContainer { + position: absolute; + top: 42px; + z-index: 1; + overflow-y: auto; + min-width: 100%; + max-height: 230px; + border: 1px solid $themeDarkColor; + border-radius: 4px; + border-top-left-radius: 0; + border-top-right-radius: 0; + background-color: $themeDarkColor; + box-shadow: inset 0 1px 1px $inputBoxShadowColor; + color: $menuItemColor; + } +} + +.list { + margin: 5px 0; + padding-left: 0; + list-style-type: none; +} + +.listItem { + padding: 0 16px; + white-space: nowrap; +} + +.highlighted { + background-color: $themeLightColor; +} + +.sectionTitle { + padding: 5px 8px; + color: $disabledColor; +} + +.addNewSeriesSuggestion { + padding: 0 3px; + cursor: pointer; +} + +@media only screen and (max-width: $breakpointSmall) { + .input { + min-width: 150px; + max-width: 200px; + } + + .container { + min-width: 0; + max-width: 200px; + } +} diff --git a/frontend/src/Components/Page/Header/SeriesSearchInput.js b/frontend/src/Components/Page/Header/SeriesSearchInput.js new file mode 100644 index 000000000..9ee608d39 --- /dev/null +++ b/frontend/src/Components/Page/Header/SeriesSearchInput.js @@ -0,0 +1,250 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Autosuggest from 'react-autosuggest'; +import jdu from 'jdu'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import SeriesSearchResult from './SeriesSearchResult'; +import styles from './SeriesSearchInput.css'; + +const ADD_NEW_TYPE = 'addNew'; + +class SeriesSearchInput extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._autosuggest = null; + + this.state = { + value: '', + suggestions: [] + }; + } + + componentDidMount() { + this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput); + } + + // + // Control + + setAutosuggestRef = (ref) => { + this._autosuggest = ref; + } + + focusInput = (event) => { + event.preventDefault(); + this._autosuggest.input.focus(); + } + + getSectionSuggestions(section) { + return section.suggestions; + } + + renderSectionTitle(section) { + return ( +
+ {section.title} +
+ ); + } + + getSuggestionValue({ title }) { + return title; + } + + renderSuggestion(item, { query }) { + if (item.type === ADD_NEW_TYPE) { + return ( +
+ Search for {query} +
+ ); + } + + return ( + + ); + } + + goToSeries(series) { + this.setState({ value: '' }); + this.props.onGoToSeries(series.titleSlug); + } + + reset() { + this.setState({ + value: '', + suggestions: [] + }); + } + + // + // Listeners + + onChange = (event, { newValue }) => { + this.setState({ value: newValue }); + } + + onKeyDown = (event) => { + if (event.key !== 'Tab' && event.key !== 'Enter') { + return; + } + + const { + suggestions, + value + } = this.state; + + const { + highlightedSectionIndex, + highlightedSuggestionIndex + } = this._autosuggest.state; + + if (!suggestions.length || highlightedSectionIndex) { + this.props.onGoToAddNewSeries(value); + this._autosuggest.input.blur(); + + return; + } + + // If an suggestion is not selected go to the first series, + // otherwise go to the selected series. + + if (highlightedSuggestionIndex == null) { + this.goToSeries(suggestions[0]); + } else { + this.goToSeries(suggestions[highlightedSuggestionIndex]); + } + } + + onBlur = () => { + this.reset(); + } + + onSuggestionsFetchRequested = ({ value }) => { + const lowerCaseValue = jdu.replace(value).toLowerCase(); + + const suggestions = _.filter(this.props.series, (series) => { + // Check the title first and if there isn't a match fallback to the alternate titles + + const titleMatch = jdu.replace(series.title).toLowerCase().contains(lowerCaseValue); + + return titleMatch || _.some(series.alternateTitles, (alternateTitle) => { + return jdu.replace(alternateTitle.title).toLowerCase().contains(lowerCaseValue); + }); + }); + + this.setState({ suggestions }); + } + + onSuggestionsClearRequested = () => { + this.reset(); + } + + onSuggestionSelected = (event, { suggestion, sectionIndex }) => { + if (suggestion.type === ADD_NEW_TYPE) { + this.props.onGoToAddNewSeries(this.state.value); + } else { + this.goToSeries(suggestion); + } + } + + // + // Render + + render() { + const { + value, + suggestions + } = this.state; + + const suggestionGroups = []; + + if (suggestions.length) { + suggestionGroups.push({ + title: 'Existing Series', + suggestions + }); + } + + if (suggestions.length <= 3) { + suggestionGroups.push({ + title: 'Add New Series', + suggestions: [ + { + type: ADD_NEW_TYPE, + title: value + } + ] + }); + } + + const inputProps = { + ref: this.setInputRef, + className: styles.input, + name: 'seriesSearch', + value, + placeholder: 'Search', + autoComplete: 'off', + spellCheck: false, + onChange: this.onChange, + onKeyDown: this.onKeyDown, + onBlur: this.onBlur, + onFocus: this.onFocus + }; + + const theme = { + container: styles.container, + containerOpen: styles.containerOpen, + suggestionsContainer: styles.seriesContainer, + suggestionsList: styles.list, + suggestion: styles.listItem, + suggestionHighlighted: styles.highlighted + }; + + return ( +
+ + + +
+ ); + } +} + +SeriesSearchInput.propTypes = { + series: PropTypes.arrayOf(PropTypes.object).isRequired, + onGoToSeries: PropTypes.func.isRequired, + onGoToAddNewSeries: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +export default keyboardShortcuts(SeriesSearchInput); diff --git a/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js new file mode 100644 index 000000000..83f111e88 --- /dev/null +++ b/frontend/src/Components/Page/Header/SeriesSearchInputConnector.js @@ -0,0 +1,31 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { push } from 'react-router-redux'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import SeriesSearchInput from './SeriesSearchInput'; + +function createMapStateToProps() { + return createSelector( + createAllSeriesSelector(), + (series) => { + return { + series: _.sortBy(series, 'sortTitle') + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onGoToSeries(titleSlug) { + dispatch(push(`${window.Sonarr.urlBase}/series/${titleSlug}`)); + }, + + onGoToAddNewSeries(query) { + dispatch(push(`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(query)}`)); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(SeriesSearchInput); diff --git a/frontend/src/Components/Page/Header/SeriesSearchResult.css b/frontend/src/Components/Page/Header/SeriesSearchResult.css new file mode 100644 index 000000000..35dc98df5 --- /dev/null +++ b/frontend/src/Components/Page/Header/SeriesSearchResult.css @@ -0,0 +1,34 @@ +.result { + display: flex; + padding: 3px; + cursor: pointer; +} + +.poster { + width: 35px; + height: 50px; +} + +.titles { + flex: 1 1 1px; +} + +.title { + flex: 1 1 1px; + margin-left: 5px; +} + +.alternateTitle { + flex: 1 1 1px; + margin-left: 5px; + color: $disabledColor; + font-size: $smallFontSize; +} + +@media only screen and (max-width: $breakpointSmall) { + .titles, + .title, + .alternateTitle { + composes: truncate from 'Styles/Mixins/truncate.css'; + } +} diff --git a/frontend/src/Components/Page/Header/SeriesSearchResult.js b/frontend/src/Components/Page/Header/SeriesSearchResult.js new file mode 100644 index 000000000..f612bfa1f --- /dev/null +++ b/frontend/src/Components/Page/Header/SeriesSearchResult.js @@ -0,0 +1,59 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import ArtistPoster from 'Artist/ArtistPoster'; +import styles from './SeriesSearchResult.css'; + +function getMatchingAlternateTile(alternateTitles, query) { + return _.first(alternateTitles, (alternateTitle) => { + return alternateTitle.title.toLowerCase().contains(query.toLowerCase()); + }); +} + +function SeriesSearchResult(props) { + const { + query, + title, + alternateTitles, + images + } = props; + + const index = title.toLowerCase().indexOf(query.toLowerCase()); + const alternateTitle = index === -1 ? + getMatchingAlternateTile(alternateTitles, query) : + null; + + return ( +
+ + +
+
+ {title} +
+ + { + !!alternateTitle && +
+ {alternateTitle.title} +
+ } +
+
+ ); +} + +SeriesSearchResult.propTypes = { + query: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + images: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default SeriesSearchResult; diff --git a/frontend/src/Components/Page/LoadingPage.css b/frontend/src/Components/Page/LoadingPage.css new file mode 100644 index 000000000..dd5852e61 --- /dev/null +++ b/frontend/src/Components/Page/LoadingPage.css @@ -0,0 +1,3 @@ +.page { + composes: page from './Page.css'; +} diff --git a/frontend/src/Components/Page/LoadingPage.js b/frontend/src/Components/Page/LoadingPage.js new file mode 100644 index 000000000..398b70c4b --- /dev/null +++ b/frontend/src/Components/Page/LoadingPage.js @@ -0,0 +1,15 @@ +import React from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import LoadingMessage from 'Components/Loading/LoadingMessage'; +import styles from './LoadingPage.css'; + +function LoadingPage() { + return ( +
+ + +
+ ); +} + +export default LoadingPage; diff --git a/frontend/src/Components/Page/Page.css b/frontend/src/Components/Page/Page.css new file mode 100644 index 000000000..9facbfc22 --- /dev/null +++ b/frontend/src/Components/Page/Page.css @@ -0,0 +1,18 @@ +.page { + display: flex; + flex-direction: column; + height: 100%; +} + +.main { + position: relative; /* need this to position inner content - is this really needed? */ + display: flex; + flex: 1 1 auto; +} + +@media only screen and (max-width: $breakpointSmall) { + .page { + flex-grow: 1; + height: initial; + } +} diff --git a/frontend/src/Components/Page/Page.js b/frontend/src/Components/Page/Page.js new file mode 100644 index 000000000..fc7502fd4 --- /dev/null +++ b/frontend/src/Components/Page/Page.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import locationShape from 'Helpers/Props/Shapes/locationShape'; +import SignalRConnector from 'Components/SignalRConnector'; +import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector'; +import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector'; +import PageHeader from './Header/PageHeader'; +import PageSidebar from './Sidebar/PageSidebar'; +import styles from './Page.css'; + +class Page extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isUpdatedModalOpen: false, + isConnectionLostModalOpen: false + }; + } + + componentDidMount() { + window.addEventListener('resize', this.onResize); + } + + componentDidUpdate(prevProps) { + const { + isDisconnected, + isUpdated + } = this.props; + + if (!prevProps.isUpdated && isUpdated) { + this.setState({ isUpdatedModalOpen: true }); + } + + if (prevProps.isDisconnected !== isDisconnected) { + this.setState({ isConnectionLostModalOpen: isDisconnected }); + } + } + + componentWillUnmount() { + window.removeEventListener('resize', this.onResize); + } + + // + // Listeners + + onResize = () => { + this.props.onResize({ + width: window.innerWidth, + height: window.innerHeight + }); + } + + onUpdatedModalClose = () => { + this.setState({ isUpdatedModalOpen: false }); + } + + onConnectionLostModalClose = () => { + this.setState({ isConnectionLostModalOpen: false }); + } + + // + // Render + + render() { + const { + className, + location, + children, + isSmallScreen, + isSidebarVisible, + onSidebarToggle, + onSidebarVisibleChange + } = this.props; + + return ( +
+ + + + +
+ + + {children} +
+ + + + +
+ ); + } +} + +Page.propTypes = { + className: PropTypes.string, + location: locationShape.isRequired, + children: PropTypes.node.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + isUpdated: PropTypes.bool.isRequired, + isDisconnected: PropTypes.bool.isRequired, + onResize: PropTypes.func.isRequired, + onSidebarToggle: PropTypes.func.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +Page.defaultProps = { + className: styles.page +}; + +export default Page; diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js new file mode 100644 index 000000000..8850bed00 --- /dev/null +++ b/frontend/src/Components/Page/PageConnector.js @@ -0,0 +1,179 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; +import { fetchArtist } from 'Store/Actions/seriesActions'; +import { fetchTags } from 'Store/Actions/tagActions'; +import { fetchQualityProfiles, fetchLanguageProfiles, fetchUISettings } from 'Store/Actions/settingsActions'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import ErrorPage from './ErrorPage'; +import LoadingPage from './LoadingPage'; +import Page from './Page'; + +function testLocalStorage() { + const key = 'sonarrTest'; + + try { + localStorage.setItem(key, key); + localStorage.removeItem(key); + + return true; + } catch (e) { + return false; + } +} + +function createMapStateToProps() { + return createSelector( + (state) => state.series, + (state) => state.tags, + (state) => state.settings, + (state) => state.app, + createDimensionsSelector(), + (series, tags, settings, app, dimensions) => { + const isPopulated = series.isPopulated && + tags.isPopulated && + settings.qualityProfiles.isPopulated && + settings.ui.isPopulated; + + const hasError = !!series.error || + !!tags.error || + !!settings.qualityProfiles.error || + !!settings.ui.error; + + return { + isPopulated, + hasError, + seriesError: series.error, + tagsError: tags.error, + qualityProfilesError: settings.qualityProfiles.error, + uiSettingsError: settings.ui.error, + isSmallScreen: dimensions.isSmallScreen, + isSidebarVisible: app.isSidebarVisible, + version: app.version, + isUpdated: app.isUpdated, + isDisconnected: app.isDisconnected + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchSeries() { + dispatch(fetchArtist()); + }, + dispatchFetchTags() { + dispatch(fetchTags()); + }, + dispatchFetchQualityProfiles() { + dispatch(fetchQualityProfiles()); + }, + dispatchFetchLanguageProfiles() { + dispatch(fetchLanguageProfiles()); + }, + dispatchFetchUISettings() { + dispatch(fetchUISettings()); + }, + dispatchFetchStatus() { + dispatch(fetchStatus()); + }, + onResize(dimensions) { + dispatch(saveDimensions(dimensions)); + }, + onSidebarVisibleChange(isSidebarVisible) { + dispatch(setIsSidebarVisible({ isSidebarVisible })); + } + }; +} + +class PageConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isLocalStorageSupported: testLocalStorage() + }; + } + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.dispatchFetchSeries(); + this.props.dispatchFetchTags(); + this.props.dispatchFetchQualityProfiles(); + this.props.dispatchFetchLanguageProfiles(); + this.props.dispatchFetchUISettings(); + this.props.dispatchFetchStatus(); + } + } + + // + // Listeners + + onSidebarToggle = () => { + this.props.onSidebarVisibleChange(!this.props.isSidebarVisible); + } + + // + // Render + + render() { + const { + isPopulated, + hasError, + dispatchFetchSeries, + dispatchFetchTags, + dispatchFetchQualityProfiles, + dispatchFetchLanguageProfiles, + dispatchFetchUISettings, + dispatchFetchStatus, + ...otherProps + } = this.props; + + if (hasError || !this.state.isLocalStorageSupported) { + return ( + + ); + } + + if (isPopulated) { + return ( + + ); + } + + return ( + + ); + } +} + +PageConnector.propTypes = { + isPopulated: PropTypes.bool.isRequired, + hasError: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + dispatchFetchSeries: PropTypes.func.isRequired, + dispatchFetchTags: PropTypes.func.isRequired, + dispatchFetchQualityProfiles: PropTypes.func.isRequired, + dispatchFetchLanguageProfiles: PropTypes.func.isRequired, + dispatchFetchUISettings: PropTypes.func.isRequired, + dispatchFetchStatus: PropTypes.func.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +export default withRouter( + connect(createMapStateToProps, createMapDispatchToProps)(PageConnector) +); diff --git a/frontend/src/Components/Page/PageContent.css b/frontend/src/Components/Page/PageContent.css new file mode 100644 index 000000000..4580077c3 --- /dev/null +++ b/frontend/src/Components/Page/PageContent.css @@ -0,0 +1,8 @@ +.content { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + overflow-x: hidden; + width: 100%; +} diff --git a/frontend/src/Components/Page/PageContent.js b/frontend/src/Components/Page/PageContent.js new file mode 100644 index 000000000..a416d268c --- /dev/null +++ b/frontend/src/Components/Page/PageContent.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import DocumentTitle from 'react-document-title'; +import styles from './PageContent.css'; + +function PageContent(props) { + const { + className, + title, + children + } = props; + + return ( + +
+ {children} +
+
+ ); +} + +PageContent.propTypes = { + className: PropTypes.string, + title: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageContent.defaultProps = { + className: styles.content +}; + +export default PageContent; diff --git a/frontend/src/Components/Page/PageContentBody.css b/frontend/src/Components/Page/PageContentBody.css new file mode 100644 index 000000000..8b41754dd --- /dev/null +++ b/frontend/src/Components/Page/PageContentBody.css @@ -0,0 +1,19 @@ +.contentBody { + /* 1px for flex-basis so the div grows correctly in Edge/Firefox */ + flex: 1 0 1px; +} + +.innerContentBody { + padding: $pageContentBodyPadding; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentBody { + flex-basis: auto; + overflow-y: hidden !important; + } + + .innerContentBody { + padding: $pageContentBodyPaddingSmallScreen; + } +} diff --git a/frontend/src/Components/Page/PageContentBody.js b/frontend/src/Components/Page/PageContentBody.js new file mode 100644 index 000000000..478633cb2 --- /dev/null +++ b/frontend/src/Components/Page/PageContentBody.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import OverlayScroller from 'Components/Scroller/OverlayScroller'; +import Scroller from 'Components/Scroller/Scroller'; +import styles from './PageContentBody.css'; + +class PageContentBody extends Component { + + // + // Render + + render() { + const { + className, + innerClassName, + isSmallScreen, + children, + dispatch, + ...otherProps + } = this.props; + + const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; + + return ( + +
+ {children} +
+
+ ); + } +} + +PageContentBody.propTypes = { + className: PropTypes.string, + innerClassName: PropTypes.string, + isSmallScreen: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired +}; + +PageContentBody.defaultProps = { + className: styles.contentBody, + innerClassName: styles.innerContentBody +}; + +export default PageContentBody; diff --git a/frontend/src/Components/Page/PageContentBodyConnector.js b/frontend/src/Components/Page/PageContentBodyConnector.js new file mode 100644 index 000000000..b5cdfbb21 --- /dev/null +++ b/frontend/src/Components/Page/PageContentBodyConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; +import PageContentBody from './PageContentBody'; + +function createMapStateToProps() { + return createSelector( + createDimensionsSelector(), + (dimensions) => { + return { + isSmallScreen: dimensions.isSmallScreen + }; + } + ); +} + +export default connect(createMapStateToProps)(PageContentBody); diff --git a/frontend/src/Components/Page/PageContentFooter.css b/frontend/src/Components/Page/PageContentFooter.css new file mode 100644 index 000000000..74bdb3811 --- /dev/null +++ b/frontend/src/Components/Page/PageContentFooter.css @@ -0,0 +1,26 @@ +.contentFooter { + display: flex; + flex: 0 0 auto; + padding: 20px; + background-color: #f1f1f1; +} + +@media only screen and (max-width: $breakpointSmall) { + .contentFooter { + display: block; + + div { + margin-top: 10px; + + &:first-child { + margin-top: 0; + } + } + } +} + +@media only screen and (max-width: $breakpointLarge) { + .contentFooter { + flex-wrap: wrap; + } +} diff --git a/frontend/src/Components/Page/PageContentFooter.js b/frontend/src/Components/Page/PageContentFooter.js new file mode 100644 index 000000000..1f6e2d21a --- /dev/null +++ b/frontend/src/Components/Page/PageContentFooter.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './PageContentFooter.css'; + +class PageContentFooter extends Component { + + // + // Render + + render() { + const { + className, + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +PageContentFooter.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageContentFooter.defaultProps = { + className: styles.contentFooter +}; + +export default PageContentFooter; diff --git a/frontend/src/Components/Page/PageJumpBar.css b/frontend/src/Components/Page/PageJumpBar.css new file mode 100644 index 000000000..225ca3022 --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBar.css @@ -0,0 +1,22 @@ +.jumpBar { + display: flex; + align-items: stretch; + align-content: stretch; + align-self: stretch; + justify-content: center; + flex: 0 0 30px; +} + +.jumpBarItems { + display: flex; + justify-content: space-around; + flex: 0 0 100%; + flex-direction: column; + overflow: hidden; +} + +@media only screen and (max-width: $breakpointSmall) { + .jumpBar { + display: none; + } +} diff --git a/frontend/src/Components/Page/PageJumpBar.js b/frontend/src/Components/Page/PageJumpBar.js new file mode 100644 index 000000000..25844ec94 --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBar.js @@ -0,0 +1,133 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import dimensions from 'Styles/Variables/dimensions'; +import PageJumpBarItem from './PageJumpBarItem'; +import styles from './PageJumpBar.css'; + +const ITEM_HEIGHT = parseInt(dimensions.jumpBarItemHeight); + +class PageJumpBar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + height: 0, + visibleItems: props.items + }; + } + + componentDidMount() { + this.computeVisibleItems(); + } + + componentDidUpdate(prevProps, prevState) { + if ( + prevProps.items !== this.props.items || + prevState.height !== this.state.height + ) { + this.computeVisibleItems(); + } + } + + // + // Control + + computeVisibleItems() { + const { + items, + minimumItems + } = this.props; + + const height = this.state.height; + const maximumItems = Math.floor(height / ITEM_HEIGHT); + const diff = items.length - maximumItems; + + if (diff < 0) { + this.setState({ visibleItems: items }); + return; + } + + if (items.length < minimumItems) { + this.setState({ visibleItems: items }); + return; + } + + const removeDiff = Math.ceil(items.length / maximumItems); + + const visibleItems = _.reduce(items, (acc, item, index) => { + if (index % removeDiff === 0) { + acc.push(item); + } + + return acc; + }, []); + + this.setState({ visibleItems }); + } + + // + // Listeners + + onMeasure = ({ height }) => { + this.setState({ height }); + } + + // + // Render + + render() { + const { + minimumItems, + onItemPress + } = this.props; + + const { + visibleItems + } = this.state; + + if (!visibleItems.length || visibleItems.length < minimumItems) { + return null; + } + + return ( +
+ +
+ { + visibleItems.map((item) => { + return ( + + ); + }) + } +
+
+
+ ); + } +} + +PageJumpBar.propTypes = { + items: PropTypes.arrayOf(PropTypes.string).isRequired, + minimumItems: PropTypes.number.isRequired, + onItemPress: PropTypes.func.isRequired +}; + +PageJumpBar.defaultProps = { + minimumItems: 5 +}; + +export default PageJumpBar; diff --git a/frontend/src/Components/Page/PageJumpBarItem.css b/frontend/src/Components/Page/PageJumpBarItem.css new file mode 100644 index 000000000..e829dd31a --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBarItem.css @@ -0,0 +1,14 @@ +.jumpBarItem { + flex: 1 0 $jumpBarItemHeight; + border-bottom: 1px solid $borderColor; + text-align: center; + font-weight: bold; + + &:hover { + color: #777; + } + + &:last-child { + border: none; + } +} diff --git a/frontend/src/Components/Page/PageJumpBarItem.js b/frontend/src/Components/Page/PageJumpBarItem.js new file mode 100644 index 000000000..aeffe4ddd --- /dev/null +++ b/frontend/src/Components/Page/PageJumpBarItem.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './PageJumpBarItem.css'; + +class PageJumpBarItem extends Component { + + // + // Listeners + + onPress = () => { + const { + label, + onItemPress + } = this.props; + + onItemPress(label); + } + + // + // Render + + render() { + return ( + + {this.props.label.toUpperCase()} + + ); + } +} + +PageJumpBarItem.propTypes = { + label: PropTypes.string.isRequired, + onItemPress: PropTypes.func.isRequired +}; + +export default PageJumpBarItem; diff --git a/frontend/src/Components/Page/PageSectionContent.js b/frontend/src/Components/Page/PageSectionContent.js new file mode 100644 index 000000000..774b88669 --- /dev/null +++ b/frontend/src/Components/Page/PageSectionContent.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; + +function PageSectionContent(props) { + const { + isFetching, + isPopulated, + error, + errorMessage, + children + } = props; + + if (isFetching) { + return ( + + ); + } else if (!isFetching && !!error) { + return ( +
{errorMessage}
+ ); + } else if (isPopulated && !error) { + return ( +
{children}
+ ); + } + + return null; +} + +PageSectionContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + errorMessage: PropTypes.string.isRequired, + children: PropTypes.node.isRequired +}; + +export default PageSectionContent; diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.css b/frontend/src/Components/Page/Sidebar/Messages/Message.css new file mode 100644 index 000000000..7d53adb69 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.css @@ -0,0 +1,42 @@ +.message { + display: flex; + border-left: 3px solid $infoColor; +} + +.iconContainer, +.text { + display: flex; + justify-content: center; + flex-direction: column; + padding: 2px 0; + color: $sidebarColor; +} + +.iconContainer { + flex: 0 0 25px; + margin-left: 24px; + padding: 10px 0; +} + +.text { + margin-right: 24px; + font-size: 13px; +} + +/* Types */ + +.error { + border-left-color: $dangerColor; +} + +.info { + border-left-color: $infoColor; +} + +.success { + border-left-color: $successColor; +} + +.warning { + border-left-color: $warningColor; +} diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.js b/frontend/src/Components/Page/Sidebar/Messages/Message.js new file mode 100644 index 000000000..809eadf5f --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Message.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import styles from './Message.css'; + +function getIconName(name) { + switch (name) { + case 'ApplicationUpdate': + return icons.RESTART; + case 'Backup': + return icons.BACKUP; + case 'CheckHealth': + return icons.HEALTH; + case 'EpisodeSearch': + return icons.SEARCH; + case 'Housekeeping': + return icons.HOUSEKEEPING; + case 'RefreshSeries': + return icons.REFRESH; + case 'RssSync': + return icons.RSS; + case 'SeasonSearch': + return icons.SEARCH; + case 'SeriesSearch': + return icons.SEARCH; + case 'UpdateSceneMapping': + return icons.REFRESH; + default: + return icons.SPINNER; + } +} + +function Message(props) { + const { + name, + message, + type + } = props; + + return ( +
+
+ +
+ +
+ {message} +
+
+ ); +} + +Message.propTypes = { + name: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + type: PropTypes.string.isRequired +}; + +export default Message; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js new file mode 100644 index 000000000..06c545c27 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/MessageConnector.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { hideMessage } from 'Store/Actions/appActions'; +import Message from './Message'; + +const mapDispatchToProps = { + hideMessage +}; + +class MessageConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._hideTimeoutId = null; + this.scheduleHideMessage(props.hideAfter); + } + + componentDidUpdate() { + this.scheduleHideMessage(this.props.hideAfter); + } + + // + // Control + + scheduleHideMessage = (hideAfter) => { + if (this._hideTimeoutId) { + clearTimeout(this._hideTimeoutId); + } + + if (hideAfter) { + this._hideTimeoutId = setTimeout(this.hideMessage, hideAfter * 1000); + } + } + + hideMessage = () => { + this.props.hideMessage({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MessageConnector.propTypes = { + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + hideAfter: PropTypes.number.isRequired, + hideMessage: PropTypes.func.isRequired +}; + +MessageConnector.defaultProps = { + // Hide messages after 60 seconds if there is no activity + // hideAfter: 60 +}; + +export default connect(undefined, mapDispatchToProps)(MessageConnector); diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.css b/frontend/src/Components/Page/Sidebar/Messages/Messages.css new file mode 100644 index 000000000..ef01ad02c --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.css @@ -0,0 +1,11 @@ +.messages { + margin-top: auto; + margin-bottom: 20px; + padding-top: 20px; +} + +@media only screen and (max-width: $breakpointSmall) { + .messages { + margin-bottom: 0; + } +} diff --git a/frontend/src/Components/Page/Sidebar/Messages/Messages.js b/frontend/src/Components/Page/Sidebar/Messages/Messages.js new file mode 100644 index 000000000..ec8876f6e --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/Messages.js @@ -0,0 +1,27 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import MessageConnector from './MessageConnector'; +import styles from './Messages.css'; + +function Messages({ messages }) { + return ( +
+ { + messages.map((message) => { + return ( + + ); + }) + } +
+ ); +} + +Messages.propTypes = { + messages: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Messages; diff --git a/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js b/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js new file mode 100644 index 000000000..5d20d9194 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/Messages/MessagesConnector.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import Messages from './Messages'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app.messages.items, + (messages) => { + return { + messages: messages.slice().reverse() + }; + } + ); +} + +export default connect(createMapStateToProps)(Messages); diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.css b/frontend/src/Components/Page/Sidebar/PageSidebar.css new file mode 100644 index 000000000..293d4ae7f --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.css @@ -0,0 +1,34 @@ +.sidebarContainer { + flex: 0 0 $sidebarWidth; + overflow: hidden; + width: $sidebarWidth; + background-color: $sidebarBackgroundColor; + transition: transform 300ms ease-in-out; + transform: translateX(0); +} + +.sidebar { + display: flex; + flex-direction: column; + overflow: hidden; + background-color: $sidebarBackgroundColor; + color: $white; +} + +@media only screen and (max-width: $breakpointSmall) { + .sidebarContainer { + position: fixed; + top: 0; + z-index: 1; + height: 100vh; + } + + .sidebar { + position: fixed; + z-index: 1; + overflow-y: auto; + width: 100%; + height: 100%; + } +} + diff --git a/frontend/src/Components/Page/Sidebar/PageSidebar.js b/frontend/src/Components/Page/Sidebar/PageSidebar.js new file mode 100644 index 000000000..91d95a493 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebar.js @@ -0,0 +1,501 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import locationShape from 'Helpers/Props/Shapes/locationShape'; +import dimensions from 'Styles/Variables/dimensions'; +import OverlayScroller from 'Components/Scroller/OverlayScroller'; +import Scroller from 'Components/Scroller/Scroller'; +import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector'; +import HealthStatusConnector from 'System/Status/Health/HealthStatusConnector'; +import MessagesConnector from './Messages/MessagesConnector'; +import PageSidebarItem from './PageSidebarItem'; +import styles from './PageSidebar.css'; + +const HEADER_HEIGHT = parseInt(dimensions.headerHeight); +const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth); + +const links = [ + { + iconName: icons.SERIES_CONTINUING, + title: 'Artist', + to: '/', + alias: '/series', + children: [ + { + title: 'Add New', + to: '/add/new' + }, + { + title: 'Import', + to: '/add/import' + }, + { + title: 'Mass Editor', + to: '/serieseditor' + }, + { + title: 'Album Studio', + to: '/seasonpass' + } + ] + }, + + { + iconName: icons.CALENDAR, + title: 'Calendar', + to: '/calendar' + }, + + { + iconName: icons.ACTIVITY, + title: 'Activity', + to: '/activity/queue', + children: [ + { + title: 'Queue', + to: '/activity/queue', + statusComponent: QueueStatusConnector + }, + { + title: 'History', + to: '/activity/history' + }, + { + title: 'Blacklist', + to: '/activity/blacklist' + } + ] + }, + + { + iconName: icons.WARNING, + title: 'Wanted', + to: '/wanted/missing', + children: [ + { + title: 'Missing', + to: '/wanted/missing' + }, + { + title: 'Cutoff Unmet', + to: '/wanted/cutoffunmet' + } + ] + }, + + { + iconName: icons.SETTINGS, + title: 'Settings', + to: '/settings/ui', + children: [ + { + title: 'UI', + to: '/settings/ui' + }, + { + title: 'Media Management', + to: '/settings/mediamanagement' + }, + { + title: 'Profiles', + to: '/settings/profiles' + }, + { + title: 'Quality', + to: '/settings/quality' + }, + { + title: 'Indexers', + to: '/settings/indexers' + }, + { + title: 'Download Clients', + to: '/settings/downloadclients' + }, + { + title: 'Connect', + to: '/settings/connect' + }, + { + title: 'Metadata', + to: '/settings/metadata' + }, + { + title: 'General', + to: '/settings/general' + } + ] + }, + + { + iconName: icons.SYSTEM, + title: 'System', + to: '/system/status', + children: [ + { + title: 'Status', + to: '/system/status', + statusComponent: HealthStatusConnector + }, + { + title: 'Tasks', + to: '/system/tasks' + }, + { + title: 'Backup', + to: '/system/backup' + }, + { + title: 'Updates', + to: '/system/updates' + }, + { + title: 'Events', + to: '/system/events' + }, + { + title: 'Log Files', + to: '/system/logs/files' + } + ] + } +]; + +function getActiveParent(pathname) { + let activeParent = links[0].to; + + links.forEach((link) => { + if (link.to && link.to === pathname) { + activeParent = link.to; + + return false; + } + + const children = link.children; + + if (children) { + children.forEach((childLink) => { + if (pathname.startsWith(childLink.to)) { + activeParent = link.to; + + return false; + } + }); + } + + if ( + (link.to !== '/' && pathname.startsWith(link.to)) || + (link.alias && pathname.startsWith(link.alias)) + ) { + activeParent = link.to; + + return false; + } + }); + + return activeParent; +} + +function hasActiveChildLink(link, pathname) { + const children = link.children; + + if (!children || !children.length) { + return false; + } + + return _.some(children, (child) => { + return child.to === pathname; + }); +} + +function getPositioning() { + const windowScroll = window.scrollY == null ? document.documentElement.scrollTop : window.scrollY; + const top = Math.max(HEADER_HEIGHT - windowScroll, 0); + const height = window.innerHeight - top; + + return { + top: `${top}px`, + height: `${height}px` + }; +} + +class PageSidebar extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._touchStartX = null; + this._touchStartY = null; + this._sidebarRef = null; + + this.state = { + top: dimensions.headerHeight, + height: `${window.innerHeight - HEADER_HEIGHT}px`, + transition: null, + transform: props.isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1 + }; + } + + componentDidMount() { + if (this.props.isSmallScreen) { + window.addEventListener('click', this.onWindowClick, { capture: true }); + window.addEventListener('scroll', this.onWindowScroll); + window.addEventListener('touchstart', this.onTouchStart); + window.addEventListener('touchend', this.onTouchEnd); + window.addEventListener('touchcancel', this.onTouchCancel); + window.addEventListener('touchmove', this.onTouchMove); + } + } + + componentDidUpdate(prevProps) { + const { + isSidebarVisible + } = this.props; + + const transform = this.state.transform; + + if (prevProps.isSidebarVisible !== isSidebarVisible) { + this._setSidebarTransform(isSidebarVisible); + } else if (transform === 0 && !isSidebarVisible) { + this.props.onSidebarVisibleChange(true); + } else if (transform === -SIDEBAR_WIDTH && isSidebarVisible) { + this.props.onSidebarVisibleChange(false); + } + } + + componentWillUnmount() { + if (this.props.isSmallScreen) { + window.removeEventListener('click', this.onWindowClick, { capture: true }); + window.removeEventListener('scroll', this.onWindowScroll); + window.removeEventListener('touchstart', this.onTouchStart); + window.removeEventListener('touchend', this.onTouchEnd); + window.removeEventListener('touchcancel', this.onTouchCancel); + window.removeEventListener('touchmove', this.onTouchMove); + } + } + + // + // Control + + _setSidebarRef = (ref) => { + this._sidebarRef = ref; + } + + _setSidebarTransform(isSidebarVisible, transition, callback) { + this.setState({ + transition, + transform: isSidebarVisible ? 0 : SIDEBAR_WIDTH * -1 + }, callback); + } + + // + // Listeners + + onWindowClick = (event) => { + const sidebar = ReactDOM.findDOMNode(this._sidebarRef); + const toggleButton = document.getElementById('sidebar-toggle-button'); + + if (!sidebar) { + return; + } + + if ( + !sidebar.contains(event.target) && + !toggleButton.contains(event.target) && + this.props.isSidebarVisible + ) { + event.preventDefault(); + event.stopPropagation(); + this.props.onSidebarVisibleChange(false); + } + } + + onWindowScroll = () => { + this.setState(getPositioning()); + } + + onTouchStart = (event) => { + const touches = event.touches; + const touchStart = touches[0].pageX; + const isSidebarVisible = this.props.isSidebarVisible; + + if (touches.length !== 1) { + return; + } + + if (isSidebarVisible && (touchStart > 210 || touchStart < 50)) { + return; + } else if (!isSidebarVisible && touchStart > 50) { + return; + } + + this._touchStartX = touchStart; + } + + onTouchEnd = (event) => { + const touches = event.changedTouches; + const currentTouch = touches[0].pageX; + + if (!this._touchStartX) { + return; + } + + if (currentTouch > this._touchStartX && currentTouch > 50) { + this._setSidebarTransform(true, 'none'); + } else if (currentTouch < this._touchStartX && currentTouch < 80) { + this._setSidebarTransform(false, 'transform 50ms ease-in-out'); + } else { + this._setSidebarTransform(this.props.isSidebarVisible); + } + + this._touchStartX = null; + } + + onTouchCancel = (event) => { + this._touchStartX = null; + } + + onTouchMove = (event) => { + const touches = event.touches; + const currentTouchX = touches[0].pageX; + const currentTouchY = touches[0].pageY; + + if (!this._touchStartX) { + return; + } + + if (Math.abs(this._touchStartY - currentTouchY) > 20) { + return; + } + + const transform = Math.min(currentTouchX - SIDEBAR_WIDTH, 0); + + this.setState({ + transition: 'none', + transform + }); + } + + onItemPress = () => { + this.props.onSidebarVisibleChange(false); + } + + // + // Render + + render() { + const { + location, + isSmallScreen + } = this.props; + + const { + top, + height, + transition, + transform + } = this.state; + + const urlBase = window.Sonarr.urlBase; + const pathname = urlBase ? location.pathname.substr(urlBase.length) || '/' : location.pathname; + const activeParent = getActiveParent(pathname); + + let containerStyle = {}; + let sidebarStyle = {}; + + if (isSmallScreen) { + containerStyle = { + transition, + transform: `translateX(${transform}px)` + }; + + sidebarStyle = { + top, + height + }; + } + + const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller; + + return ( +
+ +
+ { + links.map((link) => { + const childWithStatusComponent = _.find(link.children, (child) => { + return !!child.statusComponent; + }); + + const childStatusComponent = childWithStatusComponent ? + childWithStatusComponent.statusComponent : + null; + + const isActiveParent = activeParent === link.to; + const hasActiveChild = hasActiveChildLink(link, pathname); + + return ( + + { + link.children && link.to === activeParent && + link.children.map((child) => { + return ( + + ); + }) + } + + ); + }) + } +
+ + +
+
+ ); + } +} + +PageSidebar.propTypes = { + location: locationShape.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + isSidebarVisible: PropTypes.bool.isRequired, + onSidebarVisibleChange: PropTypes.func.isRequired +}; + +export default PageSidebar; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.css b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css new file mode 100644 index 000000000..450161705 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.css @@ -0,0 +1,48 @@ +.item { + border-left: 3px solid transparent; + color: $sidebarColor; + transition: border-left 0.3s ease-in-out; +} + +.isActiveItem { + border-left: 3px solid $themeBlue; +} + +.link { + display: block; + padding: 12px 24px; + color: $sidebarColor; + + &:hover, + &:focus { + color: $themeBlue; + text-decoration: none; + } +} + +.childLink { + composes: link; + + padding: 10px 24px; +} + +.isActiveLink { + color: $themeBlue; +} + +.isActiveParentLink { + background-color: $sidebarActiveBackgroundColor; +} + +.iconContainer { + display: inline-block; + width: 25px; +} + +.noIcon { + margin-left: 25px; +} + +.status { + float: right; +} diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarItem.js b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js new file mode 100644 index 000000000..23c6ccaf3 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarItem.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { map } from 'Helpers/elementChildren'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './PageSidebarItem.css'; + +class PageSidebarItem extends Component { + + // + // Listeners + + onPress = () => { + const { + isChildItem, + isParentItem, + onPress + } = this.props; + + if (isChildItem || !isParentItem) { + onPress(); + } + } + + // + // Render + + render() { + const { + iconName, + title, + to, + isActive, + isActiveParent, + isChildItem, + statusComponent: StatusComponent, + children + } = this.props; + + return ( +
+ + { + !!iconName && + + + + } + + + {title} + + + { + !!StatusComponent && + + + + } + + + { + children && + map(children, (child) => { + return React.cloneElement(child, { isChildItem: true }); + }) + } +
+ ); + } +} + +PageSidebarItem.propTypes = { + iconName: PropTypes.string, + title: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + isActive: PropTypes.bool, + isActiveParent: PropTypes.bool, + isParentItem: PropTypes.bool.isRequired, + isChildItem: PropTypes.bool.isRequired, + statusComponent: PropTypes.func, + children: PropTypes.node, + onPress: PropTypes.func +}; + +PageSidebarItem.defaultProps = { + isChildItem: false +}; + +export default PageSidebarItem; diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css new file mode 100644 index 000000000..4dd0cc678 --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.css @@ -0,0 +1,3 @@ +.status { + composes: label from 'Components/Label.css'; +} diff --git a/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js new file mode 100644 index 000000000..c1ea615ed --- /dev/null +++ b/frontend/src/Components/Page/Sidebar/PageSidebarStatus.js @@ -0,0 +1,35 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function PageSidebarStatus({ count, errors, warnings }) { + if (!count) { + return null; + } + + let kind = kinds.INFO; + + if (errors) { + kind = kinds.DANGER; + } else if (warnings) { + kind = kinds.WARNING; + } + + return ( + + ); +} + +PageSidebarStatus.propTypes = { + count: PropTypes.number, + errors: PropTypes.bool, + warnings: PropTypes.bool +}; + +export default PageSidebarStatus; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbar.css b/frontend/src/Components/Page/Toolbar/PageToolbar.css new file mode 100644 index 000000000..e040bc884 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbar.css @@ -0,0 +1,16 @@ +.toolbar { + display: flex; + justify-content: space-between; + flex: 0 0 auto; + padding: 0 20px; + height: $toolbarHeight; + background-color: $toolbarBackgroundColor; + color: $toolbarColor; + line-height: 60px; +} + +@media only screen and (max-width: $breakpointSmall) { + .toolbar { + padding: 0 10px; + } +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbar.js b/frontend/src/Components/Page/Toolbar/PageToolbar.js new file mode 100644 index 000000000..728f1b0d9 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbar.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './PageToolbar.css'; + +class PageToolbar extends Component { + + // + // Render + + render() { + const { + className, + children + } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +PageToolbar.propTypes = { + className: PropTypes.string, + children: PropTypes.node.isRequired +}; + +PageToolbar.defaultProps = { + className: styles.toolbar +}; + +export default PageToolbar; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.css b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css new file mode 100644 index 000000000..e788303a2 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.css @@ -0,0 +1,28 @@ +.toolbarButton { + composes: link from 'Components/Link/Link.css'; + + width: $toolbarButtonWidth; + text-align: center; + + &:hover { + color: $toobarButtonHoverColor; + } +} + +.isDisabled { + color: $disabledColor; +} + +.labelContainer { + display: flex; + align-items: center; + justify-content: center; + min-height: 16px; +} + +.label { + padding: 0 3px; + color: $toolbarLabelColor; + font-size: $extraSmallFontSize; + line-height: calc($extraSmallFontSize + 1px); +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarButton.js b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js new file mode 100644 index 000000000..752ca15ac --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarButton.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import styles from './PageToolbarButton.css'; + +function PageToolbarButton(props) { + const { + label, + iconName, + spinningName, + isDisabled, + isSpinning, + ...otherProps + } = props; + + return ( + + + +
+
+ {label} +
+
+ + ); +} + +PageToolbarButton.propTypes = { + label: PropTypes.string.isRequired, + iconName: PropTypes.string.isRequired, + spinningName: PropTypes.string, + isSpinning: PropTypes.bool, + isDisabled: PropTypes.bool +}; + +PageToolbarButton.defaultProps = { + spinningName: icons.SPINNER, + isDisabled: false, + isSpinning: false +}; + +export default PageToolbarButton; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.css b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css new file mode 100644 index 000000000..2767c163c --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css @@ -0,0 +1,26 @@ +.sectionContainer { + display: flex; + flex: 1 1 10%; +} + +.section { + display: flex; + align-items: center; + flex-grow: 1; +} + +.left { + justify-content: flex-start; +} + +.center { + justify-content: center; +} + +.right { + justify-content: flex-end; +} + +.overflowMenuItemIcon { + margin-right: 8px; +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js new file mode 100644 index 000000000..16ff98b72 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js @@ -0,0 +1,220 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import classNames from 'classnames'; +import { forEach } from 'Helpers/elementChildren'; +import { align, icons } from 'Helpers/Props'; +import dimensions from 'Styles/Variables/dimensions'; +import SpinnerIcon from 'Components/SpinnerIcon'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; +import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton'; +import styles from './PageToolbarSection.css'; + +const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth); +const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin); +const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1; +const SEPARATOR_NAME = 'PageToolbarSeparator'; + +function calculateOverflowItems(children, isMeasured, width, collapseButtons) { + let buttonCount = 0; + let separatorCount = 0; + const validChildren = []; + + forEach(children, (child) => { + const name = child.type.name; + + if (name === SEPARATOR_NAME) { + separatorCount++; + } else { + buttonCount++; + } + + validChildren.push(child); + }); + + const buttonsWidth = buttonCount * BUTTON_WIDTH; + const separatorsWidth = separatorCount + SEPARATOR_WIDTH; + const totalWidth = buttonsWidth + separatorsWidth; + + // If the width of buttons and separators is less than + // the available width return all valid children. + + if ( + !isMeasured || + !collapseButtons || + totalWidth < width + ) { + return { + buttons: validChildren, + buttonCount, + overflowItems: [] + }; + } + + const maxButtons = Math.max(Math.floor((width - separatorsWidth) / BUTTON_WIDTH), 1); + const buttons = []; + const overflowItems = []; + let actualButtons = 0; + + // Return all buttons if only one is being pushed to the overflow menu. + if (buttonCount - 1 === maxButtons) { + return { + buttons: validChildren, + buttonCount, + overflowItems: [] + }; + } + + validChildren.forEach((child, index) => { + if (actualButtons < maxButtons) { + if (child.type.name !== SEPARATOR_NAME) { + buttons.push(child); + actualButtons++; + } + } else if (child.type.name !== SEPARATOR_NAME) { + overflowItems.push(child.props); + } + }); + + return { + buttons, + buttonCount, + overflowItems + }; +} + +class PageToolbarSection extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMeasured: false, + width: 0, + buttons: [], + overflowItems: [] + }; + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ + isMeasured: true, + width + }); + } + + // + // Render + + render() { + const { + children, + alignContent, + collapseButtons + } = this.props; + + const { + isMeasured, + width + } = this.state; + + const { + buttons, + buttonCount, + overflowItems + } = calculateOverflowItems(children, isMeasured, width, collapseButtons); + + return ( + +
+ { + isMeasured ? +
+ { + buttons.map((button) => { + return button; + }) + } + + { + !!overflowItems.length && + + + + + { + overflowItems.map((item) => { + const { + iconName, + spinningName, + label, + isDisabled, + isSpinning, + ...otherProps + } = item; + + return ( + + + {label} + + ); + }) + } + + + } +
: + null + } +
+
+ ); + } + +} + +PageToolbarSection.propTypes = { + children: PropTypes.node, + alignContent: PropTypes.oneOf([align.LEFT, align.CENTER, align.RIGHT]), + collapseButtons: PropTypes.bool.isRequired +}; + +PageToolbarSection.defaultProps = { + alignContent: align.LEFT, + collapseButtons: true +}; + +export default PageToolbarSection; diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css new file mode 100644 index 000000000..968673593 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.css @@ -0,0 +1,12 @@ +.separator { + margin: 10px $toolbarSeparatorMargin; + height: 40px; + border-right: 1px solid #e5e5e5; + opacity: 0.35; +} + +@media only screen and (max-width: $breakpointSmall) { + .separator { + margin: 10px 5px; + } +} diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js new file mode 100644 index 000000000..43cea8139 --- /dev/null +++ b/frontend/src/Components/Page/Toolbar/PageToolbarSeparator.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import styles from './PageToolbarSeparator.css'; + +class PageToolbarSeparator extends Component { + + // + // Render + + render() { + return ( +
+ ); + } + +} + +export default PageToolbarSeparator; diff --git a/frontend/src/Components/ProgressBar.css b/frontend/src/Components/ProgressBar.css new file mode 100644 index 000000000..2f0019043 --- /dev/null +++ b/frontend/src/Components/ProgressBar.css @@ -0,0 +1,93 @@ +.container { + position: relative; + overflow: hidden; + width: 100%; + border-radius: 4px; + background-color: #f5f5f5; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progressBar { + position: relative; + z-index: 1; + float: left; + width: 0; + height: 100%; + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + color: $white; + transition: width 0.6s ease; +} + +.frontTextContainer { + z-index: 1; + color: $white; +} + +.backTextContainer, +.frontTextContainer { + position: absolute; + overflow: hidden; + width: 0; + height: 100%; +} + +.backText, +.frontText { + display: flex; + align-items: center; + justify-content: center; + text-align: center; + font-size: 12px; + cursor: default; +} + +.primary { + background-color: $primaryColor; +} + +.danger { + background-color: $dangerColor; +} + +.success { + background-color: $successColor; +} + +.purple { + background-color: $purple; +} + +.warning { + background-color: $warningColor; +} + +.info { + background-color: $infoColor; +} + +.small { + height: $progressBarSmallHeight; + + .backText, + .frontText { + height: $progressBarSmallHeight; + } +} + +.medium { + height: $progressBarMediumHeight; + + .backText, + .frontText { + height: $progressBarMediumHeight; + } +} + +.large { + height: $progressBarLargeHeight; + + .backText, + .frontText { + height: $progressBarLargeHeight; + } +} diff --git a/frontend/src/Components/ProgressBar.js b/frontend/src/Components/ProgressBar.js new file mode 100644 index 000000000..23dcfc70d --- /dev/null +++ b/frontend/src/Components/ProgressBar.js @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { kinds, sizes } from 'Helpers/Props'; +import styles from './ProgressBar.css'; + +function ProgressBar(props) { + const { + className, + containerClassName, + title, + progress, + precision, + showText, + text, + kind, + size, + width + } = props; + + const progressPercent = `${progress.toFixed(precision)}%`; + const progressText = text || progressPercent; + const actualWidth = width ? `${width}px` : '100%'; + + return ( +
+ { + showText && !!width && +
+
+
+ {progressText} +
+
+
+ } + +
+ { + showText && +
+
+
+ {progressText} +
+
+
+ } +
+ ); +} + +ProgressBar.propTypes = { + className: PropTypes.string, + containerClassName: PropTypes.string, + title: PropTypes.string, + progress: PropTypes.number.isRequired, + precision: PropTypes.number.isRequired, + showText: PropTypes.bool.isRequired, + text: PropTypes.string, + kind: PropTypes.oneOf(kinds.all).isRequired, + size: PropTypes.oneOf(sizes.all).isRequired, + width: PropTypes.number +}; + +ProgressBar.defaultProps = { + className: styles.progressBar, + containerClassName: styles.container, + precision: 1, + showText: false, + kind: kinds.PRIMARY, + size: sizes.MEDIUM +}; + +export default ProgressBar; diff --git a/frontend/src/Components/Router/Switch.js b/frontend/src/Components/Router/Switch.js new file mode 100644 index 000000000..0c0004a50 --- /dev/null +++ b/frontend/src/Components/Router/Switch.js @@ -0,0 +1,44 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Switch as RouterSwitch } from 'react-router-dom'; +import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; +import { map } from 'Helpers/elementChildren'; + +class Switch extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + + { + map(children, (child) => { + const { + path: childPath, + addUrlBase = true + } = child.props; + + if (!childPath) { + return child; + } + + const path = addUrlBase ? getPathWithUrlBase(childPath) : childPath; + + return React.cloneElement(child, { path }); + }) + } + + ); + } +} + +Switch.propTypes = { + children: PropTypes.node.isRequired +}; + +export default Switch; diff --git a/frontend/src/Components/Scroller/OverlayScroller.css b/frontend/src/Components/Scroller/OverlayScroller.css new file mode 100644 index 000000000..a55eca90b --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.css @@ -0,0 +1,14 @@ +.scroller { +} + +.thumb { + min-height: 50px; + border: 1px solid transparent; + border-radius: 5px; + background-color: $scrollbarBackgroundColor; + background-clip: padding-box; + + &:hover { + background-color: $scrollbarHoverBackgroundColor; + } +} diff --git a/frontend/src/Components/Scroller/OverlayScroller.js b/frontend/src/Components/Scroller/OverlayScroller.js new file mode 100644 index 000000000..9cc9edec0 --- /dev/null +++ b/frontend/src/Components/Scroller/OverlayScroller.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Scrollbars } from 'react-custom-scrollbars'; +import { scrollDirections } from 'Helpers/Props'; +import styles from './OverlayScroller.css'; + +class OverlayScroller extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scroller = null; + } + + componentDidUpdate(prevProps) { + const { + scrollTop + } = this.props; + + if (scrollTop != null && scrollTop !== prevProps.scrollTop) { + this._scroller.scrollTop(scrollTop); + } + } + + // + // Control + + _setScrollRef = (ref) => { + this._scroller = ref; + } + + _renderThumb = (props) => { + return ( +
+ ); + } + + _renderView = (props) => { + return ( +
+ ); + } + + // + // Listers + + onScroll = (event) => { + const { + scrollTop, + scrollLeft + } = event.currentTarget; + + const onScroll = this.props.onScroll; + + if (onScroll) { + onScroll({ scrollTop, scrollLeft }); + } + } + + // + // Render + + render() { + const { + autoHide, + autoScroll, + children + } = this.props; + + return ( + + {children} + + ); + } + +} + +OverlayScroller.propTypes = { + className: PropTypes.string, + trackClassName: PropTypes.string, + scrollTop: PropTypes.number, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, + autoHide: PropTypes.bool.isRequired, + autoScroll: PropTypes.bool.isRequired, + children: PropTypes.node, + onScroll: PropTypes.func +}; + +OverlayScroller.defaultProps = { + className: styles.scroller, + trackClassName: styles.thumb, + scrollDirection: scrollDirections.VERTICAL, + autoHide: false, + autoScroll: true +}; + +export default OverlayScroller; diff --git a/frontend/src/Components/Scroller/Scroller.css b/frontend/src/Components/Scroller/Scroller.css new file mode 100644 index 000000000..61005a527 --- /dev/null +++ b/frontend/src/Components/Scroller/Scroller.css @@ -0,0 +1,28 @@ +.scroller { + composes: scrollbar from 'Styles/Mixins/scroller.css'; + composes: scrollbarTrack from 'Styles/Mixins/scroller.css'; + composes: scrollbarThumb from 'Styles/Mixins/scroller.css'; +} + +.none { + overflow-x: hidden; + overflow-y: hidden; +} + +.vertical { + overflow-x: hidden; + overflow-y: scroll; + + &.autoScroll { + overflow-y: auto; + } +} + +.horizontal { + overflow-x: scroll; + overflow-y: hidden; + + &.autoScroll { + overflow-x: auto; + } +} diff --git a/frontend/src/Components/Scroller/Scroller.js b/frontend/src/Components/Scroller/Scroller.js new file mode 100644 index 000000000..701ac0cf4 --- /dev/null +++ b/frontend/src/Components/Scroller/Scroller.js @@ -0,0 +1,81 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { scrollDirections } from 'Helpers/Props'; +import styles from './Scroller.css'; + +class Scroller extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._scroller = null; + } + + componentDidMount() { + const { + scrollTop + } = this.props; + + if (this.props.scrollTop != null) { + this._scroller.scrollTop = scrollTop; + } + } + + // + // Control + + _setScrollerRef = (ref) => { + this._scroller = ref; + } + + // + // Render + + render() { + const { + className, + scrollDirection, + autoScroll, + children, + scrollTop, + onScroll, + ...otherProps + } = this.props; + + return ( +
+ {children} +
+ ); + } + +} + +Scroller.propTypes = { + className: PropTypes.string, + scrollDirection: PropTypes.oneOf([scrollDirections.NONE, scrollDirections.HORIZONTAL, scrollDirections.VERTICAL]).isRequired, + autoScroll: PropTypes.bool.isRequired, + scrollTop: PropTypes.number, + children: PropTypes.node, + onScroll: PropTypes.func +}; + +Scroller.defaultProps = { + scrollDirection: scrollDirections.VERTICAL, + autoScroll: true +}; + +export default Scroller; diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js new file mode 100644 index 000000000..06cece1da --- /dev/null +++ b/frontend/src/Components/SignalRConnector.js @@ -0,0 +1,326 @@ +import $ from 'jquery'; +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateCommand, finishCommand } from 'Store/Actions/commandActions'; +import { setAppValue, setVersion } from 'Store/Actions/appActions'; +import { update, updateItem, removeItem } from 'Store/Actions/baseActions'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions'; +require('signalR'); + +function getState(status) { + switch (status) { + case 0: + return 'connecting'; + case 1: + return 'connected'; + case 2: + return 'reconnecting'; + case 4: + return 'disconnected'; + default: + throw new Error(`invalid status ${status}`); + } +} + +function createMapStateToProps() { + return createSelector( + (state) => state.app.isReconnecting, + (state) => state.queue.paged.isPopulated, + (isReconnecting, isQueuePopulated) => { + return { + isReconnecting, + isQueuePopulated + }; + } + ); +} + +const mapDispatchToProps = { + updateCommand, + finishCommand, + setAppValue, + setVersion, + update, + updateItem, + removeItem, + fetchHealth, + fetchQueue, + fetchQueueDetails +}; + +class SignalRConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.signalRconnectionOptions = { transport: ['longPolling'] }; + this.signalRconnection = null; + this.retryInterval = 5; + this.retryTimeoutId = null; + } + + componentDidMount() { + console.log('starting signalR'); + + this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey }); + + this.signalRconnection.stateChanged(this.onStateChanged); + this.signalRconnection.received(this.onReceived); + this.signalRconnection.reconnecting(this.onReconnecting); + this.signalRconnection.disconnected(this.onDisconnected); + + this.signalRconnection.start(this.signalRconnectionOptions); + } + + componentWillUnmount() { + this.signalRconnection.stop(); + this.signalRconnection = null; + } + + // + // Control + + retryConnection = () => { + this.retryTimeoutId = setTimeout(() => { + this.signalRconnection.start(this.signalRconnectionOptions); + this.retryInterval = Math.min(this.retryInterval + 5, 30); + }, this.retryInterval * 1000); + } + + handleMessage = (message) => { + const { + name, + body + } = message; + + if (name === 'calendar') { + this.handleCalendar(body); + return; + } + + if (name === 'command') { + this.handleCommand(body); + return; + } + + if (name === 'episode') { + this.handleEpisode(body); + return; + } + + if (name === 'episodefile') { + this.handleEpisodeFile(body); + return; + } + + if (name === 'health') { + this.handleHealth(body); + return; + } + + if (name === 'series') { + this.handleSeries(body); + return; + } + + if (name === 'queue') { + this.handleQueue(body); + return; + } + + if (name === 'queue/details') { + this.handleQueueDetails(body); + return; + } + + if (name === 'queue/status') { + this.handleQueueStatus(body); + return; + } + + if (name === 'version') { + this.handleVersion(body); + return; + } + + if (name === 'wanted/cutoff') { + this.handleWantedCutoff(body); + return; + } + + if (name === 'wanted/missing') { + this.handleWantedMissing(body); + return; + } + } + + handleCalendar = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'calendar', + updateOnly: true, + ...body.resource }); + } + } + + handleCommand = (body) => { + const resource = body.resource; + const state = resource.state; + + if (state === 'completed') { + this.props.finishCommand(resource); + } else { + this.props.updateCommand(resource); + } + } + + handleEpisode = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'episodes', + updateOnly: true, + ...body.resource }); + } + } + + handleEpisodeFile = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'episodeFiles', + ...body.resource }); + } + } + + handleHealth = (body) => { + this.props.fetchHealth(); + } + + handleSeries = (body) => { + const action = body.action; + const section = 'series'; + + if (action === 'updated') { + this.props.updateItem({ section, ...body.resource }); + } else if (action === 'deleted') { + this.props.removeItem({ section, id: body.resource.id }); + } + } + + handleQueue = (body) => { + if (this.props.isQueuePopulated) { + this.props.fetchQueue(); + } + } + + handleQueueDetails = (body) => { + this.props.fetchQueueDetails(); + } + + handleQueueStatus = (body) => { + this.props.update({ section: 'queueStatus', data: body.resource }); + } + + handleVersion = (body) => { + const version = body.version; + + this.props.setVersion({ version }); + } + + handleWantedCutoff = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'cutoffUnmet', + updateOnly: true, + ...body.resource }); + } + } + + handleWantedMissing = (body) => { + if (body.action === 'updated') { + this.props.updateItem({ + section: 'missing', + updateOnly: true, + ...body.resource }); + } + } + + // + // Listeners + + onStateChanged = (change) => { + const state = getState(change.newState); + console.log(`SignalR: [${state}]`); + + if (state === 'connected') { + this.props.setAppValue({ + isConnected: true, + isReconnecting: false, + isDisconnected: false + }); + + this.retryInterval = 5; + + if (this.retryTimeoutId) { + clearTimeout(this.retryTimeoutId); + } + } + } + + onReceived = (message) => { + console.debug('SignalR: received', message.name, message.body); + + this.handleMessage(message); + } + + onReconnecting = () => { + if (window.Sonarr.unloading) { + return; + } + + this.props.setAppValue({ + isReconnecting: true + }); + } + + onDisconnected = () => { + if (this.props.isReconnecting) { + this.props.setAppValue({ + isConnected: false, + isReconnecting: true, + isDisconnected: true + }); + + this.retryConnection(); + } + } + + // + // Render + + render() { + return null; + } +} + +SignalRConnector.propTypes = { + isReconnecting: PropTypes.bool.isRequired, + isQueuePopulated: PropTypes.bool.isRequired, + updateCommand: PropTypes.func.isRequired, + finishCommand: PropTypes.func.isRequired, + setAppValue: PropTypes.func.isRequired, + setVersion: PropTypes.func.isRequired, + update: PropTypes.func.isRequired, + updateItem: PropTypes.func.isRequired, + removeItem: PropTypes.func.isRequired, + fetchHealth: PropTypes.func.isRequired, + fetchQueue: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector); diff --git a/frontend/src/Components/SpinnerIcon.js b/frontend/src/Components/SpinnerIcon.js new file mode 100644 index 000000000..4c5cbb700 --- /dev/null +++ b/frontend/src/Components/SpinnerIcon.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from './Icon'; + +function SpinnerIcon(props) { + const { + name, + spinningName, + isSpinning, + ...otherProps + } = props; + + return ( + + ); +} + +SpinnerIcon.propTypes = { + name: PropTypes.string.isRequired, + spinningName: PropTypes.string.isRequired, + isSpinning: PropTypes.bool.isRequired +}; + +SpinnerIcon.defaultProps = { + spinningName: icons.SPINNER +}; + +export default SpinnerIcon; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.css b/frontend/src/Components/Table/Cells/RelativeDateCell.css new file mode 100644 index 000000000..7be20ce5d --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.css @@ -0,0 +1,5 @@ +.cell { + composes: cell from './TableRowCell.css'; + + width: 180px; +} diff --git a/frontend/src/Components/Table/Cells/RelativeDateCell.js b/frontend/src/Components/Table/Cells/RelativeDateCell.js new file mode 100644 index 000000000..874ae4aca --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCell.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import getRelativeDate from 'Utilities/Date/getRelativeDate'; +import TableRowCell from './TableRowCell'; +import styles from './relativeDateCell.css'; + +function RelativeDateCell(props) { + const { + className, + date, + includeSeconds, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + component: Component, + dispatch, + ...otherProps + } = props; + + if (!date) { + return ( + + ); + } + + return ( + + {getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })} + + ); +} + +RelativeDateCell.propTypes = { + className: PropTypes.string.isRequired, + date: PropTypes.string, + includeSeconds: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + component: PropTypes.func, + dispatch: PropTypes.func +}; + +RelativeDateCell.defaultProps = { + className: styles.cell, + includeSeconds: false, + component: TableRowCell +}; + +export default RelativeDateCell; diff --git a/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js new file mode 100644 index 000000000..ed996abbe --- /dev/null +++ b/frontend/src/Components/Table/Cells/RelativeDateCellConnector.js @@ -0,0 +1,21 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import RelativeDateCell from './RelativeDateCell'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'showRelativeDates', + 'shortDateFormat', + 'longDateFormat', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps, null)(RelativeDateCell); diff --git a/frontend/src/Components/Table/Cells/TableRowCell.css b/frontend/src/Components/Table/Cells/TableRowCell.css new file mode 100644 index 000000000..1c3e6fc5a --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.css @@ -0,0 +1,11 @@ +.cell { + padding: 8px; + border-top: 1px solid #eee; + line-height: 1.52857143; +} + +@media only screen and (max-width: $breakpointSmall) { + .cell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/Cells/TableRowCell.js b/frontend/src/Components/Table/Cells/TableRowCell.js new file mode 100644 index 000000000..f66bbf3aa --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCell.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './TableRowCell.css'; + +class TableRowCell extends Component { + + // + // Render + + render() { + const { + className, + children, + ...otherProps + } = this.props; + + return ( + + {children} + + ); + } +} + +TableRowCell.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) +}; + +TableRowCell.defaultProps = { + className: styles.cell +}; + +export default TableRowCell; diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.css b/frontend/src/Components/Table/Cells/TableRowCellButton.css new file mode 100644 index 000000000..f01e7cba6 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.css @@ -0,0 +1,4 @@ +.cell { + composes: cell from './TableRowCell.css'; + composes: link from 'Components/Link/Link.css'; +} diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js new file mode 100644 index 000000000..ff50d3bc9 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableRowCellButton.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Link from 'Components/Link/Link'; +import TableRowCell from './TableRowCell'; +import styles from './TableRowCellButton.css'; + +function TableRowCellButton({ className, ...otherProps }) { + return ( + + ); +} + +TableRowCellButton.propTypes = { + className: PropTypes.string.isRequired +}; + +TableRowCellButton.defaultProps = { + className: styles.cell +}; + +export default TableRowCellButton; diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.css b/frontend/src/Components/Table/Cells/TableSelectCell.css new file mode 100644 index 000000000..21ab944d7 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableSelectCell.css @@ -0,0 +1,11 @@ +.selectCell { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 30px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/Cells/TableSelectCell.js b/frontend/src/Components/Table/Cells/TableSelectCell.js new file mode 100644 index 000000000..b82cf9168 --- /dev/null +++ b/frontend/src/Components/Table/Cells/TableSelectCell.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import TableRowCell from './TableRowCell'; +import styles from './TableSelectCell.css'; + +class TableSelectCell extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + id, + isSelected, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value: isSelected }); + } + + // + // Listeners + + onChange = ({ value, shiftKey }, a, b, c, d) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + className, + id, + isSelected, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +TableSelectCell.propTypes = { + className: PropTypes.string.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + isSelected: PropTypes.bool.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +TableSelectCell.defaultProps = { + className: styles.selectCell, + isSelected: false +}; + +export default TableSelectCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.css b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css new file mode 100644 index 000000000..90edf0285 --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.css @@ -0,0 +1,14 @@ +.cell { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + composes: truncate from 'Styles/Mixins/truncate.css'; + + flex-grow: 0; + flex-shrink: 1; + white-space: nowrap; +} + +@media only screen and (max-width: $breakpointSmall) { + .cell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/Cells/VirtualTableRowCell.js b/frontend/src/Components/Table/Cells/VirtualTableRowCell.js new file mode 100644 index 000000000..42999216f --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableRowCell.js @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableRowCell.css'; + +function VirtualTableRowCell(props) { + const { + className, + children + } = props; + + return ( +
+ {children} +
+ ); +} + +VirtualTableRowCell.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.oneOfType([PropTypes.string, PropTypes.node]) +}; + +VirtualTableRowCell.defaultProps = { + className: styles.cell +}; + +export default VirtualTableRowCell; diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css new file mode 100644 index 000000000..e1016aa8a --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.css @@ -0,0 +1,11 @@ +.cell { + composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css'; + + flex: 0 0 36px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js new file mode 100644 index 000000000..a773aab58 --- /dev/null +++ b/frontend/src/Components/Table/Cells/VirtualTableSelectCell.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableRowCell from './VirtualTableRowCell'; +import styles from './VirtualTableSelectCell.css'; + +export function virtualTableSelectCellRenderer(cellProps) { + const { + cellKey, + rowData, + columnData, + ...otherProps + } = cellProps; + + return ( + + ); +} + +class VirtualTableSelectCell extends Component { + + // + // Listeners + + onChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + inputClassName, + id, + isSelected, + isDisabled, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +VirtualTableSelectCell.propTypes = { + inputClassName: PropTypes.string.isRequired, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, + isSelected: PropTypes.bool.isRequired, + isDisabled: PropTypes.bool.isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +VirtualTableSelectCell.defaultProps = { + inputClassName: styles.input, + isSelected: false +}; + +export default VirtualTableSelectCell; diff --git a/frontend/src/Components/Table/Table.css b/frontend/src/Components/Table/Table.css new file mode 100644 index 000000000..46d49826a --- /dev/null +++ b/frontend/src/Components/Table/Table.css @@ -0,0 +1,16 @@ +.tableContainer { + overflow-x: auto; +} + +.table { + max-width: 100%; + width: 100%; + border-collapse: collapse; +} + +@media only screen and (max-width: $breakpointSmall) { + .tableContainer { + overflow-y: hidden; + width: 100%; + } +} diff --git a/frontend/src/Components/Table/Table.js b/frontend/src/Components/Table/Table.js new file mode 100644 index 000000000..f66eec49a --- /dev/null +++ b/frontend/src/Components/Table/Table.js @@ -0,0 +1,157 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, scrollDirections } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import Scroller from 'Components/Scroller/Scroller'; +import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal'; +import TableHeader from './TableHeader'; +import TableHeaderCell from './TableHeaderCell'; +import TableSelectAllHeaderCell from './TableSelectAllHeaderCell'; +import styles from './Table.css'; + +const tableHeaderCellProps = [ + 'sortKey', + 'sortDirection' +]; + +function getTableHeaderCellProps(props) { + return _.reduce(tableHeaderCellProps, (result, key) => { + if (props.hasOwnProperty(key)) { + result[key] = props[key]; + } + + return result; + }, {}); +} + +class Table extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isTableOptionsModalOpen: false + }; + } + + // + // Listeners + + onTableOptionsPress = () => { + this.setState({ isTableOptionsModalOpen: true }); + } + + onTableOptionsModalClose = () => { + this.setState({ isTableOptionsModalOpen: false }); + } + + // + // Render + + render() { + const { + className, + selectAll, + columns, + pageSize, + canModifyColumns, + children, + onSortPress, + onTableOptionChange, + ...otherProps + } = this.props; + + return ( + + + + { + selectAll && + + } + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if ((name === 'actions' || name === 'details') && onTableOptionChange) { + return ( + + + + ); + } + + return ( + + {column.label} + + ); + }) + } + + { + !!onTableOptionChange && + + } + + + {children} +
+
+ ); + } +} + +Table.propTypes = { + className: PropTypes.string, + selectAll: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + pageSize: PropTypes.number, + canModifyColumns: PropTypes.bool, + children: PropTypes.node, + onSortPress: PropTypes.func, + onTableOptionChange: PropTypes.func +}; + +Table.defaultProps = { + className: styles.table, + selectAll: false +}; + +export default Table; diff --git a/frontend/src/Components/Table/TableBody.js b/frontend/src/Components/Table/TableBody.js new file mode 100644 index 000000000..5cc60d6f4 --- /dev/null +++ b/frontend/src/Components/Table/TableBody.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class TableBody extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + {children} + ); + } + +} + +TableBody.propTypes = { + children: PropTypes.node +}; + +export default TableBody; diff --git a/frontend/src/Components/Table/TableHeader.js b/frontend/src/Components/Table/TableHeader.js new file mode 100644 index 000000000..81943e919 --- /dev/null +++ b/frontend/src/Components/Table/TableHeader.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class TableHeader extends Component { + + // + // Render + + render() { + const { + children + } = this.props; + + return ( + + + {children} + + + ); + } +} + +TableHeader.propTypes = { + children: PropTypes.node +}; + +export default TableHeader; diff --git a/frontend/src/Components/Table/TableHeaderCell.css b/frontend/src/Components/Table/TableHeaderCell.css new file mode 100644 index 000000000..c2c4f58c8 --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.css @@ -0,0 +1,16 @@ +.headerCell { + padding: 8px; + border: none !important; + text-align: left; + font-weight: bold; +} + +.sortIcon { + margin-left: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .headerCell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/TableHeaderCell.js b/frontend/src/Components/Table/TableHeaderCell.js new file mode 100644 index 000000000..f1417d5e9 --- /dev/null +++ b/frontend/src/Components/Table/TableHeaderCell.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './TableHeaderCell.css'; + +class VirtualTableHeaderCell extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + fixedSortDirection + } = this.props; + + if (fixedSortDirection) { + this.props.onSortPress(name, fixedSortDirection); + } else { + this.props.onSortPress(name); + } + } + + // + // Render + + render() { + const { + className, + name, + isSortable, + isVisible, + isModifiable, + sortKey, + sortDirection, + fixedSortDirection, + children, + onSortPress, + ...otherProps + } = this.props; + + const isSorting = isSortable && sortKey === name; + const sortIcon = sortDirection === sortDirections.ASCENDING ? + icons.SORT_ASCENDING : + icons.SORT_DESCENDING; + + return ( + isSortable ? + + {children} + + { + isSorting && + + } + : + + + {children} + + ); + } +} + +VirtualTableHeaderCell.propTypes = { + className: PropTypes.string, + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + isSortable: PropTypes.bool, + isVisible: PropTypes.bool, + isModifiable: PropTypes.bool, + sortKey: PropTypes.string, + fixedSortDirection: PropTypes.string, + sortDirection: PropTypes.string, + children: PropTypes.node, + onSortPress: PropTypes.func +}; + +VirtualTableHeaderCell.defaultProps = { + className: styles.headerCell, + isSortable: false +}; + +export default VirtualTableHeaderCell; diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css new file mode 100644 index 000000000..204773c3d --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.css @@ -0,0 +1,48 @@ +.column { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.label { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} + +.notDragable { + padding: 4px 0; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js new file mode 100644 index 000000000..6a8e345f8 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumn.js @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './TableOptionsColumn.css'; + +function TableOptionsColumn(props) { + const { + name, + label, + isVisible, + isModifiable, + isDragging, + connectDragSource, + onVisibleChange + } = props; + + return ( +
+
+ + + { + !!connectDragSource && + connectDragSource( +
+ +
+ ) + } +
+
+ ); +} + +TableOptionsColumn.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isVisible: PropTypes.bool.isRequired, + isModifiable: PropTypes.bool.isRequired, + index: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + connectDragSource: PropTypes.func, + onVisibleChange: PropTypes.func.isRequired +}; + +export default TableOptionsColumn; diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js new file mode 100644 index 000000000..03169f00c --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragPreview.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { TABLE_COLUMN } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import TableOptionsColumn from './TableOptionsColumn'; +import styles from './TableOptionsColumnDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class TableOptionsColumnDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== TABLE_COLUMN) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + return ( + +
+ +
+
+ ); + } +} + +TableOptionsColumnDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(TableOptionsColumnDragPreview); diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css new file mode 100644 index 000000000..9354a35c0 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.css @@ -0,0 +1,18 @@ +.columnDragSource { + padding: 4px 0; +} + +.columnPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.columnPlaceholderBefore { + margin-bottom: 8px; +} + +.columnPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js new file mode 100644 index 000000000..80f03e430 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsColumnDragSource.js @@ -0,0 +1,164 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { TABLE_COLUMN } from 'Helpers/dragTypes'; +import TableOptionsColumn from './TableOptionsColumn'; +import styles from './TableOptionsColumnDragSource.css'; + +const columnDragSource = { + beginDrag(column) { + return column; + }, + + endDrag(props, monitor, component) { + props.onColumnDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const columnDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().index; + const hoverIndex = props.index; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + if (dragIndex === hoverIndex) { + return; + } + + // When moving up, only trigger if drag position is above 50% and + // when moving down, only trigger if drag position is below 50%. + // If we're moving down the hoverIndex needs to be increased + // by one so it's ordered properly. Otherwise the hoverIndex will work. + + // Dragging downwards + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + // Dragging upwards + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + props.onColumnDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class TableOptionsColumnDragSource extends Component { + + // + // Render + + render() { + const { + name, + label, + isVisible, + isModifiable, + index, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onVisibleChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +TableOptionsColumnDragSource.propTypes = { + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + isVisible: PropTypes.bool.isRequired, + isModifiable: PropTypes.bool.isRequired, + index: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onVisibleChange: PropTypes.func.isRequired, + onColumnDragMove: PropTypes.func.isRequired, + onColumnDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + TABLE_COLUMN, + columnDropTarget, + collectDropTarget +)(DragSource( + TABLE_COLUMN, + columnDragSource, + collectDragSource +)(TableOptionsColumnDragSource)); diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.css b/frontend/src/Components/Table/TableOptions/TableOptionsModal.css new file mode 100644 index 000000000..35544f32b --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.css @@ -0,0 +1,5 @@ +.columns { + margin-top: 10px; + width: 100%; + user-select: none; +} diff --git a/frontend/src/Components/Table/TableOptions/TableOptionsModal.js b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js new file mode 100644 index 000000000..53d695e31 --- /dev/null +++ b/frontend/src/Components/Table/TableOptions/TableOptionsModal.js @@ -0,0 +1,242 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import { inputTypes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import TableOptionsColumn from './TableOptionsColumn'; +import TableOptionsColumnDragSource from './TableOptionsColumnDragSource'; +import TableOptionsColumnDragPreview from './TableOptionsColumnDragPreview'; +import styles from './TableOptionsModal.css'; + +class TableOptionsModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPageSize: !!props.pageSize, + pageSize: props.pageSize, + pageSizeError: null, + dragIndex: null, + dropIndex: null + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.pageSize !== this.state.pageSize) { + this.setState({ pageSize: this.props.pageSize }); + } + } + + // + // Listeners + + onPageSizeChange = ({ value }) => { + let pageSizeError = null; + + if (value < 5) { + pageSizeError = 'Page size must be at least 5'; + } else if (value > 250) { + pageSizeError = 'Page size must not exceed 250'; + } else { + this.props.onTableOptionChange({ pageSize: value }); + } + + this.setState({ + pageSize: value, + pageSizeError + }); + } + + onVisibleChange = ({ name, value }) => { + const columns = _.cloneDeep(this.props.columns); + + const column = _.find(columns, { name }); + column.isVisible = value; + + this.props.onTableOptionChange({ columns }); + } + + onColumnDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onColumnDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const columns = _.cloneDeep(this.props.columns); + const items = columns.splice(dragIndex, 1); + columns.splice(dropIndex, 0, items[0]); + + this.props.onTableOptionChange({ columns }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + const { + isOpen, + columns, + canModifyColumns, + onModalClose + } = this.props; + + const { + hasPageSize, + pageSize, + pageSizeError, + dragIndex, + dropIndex + } = this.state; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex < dragIndex; + const isDraggingDown = isDragging && dropIndex > dragIndex; + + return ( + + + + Table Options + + + +
+ { + hasPageSize && + + Page Size + + + + } + + { + canModifyColumns && + + Columns + +
+ + +
+ { + columns.map((column, index) => { + const { + name, + label, + columnLabel, + isVisible, + isModifiable + } = column; + + if (isModifiable !== false) { + return ( + + ); + } + + return ( + + ); + }) + } + + +
+
+
+ } +
+
+ + + +
+
+ ); + } +} + +TableOptionsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + pageSize: PropTypes.number, + canModifyColumns: PropTypes.bool.isRequired, + onTableOptionChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +TableOptionsModal.defaultProps = { + canModifyColumns: true +}; + +export default DragDropContext(HTML5Backend)(TableOptionsModal); diff --git a/frontend/src/Components/Table/TablePager.css b/frontend/src/Components/Table/TablePager.css new file mode 100644 index 000000000..e3fb645bd --- /dev/null +++ b/frontend/src/Components/Table/TablePager.css @@ -0,0 +1,70 @@ +.pager { + display: flex; + align-items: center; + justify-content: space-between; +} + +.loadingContainer, +.controlsContainer, +.recordsContainer { + flex: 0 1 33%; +} + +.controlsContainer { + display: flex; + justify-content: center; +} + +.recordsContainer { + display: flex; + justify-content: flex-end; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin: 0; + margin-left: 5px; + text-align: left; +} + +.controls { + display: flex; + align-items: center; + text-align: center; +} + +.pageNumber { + line-height: 30px; +} + +.pageLink { + padding: 0; + width: 30px; + height: 30px; + line-height: 30px; +} + +.records { + color: $disabledColor; +} + +.disabledPageButton { + color: $disabledColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .pager { + flex-wrap: wrap; + } + + .loadingContainer, + .recordsContainer { + flex: 0 1 50%; + } + + .controlsContainer { + flex: 0 1 100%; + order: -1; + } +} diff --git a/frontend/src/Components/Table/TablePager.js b/frontend/src/Components/Table/TablePager.js new file mode 100644 index 000000000..e3bc10be7 --- /dev/null +++ b/frontend/src/Components/Table/TablePager.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SelectInput from 'Components/Form/SelectInput'; +import styles from './TablePager.css'; + +class TablePager extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isShowingPageSelect: false + }; + } + + // + // Listeners + + onOpenPageSelectClick = () => { + this.setState({ isShowingPageSelect: true }); + } + + onPageSelect = ({ value: page }) => { + this.setState({ isShowingPageSelect: false }); + this.props.onPageSelect(parseInt(page)); + } + + // + // Render + + render() { + const { + page, + totalPages, + totalRecords, + isFetching, + onFirstPagePress, + onPreviousPagePress, + onNextPagePress, + onLastPagePress + } = this.props; + + const isShowingPageSelect = this.state.isShowingPageSelect; + const pages = Array.from(new Array(totalPages), (x, i) => { + const pageNumber = i + 1; + + return { + key: pageNumber, + value: pageNumber + }; + }); + + if (!page) { + return null; + } + + const isFirstPage = page === 1; + const isLastPage = page === totalPages; + + return ( +
+
+ { + isFetching && + + } +
+ +
+
+ + + + + + + + +
+ { + !isShowingPageSelect && + + {page} / {totalPages} + + } + + { + isShowingPageSelect && + + } +
+ + + + + + + + +
+
+ +
+
+ Total records: {totalRecords} +
+
+
+ ); + } + +} + +TablePager.propTypes = { + page: PropTypes.number, + totalPages: PropTypes.number, + totalRecords: PropTypes.number, + isFetching: PropTypes.bool, + onFirstPagePress: PropTypes.func.isRequired, + onPreviousPagePress: PropTypes.func.isRequired, + onNextPagePress: PropTypes.func.isRequired, + onLastPagePress: PropTypes.func.isRequired, + onPageSelect: PropTypes.func.isRequired +}; + +export default TablePager; diff --git a/frontend/src/Components/Table/TableRow.css b/frontend/src/Components/Table/TableRow.css new file mode 100644 index 000000000..9664733b4 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.css @@ -0,0 +1,7 @@ +.row { + transition: background-color 500ms; + + &:hover { + background-color: #fafbfc; + } +} diff --git a/frontend/src/Components/Table/TableRow.js b/frontend/src/Components/Table/TableRow.js new file mode 100644 index 000000000..06bbbaee9 --- /dev/null +++ b/frontend/src/Components/Table/TableRow.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './TableRow.css'; + +function TableRow(props) { + const { + className, + children, + ...otherProps + } = props; + + return ( + + {children} + + ); +} + +TableRow.propTypes = { + className: PropTypes.string.isRequired, + children: PropTypes.node +}; + +TableRow.defaultProps = { + className: styles.row +}; + +export default TableRow; diff --git a/frontend/src/Components/Table/TableRowButton.css b/frontend/src/Components/Table/TableRowButton.css new file mode 100644 index 000000000..70a2238ca --- /dev/null +++ b/frontend/src/Components/Table/TableRowButton.css @@ -0,0 +1,4 @@ +.row { + composes: link from 'Components/Link/Link.css'; + composes: row from './TableRow.css'; +} diff --git a/frontend/src/Components/Table/TableRowButton.js b/frontend/src/Components/Table/TableRowButton.js new file mode 100644 index 000000000..7ff679673 --- /dev/null +++ b/frontend/src/Components/Table/TableRowButton.js @@ -0,0 +1,16 @@ +import React from 'react'; +import Link from 'Components/Link/Link'; +import TableRow from './TableRow'; +import styles from './TableRowButton.css'; + +function TableRowButton(props) { + return ( + + ); +} + +export default TableRowButton; diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.css b/frontend/src/Components/Table/TableSelectAllHeaderCell.css new file mode 100644 index 000000000..6090e6e9c --- /dev/null +++ b/frontend/src/Components/Table/TableSelectAllHeaderCell.css @@ -0,0 +1,11 @@ +.selectAllHeaderCell { + composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css'; + + width: 30px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/TableSelectAllHeaderCell.js b/frontend/src/Components/Table/TableSelectAllHeaderCell.js new file mode 100644 index 000000000..c889c32ae --- /dev/null +++ b/frontend/src/Components/Table/TableSelectAllHeaderCell.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableHeaderCell from './TableHeaderCell'; +import styles from './TableSelectAllHeaderCell.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +function TableSelectAllHeaderCell(props) { + const { + allSelected, + allUnselected, + onSelectAllChange + } = props; + + const value = getValue(allSelected, allUnselected); + + return ( + + + + ); +} + +TableSelectAllHeaderCell.propTypes = { + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default TableSelectAllHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTable.css b/frontend/src/Components/Table/VirtualTable.css new file mode 100644 index 000000000..3287c5643 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTable.css @@ -0,0 +1,3 @@ +.tableContainer { + width: 100%; +} diff --git a/frontend/src/Components/Table/VirtualTable.js b/frontend/src/Components/Table/VirtualTable.js new file mode 100644 index 000000000..ecacfd82d --- /dev/null +++ b/frontend/src/Components/Table/VirtualTable.js @@ -0,0 +1,173 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import Measure from 'react-measure'; +import { WindowScroller } from 'react-virtualized'; +import { scrollDirections } from 'Helpers/Props'; +import Scroller from 'Components/Scroller/Scroller'; +import VirtualTableBody from './VirtualTableBody'; +import styles from './VirtualTable.css'; + +const ROW_HEIGHT = 38; + +function overscanIndicesGetter(options) { + const { + cellCount, + overscanCellsCount, + startIndex, + stopIndex + } = options; + + // The default getter takes the scroll direction into account, + // but that can cause issues. Ignore the scroll direction and + // always over return more items. + + const overscanStartIndex = startIndex - overscanCellsCount; + const overscanStopIndex = stopIndex + overscanCellsCount; + + return { + overscanStartIndex: Math.max(0, overscanStartIndex), + overscanStopIndex: Math.min(cellCount - 1, overscanStopIndex) + }; +} + +class VirtualTable extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + width: 0 + }; + + this._isInitialized = false; + this._table = null; + } + + componentDidMount() { + this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody); + } + + // + // Control + + rowGetter = ({ index }) => { + return this.props.items[index]; + } + + setTableRef = (ref) => { + this._table = ref; + } + + forceUpdateGrid = () => { + this._table.recomputeGridSize(); + } + + scrollToRow = (rowIndex) => { + const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20; + + // this._table.scrollToCell({ columnIndex: 0, rowIndex }); + this.props.onScroll({ scrollTop }); + } + + // + // Listeners + + onMeasure = ({ width }) => { + this.setState({ + width + }); + } + + onSectionRendered = () => { + if (!this._isInitialized && this._contentBodyNode) { + this.props.onRender(); + this._isInitialized = true; + } + } + + // + // Render + + render() { + const { + className, + items, + isSmallScreen, + header, + headerHeight, + scrollTop, + rowRenderer, + onScroll, + ...otherProps + } = this.props; + + const { + width + } = this.state; + + return ( + + + {({ height, isScrolling }) => { + return ( + + {header} + + + + ); + } + } + + + ); + } +} + +VirtualTable.propTypes = { + className: PropTypes.string.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + scrollTop: PropTypes.number.isRequired, + contentBody: PropTypes.object.isRequired, + isSmallScreen: PropTypes.bool.isRequired, + header: PropTypes.node.isRequired, + headerHeight: PropTypes.number.isRequired, + rowRenderer: PropTypes.func.isRequired, + onRender: PropTypes.func.isRequired, + onScroll: PropTypes.func.isRequired +}; + +VirtualTable.defaultProps = { + className: styles.tableContainer, + headerHeight: 38, + onRender: () => {} +}; + +export default VirtualTable; diff --git a/frontend/src/Components/Table/VirtualTableBody.css b/frontend/src/Components/Table/VirtualTableBody.css new file mode 100644 index 000000000..12768646d --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableBody.css @@ -0,0 +1,3 @@ +.tableBodyContainer { + position: relative; +} diff --git a/frontend/src/Components/Table/VirtualTableBody.js b/frontend/src/Components/Table/VirtualTableBody.js new file mode 100644 index 000000000..c73508895 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableBody.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { Grid } from 'react-virtualized'; +import styles from './VirtualTableBody.css'; + +class VirtualTableBody extends Grid { + + // + // Render + + render() { + const { + autoContainerWidth, + containerStyle + } = this.props; + + const { isScrolling } = this.state; + + const totalColumnsWidth = this._columnSizeAndPositionManager.getTotalSize(); + const totalRowsHeight = this._rowSizeAndPositionManager.getTotalSize(); + const childrenToDisplay = this._childrenToDisplay; + + if (childrenToDisplay.length > 0) { + return ( +
+
+ {childrenToDisplay} +
+
+ ); + } + + return ( +
+ ); + } +} + +export default VirtualTableBody; diff --git a/frontend/src/Components/Table/VirtualTableHeader.css b/frontend/src/Components/Table/VirtualTableHeader.css new file mode 100644 index 000000000..4b757c1f8 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeader.css @@ -0,0 +1,3 @@ +.header { + display: flex; +} diff --git a/frontend/src/Components/Table/VirtualTableHeader.js b/frontend/src/Components/Table/VirtualTableHeader.js new file mode 100644 index 000000000..cf6a0f47b --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeader.js @@ -0,0 +1,17 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableHeader.css'; + +function VirtualTableHeader({ children }) { + return ( +
+ {children} +
+ ); +} + +VirtualTableHeader.propTypes = { + children: PropTypes.node +}; + +export default VirtualTableHeader; diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.css b/frontend/src/Components/Table/VirtualTableHeaderCell.css new file mode 100644 index 000000000..c2c4f58c8 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.css @@ -0,0 +1,16 @@ +.headerCell { + padding: 8px; + border: none !important; + text-align: left; + font-weight: bold; +} + +.sortIcon { + margin-left: 10px; +} + +@media only screen and (max-width: $breakpointSmall) { + .headerCell { + white-space: nowrap; + } +} diff --git a/frontend/src/Components/Table/VirtualTableHeaderCell.js b/frontend/src/Components/Table/VirtualTableHeaderCell.js new file mode 100644 index 000000000..515eacc71 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableHeaderCell.js @@ -0,0 +1,107 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import Icon from 'Components/Icon'; +import styles from './VirtualTableHeaderCell.css'; + +export function headerRenderer(headerProps) { + const { + columnData = {}, + dataKey, + label + } = headerProps; + + return ( + + {label} + + ); +} + +class VirtualTableHeaderCell extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + fixedSortDirection + } = this.props; + + if (fixedSortDirection) { + this.props.onSortPress(name, fixedSortDirection); + } else { + this.props.onSortPress(name); + } + } + + // + // Render + + render() { + const { + className, + name, + isSortable, + sortKey, + sortDirection, + fixedSortDirection, + children, + onSortPress, + ...otherProps + } = this.props; + + const isSorting = isSortable && sortKey === name; + const sortIcon = sortDirection === sortDirections.ASCENDING ? + icons.SORT_ASCENDING : + icons.SORT_DESCENDING; + + return ( + isSortable ? + + {children} + + { + isSorting && + + } + : + +
+ {children} +
+ ); + } +} + +VirtualTableHeaderCell.propTypes = { + className: PropTypes.string, + name: PropTypes.string.isRequired, + label: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + isSortable: PropTypes.bool, + sortKey: PropTypes.string, + fixedSortDirection: PropTypes.string, + sortDirection: PropTypes.string, + children: PropTypes.node, + onSortPress: PropTypes.func +}; + +VirtualTableHeaderCell.defaultProps = { + className: styles.headerCell, + isSortable: false +}; + +export default VirtualTableHeaderCell; diff --git a/frontend/src/Components/Table/VirtualTableRow.css b/frontend/src/Components/Table/VirtualTableRow.css new file mode 100644 index 000000000..f4c825b64 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRow.css @@ -0,0 +1,14 @@ +.row { + display: flex; + transition: background-color 500ms; + + &:hover { + background-color: #fafbfc; + } +} + +@media only screen and (max-width: $breakpointMedium) { + .row { + overflow-x: visible !important; + } +} diff --git a/frontend/src/Components/Table/VirtualTableRow.js b/frontend/src/Components/Table/VirtualTableRow.js new file mode 100644 index 000000000..0a423902e --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableRow.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import styles from './VirtualTableRow.css'; + +function VirtualTableRow(props) { + const { + className, + children, + style, + ...otherProps + } = props; + + return ( +
+ {children} +
+ ); +} + +VirtualTableRow.propTypes = { + className: PropTypes.string.isRequired, + style: PropTypes.object.isRequired, + children: PropTypes.node +}; + +VirtualTableRow.defaultProps = { + className: styles.row +}; + +export default VirtualTableRow; diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css new file mode 100644 index 000000000..1f3f7fb30 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.css @@ -0,0 +1,11 @@ +.selectAllHeaderCell { + composes: headerCell from 'Components/Table/TableHeaderCell.css'; + + flex: 0 0 36px; +} + +.input { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js new file mode 100644 index 000000000..58b246763 --- /dev/null +++ b/frontend/src/Components/Table/VirtualTableSelectAllHeaderCell.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import CheckInput from 'Components/Form/CheckInput'; +import VirtualTableHeaderCell from './VirtualTableHeaderCell'; +import styles from './VirtualTableSelectAllHeaderCell.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +function VirtualTableSelectAllHeaderCell(props) { + const { + allSelected, + allUnselected, + onSelectAllChange + } = props; + + const value = getValue(allSelected, allUnselected); + + return ( + + + + ); +} + +VirtualTableSelectAllHeaderCell.propTypes = { + allSelected: PropTypes.bool.isRequired, + allUnselected: PropTypes.bool.isRequired, + onSelectAllChange: PropTypes.func.isRequired +}; + +export default VirtualTableSelectAllHeaderCell; diff --git a/frontend/src/Components/TagList.css b/frontend/src/Components/TagList.css new file mode 100644 index 000000000..c1e5567bd --- /dev/null +++ b/frontend/src/Components/TagList.css @@ -0,0 +1,3 @@ +.tags { + flex: 1 0 auto; +} diff --git a/frontend/src/Components/TagList.js b/frontend/src/Components/TagList.js new file mode 100644 index 000000000..485651bdc --- /dev/null +++ b/frontend/src/Components/TagList.js @@ -0,0 +1,38 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from './Label'; +import styles from './TagList.css'; + +function TagList({ tags, tagList }) { + return ( +
+ { + tags.map((t) => { + const tag = _.find(tagList, { id: t }); + + if (!tag) { + return null; + } + + return ( + + ); + }) + } +
+ ); +} + +TagList.propTypes = { + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default TagList; diff --git a/frontend/src/Components/TagListConnector.js b/frontend/src/Components/TagListConnector.js new file mode 100644 index 000000000..be7e618e3 --- /dev/null +++ b/frontend/src/Components/TagListConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import TagList from './TagList'; + +function createMapStateToProps() { + return createSelector( + createTagsSelector(), + (tagList) => { + return { + tagList + }; + } + ); +} + +export default connect(createMapStateToProps)(TagList); diff --git a/frontend/src/Components/Tooltip/Popover.css b/frontend/src/Components/Tooltip/Popover.css new file mode 100644 index 000000000..cb742eca0 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.css @@ -0,0 +1,104 @@ +.tether { + z-index: 2000; +} + +.popoverContainer { + margin: 10px 15px; +} + +.popover { + position: relative; + background-color: $white; + box-shadow: 0 5px 10px $popoverShadowColor; +} + +.arrow, +.arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-width: 11px; + border-style: solid; + border-color: transparent; +} + +.arrow::after { + border-width: 10px; + content: ''; +} + +.top { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: $popoverArrowBorderColor; + border-bottom-width: 0; + + &::after { + bottom: 1px; + margin-left: -10px; + border-top-color: $white; + border-bottom-width: 0; + content: ' '; + } +} + +.right { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: $popoverArrowBorderColor; + border-left-width: 0; + + &::after { + bottom: -10px; + left: 1px; + border-right-color: $white; + border-left-width: 0; + content: ' '; + } +} + +.bottom { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: $popoverArrowBorderColor; + + &::after { + top: 1px; + margin-left: -10px; + border-top-width: 0; + border-bottom-color: $white; + content: ' '; + } +} + +.left { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: $popoverArrowBorderColor; + + &::after { + right: 1px; + bottom: -10px; + border-right-width: 0; + border-left-color: $white; + content: ' '; + } +} + +.title { + padding: 10px 20px; + border-bottom: 1px solid $popoverTitleBorderColor; + background-color: $popoverTitleBackgroundColor; + font-size: 16px; +} + +.body { + padding: 20px; +} diff --git a/frontend/src/Components/Tooltip/Popover.js b/frontend/src/Components/Tooltip/Popover.js new file mode 100644 index 000000000..0cb2520e5 --- /dev/null +++ b/frontend/src/Components/Tooltip/Popover.js @@ -0,0 +1,136 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import { tooltipPositions } from 'Helpers/Props'; +import styles from './Popover.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [tooltipPositions.TOP]: { + ...baseTetherOptions, + attachment: 'bottom center', + targetAttachment: 'top center' + }, + + [tooltipPositions.RIGHT]: { + ...baseTetherOptions, + attachment: 'middle left', + targetAttachment: 'middle right' + }, + + [tooltipPositions.BOTTOM]: { + ...baseTetherOptions, + attachment: 'top center', + targetAttachment: 'bottom center' + }, + + [tooltipPositions.LEFT]: { + ...baseTetherOptions, + attachment: 'middle right', + targetAttachment: 'middle left' + } +}; + +class Popover extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isOpen: false + }; + } + + // + // Listeners + + onClick = () => { + this.setState({ isOpen: !this.state.isOpen }); + } + + onMouseEnter = () => { + this.setState({ isOpen: true }); + } + + onMouseLeave = () => { + this.setState({ isOpen: false }); + } + + // + // Render + + render() { + const { + anchor, + title, + body, + position + } = this.props; + + return ( + + + {anchor} + + + { + this.state.isOpen && +
+
+
+ +
+ {title} +
+ +
+ {body} +
+
+
+ } + + ); + } +} + +Popover.propTypes = { + anchor: PropTypes.node.isRequired, + title: PropTypes.string.isRequired, + body: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + position: PropTypes.oneOf(tooltipPositions.all) +}; + +Popover.defaultProps = { + position: tooltipPositions.TOP +}; + +export default Popover; diff --git a/frontend/src/Components/Tooltip/Tooltip.css b/frontend/src/Components/Tooltip/Tooltip.css new file mode 100644 index 000000000..d1d798e0f --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.css @@ -0,0 +1,161 @@ +.tether { + z-index: 2000; +} + +.tooltipContainer { + margin: 10px 15px; +} + +.tooltip { + position: relative; + + &.default { + background-color: $white; + box-shadow: 0 5px 10px $popoverShadowColor; + } + + &.inverse { + background-color: $themeDarkColor; + box-shadow: 0 5px 10px $popoverShadowInverseColor; + } +} + +.arrow, +.arrow::after { + position: absolute; + display: block; + width: 0; + height: 0; + border-width: 11px; + border-style: solid; + border-color: transparent; +} + +.arrow::after { + border-width: 10px; + content: ''; +} + +.top { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-bottom-width: 0; + + &::after { + bottom: 1px; + margin-left: -10px; + border-bottom-width: 0; + content: ' '; + + &.default { + border-top-color: $popoverArrowBorderColor; + } + + &.inverse { + border-top-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-top-color: $popoverArrowBorderColor; + } + + &.inverse { + border-top-color: $popoverArrowBorderInverseColor; + } +} + +.right { + top: 50%; + left: -11px; + margin-top: -11px; + border-left-width: 0; + + &::after { + bottom: -10px; + left: 1px; + border-left-width: 0; + content: ' '; + + &.default { + border-right-color: $popoverArrowBorderColor; + } + + &.inverse { + border-right-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-right-color: $popoverArrowBorderColor; + } + + &.inverse { + border-right-color: $popoverArrowBorderInverseColor; + } +} + +.bottom { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + + &::after { + top: 1px; + margin-left: -10px; + border-top-width: 0; + content: ' '; + + &.default { + border-bottom-color: $popoverArrowBorderColor; + } + + &.inverse { + border-bottom-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-bottom-color: $popoverArrowBorderColor; + } + + &.inverse { + border-bottom-color: $popoverArrowBorderInverseColor; + } +} + +.left { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + + &::after { + right: 1px; + bottom: -10px; + border-right-width: 0; + content: ' '; + + &.default { + border-left-color: $popoverArrowBorderColor; + } + + &.inverse { + border-left-color: $popoverArrowBorderInverseColor; + } + } + + &.default { + border-left-color: $popoverArrowBorderColor; + } + + &.inverse { + border-left-color: $popoverArrowBorderInverseColor; + } +} + +.body { + padding: 5px; +} diff --git a/frontend/src/Components/Tooltip/Tooltip.js b/frontend/src/Components/Tooltip/Tooltip.js new file mode 100644 index 000000000..5b32c5783 --- /dev/null +++ b/frontend/src/Components/Tooltip/Tooltip.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TetherComponent from 'react-tether'; +import classNames from 'classnames'; +import { kinds, tooltipPositions } from 'Helpers/Props'; +import styles from './Tooltip.css'; + +const baseTetherOptions = { + skipMoveElement: true, + constraints: [ + { + to: 'window', + attachment: 'together', + pin: true + } + ] +}; + +const tetherOptions = { + [tooltipPositions.TOP]: { + ...baseTetherOptions, + attachment: 'bottom center', + targetAttachment: 'top center' + }, + + [tooltipPositions.RIGHT]: { + ...baseTetherOptions, + attachment: 'middle left', + targetAttachment: 'middle right' + }, + + [tooltipPositions.BOTTOM]: { + ...baseTetherOptions, + attachment: 'top center', + targetAttachment: 'bottom center' + }, + + [tooltipPositions.LEFT]: { + ...baseTetherOptions, + attachment: 'middle right', + targetAttachment: 'middle left' + } +}; + +class Tooltip extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._closeTimeout = null; + + this.state = { + isOpen: false + }; + } + + // + // Listeners + + onClick = () => { + this.setState({ isOpen: !this.state.isOpen }); + } + + onMouseEnter = () => { + if (this._closeTimeout) { + clearTimeout(this._closeTimeout); + } + + this.setState({ isOpen: true }); + } + + onMouseLeave = () => { + this._closeTimeout = setTimeout(() => { + this.setState({ isOpen: false }); + }, 100); + } + + // + // Render + + render() { + const { + anchor, + tooltip, + kind, + position + } = this.props; + + return ( + + + {anchor} + + + { + this.state.isOpen && +
+
+
+ +
+ {tooltip} +
+
+
+ } + + ); + } +} + +Tooltip.propTypes = { + anchor: PropTypes.node.isRequired, + tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, + kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]), + position: PropTypes.oneOf(tooltipPositions.all) +}; + +Tooltip.defaultProps = { + kind: kinds.DEFAULT, + position: tooltipPositions.TOP +}; + +export default Tooltip; diff --git a/frontend/src/Components/keyboardShortcuts.js b/frontend/src/Components/keyboardShortcuts.js new file mode 100644 index 000000000..dc0f431cd --- /dev/null +++ b/frontend/src/Components/keyboardShortcuts.js @@ -0,0 +1,100 @@ +import React, { Component } from 'react'; +import Mousetrap from 'mousetrap'; +import getDisplayName from 'Helpers/getDisplayName'; + +export const shortcuts = { + OPEN_KEYBOARD_SHORTCUTS_MODAL: { + key: '?', + name: 'Open This Modal' + }, + + SERIES_SEARCH_INPUT: { + key: 's', + name: 'Focus Search Box' + }, + + SAVE_SETTINGS: { + key: 'mod+s', + name: 'Save Settings' + } +}; + +function keyboardShortcuts(WrappedComponent) { + class KeyboardShortcuts extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + this._mousetrapBindings = {}; + this._mousetrap = new Mousetrap(); + this._mousetrap.stopCallback = this.stopCallback; + } + + componentWillUnmount() { + this.unbindAllShortcuts(); + this._mousetrap = null; + } + + // + // Control + + bindShortcut = (key, callback, options = {}) => { + this._mousetrap.bind(key, callback); + this._mousetrapBindings[key] = options; + } + + unbindShortcut = (key) => { + delete this._mousetrapBindings[key]; + this._mousetrap.unbind(key); + } + + unbindAllShortcuts = () => { + const keys = Object.keys(this._mousetrapBindings); + + if (!keys.length) { + return; + } + + keys.forEach((binding) => { + this._mousetrap.unbind(binding); + }); + + this._mousetrapBindings = {}; + } + + stopCallback = (event, element, combo) => { + if (this._mousetrapBindings[combo].isGlobal) { + return false; + } + + return ( + element.tagName === 'INPUT' || + element.tagName === 'SELECT' || + element.tagName === 'TEXTAREA' || + (element.contentEditable && element.contentEditable === 'true') + ); + } + + // + // Render + + render() { + return ( + + ); + } + } + + KeyboardShortcuts.displayName = `KeyboardShortcut(${getDisplayName(WrappedComponent)})`; + KeyboardShortcuts.WrappedComponent = WrappedComponent; + + return KeyboardShortcuts; +} + +export default keyboardShortcuts; diff --git a/frontend/src/Components/withScrollPosition.js b/frontend/src/Components/withScrollPosition.js new file mode 100644 index 000000000..110da9ab2 --- /dev/null +++ b/frontend/src/Components/withScrollPosition.js @@ -0,0 +1,30 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import scrollPositions from 'Store/scrollPositions'; + +function withScrollPosition(WrappedComponent, scrollPositionKey) { + function ScrollPosition(props) { + const { + history + } = props; + + const scrollTop = history.action === 'POP' ? + scrollPositions[scrollPositionKey] : + 0; + + return ( + + ); + } + + ScrollPosition.propTypes = { + history: PropTypes.object.isRequired + }; + + return ScrollPosition; +} + +export default withScrollPosition; diff --git a/frontend/src/Content/Fonts/FontAwesome.otf b/frontend/src/Content/Fonts/FontAwesome.otf new file mode 100644 index 0000000000000000000000000000000000000000..401ec0f36e4f73b8efa40bd6f604fe80d286db70 GIT binary patch literal 134808 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/Roboto-Light.ttf b/frontend/src/Content/Fonts/Roboto-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..94c6bcc67e09602f6d90ac10f449b5c05c2f7021 GIT binary patch literal 162420 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 z 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/Roboto-Light.woff b/frontend/src/Content/Fonts/Roboto-Light.woff new file mode 100644 index 0000000000000000000000000000000000000000..ec6bf5749e52ad1cf959fc5129672b44f2f6d1ab GIT binary patch literal 89300 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 z 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%0Dh+`o+a49c{bs_0^sWpgeA?4;$ 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 MLV_d_fRm8?ABWrtUjP6A literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/Roboto-Light.woff2 b/frontend/src/Content/Fonts/Roboto-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..2882017887433f9a4e4e621c60187d8a96f6d355 GIT binary patch literal 62832 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

Z=XtEWT1KTfsvcBh(5e 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/Roboto-Regular.ttf b/frontend/src/Content/Fonts/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8c082c8de090865264d37594e396c4d6c0099fe4 GIT binary patch literal 162876 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/Roboto-Regular.woff b/frontend/src/Content/Fonts/Roboto-Regular.woff new file mode 100644 index 0000000000000000000000000000000000000000..464d206236bfe401e3d9f5f3849c68d4e347bbbc GIT binary patch literal 89732 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

-jC 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

imCl?nAg7CQ)=23ee*SK&-grQ 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⩔(V3B$#|xuSp(J4ReX@mv(MW~Ja0|$yoE4k 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

5q!RY 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 + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.ttf b/frontend/src/Content/Fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..35acda2fa1196aad98c2adf4378a7611dd713aa3 GIT binary patch literal 165548 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 z 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 d0mRO}xo^G_%I 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 SdSQM28?+M|D(2r_;{O0|uQ74} literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.woff2 b/frontend/src/Content/Fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..4d13fc60404b91e398a37200c4a77b645cfd9586 GIT binary patch literal 77160 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Fonts/fonts.css b/frontend/src/Content/Fonts/fonts.css new file mode 100644 index 000000000..483c61ca8 --- /dev/null +++ b/frontend/src/Content/Fonts/fonts.css @@ -0,0 +1,27 @@ +@font-face { + font-weight: 300; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Light.woff2?v=1.1.0') format('woff2'), url('Roboto-Light.woff?v=1.2.0') format('woff'), url('Roboto-Light.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: 400; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Regular.woff2?v=1.2.0') format('woff2'), url('Roboto-Regular.woff?v=1.2.0') format('woff'), url('Roboto-Regular.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: normal; + font-style: normal; + font-family: 'Roboto'; + src: url('Roboto-Regular.woff2?v=1.3.0') format('woff2'), url('Roboto-Regular.woff?v=1.2.0') format('woff'), url('Roboto-Regular.ttf?v=1.1.0') format('truetype'); +} + +@font-face { + font-weight: 400; + font-style: normal; + font-family: 'Ubuntu Mono'; + src: url('UbuntuMono-Regular.eot?#iefix') format('embedded-opentype'), url('UbuntuMono-Regular.woff') format('woff'), url('UbuntuMono-Regular.ttf') format('truetype'); +} diff --git a/src/UI/Content/Images/404.png b/frontend/src/Content/Images/404.png similarity index 100% rename from src/UI/Content/Images/404.png rename to frontend/src/Content/Images/404.png diff --git a/frontend/src/Content/Images/Icons/android-chrome-192x192.png b/frontend/src/Content/Images/Icons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..3b133f3a60036b40c50b9828804d4e7bf884f125 GIT binary patch literal 4629 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 literal 0 HcmV?d00001 diff --git a/frontend/src/Content/Images/Icons/android-chrome-512x512.png b/frontend/src/Content/Images/Icons/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..1d08e879e0e325d99f9d27c3ab9ed3b681b29727 GIT binary patch literal 93807 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%0Aj8lpX2baS>uQapwf7XfV|QK@9I!RHO1#@QRa0K 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 XLbG$9VI_3Jf0k7RS1XPO@mT->QjP?B literal 0 HcmV?d00001 diff --git a/src/UI/Content/fonts/ubuntumono-regular.eot b/frontend/src/Content/Fonts/UbuntuMono-Regular.eot similarity index 100% rename from src/UI/Content/fonts/ubuntumono-regular.eot rename to frontend/src/Content/Fonts/UbuntuMono-Regular.eot diff --git a/src/UI/Content/fonts/UbuntuMono-Regular.ttf b/frontend/src/Content/Fonts/UbuntuMono-Regular.ttf similarity index 100% rename from src/UI/Content/fonts/UbuntuMono-Regular.ttf rename to frontend/src/Content/Fonts/UbuntuMono-Regular.ttf diff --git a/src/UI/Content/fonts/ubuntumono-regular.woff b/frontend/src/Content/Fonts/UbuntuMono-Regular.woff similarity index 100% rename from src/UI/Content/fonts/ubuntumono-regular.woff rename to frontend/src/Content/Fonts/UbuntuMono-Regular.woff diff --git a/frontend/src/Content/Fonts/font-awesome.css b/frontend/src/Content/Fonts/font-awesome.css new file mode 100644 index 000000000..eab1cbb5b --- /dev/null +++ b/frontend/src/Content/Fonts/font-awesome.css @@ -0,0 +1,2337 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@font-face { + font-family: 'FontAwesome'; + src: url('fontawesome-webfont.eot?v=4.7.0'); + src: url('fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('fontawesome-webfont.woff?v=4.7.0') format('woff'), url('fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.fa-pull-left { + float: left; +} +.fa-pull-right { + float: right; +} +.fa.fa-pull-left { + margin-right: .3em; +} +.fa.fa-pull-right { + margin-left: .3em; +} +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-feed:before, +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-pp:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-intersex:before, +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-genderless:before { + content: "\f22d"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} +.fa-yc:before, +.fa-y-combinator:before { + content: "\f23b"; +} +.fa-optin-monster:before { + content: "\f23c"; +} +.fa-opencart:before { + content: "\f23d"; +} +.fa-expeditedssl:before { + content: "\f23e"; +} +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: "\f240"; +} +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: "\f241"; +} +.fa-battery-2:before, +.fa-battery-half:before { + content: "\f242"; +} +.fa-battery-1:before, +.fa-battery-quarter:before { + content: "\f243"; +} +.fa-battery-0:before, +.fa-battery-empty:before { + content: "\f244"; +} +.fa-mouse-pointer:before { + content: "\f245"; +} +.fa-i-cursor:before { + content: "\f246"; +} +.fa-object-group:before { + content: "\f247"; +} +.fa-object-ungroup:before { + content: "\f248"; +} +.fa-sticky-note:before { + content: "\f249"; +} +.fa-sticky-note-o:before { + content: "\f24a"; +} +.fa-cc-jcb:before { + content: "\f24b"; +} +.fa-cc-diners-club:before { + content: "\f24c"; +} +.fa-clone:before { + content: "\f24d"; +} +.fa-balance-scale:before { + content: "\f24e"; +} +.fa-hourglass-o:before { + content: "\f250"; +} +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: "\f251"; +} +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: "\f252"; +} +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: "\f253"; +} +.fa-hourglass:before { + content: "\f254"; +} +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: "\f255"; +} +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: "\f256"; +} +.fa-hand-scissors-o:before { + content: "\f257"; +} +.fa-hand-lizard-o:before { + content: "\f258"; +} +.fa-hand-spock-o:before { + content: "\f259"; +} +.fa-hand-pointer-o:before { + content: "\f25a"; +} +.fa-hand-peace-o:before { + content: "\f25b"; +} +.fa-trademark:before { + content: "\f25c"; +} +.fa-registered:before { + content: "\f25d"; +} +.fa-creative-commons:before { + content: "\f25e"; +} +.fa-gg:before { + content: "\f260"; +} +.fa-gg-circle:before { + content: "\f261"; +} +.fa-tripadvisor:before { + content: "\f262"; +} +.fa-odnoklassniki:before { + content: "\f263"; +} +.fa-odnoklassniki-square:before { + content: "\f264"; +} +.fa-get-pocket:before { + content: "\f265"; +} +.fa-wikipedia-w:before { + content: "\f266"; +} +.fa-safari:before { + content: "\f267"; +} +.fa-chrome:before { + content: "\f268"; +} +.fa-firefox:before { + content: "\f269"; +} +.fa-opera:before { + content: "\f26a"; +} +.fa-internet-explorer:before { + content: "\f26b"; +} +.fa-tv:before, +.fa-television:before { + content: "\f26c"; +} +.fa-contao:before { + content: "\f26d"; +} +.fa-500px:before { + content: "\f26e"; +} +.fa-amazon:before { + content: "\f270"; +} +.fa-calendar-plus-o:before { + content: "\f271"; +} +.fa-calendar-minus-o:before { + content: "\f272"; +} +.fa-calendar-times-o:before { + content: "\f273"; +} +.fa-calendar-check-o:before { + content: "\f274"; +} +.fa-industry:before { + content: "\f275"; +} +.fa-map-pin:before { + content: "\f276"; +} +.fa-map-signs:before { + content: "\f277"; +} +.fa-map-o:before { + content: "\f278"; +} +.fa-map:before { + content: "\f279"; +} +.fa-commenting:before { + content: "\f27a"; +} +.fa-commenting-o:before { + content: "\f27b"; +} +.fa-houzz:before { + content: "\f27c"; +} +.fa-vimeo:before { + content: "\f27d"; +} +.fa-black-tie:before { + content: "\f27e"; +} +.fa-fonticons:before { + content: "\f280"; +} +.fa-reddit-alien:before { + content: "\f281"; +} +.fa-edge:before { + content: "\f282"; +} +.fa-credit-card-alt:before { + content: "\f283"; +} +.fa-codiepie:before { + content: "\f284"; +} +.fa-modx:before { + content: "\f285"; +} +.fa-fort-awesome:before { + content: "\f286"; +} +.fa-usb:before { + content: "\f287"; +} +.fa-product-hunt:before { + content: "\f288"; +} +.fa-mixcloud:before { + content: "\f289"; +} +.fa-scribd:before { + content: "\f28a"; +} +.fa-pause-circle:before { + content: "\f28b"; +} +.fa-pause-circle-o:before { + content: "\f28c"; +} +.fa-stop-circle:before { + content: "\f28d"; +} +.fa-stop-circle-o:before { + content: "\f28e"; +} +.fa-shopping-bag:before { + content: "\f290"; +} +.fa-shopping-basket:before { + content: "\f291"; +} +.fa-hashtag:before { + content: "\f292"; +} +.fa-bluetooth:before { + content: "\f293"; +} +.fa-bluetooth-b:before { + content: "\f294"; +} +.fa-percent:before { + content: "\f295"; +} +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.fa-pied-piper:before { + content: "\f2ae"; +} +.fa-first-order:before { + content: "\f2b0"; +} +.fa-yoast:before { + content: "\f2b1"; +} +.fa-themeisle:before { + content: "\f2b2"; +} +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: "\f2b3"; +} +.fa-fa:before, +.fa-font-awesome:before { + content: "\f2b4"; +} +.fa-handshake-o:before { + content: "\f2b5"; +} +.fa-envelope-open:before { + content: "\f2b6"; +} +.fa-envelope-open-o:before { + content: "\f2b7"; +} +.fa-linode:before { + content: "\f2b8"; +} +.fa-address-book:before { + content: "\f2b9"; +} +.fa-address-book-o:before { + content: "\f2ba"; +} +.fa-vcard:before, +.fa-address-card:before { + content: "\f2bb"; +} +.fa-vcard-o:before, +.fa-address-card-o:before { + content: "\f2bc"; +} +.fa-user-circle:before { + content: "\f2bd"; +} +.fa-user-circle-o:before { + content: "\f2be"; +} +.fa-user-o:before { + content: "\f2c0"; +} +.fa-id-badge:before { + content: "\f2c1"; +} +.fa-drivers-license:before, +.fa-id-card:before { + content: "\f2c2"; +} +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: "\f2c3"; +} +.fa-quora:before { + content: "\f2c4"; +} +.fa-free-code-camp:before { + content: "\f2c5"; +} +.fa-telegram:before { + content: "\f2c6"; +} +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: "\f2c7"; +} +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: "\f2c8"; +} +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: "\f2c9"; +} +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: "\f2ca"; +} +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: "\f2cb"; +} +.fa-shower:before { + content: "\f2cc"; +} +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: "\f2cd"; +} +.fa-podcast:before { + content: "\f2ce"; +} +.fa-window-maximize:before { + content: "\f2d0"; +} +.fa-window-minimize:before { + content: "\f2d1"; +} +.fa-window-restore:before { + content: "\f2d2"; +} +.fa-times-rectangle:before, +.fa-window-close:before { + content: "\f2d3"; +} +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: "\f2d4"; +} +.fa-bandcamp:before { + content: "\f2d5"; +} +.fa-grav:before { + content: "\f2d6"; +} +.fa-etsy:before { + content: "\f2d7"; +} +.fa-imdb:before { + content: "\f2d8"; +} +.fa-ravelry:before { + content: "\f2d9"; +} +.fa-eercast:before { + content: "\f2da"; +} +.fa-microchip:before { + content: "\f2db"; +} +.fa-snowflake-o:before { + content: "\f2dc"; +} +.fa-superpowers:before { + content: "\f2dd"; +} +.fa-wpexplorer:before { + content: "\f2de"; +} +.fa-meetup:before { + content: "\f2e0"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/frontend/src/Content/Fonts/fontawesome-webfont.eot b/frontend/src/Content/Fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..e9f60ca953f93e35eab4108bd414bc02ddcf3928 GIT binary patch literal 165742 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 G*yFsbY~cLNXLP73aA74Mq6M9f^&YV`isWW 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

+ } + /> + } + + ); +} + +SceneInfo.propTypes = { + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired, + seriesType: PropTypes.string +}; + +export default SceneInfo; diff --git a/frontend/src/Episode/Search/EpisodeSearch.css b/frontend/src/Episode/Search/EpisodeSearch.css new file mode 100644 index 000000000..2f7ddfd19 --- /dev/null +++ b/frontend/src/Episode/Search/EpisodeSearch.css @@ -0,0 +1,16 @@ +.buttonContainer { + display: flex; + justify-content: center; + + margin-top: 10px; +} + +.button { + composes: button from 'Components/Link/Button.css'; + + width: 300px; +} + +.buttonIcon { + margin-right: 5px; +} diff --git a/frontend/src/Episode/Search/EpisodeSearch.js b/frontend/src/Episode/Search/EpisodeSearch.js new file mode 100644 index 000000000..f3ab8fdec --- /dev/null +++ b/frontend/src/Episode/Search/EpisodeSearch.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import styles from './EpisodeSearch.css'; + +function EpisodeSearch(props) { + const { + onQuickSearchPress, + onInteractiveSearchPress + } = props; + + return ( +
+
+ +
+ +
+ +
+
+ ); +} + +EpisodeSearch.propTypes = { + onQuickSearchPress: PropTypes.func.isRequired, + onInteractiveSearchPress: PropTypes.func.isRequired +}; + +export default EpisodeSearch; diff --git a/frontend/src/Episode/Search/EpisodeSearchConnector.js b/frontend/src/Episode/Search/EpisodeSearchConnector.js new file mode 100644 index 000000000..748cb9660 --- /dev/null +++ b/frontend/src/Episode/Search/EpisodeSearchConnector.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import EpisodeSearch from './EpisodeSearch'; +import InteractiveEpisodeSearchConnector from './InteractiveEpisodeSearchConnector'; + +function createMapStateToProps() { + return createSelector( + (state) => state.releases, + (releases) => { + return { + isPopulated: releases.isPopulated + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand +}; + +class EpisodeSearchConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isInteractiveSearchOpen: props.startInteractiveSearch + }; + } + + componentDidMount() { + if (this.props.isPopulated) { + this.setState({ isInteractiveSearchOpen: true }); + } + } + + // + // Listeners + + onQuickSearchPress = () => { + this.props.executeCommand({ + name: commandNames.EPISODE_SEARCH, + episodeIds: [this.props.episodeId] + }); + + this.props.onModalClose(); + } + + onInteractiveSearchPress = () => { + this.setState({ isInteractiveSearchOpen: true }); + } + + // + // Render + + render() { + if (this.state.isInteractiveSearchOpen) { + return ( + + ); + } + + return ( + + ); + } +} + +EpisodeSearchConnector.propTypes = { + episodeId: PropTypes.number.isRequired, + isPopulated: PropTypes.bool.isRequired, + startInteractiveSearch: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeSearchConnector); diff --git a/frontend/src/Episode/Search/InteractiveEpisodeSearch.js b/frontend/src/Episode/Search/InteractiveEpisodeSearch.js new file mode 100644 index 000000000..eb8f8493f --- /dev/null +++ b/frontend/src/Episode/Search/InteractiveEpisodeSearch.js @@ -0,0 +1,130 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { icons, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Icon from 'Components/Icon'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import InteractiveEpisodeSearchRow from './InteractiveEpisodeSearchRow'; + +const columns = [ + { + name: 'protocol', + label: 'Source', + isSortable: true, + isVisible: true + }, + { + name: 'age', + label: 'Age', + isSortable: true, + isVisible: true + }, + { + name: 'title', + label: 'Title', + isSortable: true, + isVisible: true + }, + { + name: 'indexer', + label: 'Indexer', + isSortable: true, + isVisible: true + }, + { + name: 'size', + label: 'Size', + isSortable: true, + isVisible: true + }, + { + name: 'peers', + label: 'Peers', + isSortable: true, + isVisible: true + }, + { + name: 'qualityWeight', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'rejections', + label: React.createElement(Icon, { name: icons.DANGER }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true + }, + { + name: 'releaseWeight', + label: React.createElement(Icon, { name: icons.DOWNLOAD }), + isSortable: true, + fixedSortDirection: sortDirections.ASCENDING, + isVisible: true + } +]; + +function InteractiveEpisodeSearch(props) { + const { + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + longDateFormat, + timeFormat, + onSortPress, + onGrabPress + } = props; + + if (isFetching) { + return ; + } else if (!isFetching && !!error) { + return
Unable to load results for this episode search. Try again later.
; + } else if (isPopulated && !items.length) { + return
No results found.
; + } + + return ( + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ ); +} + +InteractiveEpisodeSearch.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onSortPress: PropTypes.func.isRequired, + onGrabPress: PropTypes.func.isRequired +}; + +export default InteractiveEpisodeSearch; diff --git a/frontend/src/Episode/Search/InteractiveEpisodeSearchConnector.js b/frontend/src/Episode/Search/InteractiveEpisodeSearchConnector.js new file mode 100644 index 000000000..20c93813c --- /dev/null +++ b/frontend/src/Episode/Search/InteractiveEpisodeSearchConnector.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchReleases, setReleasesSort, grabRelease } from 'Store/Actions/releaseActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import InteractiveEpisodeSearch from './InteractiveEpisodeSearch'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + createUISettingsSelector(), + (releases, uiSettings) => { + return { + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat, + ...releases + }; + } + ); +} + +const mapDispatchToProps = { + fetchReleases, + setReleasesSort, + grabRelease +}; + +class InteractiveEpisodeSearchConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + episodeId, + isPopulated + } = this.props; + + // If search results are not yet isPopulated fetch them, + // otherwise re-show the existing props. + + if (!isPopulated) { + this.props.fetchReleases({ + episodeId + }); + } + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setReleasesSort({ sortKey, sortDirection }); + } + + onGrabPress = (guid) => { + this.props.grabRelease({ guid }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +InteractiveEpisodeSearchConnector.propTypes = { + episodeId: PropTypes.number.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchReleases: PropTypes.func.isRequired, + setReleasesSort: PropTypes.func.isRequired, + grabRelease: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'releases' } + )(InteractiveEpisodeSearchConnector); diff --git a/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.css b/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.css new file mode 100644 index 000000000..c77b73e7d --- /dev/null +++ b/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.css @@ -0,0 +1,25 @@ +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; +} + +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + text-align: center; +} + +.rejected, +.download { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.age, +.size { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + white-space: nowrap; +} diff --git a/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.js b/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.js new file mode 100644 index 000000000..4d32606cc --- /dev/null +++ b/frontend/src/Episode/Search/InteractiveEpisodeSearchRow.js @@ -0,0 +1,197 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import formatAge from 'Utilities/Number/formatAge'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import Link from 'Components/Link/Link'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; +import Peers from './Peers'; +import styles from './InteractiveEpisodeSearchRow.css'; + +function getDownloadIcon(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return icons.SPINNER; + } else if (isGrabbed) { + return icons.DOWNLOADING; + } else if (grabError) { + return icons.DOWNLOADING; + } + + return icons.DOWNLOAD; +} + +function getDownloadTooltip(isGrabbing, isGrabbed, grabError) { + if (isGrabbing) { + return ''; + } else if (isGrabbed) { + return 'Added to downloaded queue'; + } else if (grabError) { + return grabError; + } + + return 'Add to downloaded queue'; +} + +class InteractiveEpisodeSearchRow extends Component { + + // + // Listeners + + onGrabPress = () => { + this.props.onGrabPress(this.props.guid); + } + + // + // Render + + render() { + const { + protocol, + age, + ageHours, + ageMinutes, + publishDate, + title, + infoUrl, + indexer, + size, + seeders, + leechers, + quality, + rejections, + downloadAllowed, + isGrabbing, + isGrabbed, + longDateFormat, + timeFormat, + grabError + } = this.props; + + return ( + + + + + + + {formatAge(age, ageHours, ageMinutes)} + + + + + {title} + + + + + {indexer} + + + + {formatBytes(size)} + + + + { + protocol === 'torrent' && + + } + + + + + + + + { + !!rejections.length && + + } + title="Release Rejected" + body={ +
    + { + rejections.map((rejection, index) => { + return ( +
  • + {rejection} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ + + { + downloadAllowed && + + } + +
+ ); + } +} + +InteractiveEpisodeSearchRow.propTypes = { + guid: PropTypes.string.isRequired, + protocol: PropTypes.string.isRequired, + age: PropTypes.number.isRequired, + ageHours: PropTypes.number.isRequired, + ageMinutes: PropTypes.number.isRequired, + publishDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + infoUrl: PropTypes.string.isRequired, + indexer: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + seeders: PropTypes.number, + leechers: PropTypes.number, + quality: PropTypes.object.isRequired, + rejections: PropTypes.arrayOf(PropTypes.string).isRequired, + downloadAllowed: PropTypes.bool.isRequired, + isGrabbing: PropTypes.bool.isRequired, + isGrabbed: PropTypes.bool.isRequired, + grabError: PropTypes.string, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onGrabPress: PropTypes.func.isRequired +}; + +InteractiveEpisodeSearchRow.defaultProps = { + isGrabbing: false, + isGrabbed: false +}; + +export default InteractiveEpisodeSearchRow; diff --git a/frontend/src/Episode/Search/Peers.js b/frontend/src/Episode/Search/Peers.js new file mode 100644 index 000000000..66f7cc9f5 --- /dev/null +++ b/frontend/src/Episode/Search/Peers.js @@ -0,0 +1,57 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function getKind(seeders) { + if (seeders > 50) { + return kinds.PRIMARY; + } + + if (seeders > 10) { + return kinds.INFO; + } + + if (seeders > 0) { + return kinds.WARNING; + } + + return kinds.DANGER; +} + +function getPeersTooltipPart(peers, peersUnit) { + if (peers == null) { + return `unknown ${peersUnit}s`; + } + + if (peers === 1) { + return `1 ${peersUnit}`; + } + + return `${peers} ${peersUnit}s`; +} + +function Peers(props) { + const { + seeders, + leechers + } = props; + + const kind = getKind(seeders); + + return ( + + ); +} + +Peers.propTypes = { + seeders: PropTypes.number, + leechers: PropTypes.number +}; + +export default Peers; diff --git a/frontend/src/Episode/SeasonEpisodeNumber.css b/frontend/src/Episode/SeasonEpisodeNumber.css new file mode 100644 index 000000000..f86e1de6b --- /dev/null +++ b/frontend/src/Episode/SeasonEpisodeNumber.css @@ -0,0 +1,3 @@ +.absoluteEpisodeNumber { + margin-left: 5px; +} diff --git a/frontend/src/Episode/SeasonEpisodeNumber.js b/frontend/src/Episode/SeasonEpisodeNumber.js new file mode 100644 index 000000000..b0c9d3ee6 --- /dev/null +++ b/frontend/src/Episode/SeasonEpisodeNumber.js @@ -0,0 +1,51 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import padNumber from 'Utilities/Number/padNumber'; +import styles from './SeasonEpisodeNumber.css'; + +function SeasonEpisodeNumber(props) { + const { + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + airDate, + seriesType + } = props; + + if (seriesType === 'daily' && airDate) { + return ( + {airDate} + ); + } + + if (seriesType === 'anime') { + return ( + + {seasonNumber}x{padNumber(episodeNumber, 2)} + + { + absoluteEpisodeNumber && + + ({absoluteEpisodeNumber}) + + } + + ); + } + + return ( + + {seasonNumber}x{padNumber(episodeNumber, 2)} + + ); +} + +SeasonEpisodeNumber.propTypes = { + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + airDate: PropTypes.string, + seriesType: PropTypes.string +}; + +export default SeasonEpisodeNumber; diff --git a/frontend/src/Episode/Summary/EpisodeAiring.js b/frontend/src/Episode/Summary/EpisodeAiring.js new file mode 100644 index 000000000..54ca64325 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeAiring.js @@ -0,0 +1,86 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import formatTime from 'Utilities/Date/formatTime'; +import isInNextWeek from 'Utilities/Date/isInNextWeek'; +import isToday from 'Utilities/Date/isToday'; +import isTomorrow from 'Utilities/Date/isTomorrow'; +import { kinds, sizes } from 'Helpers/Props'; +import Label from 'Components/Label'; + +function EpisodeAiring(props) { + const { + airDateUtc, + network, + shortDateFormat, + showRelativeDates, + timeFormat + } = props; + + const networkLabel = ( + + ); + + if (!airDateUtc) { + return ( + + TBA on {networkLabel} + + ); + } + + const time = formatTime(airDateUtc, timeFormat); + + if (!showRelativeDates) { + return ( + + {moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel} + + ); + } + + if (isToday(airDateUtc)) { + return ( + + {time} on {networkLabel} + + ); + } + + if (isTomorrow(airDateUtc)) { + return ( + + Tomorrow at {time} on {networkLabel} + + ); + } + + if (isInNextWeek(airDateUtc)) { + return ( + + {moment(airDateUtc).format('dddd')} at {time} on {networkLabel} + + ); + } + + return ( + + {moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel} + + ); +} + +EpisodeAiring.propTypes = { + airDateUtc: PropTypes.string.isRequired, + network: PropTypes.string.isRequired, + shortDateFormat: PropTypes.string.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + timeFormat: PropTypes.string.isRequired +}; + +export default EpisodeAiring; diff --git a/frontend/src/Episode/Summary/EpisodeAiringConnector.js b/frontend/src/Episode/Summary/EpisodeAiringConnector.js new file mode 100644 index 000000000..508467efb --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeAiringConnector.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import EpisodeAiring from './EpisodeAiring'; + +function createMapStateToProps() { + return createSelector( + createUISettingsSelector(), + (uiSettings) => { + return _.pick(uiSettings, [ + 'shortDateFormat', + 'showRelativeDates', + 'timeFormat' + ]); + } + ); +} + +export default connect(createMapStateToProps)(EpisodeAiring); diff --git a/frontend/src/Episode/Summary/EpisodeSummary.css b/frontend/src/Episode/Summary/EpisodeSummary.css new file mode 100644 index 000000000..fb04508f3 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeSummary.css @@ -0,0 +1,48 @@ +.infoTitle { + display: inline-block; + width: 100px; + font-weight: bold; +} + +.overview, +.files { + margin-top: 20px; +} + +.filesHeader { + display: flex; + font-weight: bold; +} + +.filesHeader { + display: flex; + margin-bottom: 10px; + border-bottom: 1px solid $borderColor; +} + +.fileRow { + display: flex; +} + +.path { + composes: truncate from 'Styles/Mixins/truncate.css'; + + flex: 1 0 1px; +} + +.size, +.quality { + flex: 0 0 125px; +} + +.actions { + flex: 0 0 20px; + text-align: center; +} + +@media only screen and (max-width: $breakpointMedium) { + .size, + .quality { + flex: 0 0 80px; + } +} diff --git a/frontend/src/Episode/Summary/EpisodeSummary.js b/frontend/src/Episode/Summary/EpisodeSummary.js new file mode 100644 index 000000000..8b8388744 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeSummary.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import IconButton from 'Components/Link/IconButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import Label from 'Components/Label'; +import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import EpisodeAiringConnector from './EpisodeAiringConnector'; +import styles from './EpisodeSummary.css'; + +class EpisodeSummary extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isRemoveEpisodeFileModalOpen: false + }; + } + + // + // Listeners + + onRemoveEpisodeFilePress = () => { + this.setState({ isRemoveEpisodeFileModalOpen: true }); + } + + onConfirmRemoveEpisodeFile = () => { + this.props.onDeleteEpisodeFile(); + this.setState({ isRemoveEpisodeFileModalOpen: false }); + } + + onRemoveEpisodeFileModalClose = () => { + this.setState({ isRemoveEpisodeFileModalOpen: false }); + } + + // + // Render + + render() { + const { + qualityProfileId, + network, + overview, + airDateUtc, + path, + size, + quality, + qualityCutoffNotMet + } = this.props; + + const hasOverview = !!overview; + + return ( +
+
+ Airs + + +
+ +
+ Quality Profile + + +
+ +
+ { + hasOverview ? + overview : + 'No episode overview.' + } +
+ + { + path && +
+
+
+ Path +
+ +
+ Size +
+ +
+ Quality +
+ +
+
+ +
+
+ {path} +
+ +
+ {formatBytes(size)} +
+ +
+ +
+ +
+ +
+
+
+ } + + +
+ ); + } +} + +EpisodeSummary.propTypes = { + episodeFileId: PropTypes.number.isRequired, + qualityProfileId: PropTypes.number.isRequired, + network: PropTypes.string.isRequired, + overview: PropTypes.string, + airDateUtc: PropTypes.string.isRequired, + path: PropTypes.string, + size: PropTypes.number, + quality: PropTypes.object, + qualityCutoffNotMet: PropTypes.bool, + onDeleteEpisodeFile: PropTypes.func.isRequired +}; + +export default EpisodeSummary; diff --git a/frontend/src/Episode/Summary/EpisodeSummaryConnector.js b/frontend/src/Episode/Summary/EpisodeSummaryConnector.js new file mode 100644 index 000000000..0f5f1f132 --- /dev/null +++ b/frontend/src/Episode/Summary/EpisodeSummaryConnector.js @@ -0,0 +1,57 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { deleteEpisodeFile } from 'Store/Actions/episodeFileActions'; +import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector'; +import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import EpisodeSummary from './EpisodeSummary'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + createEpisodeSelector(), + createEpisodeFileSelector(), + (series, episode, episodeFile) => { + const { + qualityProfileId, + network + } = series; + + const { + airDateUtc, + overview + } = episode; + + const { + path, + size, + quality, + qualityCutoffNotMet + } = episodeFile || {}; + + return { + network, + qualityProfileId, + airDateUtc, + overview, + path, + size, + quality, + qualityCutoffNotMet + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + onDeleteEpisodeFile() { + dispatch(deleteEpisodeFile({ + id: props.episodeFileId, + episodeEntity: props.episodeEntity + })); + } + }; +} + +export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeSummary); diff --git a/frontend/src/Episode/episodeEntities.js b/frontend/src/Episode/episodeEntities.js new file mode 100644 index 000000000..7e6ea91e9 --- /dev/null +++ b/frontend/src/Episode/episodeEntities.js @@ -0,0 +1,13 @@ +export const CALENDAR = 'calendar'; +export const EPISODES = 'episodes'; +export const QUEUE_EPISODES = 'queue.queueEpisodes'; +export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet'; +export const WANTED_MISSING = 'wanted.missing'; + +export default { + CALENDAR, + EPISODES, + QUEUE_EPISODES, + WANTED_CUTOFF_UNMET, + WANTED_MISSING +}; diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js new file mode 100644 index 000000000..35d00caf2 --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EpisodeFileEditorModalContentConnector from './EpisodeFileEditorModalContentConnector'; + +function EpisodeFileEditorModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + { + isOpen && + + } + + ); +} + +EpisodeFileEditorModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EpisodeFileEditorModal; diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css new file mode 100644 index 000000000..49e946826 --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.css @@ -0,0 +1,8 @@ +.actions { + display: flex; + margin-right: auto; +} + +.selectInput { + margin-left: 10px; +} diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js new file mode 100644 index 000000000..8c466798f --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContent.js @@ -0,0 +1,276 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import Button from 'Components/Link/Button'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import SelectInput from 'Components/Form/SelectInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import EpisodeFileEditorRow from './EpisodeFileEditorRow'; +import styles from './EpisodeFileEditorModalContent.css'; + +const columns = [ + { + name: 'episodeNumber', + label: 'Episode', + isVisible: true + }, + { + name: 'relativePath', + label: 'Relative Path', + isVisible: true + }, + { + name: 'airDateUtc', + label: 'Air Date', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + } +]; + +class EpisodeFileEditorModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmDeleteModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.items !== this.props.items) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + const selectedIds = getSelectedIds(this.state.selectedState); + + return _.uniq(_.map(selectedIds, (id) => { + return _.find(this.props.items, { id }).episodeFileId; + })); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onDeletePress = () => { + this.setState({ isConfirmDeleteModalOpen: true }); + } + + onConfirmDelete = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + this.props.onDeletePress(this.getSelectedIds()); + } + + onConfirmDeleteModalClose = () => { + this.setState({ isConfirmDeleteModalOpen: false }); + } + + onLanguageChange = ({ value }) => { + const selectedIds = this.getSelectedIds(); + + if (!selectedIds.length) { + return; + } + + this.props.onLanguageChange(selectedIds, parseInt(value)); + } + + onQualityChange = ({ value }) => { + const selectedIds = this.getSelectedIds(); + + if (!selectedIds.length) { + return; + } + + this.props.onQualityChange(selectedIds, parseInt(value)); + } + + // + // Render + + render() { + const { + isDeleting, + items, + languages, + qualities, + seriesType, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmDeleteModalOpen + } = this.state; + + const languageOptions = _.reduceRight(languages, (acc, language) => { + acc.push({ + key: language.id, + value: language.name + }); + + return acc; + }, [{ key: 'selectLanguage', value: 'Select Language', disabled: true }]); + + const qualityOptions = _.reduceRight(qualities, (acc, quality) => { + acc.push({ + key: quality.id, + value: quality.name + }); + + return acc; + }, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]); + + const hasSelectedFiles = this.getSelectedIds().length > 0; + + return ( + + + Manage Episodes + + + + { + !items.length && +
+ No episode files to manage. +
+ } + + { + !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+ + +
+ + Delete + + +
+ +
+ +
+ +
+
+ + +
+ + +
+ ); + } +} + +EpisodeFileEditorModalContent.propTypes = { + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + seriesType: PropTypes.string.isRequired, + onDeletePress: PropTypes.func.isRequired, + onLanguageChange: PropTypes.func.isRequired, + onQualityChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EpisodeFileEditorModalContent; diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js new file mode 100644 index 000000000..3f2fc77b5 --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorModalContentConnector.js @@ -0,0 +1,162 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { deleteEpisodeFiles, updateEpisodeFiles } from 'Store/Actions/episodeFileActions'; +import { fetchLanguageProfileSchema, fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import EpisodeFileEditorModalContent from './EpisodeFileEditorModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { seasonNumber }) => seasonNumber, + (state) => state.episodes, + (state) => state.episodeFiles, + (state) => state.settings.languageProfiles.schema, + (state) => state.settings.qualityProfiles.schema, + createArtistSelector(), + ( + seasonNumber, + episodes, + episodeFiles, + languageProfilesSchema, + qualityProfileSchema, + series + ) => { + const filtered = _.filter(episodes.items, (episode) => { + if (seasonNumber >= 0 && episode.seasonNumber !== seasonNumber) { + return false; + } + + if (!episode.episodeFileId) { + return false; + } + + return _.some(episodeFiles.items, { id: episode.episodeFileId }); + }); + + const sorted = _.orderBy(filtered, ['seasonNumber', 'episodeNumber'], ['desc', 'desc']); + + const items = _.map(sorted, (episode) => { + const episodeFile = _.find(episodeFiles.items, { id: episode.episodeFileId }); + + return { + relativePath: episodeFile.relativePath, + language: episodeFile.language, + quality: episodeFile.quality, + ...episode + }; + }); + + const languages = _.map(languageProfilesSchema.languages, 'language'); + const qualities = _.map(qualityProfileSchema.items, 'quality'); + + return { + items, + seriesType: series.seriesType, + isDeleting: episodeFiles.isDeleting, + isSaving: episodeFiles.isSaving, + languages, + qualities + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + return { + dispatchFetchLanguageProfileSchema(name, path) { + dispatch(fetchLanguageProfileSchema()); + }, + + dispatchFetchQualityProfileSchema(name, path) { + dispatch(fetchQualityProfileSchema()); + }, + + dispatchUpdateEpisodeFiles(updateProps) { + dispatch(updateEpisodeFiles(updateProps)); + }, + + onDeletePress(episodeFileIds) { + dispatch(deleteEpisodeFiles({ episodeFileIds })); + }, + + + onQualityChange(episodeFileIds, qualityId) { + const quality = { + quality: _.find(this.props.qualities, { id: qualityId }), + revision: { + version: 1, + real: 0 + } + }; + + dispatch(updateEpisodeFiles({ episodeFileIds, quality })); + } + }; +} + +class EpisodeFileEditorModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.dispatchFetchLanguageProfileSchema(); + this.props.dispatchFetchQualityProfileSchema(); + } + + // + // Render + + // + // Listeners + + onLanguageChange = (episodeFileIds, languageId) => { + const language = _.find(this.props.languages, { id: languageId }); + + this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, language }); + } + + onQualityChange = (episodeFileIds, qualityId) => { + const quality = { + quality: _.find(this.props.qualities, { id: qualityId }), + revision: { + version: 1, + real: 0 + } + }; + + this.props.dispatchUpdateEpisodeFiles({ episodeFileIds, quality }); + } + + render() { + const { + dispatchFetchLanguageProfileSchema, + dispatchFetchQualityProfileSchema, + dispatchUpdateEpisodeFiles, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +EpisodeFileEditorModalContentConnector.propTypes = { + artistId: PropTypes.number.isRequired, + seasonNumber: PropTypes.number, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired, + dispatchFetchQualityProfileSchema: PropTypes.func.isRequired, + dispatchUpdateEpisodeFiles: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeFileEditorModalContentConnector); diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css new file mode 100644 index 000000000..f86e1de6b --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.css @@ -0,0 +1,3 @@ +.absoluteEpisodeNumber { + margin-left: 5px; +} diff --git a/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js new file mode 100644 index 000000000..62cedb8af --- /dev/null +++ b/frontend/src/EpisodeFile/Editor/EpisodeFileEditorRow.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import padNumber from 'Utilities/Number/padNumber'; +import Label from 'Components/Label'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import styles from './EpisodeFileEditorRow'; + +function EpisodeFileEditorRow(props) { + const { + id, + seriesType, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + relativePath, + airDateUtc, + language, + quality, + isSelected, + onSelectedChange + } = props; + + return ( + + + + + {seasonNumber}x{padNumber(episodeNumber, 2)} + + { + seriesType === 'anime' && !!absoluteEpisodeNumber && + + ({absoluteEpisodeNumber}) + + } + + + + {relativePath} + + + + + + + + + + + + + ); +} + +EpisodeFileEditorRow.propTypes = { + id: PropTypes.number.isRequired, + seriesType: PropTypes.string.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + relativePath: PropTypes.string.isRequired, + airDateUtc: PropTypes.string.isRequired, + language: PropTypes.object.isRequired, + quality: PropTypes.object.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default EpisodeFileEditorRow; diff --git a/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js b/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js new file mode 100644 index 000000000..38713c011 --- /dev/null +++ b/frontend/src/EpisodeFile/EpisodeFileLanguageConnector.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; +import EpisodeLanguage from 'Episode/EpisodeLanguage'; + +function createMapStateToProps() { + return createSelector( + createEpisodeFileSelector(), + (episodeFile) => { + return { + language: episodeFile ? episodeFile.language : undefined + }; + } + ); +} + +export default connect(createMapStateToProps)(EpisodeLanguage); diff --git a/frontend/src/EpisodeFile/MediaInfo.js b/frontend/src/EpisodeFile/MediaInfo.js new file mode 100644 index 000000000..eccb1ea6b --- /dev/null +++ b/frontend/src/EpisodeFile/MediaInfo.js @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import * as mediaInfoTypes from './mediaInfoTypes'; + +function MediaInfo(props) { + const { + type, + audioChannels, + audioCodec, + videoCodec + } = props; + + if (type === mediaInfoTypes.AUDIO) { + return ( + + { + !!audioCodec && + audioCodec + } + + { + !!audioCodec && !!audioChannels && + ' - ' + } + + { + !!audioChannels && + audioChannels.toFixed(1) + } + + ); + } + + if (type === mediaInfoTypes.VIDEO) { + return ( + + {videoCodec} + + ); + } + + + return null; +} + +MediaInfo.propTypes = { + type: PropTypes.string.isRequired, + audioChannels: PropTypes.number, + audioCodec: PropTypes.string, + videoCodec: PropTypes.string +}; + +export default MediaInfo; diff --git a/frontend/src/EpisodeFile/MediaInfoConnector.js b/frontend/src/EpisodeFile/MediaInfoConnector.js new file mode 100644 index 000000000..bbb963cf4 --- /dev/null +++ b/frontend/src/EpisodeFile/MediaInfoConnector.js @@ -0,0 +1,21 @@ +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector'; +import MediaInfo from './MediaInfo'; + +function createMapStateToProps() { + return createSelector( + createEpisodeFileSelector(), + (episodeFile) => { + if (episodeFile) { + return { + ...episodeFile.mediaInfo + }; + } + + return {}; + } + ); +} + +export default connect(createMapStateToProps)(MediaInfo); diff --git a/frontend/src/EpisodeFile/mediaInfoTypes.js b/frontend/src/EpisodeFile/mediaInfoTypes.js new file mode 100644 index 000000000..5e5a78e64 --- /dev/null +++ b/frontend/src/EpisodeFile/mediaInfoTypes.js @@ -0,0 +1,2 @@ +export const AUDIO = 'audio'; +export const VIDEO = 'video'; diff --git a/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js b/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js new file mode 100644 index 000000000..11cca7d1b --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/createRouteMatchShape.js @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; + +function createRouteMatchShape(props) { + return PropTypes.shape({ + params: PropTypes.shape({ + ...props + }).isRequired + }); +} + +export default createRouteMatchShape; diff --git a/frontend/src/Helpers/Props/Shapes/locationShape.js b/frontend/src/Helpers/Props/Shapes/locationShape.js new file mode 100644 index 000000000..80b53eb44 --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/locationShape.js @@ -0,0 +1,11 @@ +import PropTypes from 'prop-types'; + +const locationShape = PropTypes.shape({ + pathname: PropTypes.string.isRequired, + search: PropTypes.string.isRequired, + state: PropTypes.object, + action: PropTypes.string, + key: PropTypes.string +}); + +export default locationShape; diff --git a/frontend/src/Helpers/Props/Shapes/settingShape.js b/frontend/src/Helpers/Props/Shapes/settingShape.js new file mode 100644 index 000000000..cd672de27 --- /dev/null +++ b/frontend/src/Helpers/Props/Shapes/settingShape.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; + +const settingShape = { + value: PropTypes.oneOf([PropTypes.bool, PropTypes.number, PropTypes.string]), + warnings: PropTypes.arrayOf(PropTypes.string).isRequired, + errors: PropTypes.arrayOf(PropTypes.string).isRequired +}; + +export const arraySettingShape = { + ...settingShape, + value: PropTypes.array.isRequired +}; + +export const boolSettingShape = { + ...settingShape, + value: PropTypes.bool.isRequired +}; + +export const numberSettingShape = { + ...settingShape, + value: PropTypes.number.isRequired +}; + +export const stringSettingShape = { + ...settingShape, + value: PropTypes.string +}; + +export const tagSettingShape = { + ...settingShape, + value: PropTypes.arrayOf(PropTypes.number).isRequired +}; + +export default settingShape; diff --git a/frontend/src/Helpers/Props/align.js b/frontend/src/Helpers/Props/align.js new file mode 100644 index 000000000..f381959c6 --- /dev/null +++ b/frontend/src/Helpers/Props/align.js @@ -0,0 +1,5 @@ +export const LEFT = 'left'; +export const CENTER = 'center'; +export const RIGHT = 'right'; + +export const all = [LEFT, CENTER, RIGHT]; diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js new file mode 100644 index 000000000..68663ebfc --- /dev/null +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -0,0 +1,17 @@ +export const CONTAINS = 'contains'; +export const EQUAL = 'equal'; +export const GREATER_THAN = 'greaterThan'; +export const GREATER_THAN_OR_EQUAL = 'greaterThanOrEqual'; +export const LESS_THAN = 'lessThan'; +export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual'; +export const NOT_EQUAL = 'notEqual'; + +export const all = [ + CONTAINS, + EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + LESS_THAN, + LESS_THAN_OR_EQUAL, + NOT_EQUAL +]; diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js new file mode 100644 index 000000000..fbd0b8c6d --- /dev/null +++ b/frontend/src/Helpers/Props/icons.js @@ -0,0 +1,88 @@ +export const ACTIONS = 'fa fa-bolt'; +export const ACTIVITY = 'fa fa-clock-o'; +export const ADD = 'fa fa-plus'; +export const ALTERNATE_TITLES = 'fa fa-clone'; +export const ADVANCED_SETTINGS = 'fa fa-cog'; +export const ARROW_LEFT = 'fa fa-arrow-circle-left'; +export const ARROW_RIGHT = 'fa fa-arrow-circle-right'; +export const BACKUP = 'fa fa-file-archive-o'; +export const BUG = 'fa fa-bug'; +export const CALENDAR = 'fa fa-calendar'; +export const CALENDAR_O = 'fa fa-calendar-o'; +export const CARET_DOWN = 'fa fa-caret-down'; +export const CHECK = 'fa fa-check'; +export const CHECK_INDETERMINATE = 'fa fa-minus'; +export const CHECK_CIRCLE = 'fa fa-check-circle'; +export const CIRCLE_OUTLINE = 'fa fa-circle-o'; +export const CLEAR = 'fa fa-trash'; +export const CLIPBOARD = 'fa fa-clipboard'; +export const CLOSE = 'fa fa-times'; +export const COLLAPSE = 'fa fa-chevron-circle-up'; +export const COMPUTER = 'fa fa-desktop'; +export const DANGER = 'fa fa-exclamation-circle'; +export const DELETE = 'fa fa-trash'; +export const DOWNLOAD = 'fa fa-download'; +export const DOWNLOADED = 'fa fa-inbox'; +export const DOWNLOADING = 'fa fa-cloud-download'; +export const DRIVE = 'fa fa-hdd-o'; +export const EDIT = 'fa fa-wrench'; +export const EPISODE_FILE = 'fa fa-file-video-o'; +export const EXPAND = 'fa fa-chevron-circle-down'; +export const EXPAND_INDETERMINATE = 'fa fa-chevron-circle-right'; +export const EXTERNAL_LINK = 'fa fa-external-link'; +export const FATAL = 'fa fa-times-circle'; +export const FILE = 'fa fa-file-o'; +export const FILTER = 'fa fa-filter'; +export const FOLDER = 'fa fa-folder-o'; +export const FOLDER_OPEN = 'fa fa-folder-open'; +export const HEALTH = 'fa fa-medkit'; +export const HEART = 'fa fa-heart'; +export const HOUSEKEEPING = 'fa fa-home'; +export const INFO = 'fa fa-info-circle'; +export const INTERACTIVE = 'fa fa-user'; +export const LOGOUT = 'fa fa-sign-out'; +export const MISSING = 'fa fa-exclamation-triangle'; +export const MONITORED = 'fa fa-bookmark'; +export const NETWORK = 'fa fa-signal'; +export const NAVBAR_COLLAPSE = 'fa fa-bars'; +export const NOT_AIRED = 'fa fa-clock-o'; +export const ORGANIZE = 'fa fa-sitemap'; +export const OVERFLOW = 'fa fa-ellipsis-h'; +export const PAGE_FIRST = 'fa fa-fast-backward'; +export const PAGE_PREVIOUS = 'fa fa-backward'; +export const PAGE_NEXT = 'fa fa-forward'; +export const PAGE_LAST = 'fa fa-fast-forward'; +export const PARENT = 'fa fa-level-up'; +export const PAUSED = 'fa fa-pause'; +export const PENDING = 'fa fa-clock-o'; +export const PROFILE = 'fa fa-user'; +export const POSTER = 'fa fa-th'; +export const QUEUED = 'fa fa-cloud'; +export const QUICK = 'fa fa-rocket'; +export const REFRESH = 'fa fa-refresh'; +export const REMOVE = 'fa fa-remove'; +export const RESTART = 'fa fa-repeat'; +export const REORDER = 'fa fa-bars'; +export const RSS = 'fa fa-rss'; +export const SAVE = 'fa fa-floppy-o'; +export const SCHEDULED = 'fa fa-clock-o'; +export const SEARCH = 'fa fa-search'; +export const SERIES_CONTINUING = 'fa fa-play'; +export const SERIES_ENDED = 'fa fa-stop'; +export const SETTINGS = 'fa fa-cogs'; +export const SHUTDOWN = 'fa fa-power-off'; +export const SORT = 'fa fa-sort'; +export const SORT_ASCENDING = 'fa fa-sort-asc'; +export const SORT_DESCENDING = 'fa fa-sort-desc'; +export const SPIN = 'fa-spin'; +export const SPINNER = 'fa fa-spinner'; +export const SUBTRACT = 'fa fa-minus'; +export const SYSTEM = 'fa fa-laptop'; +export const TAGS = 'fa fa-tags'; +export const TBA = 'fa fa-question-circle'; +export const UNKNOWN = 'fa fa-question'; +export const UNMONITORED = 'fa fa-bookmark-o'; +export const UPDATE = 'fa fa-retweet'; +export const UNSAVED_SETTING = 'fa fa-dot-circle-o'; +export const VIEW = 'fa fa-eye'; +export const WARNING = 'fa fa-exclamation-triangle'; diff --git a/frontend/src/Helpers/Props/index.js b/frontend/src/Helpers/Props/index.js new file mode 100644 index 000000000..0a989a26f --- /dev/null +++ b/frontend/src/Helpers/Props/index.js @@ -0,0 +1,23 @@ +import * as align from './align'; +import * as inputTypes from './inputTypes'; +import * as filterTypes from './filterTypes'; +import * as icons from './icons'; +import * as kinds from './kinds'; +import * as messageTypes from './messageTypes'; +import * as sizes from './sizes'; +import * as scrollDirections from './scrollDirections'; +import * as sortDirections from './sortDirections'; +import * as tooltipPositions from './tooltipPositions'; + +export { + align, + inputTypes, + filterTypes, + icons, + kinds, + messageTypes, + sizes, + scrollDirections, + sortDirections, + tooltipPositions +}; diff --git a/frontend/src/Helpers/Props/inputTypes.js b/frontend/src/Helpers/Props/inputTypes.js new file mode 100644 index 000000000..b980dccaf --- /dev/null +++ b/frontend/src/Helpers/Props/inputTypes.js @@ -0,0 +1,33 @@ +export const CAPTCHA = 'captcha'; +export const CHECK = 'check'; +export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect'; +export const NUMBER = 'number'; +export const OAUTH = 'oauth'; +export const PASSWORD = 'password'; +export const PATH = 'path'; +export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; +export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect'; +export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; +export const SELECT = 'select'; +export const SERIES_TYPE_SELECT = 'seriesTypeSelect'; +export const TAG = 'tag'; +export const TEXT = 'text'; +export const TEXT_TAG = 'textTag'; + +export const all = [ + CAPTCHA, + CHECK, + MONITOR_EPISODES_SELECT, + NUMBER, + OAUTH, + PASSWORD, + PATH, + QUALITY_PROFILE_SELECT, + LANGUAGE_PROFILE_SELECT, + ROOT_FOLDER_SELECT, + SELECT, + SERIES_TYPE_SELECT, + TAG, + TEXT, + TEXT_TAG +]; diff --git a/frontend/src/Helpers/Props/kinds.js b/frontend/src/Helpers/Props/kinds.js new file mode 100644 index 000000000..cb2d5fabe --- /dev/null +++ b/frontend/src/Helpers/Props/kinds.js @@ -0,0 +1,19 @@ +export const DANGER = 'danger'; +export const DEFAULT = 'default'; +export const INFO = 'info'; +export const INVERSE = 'inverse'; +export const PRIMARY = 'primary'; +export const PURPLE = 'purple'; +export const SUCCESS = 'success'; +export const WARNING = 'warning'; + +export const all = [ + DANGER, + DEFAULT, + INFO, + INVERSE, + PRIMARY, + PURPLE, + SUCCESS, + WARNING +]; diff --git a/frontend/src/Helpers/Props/messageTypes.js b/frontend/src/Helpers/Props/messageTypes.js new file mode 100644 index 000000000..997354f9d --- /dev/null +++ b/frontend/src/Helpers/Props/messageTypes.js @@ -0,0 +1,11 @@ +export const ERROR = 'error'; +export const INFO = 'info'; +export const SUCCESS = 'success'; +export const WARNING = 'warning'; + +export const all = [ + ERROR, + INFO, + SUCCESS, + WARNING +]; diff --git a/frontend/src/Helpers/Props/scrollDirections.js b/frontend/src/Helpers/Props/scrollDirections.js new file mode 100644 index 000000000..5e4a4fe08 --- /dev/null +++ b/frontend/src/Helpers/Props/scrollDirections.js @@ -0,0 +1,5 @@ +export const NONE = 'none'; +export const HORIZONTAL = 'horizontal'; +export const VERTICAL = 'vertical'; + +export const all = [NONE, HORIZONTAL, VERTICAL]; diff --git a/frontend/src/Helpers/Props/sizes.js b/frontend/src/Helpers/Props/sizes.js new file mode 100644 index 000000000..e572e8fc8 --- /dev/null +++ b/frontend/src/Helpers/Props/sizes.js @@ -0,0 +1,5 @@ +export const SMALL = 'small'; +export const MEDIUM = 'medium'; +export const LARGE = 'large'; + +export const all = [SMALL, MEDIUM, LARGE]; diff --git a/frontend/src/Helpers/Props/sortDirections.js b/frontend/src/Helpers/Props/sortDirections.js new file mode 100644 index 000000000..ff3b17bb6 --- /dev/null +++ b/frontend/src/Helpers/Props/sortDirections.js @@ -0,0 +1,4 @@ +export const ASCENDING = 'ascending'; +export const DESCENDING = 'descending'; + +export const all = [ASCENDING, DESCENDING]; diff --git a/frontend/src/Helpers/Props/tooltipPositions.js b/frontend/src/Helpers/Props/tooltipPositions.js new file mode 100644 index 000000000..bca3c4ed4 --- /dev/null +++ b/frontend/src/Helpers/Props/tooltipPositions.js @@ -0,0 +1,11 @@ +export const TOP = 'top'; +export const RIGHT = 'right'; +export const BOTTOM = 'bottom'; +export const LEFT = 'left'; + +export const all = [ + TOP, + RIGHT, + BOTTOM, + LEFT +]; diff --git a/frontend/src/Helpers/dragTypes.js b/frontend/src/Helpers/dragTypes.js new file mode 100644 index 000000000..ed6ba080d --- /dev/null +++ b/frontend/src/Helpers/dragTypes.js @@ -0,0 +1,3 @@ +export const QUALITY_PROFILE_ITEM = 'qualityProfileItem'; +export const DELAY_PROFILE = 'delayProfile'; +export const TABLE_COLUMN = 'tableColumn'; diff --git a/frontend/src/Helpers/elementChildren.js b/frontend/src/Helpers/elementChildren.js new file mode 100644 index 000000000..1c10b2f0e --- /dev/null +++ b/frontend/src/Helpers/elementChildren.js @@ -0,0 +1,149 @@ +// https://github.com/react-bootstrap/react-element-children + +import React from 'react'; + +/** + * Iterates through children that are typically specified as `props.children`, + * but only maps over children that are "valid components". + * + * The mapFunction provided index will be normalised to the components mapped, + * so an invalid component would not increase the index. + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for func. + * @return {object} Object containing the ordered map of results. + */ +export function map(children, func, context) { + let index = 0; + + return React.Children.map(children, (child) => { + if (!React.isValidElement(child)) { + return child; + } + + return func.call(context, child, index++); + }); +} + +/** + * Iterates through children that are "valid components". + * + * The provided forEachFunc(child, index) will be called for each + * leaf child with the index reflecting the position relative to "valid components". + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for context. + */ +export function forEach(children, func, context) { + let index = 0; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; + } + + func.call(context, child, index++); + }); +} + +/** + * Count the number of "valid components" in the Children container. + * + * @param {?*} children Children tree container. + * @returns {number} + */ +export function count(children) { + let result = 0; + + React.Children.forEach(children, (child) => { + if (!React.isValidElement(child)) { + return; + } + + ++result; + }); + + return result; +} + +/** + * Finds children that are typically specified as `props.children`, + * but only iterates over children that are "valid components". + * + * The provided forEachFunc(child, index) will be called for each + * leaf child with the index reflecting the position relative to "valid components". + * + * @param {?*} children Children tree container. + * @param {function(*, int)} func. + * @param {*} context Context for func. + * @returns {array} of children that meet the func return statement + */ +export function filter(children, func, context) { + const result = []; + + forEach(children, (child, index) => { + if (func.call(context, child, index)) { + result.push(child); + } + }); + + return result; +} + +export function find(children, func, context) { + let result = null; + + forEach(children, (child, index) => { + if (result) { + return; + } + if (func.call(context, child, index)) { + result = child; + } + }); + + return result; +} + +export function every(children, func, context) { + let result = true; + + forEach(children, (child, index) => { + if (!result) { + return; + } + if (!func.call(context, child, index)) { + result = false; + } + }); + + return result; +} + +export function some(children, func, context) { + let result = false; + + forEach(children, (child, index) => { + if (result) { + return; + } + + if (func.call(context, child, index)) { + result = true; + } + }); + + return result; +} + +export function toArray(children) { + const result = []; + + forEach(children, (child) => { + result.push(child); + }); + + return result; +} diff --git a/frontend/src/Helpers/getDisplayName.js b/frontend/src/Helpers/getDisplayName.js new file mode 100644 index 000000000..512702c87 --- /dev/null +++ b/frontend/src/Helpers/getDisplayName.js @@ -0,0 +1,3 @@ +export default function getDisplayName(Component) { + return Component.displayName || Component.name || 'Component'; +} diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js b/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js new file mode 100644 index 000000000..31ea74d23 --- /dev/null +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectEpisodeModalContentConnector from './SelectEpisodeModalContentConnector'; + +class SelectEpisodeModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectEpisodeModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectEpisodeModal; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.js b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.js new file mode 100644 index 000000000..c6b6154ff --- /dev/null +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContent.js @@ -0,0 +1,183 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import SelectEpisodeRow from './SelectEpisodeRow'; + +const columns = [ + { + name: 'episodeNumber', + label: '#', + isSortable: true, + isVisible: true + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'airDate', + label: 'Air Date', + isVisible: true + } +]; + +class SelectEpisodeModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onEpisodesSelect = () => { + this.props.onEpisodesSelect(this.getSelectedIds()); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + onSortPress, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + const errorMessage = error && error.message || 'Unable to load episodes'; + + return ( + + + Manual Import - Select Episode(s) + + + + { + isFetching && + + } + + { + error && +
{errorMessage}
+ } + + { + isPopulated && !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + + { + isPopulated && !items.length && + 'No episodes were found for the selected season' + } +
+ + + + + + +
+ ); + } +} + +SelectEpisodeModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + onEpisodesSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectEpisodeModalContent; diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContentConnector.js b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContentConnector.js new file mode 100644 index 000000000..e107e9dd5 --- /dev/null +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeModalContentConnector.js @@ -0,0 +1,103 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchEpisodes, setEpisodesSort, clearEpisodes } from 'Store/Actions/episodeActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import SelectEpisodeModalContent from './SelectEpisodeModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (episodes) => { + return episodes; + } + ); +} + +const mapDispatchToProps = { + fetchEpisodes, + setEpisodesSort, + clearEpisodes, + updateInteractiveImportItem +}; + +class SelectEpisodeModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + artistId, + seasonNumber + } = this.props; + + this.props.fetchEpisodes({ artistId, seasonNumber }); + } + + componentWillUnmount() { + // This clears the episodes for the queue and hides the queue + // We'll need another place to store episodes for manual import + this.props.clearEpisodes(); + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setEpisodesSort({ sortKey, sortDirection }); + } + + onEpisodesSelect = (episodeIds) => { + const episodes = _.reduce(this.props.items, (acc, item) => { + if (episodeIds.indexOf(item.id) > -1) { + acc.push(item); + } + + return acc; + }, []); + + this.props.updateInteractiveImportItem({ + id: this.props.id, + episodes: _.sortBy(episodes, 'episodeNumber') + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectEpisodeModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + artistId: PropTypes.number.isRequired, + seasonNumber: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchEpisodes: PropTypes.func.isRequired, + setEpisodesSort: PropTypes.func.isRequired, + clearEpisodes: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'episodes' } + )(SelectEpisodeModalContentConnector); diff --git a/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js b/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js new file mode 100644 index 000000000..ba455121a --- /dev/null +++ b/frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js @@ -0,0 +1,67 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; + +class SelectEpisodeRow extends Component { + + // + // Listeners + + onPress = () => { + const { + id, + isSelected + } = this.props; + + this.props.onSelectedChange({ id, value: !isSelected }); + } + + // + // Render + + render() { + const { + id, + episodeNumber, + title, + airDate, + isSelected, + onSelectedChange + } = this.props; + + return ( + + + + + {episodeNumber} + + + + {title} + + + + {airDate} + + + ); + } +} + +SelectEpisodeRow.propTypes = { + id: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + airDate: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default SelectEpisodeRow; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css new file mode 100644 index 000000000..86418a2dd --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.css @@ -0,0 +1,24 @@ +.recentFoldersContainer { + margin-top: 15px; +} + +.buttonsContainer { + margin-top: 30px; +} + +.buttonContainer { + display: flex; + justify-content: center; + + margin-top: 10px; +} + +.button { + composes: button from 'Components/Link/Button.css'; + + width: 300px; +} + +.buttonIcon { + margin-right: 5px; +} diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js new file mode 100644 index 000000000..cabd33d7c --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContent.js @@ -0,0 +1,161 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import PathInputConnector from 'Components/Form/PathInputConnector'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import RecentFolderRow from './RecentFolderRow'; +import styles from './InteractiveImportSelectFolderModalContent.css'; + +const recentFoldersColumns = [ + { + name: 'folder', + label: 'Folder' + }, + { + name: 'lastUsed', + label: 'Last Used' + } +]; + +class InteractiveImportSelectFolderModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + folder: '' + }; + } + + // + // Listeners + + onPathChange = ({ value }) => { + this.setState({ folder: value }); + } + + onRecentPathPress = (folder) => { + this.setState({ folder }); + } + + onQuickImportPress = () => { + this.props.onQuickImportPress(this.state.folder); + } + + onInteractiveImportPress = () => { + this.props.onInteractiveImportPress(this.state.folder); + } + + // + // Render + + render() { + const { + recentFolders, + onModalClose + } = this.props; + + const folder = this.state.folder; + + return ( + + + Manual Import - Select Folder + + + + + + { + !!recentFolders.length && +
+ + + { + recentFolders.map((recentFolder) => { + return ( + + ); + }) + } + +
+
+ } + +
+
+ +
+ +
+ +
+
+
+ + + + +
+ ); + } +} + +InteractiveImportSelectFolderModalContent.propTypes = { + recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired, + onQuickImportPress: PropTypes.func.isRequired, + onInteractiveImportPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default InteractiveImportSelectFolderModalContent; diff --git a/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js new file mode 100644 index 000000000..2df7b1c4c --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/InteractiveImportSelectFolderModalContentConnector.js @@ -0,0 +1,73 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { addRecentFolder } from 'Store/Actions/interactiveImportActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.interactiveImport.recentFolders, + (recentFolders) => { + return { + recentFolders + }; + } + ); +} + +const mapDispatchToProps = { + addRecentFolder, + executeCommand +}; + +class InteractiveImportSelectFolderModalContentConnector extends Component { + + // + // Listeners + + onQuickImportPress = (folder) => { + this.props.addRecentFolder({ folder }); + + this.props.executeCommand({ + name: commandNames.DOWNLOADED_EPSIODES_SCAN, + path: folder + }); + + this.props.onModalClose(); + } + + onInteractiveImportPress = (folder) => { + this.props.addRecentFolder({ folder }); + this.props.onFolderSelect(folder); + } + + // + // Render + + render() { + if (this.path) { + return null; + } + + return ( + + ); + } +} + +InteractiveImportSelectFolderModalContentConnector.propTypes = { + path: PropTypes.string, + onFolderSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + addRecentFolder: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportSelectFolderModalContentConnector); diff --git a/frontend/src/InteractiveImport/Folder/RecentFolderRow.js b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js new file mode 100644 index 000000000..bc32f5749 --- /dev/null +++ b/frontend/src/InteractiveImport/Folder/RecentFolderRow.js @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; + +class RecentFolderRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onPress(this.props.folder); + } + + // + // Render + + render() { + const { + folder, + lastUsed + } = this.props; + + return ( + + {folder} + + + + ); + } +} + +RecentFolderRow.propTypes = { + folder: PropTypes.string.isRequired, + lastUsed: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default RecentFolderRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css new file mode 100644 index 000000000..964faac31 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css @@ -0,0 +1,45 @@ +.footer { + composes: modalFooter from 'Components/Modal/ModalFooter.css'; + + justify-content: space-between; + padding: 15px; +} + +.leftButtons, +.centerButtons, +.rightButtons { + display: flex; + flex: 1 0 33%; + flex-wrap: wrap; +} + +.centerButtons { + justify-content: center; +} + +.rightButtons { + justify-content: flex-end; +} + +.importMode { + composes: select from 'Components/Form/SelectInput.css'; + + width: auto; +} + +.errorMessage { + color: $dangerColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .footer { + a, + button { + margin-left: 0; + + &:first-child { + margin-bottom: 5px; + } + } + } +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js new file mode 100644 index 000000000..108220c5e --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js @@ -0,0 +1,320 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { icons, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Icon from 'Components/Icon'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SelectInput from 'Components/Form/SelectInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; +import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; +import InteractiveImportRow from './InteractiveImportRow'; +import styles from './InteractiveImportModalContent.css'; + +const columns = [ + { + name: 'relativePath', + label: 'Relative Path', + isSortable: true, + isVisible: true + }, + { + name: 'series', + label: 'Series', + isSortable: true, + isVisible: true + }, + { + name: 'season', + label: 'Season', + isVisible: true + }, + { + name: 'episodes', + label: 'Episode(s)', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'size', + label: 'Size', + isVisible: true + }, + { + name: 'rejections', + label: React.createElement(Icon, { + name: icons.DANGER, + kind: kinds.DANGER + }), + isVisible: true + } +]; + +class InteractiveImportModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + invalidRowsSelected: [], + isSelectSeriesModalOpen: false, + isSelectSeasonModalOpen: false + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onValidRowChange = (id, isValid) => { + this.setState((state) => { + if (isValid) { + return { + invalidRowsSelected: _.without(state.invalidRowsSelected, id) + }; + } + + return { + invalidRowsSelected: [...state.invalidRowsSelected, id] + }; + }); + } + + onImportSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onImportSelectedPress(selected, this.state.importMode); + } + + onImportModeChange = ({ value }) => { + this.props.onImportModeChange(value); + } + + onSelectSeriesPress = () => { + this.setState({ isSelectSeriesModalOpen: true }); + } + + onSelectSeasonPress = () => { + this.setState({ isSelectSeasonModalOpen: true }); + } + + onSelectSeriesModalClose = () => { + this.setState({ isSelectSeriesModalOpen: false }); + } + + onSelectSeasonModalClose = () => { + this.setState({ isSelectSeasonModalOpen: false }); + } + + // + // Render + + render() { + const { + downloadId, + title, + folder, + isFetching, + isPopulated, + error, + items, + sortKey, + sortDirection, + importMode, + interactiveImportErrorMessage, + onSortPress, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + invalidRowsSelected, + isSelectSeriesModalOpen, + isSelectSeasonModalOpen + } = this.state; + + const selectedIds = this.getSelectedIds(); + const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null; + const errorMessage = error && error.message || 'Unable to load manual import items'; + + const importModeOptions = [ + { key: 'move', value: 'Move Files' }, + { key: 'copy', value: 'Copy Files' } + ]; + + return ( + + + Manual Import - {title || folder} + + + + { + isFetching && + + } + + { + error && +
{errorMessage}
+ } + + { + isPopulated && !!items.length && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } + + { + isPopulated && !items.length && + 'No video files were found in the selected folder' + } +
+ + + { + !downloadId && +
+ +
+ } + +
+ + + +
+ +
+ + + { + interactiveImportErrorMessage && + {interactiveImportErrorMessage} + } + + +
+
+ + + + +
+ ); + } +} + +InteractiveImportModalContent.propTypes = { + downloadId: PropTypes.string, + importMode: PropTypes.string.isRequired, + title: PropTypes.string, + folder: PropTypes.string, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.string, + interactiveImportErrorMessage: PropTypes.string, + onSortPress: PropTypes.func.isRequired, + onImportModeChange: PropTypes.func.isRequired, + onImportSelectedPress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +InteractiveImportModalContent.defaultProps = { + importMode: 'move' +}; + +export default InteractiveImportModalContent; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js new file mode 100644 index 000000000..030bb5c96 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContentConnector.js @@ -0,0 +1,152 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import InteractiveImportModalContent from './InteractiveImportModalContent'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (interactiveImport) => { + return interactiveImport; + } + ); +} + +const mapDispatchToProps = { + fetchInteractiveImportItems, + setInteractiveImportSort, + setInteractiveImportMode, + clearInteractiveImport, + executeCommand +}; + +class InteractiveImportModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + interactiveImportErrorMessage: null + }; + } + + componentDidMount() { + const { + downloadId, + folder + } = this.props; + + this.props.fetchInteractiveImportItems({ downloadId, folder }); + } + + componentWillUnmount() { + this.props.clearInteractiveImport(); + } + + // + // Listeners + + onSortPress = (sortKey, sortDirection) => { + this.props.setInteractiveImportSort({ sortKey, sortDirection }); + } + + onImportModeChange = (importMode) => { + this.props.setInteractiveImportMode({ importMode }); + } + + onImportSelectedPress = (selected, importMode) => { + const files = []; + + _.forEach(this.props.items, (item) => { + const isSelected = selected.indexOf(item.id) > -1; + + if (isSelected) { + const { + series, + seasonNumber, + episodes, + quality + } = item; + + if (!series) { + this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' }); + return false; + } + + if (isNaN(seasonNumber)) { + this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' }); + return false; + } + + if (!episodes || !episodes.length) { + this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' }); + return false; + } + + files.push({ + path: item.path, + artistId: series.id, + episodeIds: _.map(episodes, 'id'), + quality, + downloadId: this.props.downloadId + }); + } + }); + + if (!files.length) { + return; + } + + this.props.executeCommand({ + name: commandNames.INTERACTIVE_IMPORT, + files, + importMode + }); + + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +InteractiveImportModalContentConnector.propTypes = { + downloadId: PropTypes.string, + folder: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchInteractiveImportItems: PropTypes.func.isRequired, + setInteractiveImportSort: PropTypes.func.isRequired, + clearInteractiveImport: PropTypes.func.isRequired, + setInteractiveImportMode: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'interactiveImport' } + )(InteractiveImportModalContentConnector); diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css new file mode 100644 index 000000000..22234718f --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.css @@ -0,0 +1,11 @@ +.relativePath { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + word-break: break-all; +} + +.quality { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + text-align: center; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js new file mode 100644 index 000000000..6638c7879 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js @@ -0,0 +1,294 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { icons, kinds, tooltipPositions } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import Popover from 'Components/Tooltip/Popover'; +import EpisodeQuality from 'Episode/EpisodeQuality'; +import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal'; +import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal'; +import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal'; +import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal'; +import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder'; +import styles from './InteractiveImportRow.css'; + +class InteractiveImportRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isSelectSeriesModalOpen: false, + isSelectSeasonModalOpen: false, + isSelectEpisodeModalOpen: false, + isSelectQualityModalOpen: false + }; + } + + componentDidMount() { + const { + id, + series, + seasonNumber, + episodes, + quality + } = this.props; + + if (series && seasonNumber !== undefined && episodes.length && quality) { + this.props.onSelectedChange({ id, value: true }); + } + } + + componentDidUpdate(prevProps) { + const { + id, + series, + seasonNumber, + episodes, + quality, + isSelected, + onValidRowChange + } = this.props; + + if (prevProps.isSelected === isSelected) { + return; + } + + const isValid = !!(series && seasonNumber != null && episodes.length && quality); + + if (isSelected && !isValid) { + onValidRowChange(id, false); + } else { + onValidRowChange(id, true); + } + } + + // + // Control + + selectRowAfterChange = (value) => { + const { + id, + isSelected + } = this.props; + + if (!isSelected && value === true) { + this.props.onSelectedChange({ id, value }); + } + } + + // + // Listeners + + onSelectSeriesPress = () => { + this.setState({ isSelectSeriesModalOpen: true }); + } + + onSelectSeasonPress = () => { + this.setState({ isSelectSeasonModalOpen: true }); + } + + onSelectEpisodePress = () => { + this.setState({ isSelectEpisodeModalOpen: true }); + } + + onSelectQualityPress = () => { + this.setState({ isSelectQualityModalOpen: true }); + } + + onSelectSeriesModalClose = (changed) => { + this.setState({ isSelectSeriesModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectSeasonModalClose = (changed) => { + this.setState({ isSelectSeasonModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectEpisodeModalClose = (changed) => { + this.setState({ isSelectEpisodeModalOpen: false }); + this.selectRowAfterChange(changed); + } + + onSelectQualityModalClose = (changed) => { + this.setState({ isSelectQualityModalOpen: false }); + this.selectRowAfterChange(changed); + } + + // + // Render + + render() { + const { + id, + relativePath, + series, + seasonNumber, + episodes, + quality, + size, + rejections, + isSelected, + onSelectedChange + } = this.props; + + const { + isSelectSeriesModalOpen, + isSelectSeasonModalOpen, + isSelectEpisodeModalOpen, + isSelectQualityModalOpen + } = this.state; + + const seriesTitle = series ? series.title : ''; + const episodeNumbers = episodes.map((episode) => episode.episodeNumber) + .join(', '); + + const showSeriesPlaceholder = isSelected && !series; + const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber); + const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length; + + return ( + + + + + {relativePath} + + + + { + showSeriesPlaceholder ? : seriesTitle + } + + + + { + showSeasonNumberPlaceholder ? : seasonNumber + } + + + + { + showEpisodeNumbersPlaceholder ? : episodeNumbers + } + + + + + + + + {formatBytes(size)} + + + + { + !!rejections.length && + + } + title="Release Rejected" + body={ +
    + { + rejections.map((rejection, index) => { + return ( +
  • + {rejection.reason} +
  • + ); + }) + } +
+ } + position={tooltipPositions.LEFT} + /> + } +
+ + + + + + + + 1} + real={quality.revision.real > 0} + onModalClose={this.onSelectQualityModalClose} + /> +
+ ); + } + +} + +InteractiveImportRow.propTypes = { + id: PropTypes.number.isRequired, + relativePath: PropTypes.string.isRequired, + series: PropTypes.object, + seasonNumber: PropTypes.number, + episodes: PropTypes.arrayOf(PropTypes.object).isRequired, + quality: PropTypes.object, + size: PropTypes.number.isRequired, + rejections: PropTypes.arrayOf(PropTypes.object).isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onValidRowChange: PropTypes.func.isRequired +}; + +InteractiveImportRow.defaultProps = { + episodes: [] +}; + +export default InteractiveImportRow; diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css new file mode 100644 index 000000000..941988144 --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.css @@ -0,0 +1,7 @@ +.placeholder { + display: inline-block; + margin: -8px 0; + width: 100%; + height: 25px; + border: 2px dashed $dangerColor; +} diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js new file mode 100644 index 000000000..250106c0c --- /dev/null +++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRowCellPlaceholder.js @@ -0,0 +1,10 @@ +import React from 'react'; +import styles from './InteractiveImportRowCellPlaceholder.css'; + +function InteractiveImportRowCellPlaceholder() { + return ( + + ); +} + +export default InteractiveImportRowCellPlaceholder; diff --git a/frontend/src/InteractiveImport/InteractiveImportModal.js b/frontend/src/InteractiveImport/InteractiveImportModal.js new file mode 100644 index 000000000..0ea6fd9cb --- /dev/null +++ b/frontend/src/InteractiveImport/InteractiveImportModal.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector'; +import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector'; + +class InteractiveImportModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + folder: null + }; + } + + componentDidUpdate(prevProps) { + if (prevProps.isOpen && !this.props.isOpen) { + this.setState({ folder: null }); + } + } + + // + // Listeners + + onFolderSelect = (folder) => { + this.setState({ folder }); + } + + // + // Render + + render() { + const { + isOpen, + folder, + downloadId, + onModalClose, + ...otherProps + } = this.props; + + const folderPath = folder || this.state.folder; + + return ( + + { + folderPath || downloadId ? + : + + } + + ); + } +} + +InteractiveImportModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + folder: PropTypes.string, + downloadId: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default InteractiveImportModal; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModal.js b/frontend/src/InteractiveImport/Quality/SelectQualityModal.js new file mode 100644 index 000000000..d3e31d2dd --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectQualityModalContentConnector from './SelectQualityModalContentConnector'; + +class SelectQualityModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectQualityModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModal; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js new file mode 100644 index 000000000..8649763a9 --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +class SelectQualityModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + const { + qualityId, + proper, + real + } = props; + + this.state = { + qualityId, + proper, + real + }; + } + + // + // Listeners + + onQualityChange = ({ value }) => { + this.setState({ qualityId: parseInt(value) }); + } + + onProperChange = ({ value }) => { + this.setState({ proper: value }); + } + + onRealChange = ({ value }) => { + this.setState({ real: value }); + } + + onQualitySelect = () => { + this.props.onQualitySelect(this.state); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + onModalClose + } = this.props; + + const { + qualityId, + proper, + real + } = this.state; + + const qualityOptions = items.map(({ quality }) => { + return { + key: quality.id, + value: quality.name + }; + }); + + return ( + + + Manual Import - Select Quality + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to load qualities
+ } + + { + isPopulated && !error && +
+ + Quality + + + + + + Proper + + + + + + Real + + + +
+ } +
+ + + + + + +
+ ); + } +} + +SelectQualityModalContent.propTypes = { + qualityId: PropTypes.number.isRequired, + proper: PropTypes.bool.isRequired, + real: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onQualitySelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectQualityModalContent; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js new file mode 100644 index 000000000..e50b3af2e --- /dev/null +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContentConnector.js @@ -0,0 +1,94 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import SelectQualityModalContent from './SelectQualityModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityProfiles, + (qualityProfiles) => { + const { + isFetchingSchema: isFetching, + schemaPopulated: isPopulated, + schemaError: error, + schema + } = qualityProfiles; + + return { + isFetching, + isPopulated, + error, + items: schema.items || [] + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfileSchema, + updateInteractiveImportItem +}; + +class SelectQualityModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount = () => { + if (!this.props.isPopulated) { + this.props.fetchQualityProfileSchema(); + } + } + + // + // Listeners + + onQualitySelect = ({ qualityId, proper, real }) => { + const quality = _.find(this.props.items, + (item) => item.quality.id === qualityId).quality; + + const revision = { + version: proper ? 2 : 1, + real: real ? 1 : 0 + }; + + this.props.updateInteractiveImportItem({ + id: this.props.id, + quality: { + quality, + revision + } + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectQualityModalContentConnector.propTypes = { + id: PropTypes.number.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + fetchQualityProfileSchema: PropTypes.func.isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector); diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonModal.js b/frontend/src/InteractiveImport/Season/SelectSeasonModal.js new file mode 100644 index 000000000..9de9ee493 --- /dev/null +++ b/frontend/src/InteractiveImport/Season/SelectSeasonModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectSeasonModalContentConnector from './SelectSeasonModalContentConnector'; + +class SelectSeasonModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectSeasonModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectSeasonModal; diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.js b/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.js new file mode 100644 index 000000000..267174491 --- /dev/null +++ b/frontend/src/InteractiveImport/Season/SelectSeasonModalContent.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import SelectSeasonRow from './SelectSeasonRow'; + +class SelectSeasonModalContent extends Component { + + // + // Render + + render() { + const { + items, + onSeasonSelect, + onModalClose + } = this.props; + + return ( + + + Manual Import - Select Season + + + + { + items.map((item) => { + return ( + + ); + }) + } + + + + + + + ); + } +} + +SelectSeasonModalContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeasonSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectSeasonModalContent; diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonModalContentConnector.js b/frontend/src/InteractiveImport/Season/SelectSeasonModalContentConnector.js new file mode 100644 index 000000000..65304b339 --- /dev/null +++ b/frontend/src/InteractiveImport/Season/SelectSeasonModalContentConnector.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import SelectSeasonModalContent from './SelectSeasonModalContent'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (series) => { + return { + items: series.seasons + }; + } + ); +} + +const mapDispatchToProps = { + updateInteractiveImportItem +}; + +class SelectSeasonModalContentConnector extends Component { + + // + // Listeners + + onSeasonSelect = (seasonNumber) => { + this.props.ids.forEach((id) => { + this.props.updateInteractiveImportItem({ + id, + seasonNumber, + episodes: [] + }); + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectSeasonModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + artistId: PropTypes.number.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeasonModalContentConnector); diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonRow.css b/frontend/src/InteractiveImport/Season/SelectSeasonRow.css new file mode 100644 index 000000000..c43d879f4 --- /dev/null +++ b/frontend/src/InteractiveImport/Season/SelectSeasonRow.css @@ -0,0 +1,4 @@ +.season { + padding: 8px; + border-bottom: 1px solid $borderColor; +} diff --git a/frontend/src/InteractiveImport/Season/SelectSeasonRow.js b/frontend/src/InteractiveImport/Season/SelectSeasonRow.js new file mode 100644 index 000000000..d6cf5aea1 --- /dev/null +++ b/frontend/src/InteractiveImport/Season/SelectSeasonRow.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './SelectSeasonRow.css'; + +class SelectSeasonRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onSeasonSelect(this.props.seasonNumber); + } + + // + // Render + + render() { + const seasonNumber = this.props.seasonNumber; + + return ( + + { + seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}` + } + + ); + } +} + +SelectSeasonRow.propTypes = { + seasonNumber: PropTypes.number.isRequired, + onSeasonSelect: PropTypes.func.isRequired +}; + +export default SelectSeasonRow; diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModal.js b/frontend/src/InteractiveImport/Series/SelectSeriesModal.js new file mode 100644 index 000000000..1a1ceffca --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModal.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Modal from 'Components/Modal/Modal'; +import SelectSeriesModalContentConnector from './SelectSeriesModalContentConnector'; + +class SelectSeriesModal extends Component { + + // + // Render + + render() { + const { + isOpen, + onModalClose, + ...otherProps + } = this.props; + + return ( + + + + ); + } +} + +SelectSeriesModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectSeriesModal; diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css new file mode 100644 index 000000000..d297be072 --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.css @@ -0,0 +1,18 @@ +.modalBody { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + display: flex; + flex: 1 1 auto; + flex-direction: column; +} + +.filterInput { + composes: text from 'Components/Form/TextInput.css'; + + flex: 0 0 auto; + margin-bottom: 20px; +} + +.scroller { + flex: 1 1 auto; +} diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js new file mode 100644 index 000000000..e7456d73f --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContent.js @@ -0,0 +1,99 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import TextInput from 'Components/Form/TextInput'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import SelectSeriesRow from './SelectSeriesRow'; +import styles from './SelectSeriesModalContent.css'; + +class SelectSeriesModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + filter: '' + }; + } + + // + // Listeners + + onFilterChange = ({ value }) => { + this.setState({ filter: value.toLowerCase() }); + } + + // + // Render + + render() { + const { + items, + onSeriesSelect, + onModalClose + } = this.props; + + const filter = this.state.filter; + + return ( + + + Manual Import - Select Series + + + + + + + { + items.map((item) => { + return item.title.toLowerCase().includes(filter) ? + ( + + ) : + null; + }) + } + + + + + + + + ); + } +} + +SelectSeriesModalContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onSeriesSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default SelectSeriesModalContent; diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesModalContentConnector.js b/frontend/src/InteractiveImport/Series/SelectSeriesModalContentConnector.js new file mode 100644 index 000000000..daf7ca3f6 --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesModalContentConnector.js @@ -0,0 +1,65 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import SelectSeriesModalContent from './SelectSeriesModalContent'; + +function createMapStateToProps() { + return createSelector( + createAllSeriesSelector(), + (items) => { + return { + items + }; + } + ); +} + +const mapDispatchToProps = { + updateInteractiveImportItem +}; + +class SelectSeriesModalContentConnector extends Component { + + // + // Listeners + + onSeriesSelect = (artistId) => { + const series = _.find(this.props.items, { id: artistId }); + + this.props.ids.forEach((id) => { + this.props.updateInteractiveImportItem({ + id, + series, + seasonNumber: undefined, + episodes: [] + }); + }); + + this.props.onModalClose(true); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SelectSeriesModalContentConnector.propTypes = { + ids: PropTypes.arrayOf(PropTypes.number).isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + updateInteractiveImportItem: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeriesModalContentConnector); diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.css b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css new file mode 100644 index 000000000..f2573d585 --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesRow.css @@ -0,0 +1,4 @@ +.series { + padding: 8px; + border-bottom: 1px solid $borderColor; +} diff --git a/frontend/src/InteractiveImport/Series/SelectSeriesRow.js b/frontend/src/InteractiveImport/Series/SelectSeriesRow.js new file mode 100644 index 000000000..49af64ecf --- /dev/null +++ b/frontend/src/InteractiveImport/Series/SelectSeriesRow.js @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import styles from './SelectSeriesRow.css'; + +class SelectSeriesRow extends Component { + + // + // Listeners + + onPress = () => { + this.props.onSeriesSelect(this.props.id); + } + + // + // Render + + render() { + return ( + + {this.props.title} + + ); + } +} + +SelectSeriesRow.propTypes = { + id: PropTypes.number.isRequired, + title: PropTypes.string.isRequired, + onSeriesSelect: PropTypes.func.isRequired +}; + +export default SelectSeriesRow; diff --git a/frontend/src/JsLibraries/jquery.js b/frontend/src/JsLibraries/jquery.js new file mode 100644 index 000000000..aa4d89cb6 --- /dev/null +++ b/frontend/src/JsLibraries/jquery.js @@ -0,0 +1,9842 @@ +/*! + * jQuery JavaScript Library v2.2.2 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-03-17T17:51Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +//"use strict"; +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "2.2.2", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; + }, + + isPlainObject: function( obj ) { + var key; + + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call( obj, "constructor" ) && + !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only isSaving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.2.1 + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-17 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * isDeleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var m = context.getElementById( id ); + return m ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + docElem.appendChild( div ).innerHTML = "
" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( function() { + + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // Add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ); + } else { + --remaining; + } + } + } + + // If we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +} ); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when isDeleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data, camelKey; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName( "tbody" )[ 0 ] || + elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + + // Keep domManip exposed until 3.0 (gh-2225) + domManip: domManip, + + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); + + +var iframe, + elemdisplay = { + + // Support: Firefox + // We have to pre-define these values for FF (#10227) + HTML: "block", + BODY: "block" + }; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ + +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + display = jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = ( iframe || jQuery( ""); + + $("body").append(tempFrame); + tempFrame.remove(); + tempFrame = null; + }, loadingFixInterval); + } + + attachedTo++; + } + }, + cancel: function() { + // Only clear the interval if there's only one more object that the loadPreventer is attachedTo + if (attachedTo === 1) { + window.clearInterval(loadingFixIntervalId); + } + + if (attachedTo > 0) { + attachedTo--; + } + } + }; + })(); + + signalR.transports.foreverFrame = { + name: "foreverFrame", + + supportsKeepAlive: true, + + timeOut: 3000, + + start: function(connection, onSuccess, onFailed) { + var that = this, + frameId = (transportLogic.foreverFrame.count += 1), + url, + frame = $(""); + + if (window.EventSource) { + // If the browser supports SSE, don't use Forever Frame + if (onFailed) { + connection.log("This browser supports SSE, skipping Forever Frame."); + onFailed(); + } + return; + } + + // Start preventing loading icon + // This will only perform work if the loadPreventer is not attached to another connection. + loadPreventer.prevent(); + + // Build the url + url = transportLogic.getUrl(connection, this.name); + url += "&frameId=" + frameId; + + // Set body prior to setting URL to avoid caching issues. + $("body").append(frame); + + frame.prop("src", url); + transportLogic.foreverFrame.connections[frameId] = connection; + + connection.log("Binding to iframe's readystatechange event."); + frame.bind("readystatechange", function() { + if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { + connection.log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting"); + + that.reconnect(connection); + } + }); + + connection.frame = frame[0]; + connection.frameId = frameId; + + if (onSuccess) { + connection.onSuccess = onSuccess; + } + + // After connecting, if after the specified timeout there's no response stop the connection + // and raise on failed + window.setTimeout(function() { + if (connection.onSuccess) { + connection.log("Failed to connect using forever frame source, it timed out after " + that.timeOut + "ms."); + that.stop(connection); + + if (onFailed) { + onFailed(); + } + } + }, that.timeOut); + }, + + reconnect: function(connection) { + var that = this; + window.setTimeout(function() { + if (connection.frame && transportLogic.ensureReconnectingState(connection)) { + var frame = connection.frame, + src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; + connection.log("Updating iframe src to '" + src + "'."); + frame.src = src; + } + }, connection.reconnectDelay); + }, + + lostConnection: function(connection) { + this.reconnect(connection); + }, + + send: function(connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + receive: function(connection, data) { + var cw; + + transportLogic.processMessages(connection, data); + // Delete the script & div elements + connection.frameMessageCount = (connection.frameMessageCount || 0) + 1; + if (connection.frameMessageCount > 50) { + connection.frameMessageCount = 0; + cw = connection.frame.contentWindow || connection.frame.contentDocument; + if (cw && cw.document) { + $("body", cw.document).empty(); + } + } + }, + + stop: function(connection) { + var cw = null; + + // Stop attempting to prevent loading icon + loadPreventer.cancel(); + + if (connection.frame) { + if (connection.frame.stop) { + connection.frame.stop(); + } else { + try { + cw = connection.frame.contentWindow || connection.frame.contentDocument; + if (cw.document && cw.document.execCommand) { + cw.document.execCommand("Stop"); + } + } catch (e) { + connection.log("SignalR: Error occured when stopping foreverFrame transport. Message = " + e.message); + } + } + $(connection.frame).remove(); + delete transportLogic.foreverFrame.connections[connection.frameId] + ; + connection.frame = null; + connection.frameId = null; + delete connection.frame; + delete connection.frameId; + connection.log("Stopping forever frame"); + } + }, + + abort: function(connection, async) { + transportLogic.ajaxAbort(connection, async); + }, + + getConnection: function(id) { + return transportLogic.foreverFrame.connections[id]; + }, + + started: function(connection) { + if (connection.onSuccess) { + connection.onSuccess(); + connection.onSuccess = null; + delete connection.onSuccess; + } else if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + // If there's no onSuccess handler we assume this is a reconnect + $(connection).triggerHandler(events.onReconnect); + } + } + }; +}(window.jQuery, window)); +/* jquery.signalR.transports.longPolling.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +( function($, window) { + "use strict"; + + var signalR = $.signalR, + events = $.signalR.events, + changeState = $.signalR.changeState, + isDisconnecting = $.signalR.isDisconnecting, + transportLogic = signalR.transports._logic; + + signalR.transports.longPolling = { + name: "longPolling", + + supportsKeepAlive: false, + + reconnectDelay: 3000, + + init: function(connection, onComplete) { + /// Pings the server to ensure availability + /// Connection associated with the server ping + /// Callback to call once initialization has completed + + var that = this, + pingLoop, + // pingFail is used to loop the re-ping behavior. When we fail we want to re-try. + pingFail = function(reason) { + if (isDisconnecting(connection) === false) { + connection.log("SignalR: Server ping failed because '" + reason + "', re-trying ping."); + window.setTimeout(pingLoop, that.reconnectDelay); + } + }; + + connection.log("SignalR: Initializing long polling connection with server."); + pingLoop = function() { + // Ping the server, on successful ping call the onComplete method, otherwise if we fail call the pingFail + transportLogic.pingServer(connection, that.name).done(onComplete).fail(pingFail); + }; + + pingLoop(); + }, + + start: function(connection, onSuccess, onFailed) { + /// Starts the long polling connection + /// The SignalR connection to start + var that = this, + initialConnectedFired = false, + fireConnect = function() { + if (initialConnectedFired) { + return; + } + initialConnectedFired = true; + onSuccess(); + connection.log("Longpolling connected"); + }, + reconnectErrors = 0, + reconnectTimeoutId = null, + fireReconnected = function(instance) { + window.clearTimeout(reconnectTimeoutId); + reconnectTimeoutId = null; + + if (changeState(connection, + signalR.connectionState.reconnecting, + signalR.connectionState.connected) === true) { + // Successfully reconnected! + connection.log("Raising the reconnect event"); + $(instance).triggerHandler(events.onReconnect); + } + }, + // 1 hour + maxFireReconnectedTimeout = 3600000; + + if (connection.pollXhr) { + connection.log("Polling xhr requests already exists, aborting."); + connection.stop(); + } + + // We start with an initialization procedure which pings the server to verify that it is there. + // On scucessful initialization we'll then proceed with starting the transport. + that.init(connection, function() { + connection.messageId = null; + + window.setTimeout(function() { + ( function poll(instance, raiseReconnect) { + var messageId = instance.messageId, + connect = (messageId === null), + reconnecting = !connect, + polling = !raiseReconnect, + url = transportLogic.getUrl(instance, that.name, reconnecting, polling); + + // If we've disconnected during the time we've tried to re-instantiate the poll then stop. + if (isDisconnecting(instance) === true) { + return; + } + + connection.log("Attempting to connect to '" + url + "' using longPolling."); + instance.pollXhr = $.ajax({ + url: url, + global: true, + cache: false, + type: "GET", + dataType: connection.ajaxDataType, + contentType: connection.contentType, + success: function(minData) { + var delay = 0, + data; + + // Reset our reconnect errors so if we transition into a reconnecting state again we trigger + // reconnected quickly + reconnectErrors = 0; + + // If there's currently a timeout to trigger reconnect, fire it now before processing messages + if (reconnectTimeoutId !== null) { + fireReconnected(); + } + + fireConnect(); + + if (minData) { + data = transportLogic.maximizePersistentResponse(minData); + } + + transportLogic.processMessages(instance, minData); + + if (data && + $.type(data.LongPollDelay) === "number") { + delay = data.LongPollDelay; + } + + if (data && data.Disconnect) { + return; + } + + if (isDisconnecting(instance) === true) { + return; + } + + // We never want to pass a raiseReconnect flag after a successful poll. This is handled via the error function + if (delay > 0) { + window.setTimeout(function() { + poll(instance, false); + }, delay); + } else { + poll(instance, false); + } + }, + + error: function(data, textStatus) { + // Stop trying to trigger reconnect, connection is in an error state + // If we're not in the reconnect state this will noop + window.clearTimeout(reconnectTimeoutId); + reconnectTimeoutId = null; + + if (textStatus === "abort") { + connection.log("Aborted xhr requst."); + return; + } + + // Increment our reconnect errors, we assume all errors to be reconnect errors + // In the case that it's our first error this will cause Reconnect to be fired + // after 1 second due to reconnectErrors being = 1. + reconnectErrors++; + + if (connection.state !== signalR.connectionState.reconnecting) { + connection.log("An error occurred using longPolling. Status = " + textStatus + ". " + data.responseText); + $(instance).triggerHandler(events.onError, [data.responseText]); + } + + // Transition into the reconnecting state + transportLogic.ensureReconnectingState(instance); + + // If we've errored out we need to verify that the server is still there, so re-start initialization process + // This will ping the server until it successfully gets a response. + that.init(instance, function() { + // Call poll with the raiseReconnect flag as true + poll(instance, true); + }); + } + }); + + + // This will only ever pass after an error has occured via the poll ajax procedure. + if (reconnecting && raiseReconnect === true) { + // We wait to reconnect depending on how many times we've failed to reconnect. + // This is essentially a heuristic that will exponentially increase in wait time before + // triggering reconnected. This depends on the "error" handler of Poll to cancel this + // timeout if it triggers before the Reconnected event fires. + // The Math.min at the end is to ensure that the reconnect timeout does not overflow. + reconnectTimeoutId = window.setTimeout(function() { + fireReconnected(instance); + }, Math.min(1000 * (Math.pow(2, reconnectErrors) - 1), maxFireReconnectedTimeout)); + } + }(connection)); + + // Set an arbitrary timeout to trigger onSuccess, this will alot for enough time on the server to wire up the connection. + // Will be fixed by #1189 and this code can be modified to not be a timeout + window.setTimeout(function() { + // Trigger the onSuccess() method because we've now instantiated a connection + fireConnect(); + }, 250); + }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab + }); + }, + + lostConnection: function(connection) { + throw new Error("Lost Connection not handled for LongPolling"); + }, + + send: function(connection, data) { + transportLogic.ajaxSend(connection, data); + }, + + stop: function(connection) { + /// Stops the long polling connection + /// The SignalR connection to stop + if (connection.pollXhr) { + connection.pollXhr.abort(); + connection.pollXhr = null; + delete connection.pollXhr; + } + }, + + abort: function(connection, async) { + transportLogic.ajaxAbort(connection, async); + } + }; +}(window.jQuery, window)); +/* jquery.signalR.hubs.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// + +( function($, window) { + "use strict"; + + // we use a global id for tracking callbacks so the server doesn't have to send extra info like hub name + var eventNamespace = ".hubProxy"; + + function makeEventName(event) { + return event + eventNamespace; + } + + // Equivalent to Array.prototype.map + function map(arr, fun, thisp) { + var i, + length = arr.length, + result = []; + for (i = 0; i < length; i += 1) { + if (arr.hasOwnProperty(i)) { + result[i] = fun.call(thisp, arr[i], i, arr); + } + } + return result; + } + + function getArgValue(a) { + return $.isFunction(a) ? null : ($.type(a) === "undefined" ? null : a); + } + + function hasMembers(obj) { + for (var key in obj) { + // If we have any properties in our callback map then we have callbacks and can exit the loop via return + if (obj.hasOwnProperty(key)) { + return true; + } + } + + return false; + } + + function clearInvocationCallbacks(connection, error) { + /// + var callbacks = connection._.invocationCallbacks, + callback; + + connection.log("Clearing hub invocation callbacks with error: " + error); + + // Reset the callback cache now as we have a local var referencing it + connection._.invocationCallbackId = 0; + delete connection._.invocationCallbacks; + connection._.invocationCallbacks = {}; + + // Loop over the callbacks and invoke them. + // We do this using a local var reference and *after* we've cleared the cache + // so that if a fail callback itself tries to invoke another method we don't + // end up with its callback in the list we're looping over. + for (var callbackId in callbacks) { + callback = callbacks[callbackId]; + callback.method.call(callback.scope, { E: error }); + } + } + + // hubProxy + function hubProxy(hubConnection, hubName) { + /// + /// Creates a new proxy object for the given hub connection that can be used to invoke + /// methods on server hubs and handle client method invocation requests from the server. + /// + return new hubProxy.fn.init(hubConnection, hubName); + } + + hubProxy.fn = hubProxy.prototype = { + init: function(connection, hubName) { + this.state = {}; + this.connection = connection; + this.hubName = hubName; + this._ = { + callbackMap: {} + }; + }, + + hasSubscriptions: function() { + return hasMembers(this._.callbackMap); + }, + + on: function(eventName, callback) { + /// Wires up a callback to be invoked when a invocation request is received from the server hub. + /// The name of the hub event to register the callback for. + /// The callback to be invoked. + var self = this, + callbackMap = self._.callbackMap; + + // Normalize the event name to lowercase + eventName = eventName.toLowerCase(); + + // If there is not an event registered for this callback yet we want to create its event space in the callback map. + if (!callbackMap[eventName]) { + callbackMap[eventName] = {}; + } + + // Map the callback to our encompassed function + callbackMap[eventName][callback] = function(e, data) { + callback.apply(self, data); + }; + + $(self).bind(makeEventName(eventName), callbackMap[eventName][callback]); + + return self; + }, + + off: function(eventName, callback) { + /// Removes the callback invocation request from the server hub for the given event name. + /// The name of the hub event to unregister the callback for. + /// The callback to be invoked. + var self = this, + callbackMap = self._.callbackMap, + callbackSpace; + + // Normalize the event name to lowercase + eventName = eventName.toLowerCase(); + + callbackSpace = callbackMap[eventName]; + + // Verify that there is an event space to unbind + if (callbackSpace) { + // Only unbind if there's an event bound with eventName and a callback with the specified callback + if (callbackSpace[callback]) { + $(self).unbind(makeEventName(eventName), callbackSpace[callback]); + + // Remove the callback from the callback map + delete callbackSpace[callback] + ; + + // Check if there are any members left on the event, if not we need to destroy it. + if (!hasMembers(callbackSpace)) { + delete callbackMap[eventName] + ; + } + } else if (!callback) { // Check if we're removing the whole event and we didn't error because of an invalid callback + $(self).unbind(makeEventName(eventName)); + + delete callbackMap[eventName] + ; + } + } + + return self; + }, + + invoke: function(methodName) { + /// Invokes a server hub method with the given arguments. + /// The name of the server hub method. + + var self = this, + connection = self.connection, + args = $.makeArray(arguments).slice(1), + argValues = map(args, getArgValue), + data = { H: self.hubName, M: methodName, A: argValues, I: connection._.invocationCallbackId }, + d = $.Deferred(), + callback = function(minResult) { + var result = self._maximizeHubResponse(minResult); + + // Update the hub state + $.extend(self.state, result.State); + + if (result.Error) { + // Server hub method threw an exception, log it & reject the deferred + if (result.StackTrace) { + connection.log(result.Error + "\n" + result.StackTrace); + } + d.rejectWith(self, [result.Error]); + } else { + // Server invocation succeeded, resolve the deferred + d.resolveWith(self, [result.Result]); + } + }; + + connection._.invocationCallbacks[connection._.invocationCallbackId.toString()] = { scope: self, method: callback }; + connection._.invocationCallbackId += 1; + + if (!$.isEmptyObject(self.state)) { + data.S = self.state; + } + + connection.send(window.JSON.stringify(data)); + + return d.promise(); + }, + + _maximizeHubResponse: function(minHubResponse) { + return { + State: minHubResponse.S, + Result: minHubResponse.R, + Id: minHubResponse.I, + Error: minHubResponse.E, + StackTrace: minHubResponse.T + }; + } + }; + + hubProxy.fn.init.prototype = hubProxy.fn; + + // hubConnection + function hubConnection(url, options) { + /// Creates a new hub connection. + /// [Optional] The hub route url, defaults to "/signalr". + /// [Optional] Settings to use when creating the hubConnection. + var settings = { + qs: null, + logging: false, + useDefaultPath: true + }; + + $.extend(settings, options); + + if (!url || settings.useDefaultPath) { + url = (url || "") + "/signalr"; + } + return new hubConnection.fn.init(url, settings); + } + + hubConnection.fn = hubConnection.prototype = $.connection(); + + hubConnection.fn.init = function(url, options) { + var settings = { + qs: null, + logging: false, + useDefaultPath: true + }, + connection = this; + + $.extend(settings, options); + + // Call the base constructor + $.signalR.fn.init.call(connection, url, settings.qs, settings.logging); + + // Object to store hub proxies for this connection + connection.proxies = {}; + + connection._.invocationCallbackId = 0; + connection._.invocationCallbacks = {}; + + // Wire up the received handler + connection.received(function(minData) { + var data, proxy, dataCallbackId, callback, hubName, eventName; + if (!minData) { + return; + } + + if (typeof (minData.I) !== "undefined") { + // We received the return value from a server method invocation, look up callback by id and call it + dataCallbackId = minData.I.toString(); + callback = connection._.invocationCallbacks[dataCallbackId]; + if (callback) { + // Delete the callback from the proxy + connection._.invocationCallbacks[dataCallbackId] = null; + delete connection._.invocationCallbacks[dataCallbackId] + ; + + // Invoke the callback + callback.method.call(callback.scope, minData); + } + } else { + data = this._maximizeClientHubInvocation(minData); + + // We received a client invocation request, i.e. broadcast from server hub + connection.log("Triggering client hub event '" + data.Method + "' on hub '" + data.Hub + "'."); + + // Normalize the names to lowercase + hubName = data.Hub.toLowerCase(); + eventName = data.Method.toLowerCase(); + + // Trigger the local invocation event + proxy = this.proxies[hubName]; + + // Update the hub state + $.extend(proxy.state, data.State); + $(proxy).triggerHandler(makeEventName(eventName), [data.Args]); + } + }); + + connection.error(function(errData, origData) { + var data, callbackId, callback; + + if (connection.transport && connection.transport.name === "webSockets") { + // WebSockets connections have all callbacks removed on reconnect instead + // as WebSockets sends are fire & forget + return; + } + + if (!origData) { + // No original data passed so this is not a send error + return; + } + + try { + data = window.JSON.parse(origData); + if (!data.I) { + // The original data doesn't have a callback ID so not a send error + return; + } + } catch (e) { + // The original data is not a JSON payload so this is not a send error + return; + } + + callbackId = data.I; + callback = connection._.invocationCallbacks[callbackId]; + + // Invoke the callback with an error to reject the promise + callback.method.call(callback.scope, { E: errData }); + + // Delete the callback + connection._.invocationCallbacks[callbackId] = null; + delete connection._.invocationCallbacks[callbackId] + ; + }); + + connection.reconnecting(function() { + if (connection.transport && connection.transport.name === "webSockets") { + clearInvocationCallbacks(connection, "Connection started reconnecting before invocation result was received."); + } + }); + + connection.disconnected(function() { + clearInvocationCallbacks(connection, "Connection was disconnected before invocation result was received."); + }); + }; + + hubConnection.fn._maximizeClientHubInvocation = function(minClientHubInvocation) { + return { + Hub: minClientHubInvocation.H, + Method: minClientHubInvocation.M, + Args: minClientHubInvocation.A, + State: minClientHubInvocation.S + }; + }; + + hubConnection.fn._registerSubscribedHubs = function() { + /// + /// Sets the starting event to loop through the known hubs and register any new hubs + /// that have been added to the proxy. + /// + + if (!this._subscribedToHubs) { + this._subscribedToHubs = true; + this.starting(function() { + // Set the connection's data object with all the hub proxies with active subscriptions. + // These proxies will receive notifications from the server. + var subscribedHubs = []; + + $.each(this.proxies, function(key) { + if (this.hasSubscriptions()) { + subscribedHubs.push({ name: key }); + } + }); + + this.data = window.JSON.stringify(subscribedHubs); + }); + } + }; + + hubConnection.fn.createHubProxy = function(hubName) { + /// + /// Creates a new proxy object for the given hub connection that can be used to invoke + /// methods on server hubs and handle client method invocation requests from the server. + /// + /// + /// The name of the hub on the server to create the proxy for. + /// + + // Normalize the name to lowercase + hubName = hubName.toLowerCase(); + + var proxy = this.proxies[hubName]; + if (!proxy) { + proxy = hubProxy(this, hubName); + this.proxies[hubName] = proxy; + } + + this._registerSubscribedHubs(); + + return proxy; + }; + + hubConnection.fn.init.prototype = hubConnection.fn; + + $.hubConnection = hubConnection; +}(window.jQuery, window)); +/* jquery.signalR.version.js */ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +/*global window:false */ +/// +( function($) { + $.signalR.version = "1.1.3"; +}(window.jQuery)); diff --git a/frontend/src/Organize/OrganizePreviewModal.js b/frontend/src/Organize/OrganizePreviewModal.js new file mode 100644 index 000000000..647f4ddf8 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModal.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import OrganizePreviewModalContentConnector from './OrganizePreviewModalContentConnector'; + +function OrganizePreviewModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + { + isOpen && + + } + + ); +} + +OrganizePreviewModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizePreviewModal; diff --git a/frontend/src/Organize/OrganizePreviewModalConnector.js b/frontend/src/Organize/OrganizePreviewModalConnector.js new file mode 100644 index 000000000..ace733c86 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import OrganizePreviewModal from './OrganizePreviewModal'; + +const mapDispatchToProps = { + clearOrganizePreview +}; + +class OrganizePreviewModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearOrganizePreview(); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +OrganizePreviewModalConnector.propTypes = { + clearOrganizePreview: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector); diff --git a/frontend/src/Organize/OrganizePreviewModalContent.css b/frontend/src/Organize/OrganizePreviewModalContent.css new file mode 100644 index 000000000..7de056fcc --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.css @@ -0,0 +1,24 @@ +.path { + margin-left: 5px; + font-weight: bold; +} + +.episodeFormat { + margin-left: 5px; + font-family: $monoSpaceFontFamily; +} + +.previews { + margin-top: 10px; +} + +.selectAllInputContainer { + margin-right: auto; + line-height: 30px; +} + +.selectAllInput { + composes: input from 'Components/Form/CheckInput.css'; + + margin: 0; +} diff --git a/frontend/src/Organize/OrganizePreviewModalContent.js b/frontend/src/Organize/OrganizePreviewModalContent.js new file mode 100644 index 000000000..5e9124db6 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContent.js @@ -0,0 +1,200 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import CheckInput from 'Components/Form/CheckInput'; +import OrganizePreviewRow from './OrganizePreviewRow'; +import styles from './OrganizePreviewModalContent.css'; + +function getValue(allSelected, allUnselected) { + if (allSelected) { + return true; + } else if (allUnselected) { + return false; + } + + return null; +} + +class OrganizePreviewModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onOrganizePress = () => { + this.props.onOrganizePress(this.getSelectedIds()); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + renameEpisodes, + episodeFormat, + path, + onModalClose + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + const selectAllValue = getValue(allSelected, allUnselected); + + return ( + + + Organize & Rename + + + + { + isFetching && + + } + + { + !isFetching && error && +
Error loading previews
+ } + + { + !isFetching && isPopulated && !items.length && +
+ { + renameEpisodes ? +
Success! My work is done, no files to rename.
: +
Renaming is disabled, nothing to rename
+ } +
+ } + + { + !isFetching && isPopulated && !!items.length && +
+ +
+ All paths are relative to: + + {path} + +
+ +
+ Naming pattern: + + {episodeFormat} + +
+
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+
+ } +
+ + + { + isPopulated && !!items.length && + + } + + + + + +
+ ); + } +} + +OrganizePreviewModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + path: PropTypes.string.isRequired, + renameEpisodes: PropTypes.bool, + episodeFormat: PropTypes.string, + onOrganizePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default OrganizePreviewModalContent; diff --git a/frontend/src/Organize/OrganizePreviewModalContentConnector.js b/frontend/src/Organize/OrganizePreviewModalContentConnector.js new file mode 100644 index 000000000..6fbd2b9eb --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewModalContentConnector.js @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions'; +import { fetchNamingSettings } from 'Store/Actions/settingsActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import OrganizePreviewModalContent from './OrganizePreviewModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.organizePreview, + (state) => state.settings.naming, + createArtistSelector(), + (organizePreview, naming, series) => { + const props = { ...organizePreview }; + props.isFetching = organizePreview.isFetching || naming.isFetching; + props.isPopulated = organizePreview.isPopulated && naming.isPopulated; + props.error = organizePreview.error || naming.error; + props.renameEpisodes = naming.item.renameEpisodes; + props.episodeFormat = naming.item[`${series.seriesType}EpisodeFormat`]; + props.path = series.path; + + return props; + } + ); +} + +const mapDispatchToProps = { + fetchOrganizePreview, + fetchNamingSettings, + executeCommand +}; + +class OrganizePreviewModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + seriesId, + seasonNumber + } = this.props; + + this.props.fetchOrganizePreview({ + seriesId, + seasonNumber + }); + + this.props.fetchNamingSettings(); + } + + // + // Listeners + + onOrganizePress = (files) => { + this.props.executeCommand({ + name: commandNames.RENAME_FILES, + seriesId: this.props.seriesId, + files + }); + + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +OrganizePreviewModalContentConnector.propTypes = { + seriesId: PropTypes.number.isRequired, + seasonNumber: PropTypes.number, + fetchOrganizePreview: PropTypes.func.isRequired, + fetchNamingSettings: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector); diff --git a/frontend/src/Organize/OrganizePreviewRow.css b/frontend/src/Organize/OrganizePreviewRow.css new file mode 100644 index 000000000..1b3c8ca47 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.css @@ -0,0 +1,20 @@ +.row { + display: flex; + margin-bottom: 5px; + padding: 5px 0; + border-bottom: 1px solid $borderColor; + + &:last-of-type { + margin-bottom: 0; + padding-bottom: 0; + border-bottom: none; + } +} + +.selectedContainer { + margin-right: 30px; +} + +.path { + margin-left: 10px; +} diff --git a/frontend/src/Organize/OrganizePreviewRow.js b/frontend/src/Organize/OrganizePreviewRow.js new file mode 100644 index 000000000..340232a98 --- /dev/null +++ b/frontend/src/Organize/OrganizePreviewRow.js @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './OrganizePreviewRow.css'; + +class OrganizePreviewRow extends Component { + + // + // Lifecycle + + componentDidMount() { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value: true }); + } + + // + // Listeners + + onSelectedChange = ({ value, shiftKey }) => { + const { + id, + onSelectedChange + } = this.props; + + onSelectedChange({ id, value, shiftKey }); + } + + // + // Render + + render() { + const { + id, + existingPath, + newPath, + isSelected + } = this.props; + + return ( +
+ + +
+
+ + + + {existingPath} + +
+ +
+ + + + {newPath} + +
+
+
+ ); + } +} + +OrganizePreviewRow.propTypes = { + id: PropTypes.number.isRequired, + existingPath: PropTypes.string.isRequired, + newPath: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired +}; + +export default OrganizePreviewRow; diff --git a/frontend/src/SeasonPass/SeasonPass.js b/frontend/src/SeasonPass/SeasonPass.js new file mode 100644 index 000000000..681b68fd2 --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPass.js @@ -0,0 +1,257 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, sortDirections } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import NoArtist from 'Artist/NoArtist'; +import SeasonPassRowConnector from './SeasonPassRowConnector'; +import SeasonPassFooter from './SeasonPassFooter'; + +const columns = [ + { + name: 'status', + isVisible: true + }, + { + name: 'sortTitle', + label: 'Title', + isSortable: true, + isVisible: true + }, + { + name: 'monitored', + isVisible: true + }, + { + name: 'seasonCount', + label: 'Seasons', + isSortable: true, + isVisible: true + } +]; + +class SeasonPass extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {} + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = this.props; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.onSelectAllChange({ value: false }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onUpdateSelectedPress = (changes) => { + this.props.onUpdateSelectedPress({ + artistIds: this.getSelectedIds(), + ...changes + }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + filterKey, + filterValue, + sortKey, + sortDirection, + isSaving, + saveError, + onSortPress, + onFilterSelect + } = this.props; + + const { + allSelected, + allUnselected, + selectedState + } = this.state; + + return ( + + + + + + + + All + + + + Monitored Only + + + + Continuing Only + + + + Ended Only + + + + Missing Episodes + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && !!error && +
Unable to load the calendar
+ } + + { + !error && isPopulated && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !error && isPopulated && !items.length && + + } +
+ + +
+ ); + } +} + +SeasonPass.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + sortKey: PropTypes.string, + sortDirection: PropTypes.oneOf(sortDirections.all), + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onSortPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default SeasonPass; diff --git a/frontend/src/SeasonPass/SeasonPassConnector.js b/frontend/src/SeasonPass/SeasonPassConnector.js new file mode 100644 index 000000000..653db2b02 --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassConnector.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import connectSection from 'Store/connectSection'; +import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; +import { setSeasonPassSort, setSeasonPassFilter, saveSeasonPass } from 'Store/Actions/seasonPassActions'; +import SeasonPass from './SeasonPass'; + +function createMapStateToProps() { + return createSelector( + createClientSideCollectionSelector(), + (series) => { + return { + ...series + }; + } + ); +} + +const mapDispatchToProps = { + setSeasonPassSort, + setSeasonPassFilter, + saveSeasonPass +}; + +class SeasonPassConnector extends Component { + + // + // Listeners + + onSortPress = (sortKey) => { + this.props.setSeasonPassSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue, filterType) => { + this.props.setSeasonPassFilter({ filterKey, filterValue, filterType }); + } + + onUpdateSelectedPress = (payload) => { + this.props.saveSeasonPass(payload); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SeasonPassConnector.propTypes = { + setSeasonPassSort: PropTypes.func.isRequired, + setSeasonPassFilter: PropTypes.func.isRequired, + saveSeasonPass: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'series', uiSection: 'seasonPass' } + )(SeasonPassConnector); diff --git a/frontend/src/SeasonPass/SeasonPassFooter.css b/frontend/src/SeasonPass/SeasonPassFooter.css new file mode 100644 index 000000000..c18eb660f --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassFooter.css @@ -0,0 +1,14 @@ +.inputContainer { + margin-right: 20px; +} + +.label { + margin-bottom: 3px; + font-weight: bold; +} + +.updateSelectedButton { + composes: button from 'Components/Link/SpinnerButton.css'; + + height: 35px; +} diff --git a/frontend/src/SeasonPass/SeasonPassFooter.js b/frontend/src/SeasonPass/SeasonPassFooter.js new file mode 100644 index 000000000..7ce8dc491 --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassFooter.js @@ -0,0 +1,145 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import MonitorEpisodesSelectInput from 'Components/Form/MonitorEpisodesSelectInput'; +import SelectInput from 'Components/Form/SelectInput'; +import PageContentFooter from 'Components/Page/PageContentFooter'; +import styles from './SeasonPassFooter.css'; + +const NO_CHANGE = 'noChange'; + +class SeasonPassFooter extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + monitored: NO_CHANGE, + monitor: NO_CHANGE + }; + } + + componentDidUpdate(prevProps) { + const { + isSaving, + saveError + } = prevProps; + + if (prevProps.isSaving && !isSaving && !saveError) { + this.setState({ + monitored: NO_CHANGE, + monitor: NO_CHANGE + }); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.setState({ [name]: value }); + } + + onUpdateSelectedPress = () => { + const { + monitor, + monitored + } = this.state; + + const changes = {}; + + if (monitored !== NO_CHANGE) { + changes.monitored = monitored === 'monitored'; + } + + if (monitor !== NO_CHANGE) { + changes.monitor = monitor; + } + + this.props.onUpdateSelectedPress(changes); + } + + // + // Render + + render() { + const { + selectedCount, + isSaving + } = this.props; + + const { + monitored, + monitor + } = this.state; + + const monitoredOptions = [ + { key: NO_CHANGE, value: 'No Change', disabled: true }, + { key: 'monitored', value: 'Monitored' }, + { key: 'unmonitored', value: 'Unmonitored' } + ]; + + const noChanges = monitored === NO_CHANGE && monitor === NO_CHANGE; + + return ( + +
+
+ Monitor Series +
+ + +
+ +
+
+ Monitor Episodes +
+ + +
+ +
+
+ {selectedCount} Series Selected +
+ + + Update Selected + +
+
+ ); + } +} + +SeasonPassFooter.propTypes = { + selectedCount: PropTypes.number.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + onUpdateSelectedPress: PropTypes.func.isRequired +}; + +export default SeasonPassFooter; diff --git a/frontend/src/SeasonPass/SeasonPassRow.css b/frontend/src/SeasonPass/SeasonPassRow.css new file mode 100644 index 000000000..a053c6bef --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassRow.css @@ -0,0 +1,20 @@ +.status, +.monitored { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 50px; +} + +.title { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 1px; + white-space: nowrap; +} + +.seasons { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/SeasonPass/SeasonPassRow.js b/frontend/src/SeasonPass/SeasonPassRow.js new file mode 100644 index 000000000..65984e1f2 --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassRow.js @@ -0,0 +1,101 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import SeasonPassSeason from './SeasonPassSeason'; +import styles from './SeasonPassRow.css'; + +class SeasonPassRow extends Component { + + // + // Render + + render() { + const { + artistId, + status, + titleSlug, + title, + monitored, + seasons, + isSaving, + isSelected, + onSelectedChange, + onSeriesMonitoredPress, + onSeasonMonitoredPress + } = this.props; + + return ( + + + + + + + + + + + + + + + + + { + seasons.map((season) => { + return ( + + ); + }) + } + + + ); + } +} + +SeasonPassRow.propTypes = { + artistId: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + titleSlug: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + monitored: PropTypes.bool.isRequired, + seasons: PropTypes.arrayOf(PropTypes.object).isRequired, + isSaving: PropTypes.bool.isRequired, + isSelected: PropTypes.bool, + onSelectedChange: PropTypes.func.isRequired, + onSeriesMonitoredPress: PropTypes.func.isRequired, + onSeasonMonitoredPress: PropTypes.func.isRequired +}; + +SeasonPassRow.defaultProps = { + isSaving: false +}; + +export default SeasonPassRow; diff --git a/frontend/src/SeasonPass/SeasonPassRowConnector.js b/frontend/src/SeasonPass/SeasonPassRowConnector.js new file mode 100644 index 000000000..366170634 --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassRowConnector.js @@ -0,0 +1,77 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createArtistSelector from 'Store/Selectors/createArtistSelector'; +import { toggleSeriesMonitored, toggleSeasonMonitored } from 'Store/Actions/seriesActions'; +import SeasonPassRow from './SeasonPassRow'; + +function createMapStateToProps() { + return createSelector( + createArtistSelector(), + (series) => { + return _.pick(series, [ + 'status', + 'titleSlug', + 'title', + 'monitored', + 'seasons', + 'isSaving' + ]); + } + ); +} + +const mapDispatchToProps = { + toggleSeriesMonitored, + toggleSeasonMonitored +}; + +class SeasonPassRowConnector extends Component { + + // + // Listeners + + onSeriesMonitoredPress = () => { + const { + artistId, + monitored + } = this.props; + + this.props.toggleSeriesMonitored({ + artistId, + monitored: !monitored + }); + } + + onSeasonMonitoredPress = (seasonNumber, monitored) => { + this.props.toggleSeasonMonitored({ + artistId: this.props.artistId, + seasonNumber, + monitored + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +SeasonPassRowConnector.propTypes = { + artistId: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + toggleSeriesMonitored: PropTypes.func.isRequired, + toggleSeasonMonitored: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(SeasonPassRowConnector); diff --git a/frontend/src/SeasonPass/SeasonPassSeason.css b/frontend/src/SeasonPass/SeasonPassSeason.css new file mode 100644 index 000000000..603d98ecd --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassSeason.css @@ -0,0 +1,24 @@ +.season { + display: flex; + align-items: stretch; + overflow: hidden; + margin: 2px 4px; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: #eee; + cursor: default; +} + +.info { + padding: 0 4px; +} + +.episodes { + padding: 0 4px; + background-color: $white; + color: $defaultColor; +} + +.allEpisodes { + background-color: #e0ffe0; +} diff --git a/frontend/src/SeasonPass/SeasonPassSeason.js b/frontend/src/SeasonPass/SeasonPassSeason.js new file mode 100644 index 000000000..152221b8c --- /dev/null +++ b/frontend/src/SeasonPass/SeasonPassSeason.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import padNumber from 'Utilities/Number/padNumber'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import styles from './SeasonPassSeason.css'; + +class SeasonPassSeason extends Component { + + // + // Listeners + + onSeasonMonitoredPress = () => { + const { + seasonNumber, + monitored + } = this.props; + + this.props.onSeasonMonitoredPress(seasonNumber, !monitored); + } + + // + // Render + + render() { + const { + seasonNumber, + monitored, + statistics, + isSaving + } = this.props; + + const { + episodeFileCount, + totalEpisodeCount, + percentOfEpisodes + } = statistics; + + return ( +
+
+ + + + { + seasonNumber === 0 ? 'Specials' : `S${padNumber(seasonNumber, 2)}` + } + +
+ +
+ { + totalEpisodeCount === 0 ? '0/0' : `${episodeFileCount}/${totalEpisodeCount}` + } +
+
+ ); + } +} + +SeasonPassSeason.propTypes = { + seasonNumber: PropTypes.number.isRequired, + monitored: PropTypes.bool.isRequired, + statistics: PropTypes.object.isRequired, + isSaving: PropTypes.bool.isRequired, + onSeasonMonitoredPress: PropTypes.func.isRequired +}; + +SeasonPassSeason.defaultProps = { + isSaving: false, + statistics: { + episodeFileCount: 0, + totalEpisodeCount: 0, + percentOfEpisodes: 0 + } +}; + +export default SeasonPassSeason; diff --git a/frontend/src/Settings/DownloadClients/DownloadClientSettings.js b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js new file mode 100644 index 000000000..414887fd5 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClientSettings.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector'; +import DownloadClientOptionsConnector from './Options/DownloadClientOptionsConnector'; +import RemotePathMappingsConnector from './RemotePathMappings/RemotePathMappingsConnector'; + +class DownloadClientSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setDownloadClientOptionsRef = (ref) => { + this._downloadClientOptions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._downloadClientOptions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + + + + + ); + } +} + +export default DownloadClientSettings; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css new file mode 100644 index 000000000..1635faeac --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.css @@ -0,0 +1,44 @@ +.downloadClient { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + composes: cover from 'Styles/Mixins/cover.css'; +} + +.overlay { + composes: linkOverlay from 'Styles/Mixins/linkOverlay.css'; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js new file mode 100644 index 000000000..3a2265d28 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddDownloadClientPresetMenuItem from './AddDownloadClientPresetMenuItem'; +import styles from './AddDownloadClientItem.css'; + +class AddDownloadClientItem extends Component { + + // + // Listeners + + onDownloadClientSelect = () => { + const { + implementation + } = this.props; + + this.props.onDownloadClientSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onDownloadClientSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddDownloadClientItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onDownloadClientSelect: PropTypes.func.isRequired +}; + +export default AddDownloadClientItem; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js new file mode 100644 index 000000000..0c21e7dbd --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddDownloadClientModalContentConnector from './AddDownloadClientModalContentConnector'; + +function AddDownloadClientModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddDownloadClientModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddDownloadClientModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css new file mode 100644 index 000000000..b4d5c6787 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.css @@ -0,0 +1,5 @@ +.downloadClients { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js new file mode 100644 index 000000000..002bd5eb0 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContent.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddDownloadClientItem from './AddDownloadClientItem'; +import styles from './AddDownloadClientModalContent.css'; + +class AddDownloadClientModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + error, + isPopulated, + usenetDownloadClients, + torrentDownloadClients, + onDownloadClientSelect, + onModalClose + } = this.props; + + return ( + + + Add DownloadClient + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new downloadClient, please try again.
+ } + + { + !isFetching && !error && +
+ + +
Sonarr supports any downloadClient that uses the Newznab standard, as well as other downloadClients listed below.
+
For more information on the individual downloadClients, clink on the info buttons.
+
+ +
+
+ { + usenetDownloadClients.map((downloadClient) => { + return ( + + ); + }) + } +
+
+ +
+
+ { + torrentDownloadClients.map((downloadClient) => { + return ( + + ); + }) + } +
+
+
+ } +
+ + + +
+ ); + } +} + +AddDownloadClientModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isPopulated: PropTypes.bool.isRequired, + usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, + torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, + onDownloadClientSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddDownloadClientModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js new file mode 100644 index 000000000..d6015b934 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientModalContentConnector.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDownloadClientSchema, selectDownloadClientSchema } from 'Store/Actions/settingsActions'; +import AddDownloadClientModalContent from './AddDownloadClientModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.downloadClients, + (downloadClients) => { + const { + isFetching, + error, + isPopulated, + schema + } = downloadClients; + + const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' }); + const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' }); + + return { + isFetching, + error, + isPopulated, + usenetDownloadClients, + torrentDownloadClients + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClientSchema, + selectDownloadClientSchema +}; + +class AddDownloadClientModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClientSchema(); + } + + // + // Listeners + + onDownloadClientSelect = ({ implementation }) => { + this.props.selectDownloadClientSchema({ implementation }); + this.props.onModalClose({ downloadClientSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddDownloadClientModalContentConnector.propTypes = { + fetchDownloadClientSchema: PropTypes.func.isRequired, + selectDownloadClientSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddDownloadClientModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js new file mode 100644 index 000000000..f356f8140 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/AddDownloadClientPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddDownloadClientPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddDownloadClientPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddDownloadClientPresetMenuItem; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css new file mode 100644 index 000000000..609cea818 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.css @@ -0,0 +1,19 @@ +.downloadClient { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + composes: truncate from 'Styles/Mixins/truncate.css'; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js new file mode 100644 index 000000000..b4a725303 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClient.js @@ -0,0 +1,106 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; +import styles from './DownloadClient.css'; + +class DownloadClient extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditDownloadClientModalOpen: false, + isDeleteDownloadClientModalOpen: false + }; + } + + // + // Listeners + + onEditDownloadClientPress = () => { + this.setState({ isEditDownloadClientModalOpen: true }); + } + + onEditDownloadClientModalClose = () => { + this.setState({ isEditDownloadClientModalOpen: false }); + } + + onDeleteDownloadClientPress = () => { + this.setState({ + isEditDownloadClientModalOpen: false, + isDeleteDownloadClientModalOpen: true + }); + } + + onDeleteDownloadClientModalClose= () => { + this.setState({ isDeleteDownloadClientModalOpen: false }); + } + + onConfirmDeleteDownloadClient = () => { + this.props.onConfirmDeleteDownloadClient(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + enable + } = this.props; + + return ( + +
+ {name} +
+ +
+ +
+ + + + +
+ ); + } +} + +DownloadClient.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enable: PropTypes.bool.isRequired, + onConfirmDeleteDownloadClient: PropTypes.func.isRequired +}; + +export default DownloadClient; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css new file mode 100644 index 000000000..ad53e6311 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.css @@ -0,0 +1,20 @@ +.downloadClients { + display: flex; + flex-wrap: wrap; +} + +.addDownloadClient { + composes: downloadClient from './DownloadClient.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js new file mode 100644 index 000000000..fe5371e4f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClients.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import DownloadClient from './DownloadClient'; +import AddDownloadClientModal from './AddDownloadClientModal'; +import EditDownloadClientModalConnector from './EditDownloadClientModalConnector'; +import styles from './DownloadClients.css'; + +class DownloadClients extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddDownloadClientModalOpen: false, + isEditDownloadClientModalOpen: false + }; + } + + // + // Listeners + + onAddDownloadClientPress = () => { + this.setState({ isAddDownloadClientModalOpen: true }); + } + + onAddDownloadClientModalClose = ({ downloadClientSelected = false } = {}) => { + this.setState({ + isAddDownloadClientModalOpen: false, + isEditDownloadClientModalOpen: downloadClientSelected + }); + } + + onEditDownloadClientModalClose = () => { + this.setState({ isEditDownloadClientModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteDownloadClient, + ...otherProps + } = this.props; + + const { + isAddDownloadClientModalOpen, + isEditDownloadClientModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +DownloadClients.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteDownloadClient: PropTypes.func.isRequired +}; + +export default DownloadClients; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js new file mode 100644 index 000000000..d318bc163 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/DownloadClientsConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDownloadClients, deleteDownloadClient } from 'Store/Actions/settingsActions'; +import DownloadClients from './DownloadClients'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.downloadClients, + (downloadClients) => { + return { + ...downloadClients + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClients, + deleteDownloadClient +}; + +class DownloadClientsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClients(); + } + + // + // Listeners + + onConfirmDeleteDownloadClient = (id) => { + this.props.deleteDownloadClient({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DownloadClientsConnector.propTypes = { + fetchDownloadClients: PropTypes.func.isRequired, + deleteDownloadClient: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientsConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js new file mode 100644 index 000000000..f82e7eea1 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditDownloadClientModalContentConnector from './EditDownloadClientModalContentConnector'; + +function EditDownloadClientModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditDownloadClientModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditDownloadClientModal; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js new file mode 100644 index 000000000..e6778452b --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditDownloadClientModal from './EditDownloadClientModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditDownloadClientModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'downloadClients' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDownloadClientModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditDownloadClientModalConnector); diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css new file mode 100644 index 000000000..c73406b57 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.css @@ -0,0 +1,11 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + +.message { + composes: alert from 'Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js new file mode 100644 index 000000000..7475084c8 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContent.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditDownloadClientModalContent.css'; + +class EditDownloadClientModalContent extends Component { + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteDownloadClientPress, + ...otherProps + } = this.props; + + const { + id, + name, + enable, + fields, + message + } = item; + + return ( + + + {id ? 'Edit DownloadClient' : 'Add DownloadClient'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new downloadClient, please try again.
+ } + + { + !isFetching && !error && +
+ { + !!message && + + {message.value.message} + + } + + + Name + + + + + + Enable + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); + } +} + +EditDownloadClientModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + isTesting: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteDownloadClientPress: PropTypes.func +}; + +export default EditDownloadClientModalContent; diff --git a/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js new file mode 100644 index 000000000..bf9acb98c --- /dev/null +++ b/frontend/src/Settings/DownloadClients/DownloadClients/EditDownloadClientModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setDownloadClientValue, setDownloadClientFieldValue, saveDownloadClient, testDownloadClient } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditDownloadClientModalContent from './EditDownloadClientModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, downloadClient) => { + return { + advancedSettings, + ...downloadClient + }; + } + ); +} + +const mapDispatchToProps = { + setDownloadClientValue, + setDownloadClientFieldValue, + saveDownloadClient, + testDownloadClient +}; + +class EditDownloadClientModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDownloadClientValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setDownloadClientFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveDownloadClient({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testDownloadClient({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDownloadClientModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setDownloadClientValue: PropTypes.func.isRequired, + setDownloadClientFieldValue: PropTypes.func.isRequired, + saveDownloadClient: PropTypes.func.isRequired, + testDownloadClient: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'downloadClients' } +)(EditDownloadClientModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js new file mode 100644 index 000000000..2ec9b417d --- /dev/null +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptions.js @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +function DownloadClientOptions(props) { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + onInputChange + } = props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load download client options
+ } + + { + hasSettings && !isFetching && !error && +
+
+
+ + Enable + + + + + + Remove + + + +
+
+ +
+
+ + Redownload + + + + + + Remove + + + +
+
+
+ } +
+ ); +} + +DownloadClientOptions.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default DownloadClientOptions; diff --git a/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js new file mode 100644 index 000000000..2acfc6275 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/Options/DownloadClientOptionsConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchDownloadClientOptions, setDownloadClientOptionsValue, saveDownloadClientOptions } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import DownloadClientOptions from './DownloadClientOptions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchDownloadClientOptions, + setDownloadClientOptionsValue, + saveDownloadClientOptions, + clearPendingChanges +}; + +class DownloadClientOptionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDownloadClientOptions(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveDownloadClientOptions(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDownloadClientOptionsValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DownloadClientOptionsConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + fetchDownloadClientOptions: PropTypes.func.isRequired, + setDownloadClientOptionsValue: PropTypes.func.isRequired, + saveDownloadClientOptions: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'downloadClientOptions' } + )(DownloadClientOptionsConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js new file mode 100644 index 000000000..5ba30d614 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditRemotePathMappingModalContentConnector from './EditRemotePathMappingModalContentConnector'; + +function EditRemotePathMappingModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditRemotePathMappingModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditRemotePathMappingModal; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js new file mode 100644 index 000000000..bd8bca75f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditRemotePathMappingModal from './EditRemotePathMappingModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditRemotePathMappingModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'remotePathMappings' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRemotePathMappingModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css new file mode 100644 index 000000000..0071acc4e --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.css @@ -0,0 +1,12 @@ +.body { + composes: modalBody from 'Components/Modal/ModalBody.css'; + + flex: 1 1 430px; +} + +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js new file mode 100644 index 000000000..2e21ea87b --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { stringSettingShape } from 'Helpers/Props/Shapes/settingShape'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditRemotePathMappingModalContent.css'; + +function EditRemotePathMappingModalContent(props) { + const { + id, + isFetching, + error, + isSaving, + saveError, + item, + onInputChange, + onSavePress, + onModalClose, + onDeleteRemotePathMappingPress, + ...otherProps + } = props; + + const { + host, + remotePath, + localPath + } = item; + + return ( + + + {id ? 'Edit Remote Path Mapping' : 'Add Remote Path Mapping'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new remote path mapping, please try again.
+ } + + { + !isFetching && !error && +
+ + Host + + + + + + Remote Path + + + + + + Local Path + + + +
+ } +
+ + + { + id && + + } + + + + + Save + + +
+ ); +} + +const remotePathMappingShape = { + host: PropTypes.shape(stringSettingShape).isRequired, + remotePath: PropTypes.shape(stringSettingShape).isRequired, + localPath: PropTypes.shape(stringSettingShape).isRequired +}; + +EditRemotePathMappingModalContent.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.shape(remotePathMappingShape).isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteRemotePathMappingPress: PropTypes.func +}; + +export default EditRemotePathMappingModalContent; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js new file mode 100644 index 000000000..00aa7b8ac --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/EditRemotePathMappingModalContentConnector.js @@ -0,0 +1,119 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setRemotePathMappingValue, saveRemotePathMapping } from 'Store/Actions/settingsActions'; +import EditRemotePathMappingModalContent from './EditRemotePathMappingModalContent'; + +const newRemotePathMapping = { + host: '', + remotePath: '', + localPath: '' +}; + +function createRemotePathMappingSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.remotePathMappings, + (id, remotePathMappings) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = remotePathMappings; + + const mapping = id ? _.find(items, { id }) : newRemotePathMapping; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createRemotePathMappingSelector(), + (remotePathMapping) => { + return { + ...remotePathMapping + }; + } + ); +} + +const mapDispatchToProps = { + setRemotePathMappingValue, + saveRemotePathMapping +}; + +class EditRemotePathMappingModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newRemotePathMapping).forEach((name) => { + this.props.setRemotePathMappingValue({ + name, + value: newRemotePathMapping[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setRemotePathMappingValue({ name, value }); + } + + onSavePress = () => { + this.props.saveRemotePathMapping({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRemotePathMappingModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setRemotePathMappingValue: PropTypes.func.isRequired, + saveRemotePathMapping: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditRemotePathMappingModalContentConnector); diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css new file mode 100644 index 000000000..a79efda26 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.css @@ -0,0 +1,23 @@ +.remotePathMapping { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.host { + flex: 0 0 300px; +} + +.path { + flex: 0 0 400px; +} + +.actions { + display: flex; + justify-content: flex-end; + flex: 1 0 auto; + padding-right: 10px; +} diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js new file mode 100644 index 000000000..c0c0a988f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMapping.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; +import styles from './RemotePathMapping.css'; + +class RemotePathMapping extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditRemotePathMappingModalOpen: false, + isDeleteRemotePathMappingModalOpen: false + }; + } + + // + // Listeners + + onEditRemotePathMappingPress = () => { + this.setState({ isEditRemotePathMappingModalOpen: true }); + } + + onEditRemotePathMappingModalClose = () => { + this.setState({ isEditRemotePathMappingModalOpen: false }); + } + + onDeleteRemotePathMappingPress = () => { + this.setState({ + isEditRemotePathMappingModalOpen: false, + isDeleteRemotePathMappingModalOpen: true + }); + } + + onDeleteRemotePathMappingModalClose = () => { + this.setState({ isDeleteRemotePathMappingModalOpen: false }); + } + + onConfirmDeleteRemotePathMapping = () => { + this.props.onConfirmDeleteRemotePathMapping(this.props.id); + } + + // + // Render + + render() { + const { + id, + host, + remotePath, + localPath + } = this.props; + + return ( +
+
{host}
+
{remotePath}
+
{localPath}
+ +
+ + + +
+ + + + +
+ ); + } +} + +RemotePathMapping.propTypes = { + id: PropTypes.number.isRequired, + host: PropTypes.string.isRequired, + remotePath: PropTypes.string.isRequired, + localPath: PropTypes.string.isRequired, + onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired +}; + +RemotePathMapping.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default RemotePathMapping; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css new file mode 100644 index 000000000..4ef9dcb0f --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.css @@ -0,0 +1,23 @@ +.remotePathMappingsHeader { + display: flex; + margin-bottom: 10px; + font-weight: bold; +} + +.host { + flex: 0 0 300px; +} + +.path { + flex: 0 0 400px; +} + +.addRemotePathMapping { + display: flex; + justify-content: flex-end; + padding-right: 10px; +} + +.addButton { + text-align: center; +} diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js new file mode 100644 index 000000000..93d022e02 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappings.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import RemotePathMapping from './RemotePathMapping'; +import EditRemotePathMappingModalConnector from './EditRemotePathMappingModalConnector'; +import styles from './RemotePathMappings.css'; + +class RemotePathMappings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddRemotePathMappingModalOpen: false + }; + } + + // + // Listeners + + onAddRemotePathMappingPress = () => { + this.setState({ isAddRemotePathMappingModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isAddRemotePathMappingModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteRemotePathMapping, + ...otherProps + } = this.props; + + return ( +
+ +
+
Host
+
Remote Path
+
Local Path
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } +
+ +
+ + + +
+ + +
+
+ ); + } +} + +RemotePathMappings.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRemotePathMapping: PropTypes.func.isRequired +}; + +export default RemotePathMappings; diff --git a/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js new file mode 100644 index 000000000..4900119a3 --- /dev/null +++ b/frontend/src/Settings/DownloadClients/RemotePathMappings/RemotePathMappingsConnector.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchRemotePathMappings, deleteRemotePathMapping } from 'Store/Actions/settingsActions'; +import RemotePathMappings from './RemotePathMappings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.remotePathMappings, + (remotePathMappings) => { + return { + ...remotePathMappings + }; + } + ); +} + +const mapDispatchToProps = { + fetchRemotePathMappings, + deleteRemotePathMapping +}; + +class RemotePathMappingsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRemotePathMappings(); + } + + // + // Listeners + + onConfirmDeleteRemotePathMapping = (id) => { + this.props.deleteRemotePathMapping({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +RemotePathMappingsConnector.propTypes = { + fetchRemotePathMappings: PropTypes.func.isRequired, + deleteRemotePathMapping: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(RemotePathMappingsConnector); diff --git a/frontend/src/Settings/General/GeneralSettings.js b/frontend/src/Settings/General/GeneralSettings.js new file mode 100644 index 000000000..3ab73148f --- /dev/null +++ b/frontend/src/Settings/General/GeneralSettings.js @@ -0,0 +1,656 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons, inputTypes, kinds, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import ClipboardButton from 'Components/Link/ClipboardButton'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormInputButton from 'Components/Form/FormInputButton'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; + +class GeneralSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isConfirmApiKeyResetModalOpen: false, + isRestartRequiredModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + const { + settings, + isSaving, + saveError + } = this.props; + + if (isSaving || saveError || !prevProps.isSaving) { + return; + } + + const prevSettings = prevProps.settings; + + const keys = [ + 'bindAddress', + 'port', + 'urlBase', + 'enableSsl', + 'sslPort', + 'sslCertHash', + 'authenticationMethod', + 'username', + 'password', + 'apiKey' + ]; + + const pendingRestart = _.some(keys, (key) => { + const setting = settings[key]; + const prevSetting = prevSettings[key]; + + if (!setting || !prevSetting) { + return false; + } + + const previousValue = prevSetting.previousValue; + const value = setting.value; + + return previousValue != null && previousValue !== value; + }); + + this.setState({ isRestartRequiredModalOpen: pendingRestart }); + } + + // + // Listeners + + onApikeyFocus = (event) => { + event.target.select(); + } + + onResetApiKeyPress = () => { + this.setState({ isConfirmApiKeyResetModalOpen: true }); + } + + onConfirmResetApiKey = () => { + this.setState({ isConfirmApiKeyResetModalOpen: false }); + this.props.onConfirmResetApiKey(); + } + + onCloseResetApiKeyModal = () => { + this.setState({ isConfirmApiKeyResetModalOpen: false }); + } + + onConfirmRestart = () => { + this.setState({ isRestartRequiredModalOpen: false }); + this.props.onConfirmRestart(); + } + + onCloseRestartRequiredModalOpen = () => { + this.setState({ isRestartRequiredModalOpen: false }); + } + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + isPopulated, + error, + settings, + hasSettings, + isResettingApiKey, + isMono, + isWindows, + mode, + onInputChange, + ...otherProps + } = this.props; + + const { + isConfirmApiKeyResetModalOpen, + isRestartRequiredModalOpen + } = this.state; + + const { + bindAddress, + port, + urlBase, + enableSsl, + sslPort, + sslCertHash, + launchBrowser, + authenticationMethod, + username, + password, + apiKey, + proxyEnabled, + proxyType, + proxyHostname, + proxyPort, + proxyUsername, + proxyPassword, + proxyBypassFilter, + proxyBypassLocalAddresses, + logLevel, + analyticsEnabled, + branch, + updateAutomatically, + updateMechanism, + updateScriptPath + } = settings; + + const authenticationMethodOptions = [ + { key: 'none', value: 'None' }, + { key: 'basic', value: 'Basic (Browser Popup)' }, + { key: 'forms', value: 'Forms (Login Page)' } + ]; + + const proxyTypeOptions = [ + { key: 'http', value: 'HTTP(S)' }, + { key: 'socks4', value: 'Socks4' }, + { key: 'socks5', value: 'Socks5 (Support TOR)' } + ]; + + const logLevelOptions = [ + { key: 'info', value: 'Info' }, + { key: 'debug', value: 'Debug' }, + { key: 'trace', value: 'Trace' } + ]; + + const updateOptions = [ + { key: 'builtIn', value: 'Built-In' }, + { key: 'script', value: 'Script' } + ]; + + const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none'; + + return ( + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
Unable to load General settings
+ } + + { + hasSettings && isPopulated && !error && +
+
+ + Bind Address + + + + + + Port Number + + + + + + URL Base + + + + + + Enable SSL + + + + + { + enableSsl.value && + + SSL Port + + + + } + + { + isWindows && enableSsl.value && + + SSL Cert Hash + + + + } + + { + mode !== 'service' && + + Open browser on start + + + + } + +
+ +
+ + Authentication + + + + + { + authenticationEnabled && + + Username + + + + } + + { + authenticationEnabled && + + Password + + + + } + + + API Key + + , + + + + + ]} + onChange={onInputChange} + onFocus={this.onApikeyFocus} + {...apiKey} + /> + +
+ +
+ + Use Proxy + + + + + { + proxyEnabled.value && +
+ + Proxy Type + + + + + + Hostname + + + + + + Port + + + + + + Username + + + + + + Password + + + + + + Ignored Addresses + + + + + + Bypass Proxy for Local Addresses + + + +
+ } +
+ +
+ + Log Level + + + +
+ +
+ + Send Anonymous Usage Data + + + +
+ + { + advancedSettings && +
+ + Branch + + + + + { + isMono && +
+ + Automatic + + + + + + Mechanism + + + + + { + updateMechanism.value === 'script' && + + Script Path + + + + } +
+ } +
+ } +
+ } +
+ + + + +
+ ); + } + +} + +GeneralSettings.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + settings: PropTypes.object.isRequired, + isResettingApiKey: PropTypes.bool.isRequired, + hasSettings: PropTypes.bool.isRequired, + isMono: PropTypes.bool.isRequired, + isWindows: PropTypes.bool.isRequired, + mode: PropTypes.string.isRequired, + onInputChange: PropTypes.func.isRequired, + onConfirmResetApiKey: PropTypes.func.isRequired, + onConfirmRestart: PropTypes.func.isRequired +}; + +export default GeneralSettings; diff --git a/frontend/src/Settings/General/GeneralSettingsConnector.js b/frontend/src/Settings/General/GeneralSettingsConnector.js new file mode 100644 index 000000000..0bc539d3e --- /dev/null +++ b/frontend/src/Settings/General/GeneralSettingsConnector.js @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { setGeneralSettingsValue, saveGeneralSettings, fetchGeneralSettings } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { restart } from 'Store/Actions/systemActions'; +import connectSection from 'Store/connectSection'; +import * as commandNames from 'Commands/commandNames'; +import GeneralSettings from './GeneralSettings'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + createCommandsSelector(), + createSystemStatusSelector(), + (advancedSettings, sectionSettings, commands, systemStatus) => { + const isResettingApiKey = _.some(commands, { name: commandNames.RESET_API_KEY }); + + return { + advancedSettings, + isResettingApiKey, + isMono: systemStatus.isMono, + isWindows: systemStatus.isWindows, + mode: systemStatus.mode, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + setGeneralSettingsValue, + saveGeneralSettings, + fetchGeneralSettings, + executeCommand, + restart, + clearPendingChanges +}; + +class GeneralSettingsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchGeneralSettings(); + } + + componentDidUpdate(prevProps) { + if (!this.props.isResettingApiKey && prevProps.isResettingApiKey) { + this.props.fetchGeneralSettings(); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setGeneralSettingsValue({ name, value }); + } + + onSavePress = () => { + this.props.saveGeneralSettings(); + } + + onConfirmResetApiKey = () => { + this.props.executeCommand({ name: commandNames.RESET_API_KEY }); + } + + onConfirmRestart = () => { + this.props.restart(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +GeneralSettingsConnector.propTypes = { + section: PropTypes.string.isRequired, + isResettingApiKey: PropTypes.bool.isRequired, + setGeneralSettingsValue: PropTypes.func.isRequired, + saveGeneralSettings: PropTypes.func.isRequired, + fetchGeneralSettings: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + restart: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'general' } + )(GeneralSettingsConnector); diff --git a/frontend/src/Settings/Indexers/IndexerSettings.js b/frontend/src/Settings/Indexers/IndexerSettings.js new file mode 100644 index 000000000..2e526c080 --- /dev/null +++ b/frontend/src/Settings/Indexers/IndexerSettings.js @@ -0,0 +1,65 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import IndexersConnector from './Indexers/IndexersConnector'; +import IndexerOptionsConnector from './Options/IndexerOptionsConnector'; +import RestrictionsConnector from './Restrictions/RestrictionsConnector'; + +class IndexerSettings extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setIndexerOptionsRef = (ref) => { + this._indexerOptions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._indexerOptions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + + + + + ); + } +} + +export default IndexerSettings; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css new file mode 100644 index 000000000..e9dd6d1d5 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.css @@ -0,0 +1,44 @@ +.indexer { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + composes: cover from 'Styles/Mixins/cover.css'; +} + +.overlay { + composes: linkOverlay from 'Styles/Mixins/linkOverlay.css'; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js new file mode 100644 index 000000000..21db4ecf1 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem'; +import styles from './AddIndexerItem.css'; + +class AddIndexerItem extends Component { + + // + // Listeners + + onIndexerSelect = () => { + const { + implementation + } = this.props; + + this.props.onIndexerSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onIndexerSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddIndexerItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onIndexerSelect: PropTypes.func.isRequired +}; + +export default AddIndexerItem; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js new file mode 100644 index 000000000..d05e8eb9a --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddIndexerModalContentConnector from './AddIndexerModalContentConnector'; + +function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css new file mode 100644 index 000000000..946305dff --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.css @@ -0,0 +1,5 @@ +.indexers { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js new file mode 100644 index 000000000..bdb322fe0 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContent.js @@ -0,0 +1,115 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import FieldSet from 'Components/FieldSet'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddIndexerItem from './AddIndexerItem'; +import styles from './AddIndexerModalContent.css'; + +class AddIndexerModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + usenetIndexers, + torrentIndexers, + onIndexerSelect, + onModalClose + } = this.props; + + return ( + + + Add Indexer + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new indexer, please try again.
+ } + + { + isPopulated && !error && +
+ + +
Sonarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.
+
For more information on the individual indexers, clink on the info buttons.
+
+ +
+
+ { + usenetIndexers.map((indexer) => { + return ( + + ); + }) + } +
+
+ +
+
+ { + torrentIndexers.map((indexer) => { + return ( + + ); + }) + } +
+
+
+ } +
+ + + +
+ ); + } +} + +AddIndexerModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, + torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired, + onIndexerSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js new file mode 100644 index 000000000..986466c09 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerModalContentConnector.js @@ -0,0 +1,75 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions'; +import AddIndexerModalContent from './AddIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers, + (indexers) => { + const { + isFetching, + error, + isPopulated, + schema + } = indexers; + + const usenetIndexers = _.filter(schema, { protocol: 'usenet' }); + const torrentIndexers = _.filter(schema, { protocol: 'torrent' }); + + return { + isFetching, + error, + isPopulated, + usenetIndexers, + torrentIndexers + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerSchema, + selectIndexerSchema +}; + +class AddIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerSchema(); + } + + // + // Listeners + + onIndexerSelect = ({ implementation, name }) => { + this.props.selectIndexerSchema({ implementation, presetName: name }); + this.props.onModalClose({ indexerSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddIndexerModalContentConnector.propTypes = { + fetchIndexerSchema: PropTypes.func.isRequired, + selectIndexerSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js new file mode 100644 index 000000000..ddea8b043 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/AddIndexerPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddIndexerPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddIndexerPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddIndexerPresetMenuItem; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js new file mode 100644 index 000000000..fef70e29f --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditIndexerModalContentConnector from './EditIndexerModalContentConnector'; + +function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditIndexerModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditIndexerModal; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js new file mode 100644 index 000000000..9e34e93c4 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditIndexerModal from './EditIndexerModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditIndexerModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'indexers' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditIndexerModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditIndexerModalConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js new file mode 100644 index 000000000..00b41730c --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContent.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditIndexerModalContent.css'; + +function EditIndexerModalContent(props) { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteIndexerPress, + ...otherProps + } = props; + + const { + id, + name, + enableRss, + enableSearch, + supportsRss, + supportsSearch, + fields + } = item; + + return ( + + + {id ? 'Edit Indexer' : 'Add Indexer'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new indexer, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Enable RSS + + + + + + Enable Search + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); +} + +EditIndexerModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + isTesting: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteIndexerPress: PropTypes.func +}; + +export default EditIndexerModalContent; diff --git a/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js new file mode 100644 index 000000000..c4d9e597e --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/EditIndexerModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setIndexerValue, setIndexerFieldValue, saveIndexer, testIndexer } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditIndexerModalContent from './EditIndexerModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, indexer) => { + return { + advancedSettings, + ...indexer + }; + } + ); +} + +const mapDispatchToProps = { + setIndexerValue, + setIndexerFieldValue, + saveIndexer, + testIndexer +}; + +class EditIndexerModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setIndexerValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setIndexerFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveIndexer({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testIndexer({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditIndexerModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setIndexerValue: PropTypes.func.isRequired, + setIndexerFieldValue: PropTypes.func.isRequired, + saveIndexer: PropTypes.func.isRequired, + testIndexer: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'indexers' } +)(EditIndexerModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.css b/frontend/src/Settings/Indexers/Indexers/Indexer.css new file mode 100644 index 000000000..9cd62404c --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.css @@ -0,0 +1,19 @@ +.indexer { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + composes: truncate from 'Styles/Mixins/truncate.css'; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexer.js b/frontend/src/Settings/Indexers/Indexers/Indexer.js new file mode 100644 index 000000000..221a6a239 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexer.js @@ -0,0 +1,131 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditIndexerModalConnector from './EditIndexerModalConnector'; +import styles from './Indexer.css'; + +function getLabelKind(supports, enabled) { + if (!supports) { + return kinds.DEFAULT; + } + + if (!enabled) { + return kinds.DANGER; + } + + return kinds.SUCCESS; +} + +class Indexer extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditIndexerModalOpen: false, + isDeleteIndexerModalOpen: false + }; + } + + // + // Listeners + + onEditIndexerPress = () => { + this.setState({ isEditIndexerModalOpen: true }); + } + + onEditIndexerModalClose = () => { + this.setState({ isEditIndexerModalOpen: false }); + } + + onDeleteIndexerPress = () => { + this.setState({ + isEditIndexerModalOpen: false, + isDeleteIndexerModalOpen: true + }); + } + + onDeleteIndexerModalClose= () => { + this.setState({ isDeleteIndexerModalOpen: false }); + } + + onConfirmDeleteIndexer = () => { + this.props.onConfirmDeleteIndexer(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + enableRss, + enableSearch, + supportsRss, + supportsSearch + } = this.props; + + return ( + +
+ {name} +
+ +
+ + + +
+ + + + +
+ ); + } +} + +Indexer.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enableRss: PropTypes.bool.isRequired, + enableSearch: PropTypes.bool.isRequired, + supportsRss: PropTypes.bool.isRequired, + supportsSearch: PropTypes.bool.isRequired, + onConfirmDeleteIndexer: PropTypes.func.isRequired +}; + +export default Indexer; diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.css b/frontend/src/Settings/Indexers/Indexers/Indexers.css new file mode 100644 index 000000000..ec8cb2891 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.css @@ -0,0 +1,20 @@ +.indexers { + display: flex; + flex-wrap: wrap; +} + +.addIndexer { + composes: indexer from './Indexer.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Indexers/Indexers/Indexers.js b/frontend/src/Settings/Indexers/Indexers/Indexers.js new file mode 100644 index 000000000..8b7d37a84 --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/Indexers.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Indexer from './Indexer'; +import AddIndexerModal from './AddIndexerModal'; +import EditIndexerModalConnector from './EditIndexerModalConnector'; +import styles from './Indexers.css'; + +class Indexers extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddIndexerModalOpen: false, + isEditIndexerModalOpen: false + }; + } + + // + // Listeners + + onAddIndexerPress = () => { + this.setState({ isAddIndexerModalOpen: true }); + } + + onAddIndexerModalClose = ({ indexerSelected = false } = {}) => { + this.setState({ + isAddIndexerModalOpen: false, + isEditIndexerModalOpen: indexerSelected + }); + } + + onEditIndexerModalClose = () => { + this.setState({ isEditIndexerModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteIndexer, + ...otherProps + } = this.props; + + const { + isAddIndexerModalOpen, + isEditIndexerModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +Indexers.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteIndexer: PropTypes.func.isRequired +}; + +export default Indexers; diff --git a/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js new file mode 100644 index 000000000..415dae32b --- /dev/null +++ b/frontend/src/Settings/Indexers/Indexers/IndexersConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchIndexers, deleteIndexer } from 'Store/Actions/settingsActions'; +import Indexers from './Indexers'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.indexers, + (indexers) => { + return { + ...indexers + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexers, + deleteIndexer +}; + +class IndexersConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexers(); + } + + // + // Listeners + + onConfirmDeleteIndexer = (id) => { + this.props.deleteIndexer({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +IndexersConnector.propTypes = { + fetchIndexers: PropTypes.func.isRequired, + deleteIndexer: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector); diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptions.js b/frontend/src/Settings/Indexers/Options/IndexerOptions.js new file mode 100644 index 000000000..0a39ec7b7 --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/IndexerOptions.js @@ -0,0 +1,93 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; + +function IndexerOptions(props) { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + onInputChange + } = props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load indexer options
+ } + + { + hasSettings && !isFetching && !error && +
+ + Minimum Age + + + + + + Retention + + + + + + RSS Sync Interval + + + +
+ } +
+ ); +} + +IndexerOptions.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default IndexerOptions; diff --git a/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js new file mode 100644 index 000000000..b88894ebe --- /dev/null +++ b/frontend/src/Settings/Indexers/Options/IndexerOptionsConnector.js @@ -0,0 +1,92 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import IndexerOptions from './IndexerOptions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createSettingsSectionSelector(), + (advancedSettings, sectionSettings) => { + return { + advancedSettings, + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchIndexerOptions, + setIndexerOptionsValue, + saveIndexerOptions, + clearPendingChanges +}; + +class IndexerOptionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchIndexerOptions(); + } + + componentDidUpdate(prevProps) { + if (this.props.hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(this.props.hasPendingChanges); + } + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + save = () => { + this.props.saveIndexerOptions(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setIndexerOptionsValue({ name, value }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +IndexerOptionsConnector.propTypes = { + section: PropTypes.string.isRequired, + hasPendingChanges: PropTypes.bool.isRequired, + fetchIndexerOptions: PropTypes.func.isRequired, + setIndexerOptionsValue: PropTypes.func.isRequired, + saveIndexerOptions: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + { withRef: true }, + { section: 'indexerOptions' } + )(IndexerOptionsConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js new file mode 100644 index 000000000..e9f42df98 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector'; + +function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditRestrictionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditRestrictionModal; diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js new file mode 100644 index 000000000..4483f7894 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditRestrictionModal from './EditRestrictionModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditRestrictionModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'restrictions' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRestrictionModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditRestrictionModalConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js new file mode 100644 index 000000000..37f8cd760 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContent.js @@ -0,0 +1,126 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditRestrictionModalContent.css'; + +function EditRestrictionModalContent(props) { + const { + isSaving, + saveError, + item, + onInputChange, + onModalClose, + onSavePress, + onDeleteRestrictionPress, + ...otherProps + } = props; + + const { + id, + required, + ignored, + tags + } = item; + + return ( + + + {id ? 'Edit Restriction' : 'Add Restriction'} + + + +
+ + Must Contain + + + + + + Must Not Contain + + + + + + Tags + + + +
+
+ + { + id && + + } + + + + + Save + + +
+ ); +} + +EditRestrictionModalContent.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onDeleteRestrictionPress: PropTypes.func +}; + +export default EditRestrictionModalContent; diff --git a/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js new file mode 100644 index 000000000..322b0a8d9 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/EditRestrictionModalContentConnector.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setRestrictionValue, saveRestriction } from 'Store/Actions/settingsActions'; +import EditRestrictionModalContent from './EditRestrictionModalContent'; + +const newRestriction = { + required: '', + ignored: '', + tags: [] +}; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.restrictions, + (id, restrictions) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = restrictions; + + const profile = id ? _.find(items, { id }) : newRestriction; + const settings = selectSettings(profile, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setRestrictionValue, + saveRestriction +}; + +class EditRestrictionModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newRestriction).forEach((name) => { + this.props.setRestrictionValue({ + name, + value: newRestriction[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setRestrictionValue({ name, value }); + } + + onSavePress = () => { + this.props.saveRestriction({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditRestrictionModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setRestrictionValue: PropTypes.func.isRequired, + saveRestriction: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditRestrictionModalContentConnector); diff --git a/frontend/src/Settings/Indexers/Restrictions/Restriction.css b/frontend/src/Settings/Indexers/Restrictions/Restriction.css new file mode 100644 index 000000000..0e84466f9 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restriction.css @@ -0,0 +1,11 @@ +.restriction { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/Restriction.js b/frontend/src/Settings/Indexers/Restrictions/Restriction.js new file mode 100644 index 000000000..bdd457aca --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restriction.js @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import split from 'Utilities/String/split'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import TagList from 'Components/TagList'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditRestrictionModalConnector from './EditRestrictionModalConnector'; +import styles from './Restriction.css'; + +class Restriction extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditRestrictionModalOpen: false, + isDeleteRestrictionModalOpen: false + }; + } + + // + // Listeners + + onEditRestrictionPress = () => { + this.setState({ isEditRestrictionModalOpen: true }); + } + + onEditRestrictionModalClose = () => { + this.setState({ isEditRestrictionModalOpen: false }); + } + + onDeleteRestrictionPress = () => { + this.setState({ + isEditRestrictionModalOpen: false, + isDeleteRestrictionModalOpen: true + }); + } + + onDeleteRestrictionModalClose= () => { + this.setState({ isDeleteRestrictionModalOpen: false }); + } + + onConfirmDeleteRestriction = () => { + this.props.onConfirmDeleteRestriction(this.props.id); + } + + // + // Render + + render() { + const { + id, + required, + ignored, + tags, + tagList + } = this.props; + + return ( + +
+ { + split(required).map((item) => { + if (!item) { + return null; + } + + return ( + + ); + }) + } +
+ +
+ { + split(ignored).map((item) => { + if (!item) { + return null; + } + + return ( + + ); + }) + } +
+ + + + + + +
+ ); + } +} + +Restriction.propTypes = { + id: PropTypes.number.isRequired, + required: PropTypes.string.isRequired, + ignored: PropTypes.string.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRestriction: PropTypes.func.isRequired +}; + +Restriction.defaultProps = { + required: '', + ignored: '' +}; + +export default Restriction; diff --git a/frontend/src/Settings/Indexers/Restrictions/Restrictions.css b/frontend/src/Settings/Indexers/Restrictions/Restrictions.css new file mode 100644 index 000000000..904a66a57 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restrictions.css @@ -0,0 +1,20 @@ +.restrictions { + display: flex; + flex-wrap: wrap; +} + +.addRestriction { + composes: restriction from './Restriction.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Indexers/Restrictions/Restrictions.js b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js new file mode 100644 index 000000000..411b95ea8 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/Restrictions.js @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Restriction from './Restriction'; +import EditRestrictionModalConnector from './EditRestrictionModalConnector'; +import styles from './Restrictions.css'; + +class Restrictions extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddRestrictionModalOpen: false + }; + } + + // + // Listeners + + onAddRestrictionPress = () => { + this.setState({ isAddRestrictionModalOpen: true }); + } + + onAddRestrictionModalClose = () => { + this.setState({ isAddRestrictionModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + tagList, + onConfirmDeleteRestriction, + ...otherProps + } = this.props; + + return ( +
+ +
+ +
+ +
+
+ + { + items.map((item) => { + return ( + + ); + }) + } +
+ + +
+
+ ); + } +} + +Restrictions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteRestriction: PropTypes.func.isRequired +}; + +export default Restrictions; diff --git a/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js b/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js new file mode 100644 index 000000000..c53c05de2 --- /dev/null +++ b/frontend/src/Settings/Indexers/Restrictions/RestrictionsConnector.js @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchRestrictions, deleteRestriction } from 'Store/Actions/settingsActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import Restrictions from './Restrictions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.restrictions, + createTagsSelector(), + (restrictions, tagList) => { + return { + ...restrictions, + tagList + }; + } + ); +} + +const mapDispatchToProps = { + fetchRestrictions, + deleteRestriction +}; + +class RestrictionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchRestrictions(); + } + + // + // Listeners + + onConfirmDeleteRestriction = (id) => { + this.props.deleteRestriction({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +RestrictionsConnector.propTypes = { + fetchRestrictions: PropTypes.func.isRequired, + deleteRestriction: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(RestrictionsConnector); diff --git a/frontend/src/Settings/MediaManagement/MediaManagement.js b/frontend/src/Settings/MediaManagement/MediaManagement.js new file mode 100644 index 000000000..88ff3cbf5 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/MediaManagement.js @@ -0,0 +1,347 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import NamingConnector from './Naming/NamingConnector'; + +class MediaManagement extends Component { + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + isMono, + onInputChange, + onSavePress, + ...otherProps + } = this.props; + + const fileDateOptions = [ + { key: 'none', value: 'None' }, + { key: 'localAirDate', value: 'Local Air Date' }, + { key: 'utcAirDate', value: 'UTC Air Date' } + ]; + + return ( + + + + + { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Media Management settings
+ } + + { + hasSettings && !isFetching && !error && +
+ + + { + advancedSettings && +
+ + Create empty series folders + + + +
+ } + + { + advancedSettings && +
+ { + isMono && + + Skip Free Space Check + + + + } + + + Use Hardlinks instead of Copy + + + + + + Import Extra Files + + + + + { + settings.importExtraFiles.value && + + Import Extra Files + + + + } +
+ } + +
+ + Ignore Deleted Episodes + + + + + + Download Propers + + + + + + Analyse video files + + + + + + Change File Date + + + + + + Recycling Bin + + + +
+ + { + advancedSettings && isMono && +
+ + Set Permissions + + + + + + File chmod mask + + + + + + Folder chmod mask + + + + + + chown User + + + + + + chown Group + + + +
+ } + + } +
+
+ ); + } + +} + +MediaManagement.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + isMono: PropTypes.bool.isRequired, + onSavePress: PropTypes.func.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default MediaManagement; diff --git a/frontend/src/Settings/MediaManagement/MediaManagementConnector.js b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js new file mode 100644 index 000000000..ae3af03f3 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/MediaManagementConnector.js @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector'; +import { fetchMediaManagementSettings, setMediaManagementSettingsValue, saveMediaManagementSettings, saveNamingSettings } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import MediaManagement from './MediaManagement'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.naming, + createSettingsSectionSelector(), + createSystemStatusSelector(), + (advancedSettings, namingSettings, sectionSettings, systemStatus) => { + return { + advancedSettings, + ...sectionSettings, + hasPendingChanges: !_.isEmpty(namingSettings.pendingChanges) || sectionSettings.hasPendingChanges, + isMono: systemStatus.isMono + }; + } + ); +} + +const mapDispatchToProps = { + fetchMediaManagementSettings, + setMediaManagementSettingsValue, + saveMediaManagementSettings, + saveNamingSettings, + clearPendingChanges +}; + +class MediaManagementConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMediaManagementSettings(); + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMediaManagementSettingsValue({ name, value }); + } + + onSavePress = () => { + this.props.saveMediaManagementSettings(); + this.props.saveNamingSettings(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MediaManagementConnector.propTypes = { + section: PropTypes.string.isRequired, + fetchMediaManagementSettings: PropTypes.func.isRequired, + setMediaManagementSettingsValue: PropTypes.func.isRequired, + saveMediaManagementSettings: PropTypes.func.isRequired, + saveNamingSettings: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'mediaManagement' } + )(MediaManagementConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.css b/frontend/src/Settings/MediaManagement/Naming/Naming.css new file mode 100644 index 000000000..a0fbf7f19 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.css @@ -0,0 +1,5 @@ +.namingInput { + composes: text from 'Components/Form/TextInput.css'; + + font-family: $monoSpaceFontFamily; +} diff --git a/frontend/src/Settings/MediaManagement/Naming/Naming.js b/frontend/src/Settings/MediaManagement/Naming/Naming.js new file mode 100644 index 000000000..91c617c56 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/Naming.js @@ -0,0 +1,345 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { inputTypes, sizes } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FormInputButton from 'Components/Form/FormInputButton'; +import FieldSet from 'Components/FieldSet'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import NamingModal from './NamingModal'; +import styles from './Naming.css'; + +class Naming extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isNamingModalOpen: false, + namingModalOptions: null + }; + } + + // + // Listeners + + onStandardNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'standardEpisodeFormat', + season: true, + episode: true, + additional: true + } + }); + } + + onDailyNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'dailyEpisodeFormat', + season: true, + episode: true, + daily: true, + additional: true + } + }); + } + + onAnimeNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'animeEpisodeFormat', + season: true, + episode: true, + anime: true, + additional: true + } + }); + } + + onSeriesFolderNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'seriesFolderFormat' + } + }); + } + + onSeasonFolderNamingModalOpenClick = () => { + this.setState({ + isNamingModalOpen: true, + namingModalOptions: { + name: 'seasonFolderFormat', + season: true + } + }); + } + + onNamingModalClose = () => { + this.setState({ isNamingModalOpen: false }); + } + + // + // Render + + render() { + const { + advancedSettings, + isFetching, + error, + settings, + hasSettings, + examples, + examplesPopulated, + onInputChange + } = this.props; + + const { + isNamingModalOpen, + namingModalOptions + } = this.state; + + const renameEpisodes = hasSettings && settings.renameEpisodes.value; + + const multiEpisodeStyleOptions = [ + { key: 0, value: 'Extend' }, + { key: 1, value: 'Duplicate' }, + { key: 2, value: 'Repeat' }, + { key: 3, value: 'Scene' }, + { key: 4, value: 'Range' }, + { key: 5, value: 'Prefixed Range' } + ]; + + const standardEpisodeFormatHelpTexts = []; + const standardEpisodeFormatErrors = []; + const dailyEpisodeFormatHelpTexts = []; + const dailyEpisodeFormatErrors = []; + const animeEpisodeFormatHelpTexts = []; + const animeEpisodeFormatErrors = []; + const seriesFolderFormatHelpTexts = []; + const seriesFolderFormatErrors = []; + const seasonFolderFormatHelpTexts = []; + const seasonFolderFormatErrors = []; + + if (examplesPopulated) { + if (examples.singleEpisodeExample) { + standardEpisodeFormatHelpTexts.push(`Single Episode: ${examples.singleEpisodeExample}`); + } else { + standardEpisodeFormatErrors.push('Single Episode: Invalid Format'); + } + + if (examples.multiEpisodeExample) { + standardEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.multiEpisodeExample}`); + } else { + standardEpisodeFormatErrors.push('Multi Episode: Invalid Format'); + } + + if (examples.dailyEpisodeExample) { + dailyEpisodeFormatHelpTexts.push(`Example: ${examples.dailyEpisodeExample}`); + } else { + dailyEpisodeFormatErrors.push('Invalid Format'); + } + + if (examples.animeEpisodeExample) { + animeEpisodeFormatHelpTexts.push(`Single Episode: ${examples.animeEpisodeExample}`); + } else { + animeEpisodeFormatErrors.push('Single Episode: Invalid Format'); + } + + if (examples.animeMultiEpisodeExample) { + animeEpisodeFormatHelpTexts.push(`Multi Episode: ${examples.animeMultiEpisodeExample}`); + } else { + animeEpisodeFormatErrors.push('Multi Episode: Invalid Format'); + } + + if (examples.seriesFolderExample) { + seriesFolderFormatHelpTexts.push(`Example: ${examples.seriesFolderExample}`); + } else { + seriesFolderFormatErrors.push('Invalid Format'); + } + + if (examples.seasonFolderExample) { + seasonFolderFormatHelpTexts.push(`Example: ${examples.seasonFolderExample}`); + } else { + seasonFolderFormatErrors.push('Invalid Format'); + } + } + + return ( +
+ { + isFetching && + + } + + { + !isFetching && error && +
Unable to load Naming settings
+ } + + { + hasSettings && !isFetching && !error && +
+ + Rename Episodes + + + + + + Replace Illegal Characters + + + + + { + renameEpisodes && +
+ + Standard Episode Format + + ?} + onChange={onInputChange} + {...settings.standardEpisodeFormat} + helpTexts={standardEpisodeFormatHelpTexts} + errors={[...standardEpisodeFormatErrors, ...settings.standardEpisodeFormat.errors]} + /> + + + + Daily Episode Format + + ?} + onChange={onInputChange} + {...settings.dailyEpisodeFormat} + helpTexts={dailyEpisodeFormatHelpTexts} + errors={[...dailyEpisodeFormatErrors, ...settings.dailyEpisodeFormat.errors]} + /> + + + + Anime Episode Format + + ?} + onChange={onInputChange} + {...settings.animeEpisodeFormat} + helpTexts={animeEpisodeFormatHelpTexts} + errors={[...animeEpisodeFormatErrors, ...settings.animeEpisodeFormat.errors]} + /> + +
+ } + + + Series Folder Format + + ?} + onChange={onInputChange} + {...settings.seriesFolderFormat} + helpTexts={['Only used when adding a new series', ...seriesFolderFormatHelpTexts]} + errors={[...seriesFolderFormatErrors, ...settings.seriesFolderFormat.errors]} + /> + + + + Season Folder Format + + ?} + onChange={onInputChange} + {...settings.seasonFolderFormat} + helpTexts={seasonFolderFormatHelpTexts} + errors={[...seasonFolderFormatErrors, ...settings.seasonFolderFormat.errors]} + /> + + + + Multi-Episode Style + + + + + { + namingModalOptions && + + } + + } +
+ ); + } + +} + +Naming.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + settings: PropTypes.object.isRequired, + hasSettings: PropTypes.bool.isRequired, + examples: PropTypes.object.isRequired, + examplesPopulated: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired +}; + +export default Naming; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js new file mode 100644 index 000000000..59355c4b4 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingConnector.js @@ -0,0 +1,102 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector'; +import { fetchNamingSettings, setNamingSettingsValue, fetchNamingExamples } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import connectSection from 'Store/connectSection'; +import Naming from './Naming'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.namingExamples, + createSettingsSectionSelector(), + (advancedSettings, examples, sectionSettings) => { + return { + advancedSettings, + examples: examples.item, + examplesPopulated: !_.isEmpty(examples.item), + ...sectionSettings + }; + } + ); +} + +const mapDispatchToProps = { + fetchNamingSettings, + setNamingSettingsValue, + fetchNamingExamples, + clearPendingChanges +}; + +class NamingConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this._namingExampleTimeout = null; + } + + componentDidMount() { + this.props.fetchNamingSettings(); + this.props.fetchNamingExamples(); + } + + componentWillUnmount() { + this.props.clearPendingChanges({ section: this.props.section }); + } + + // + // Control + + _fetchNamingExamples = () => { + this.props.fetchNamingExamples(); + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setNamingSettingsValue({ name, value }); + + if (this._namingExampleTimeout) { + clearTimeout(this._namingExampleTimeout); + } + + this._namingExampleTimeout = setTimeout(this._fetchNamingExamples, 1000); + } + + // + // Render + + render() { + return ( + + ); + } +} + +NamingConnector.propTypes = { + section: PropTypes.string.isRequired, + fetchNamingSettings: PropTypes.func.isRequired, + setNamingSettingsValue: PropTypes.func.isRequired, + fetchNamingExamples: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'naming' } + )(NamingConnector); diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.css b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css new file mode 100644 index 000000000..708a763eb --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.css @@ -0,0 +1,17 @@ +.groups { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + margin-bottom: 20px; +} + +.namingSelectContainer { + display: flex; + justify-content: flex-end; +} + +.namingSelect { + composes: select from 'Components/Form/SelectInput.css'; + + width: 200px; +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingModal.js b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js new file mode 100644 index 000000000..59800e52a --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingModal.js @@ -0,0 +1,469 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Button from 'Components/Link/Button'; +import SelectInput from 'Components/Form/SelectInput'; +import TextInput from 'Components/Form/TextInput'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import NamingOption from './NamingOption'; +import styles from './NamingModal.css'; + +class NamingModal extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + case: 'title' + }; + } + + // + // Listeners + + onNamingCaseChange = (event) => { + this.setState({ case: event.value }); + } + + // + // Render + + render() { + const { + name, + value, + isOpen, + advancedSettings, + season, + episode, + daily, + anime, + additional, + onInputChange, + onModalClose + } = this.props; + + const namingOptions = [ + { key: 'title', value: 'Default Case' }, + { key: 'lower', value: 'Lower Case' }, + { key: 'upper', value: 'Upper Case' } + ]; + + const fileNameTokens = [ + { + token: '{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}', + example: 'Series Title (2010) - S01E01 - Episode Title HDTV-720p Proper' + }, + { + token: '{Series Title} - {season:0}x{episode:00} - {Episode Title} {Quality Full}', + example: 'Series Title (2010) - 1x01 - Episode Title HDTV-720p Proper' + }, + { + token: '{Series.Title}.S{season:00}E{episode:00}.{EpisodeClean.Title}.{Quality.Full}', + example: 'Series.Title.(2010).S01E01.Episode.Title.HDTV-720p' + } + ]; + + const seriesTokens = [ + { token: '{Series Title}', example: 'Series Title (2010)' }, + { token: '{Series.Title}', example: 'Series.Title.(2010)' }, + { token: '{Series_Title}', example: 'Series_Title_(2010)' }, + + { token: '{Series TitleThe}', example: 'Series Title, The (2010)' }, + + { token: '{Series CleanTitle}', example: 'Series Title 2010' }, + { token: '{Series.CleanTitle}', example: 'Series.Title.2010' }, + { token: '{Series_CleanTitle}', example: 'Series_Title_2010' } + ]; + + const seasonTokens = [ + { token: '{season:0}', example: '1' }, + { token: '{season:00}', example: '01' } + ]; + + const episodeTokens = [ + { token: '{episode:0}', example: '1' }, + { token: '{episode:00}', example: '01' } + ]; + + const airDateTokens = [ + { token: '{Air-Date}', example: '2016-03-20' }, + { token: '{Air Date}', example: '2016 03 20' }, + { token: '{Air.Date}', example: '2016.03.20' }, + { token: '{Air_Date}', example: '2016_03_20' } + ]; + + const absoluteTokens = [ + { token: '{absolute:0}', example: '1' }, + { token: '{absolute:00}', example: '01' }, + { token: '{absolute:000}', example: '001' } + ]; + + const episodeTitleTokens = [ + { token: '{Episode Title}', example: 'Episode Title' }, + { token: '{Episode.Title}', example: 'Episode.Title' }, + { token: '{Episode_Title}', example: 'Episode_Title' }, + { token: '{Episode CleanTitle}', example: 'Episode Title' }, + { token: '{Episode.CleanTitle}', example: 'Episode.Title' }, + { token: '{Episode_CleanTitle}', example: 'Episode_Title' } + ]; + + const qualityTokens = [ + { token: '{Quality Full}', example: 'HDTV 720p Proper' }, + { token: '{Quality-Full}', example: 'HDTV-720p-Proper' }, + { token: '{Quality.Full}', example: 'HDTV.720p.Proper' }, + { token: '{Quality_Full}', example: 'HDTV_720p_Proper' }, + { token: '{Quality Title}', example: 'HDTV 720p' }, + { token: '{Quality-Title}', example: 'HDTV-720p' }, + { token: '{Quality.Title}', example: 'HDTV.720p' }, + { token: '{Quality_Title}', example: 'HDTV_720p' } + ]; + + const mediaInfoTokens = [ + { token: '{MediaInfo Simple}', example: 'x264 DTS' }, + { token: '{MediaInfo.Simple}', example: 'x264.DTS' }, + { token: '{MediaInfo_Simple}', example: 'x264_DTS' }, + { token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]' }, + { token: '{MediaInfo.Full}', example: 'x264.DTS.[EN+DE]' }, + { token: '{MediaInfo_Full}', example: 'x264_DTS_[EN+DE]' }, + { token: '{MediaInfo VideoCodec}', example: 'x264' }, + { token: '{MediaInfo AudioFormat}', example: 'DTS' }, + { token: '{MediaInfo AudioChannels}', example: '5.1' } + ]; + + const releaseGroupTokens = [ + { token: '{Release Group}', example: 'Rls Grp' }, + { token: '{Release.Group}', example: 'Rls.Grp' }, + { token: '{Release_Group}', example: 'Rls_Grp' } + ]; + + const originalTokens = [ + { token: '{Original Title}', example: 'Series.Title.S01E01.HDTV.x264-EVOLVE' }, + { token: '{Original Filename}', example: 'series.title.s01e01.hdtv.x264-EVOLVE' } + ]; + + return ( + + + + File Name Tokens + + + +
+ +
+ + { + !advancedSettings && +
+
+ { + fileNameTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ } + +
+
+ { + seriesTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ + { + season && +
+
+ { + seasonTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ } + + { + episode && +
+
+
+ { + episodeTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ + { + daily && +
+
+ { + airDateTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ } + + { + anime && +
+
+ { + absoluteTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ } +
+ } + + { + additional && +
+
+
+ { + episodeTitleTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + qualityTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + mediaInfoTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + releaseGroupTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+ +
+
+ { + originalTokens.map(({ token, example }) => { + return ( + + ); + } + ) + } +
+
+
+ } +
+ + + + + +
+
+ ); + } +} + +NamingModal.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + isOpen: PropTypes.bool.isRequired, + advancedSettings: PropTypes.bool.isRequired, + season: PropTypes.bool.isRequired, + episode: PropTypes.bool.isRequired, + daily: PropTypes.bool.isRequired, + anime: PropTypes.bool.isRequired, + additional: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +NamingModal.defaultProps = { + season: false, + episode: false, + daily: false, + anime: false, + additional: false +}; + +export default NamingModal; diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.css b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css new file mode 100644 index 000000000..299c98936 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.css @@ -0,0 +1,66 @@ +.option { + display: flex; + align-items: center; + flex-wrap: wrap; + margin: 3px; + border: 1px solid $borderColor; + + &:hover { + .token { + background-color: #ddd; + } + + .example { + background-color: #ccc; + } + } +} + +.small { + width: 420px; +} + +.large { + width: 100%; +} + +.token { + flex: 0 0 50%; + padding: 6px 16px; + background-color: #eee; + font-family: $monoSpaceFontFamily; +} + +.example { + flex: 0 0 50%; + padding: 6px 16px; + background-color: #ddd; +} + +.lower { + text-transform: lowercase; +} + +.upper { + text-transform: uppercase; +} + +.isFullFilename { + .token, + .example { + flex: 1 0 auto; + } +} + +@media only screen and (max-width: $breakpointSmall) { + .option.small { + width: 100%; + } +} + +@media only screen and (max-width: $breakpointExtraSmall) { + .token, + .example { + flex: 1 0 auto; + } +} diff --git a/frontend/src/Settings/MediaManagement/Naming/NamingOption.js b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js new file mode 100644 index 000000000..ee8361a14 --- /dev/null +++ b/frontend/src/Settings/MediaManagement/Naming/NamingOption.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { sizes } from 'Helpers/Props'; +import Link from 'Components/Link/Link'; +import styles from './NamingOption.css'; + +class NamingOption extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + value, + token, + tokenCase, + isFullFilename, + onInputChange + } = this.props; + + let newValue = token; + + if (tokenCase === 'lower') { + newValue = token.toLowerCase(); + } else if (tokenCase === 'upper') { + newValue = token.toUpperCase(); + } + + if (isFullFilename) { + onInputChange({ name, value: newValue }); + } else { + onInputChange({ + name, + value: `${value}${newValue}` + }); + } + } + + // + // Render + render() { + const { + token, + example, + tokenCase, + isFullFilename, + size + } = this.props; + + return ( + +
{token}
+
{example}
+ + ); + } +} + +NamingOption.propTypes = { + name: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + token: PropTypes.string.isRequired, + example: PropTypes.string.isRequired, + tokenCase: PropTypes.string.isRequired, + isFullFilename: PropTypes.bool.isRequired, + size: PropTypes.oneOf([sizes.SMALL, sizes.LARGE]), + onInputChange: PropTypes.func.isRequired +}; + +NamingOption.defaultProps = { + size: sizes.SMALL, + isFullFilename: false +}; + +export default NamingOption; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js new file mode 100644 index 000000000..98631932a --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditMetadataModalContentConnector from './EditMetadataModalContentConnector'; + +function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditMetadataModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditMetadataModal; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js new file mode 100644 index 000000000..a065e2f08 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditMetadataModal from './EditMetadataModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditMetadataModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'metadatas' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditMetadataModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditMetadataModalConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js new file mode 100644 index 000000000..9ed3aa48f --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContent.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; + +function EditMetadataModalContent(props) { + const { + isSaving, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + ...otherProps + } = props; + + const { + name, + enable, + fields + } = item; + + return ( + + + Edit {name.value} Metadata + + + +
+ + Enable + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + +
+ + + + + + Save + + +
+ ); +} + +EditMetadataModalContent.propTypes = { + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onDeleteMetadataPress: PropTypes.func +}; + +export default EditMetadataModalContent; diff --git a/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js new file mode 100644 index 000000000..2cd7636a0 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/EditMetadataModalContentConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setMetadataValue, setMetadataFieldValue, saveMetadata } from 'Store/Actions/settingsActions'; +import EditMetadataModalContent from './EditMetadataModalContent'; + +function createMapStateToProps() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.metadata, + (id, metadata) => { + const { + isSaving, + saveError, + pendingChanges, + items + } = metadata; + + const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError); + + return { + id, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +const mapDispatchToProps = { + setMetadataValue, + setMetadataFieldValue, + saveMetadata +}; + +class EditMetadataModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setMetadataValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setMetadataFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveMetadata({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditMetadataModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setMetadataValue: PropTypes.func.isRequired, + setMetadataFieldValue: PropTypes.func.isRequired, + saveMetadata: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector); diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.css b/frontend/src/Settings/Metadata/Metadata/Metadata.css new file mode 100644 index 000000000..2de4023de --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.css @@ -0,0 +1,17 @@ +.metadata { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.label { + composes: label from 'Components/Label.css'; + + width: 100%; +} diff --git a/frontend/src/Settings/Metadata/Metadata/Metadata.js b/frontend/src/Settings/Metadata/Metadata/Metadata.js new file mode 100644 index 000000000..fb6495f7c --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadata.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import EditMetadataModalConnector from './EditMetadataModalConnector'; +import styles from './Metadata.css'; + +function getKind(enable) { + if (enable) { + return kinds.SUCCESS; + } + + return kinds.DANGER; +} + +class Metadata extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditMetadataModalOpen: false + }; + } + + // + // Listeners + + onEditMetadataPress = () => { + this.setState({ isEditMetadataModalOpen: true }); + } + + onEditMetadataModalClose = () => { + this.setState({ isEditMetadataModalOpen: false }); + } + + // + // Render + + render() { + const { + id, + name, + enable, + fields + } = this.props; + + return ( + +
+ {name} +
+ +
+ +
+ +
+ { + fields.map((field) => { + return ( + + ); + }) + } +
+ + +
+ ); + } +} + +Metadata.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + enable: PropTypes.bool.isRequired, + fields: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Metadata; diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.css b/frontend/src/Settings/Metadata/Metadata/Metadatas.css new file mode 100644 index 000000000..fb1bd6080 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.css @@ -0,0 +1,4 @@ +.metadatas { + display: flex; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Metadata/Metadata/Metadatas.js b/frontend/src/Settings/Metadata/Metadata/Metadatas.js new file mode 100644 index 000000000..03343ad82 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/Metadatas.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Metadata from './Metadata'; +import styles from './Metadatas.css'; + +function Metadatas(props) { + const { + items, + ...otherProps + } = props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } +
+
+
+ ); +} + +Metadatas.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default Metadatas; diff --git a/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js new file mode 100644 index 000000000..fb7153950 --- /dev/null +++ b/frontend/src/Settings/Metadata/Metadata/MetadatasConnector.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchMetadata } from 'Store/Actions/settingsActions'; +import Metadatas from './Metadatas'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.metadata, + (metadata) => { + return { + ...metadata + }; + } + ); +} + +const mapDispatchToProps = { + fetchMetadata +}; + +class MetadatasConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchMetadata(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MetadatasConnector.propTypes = { + fetchMetadata: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector); diff --git a/frontend/src/Settings/Metadata/MetadataSettings.js b/frontend/src/Settings/Metadata/MetadataSettings.js new file mode 100644 index 000000000..001936ab7 --- /dev/null +++ b/frontend/src/Settings/Metadata/MetadataSettings.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import MetadatasConnector from './Metadata/MetadatasConnector'; + +function MetadataSettings() { + return ( + + + + + + + + ); +} + +export default MetadataSettings; diff --git a/frontend/src/Settings/Notifications/NotificationSettings.js b/frontend/src/Settings/Notifications/NotificationSettings.js new file mode 100644 index 000000000..c9bed6501 --- /dev/null +++ b/frontend/src/Settings/Notifications/NotificationSettings.js @@ -0,0 +1,21 @@ +import React from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import NotificationsConnector from './Notifications/NotificationsConnector'; + +function NotificationSettings() { + return ( + + + + + + + + ); +} + +export default NotificationSettings; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css new file mode 100644 index 000000000..3415de2e1 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.css @@ -0,0 +1,44 @@ +.notification { + composes: card from 'Components/Card.css'; + + position: relative; + width: 300px; + height: 100px; +} + +.underlay { + composes: cover from 'Styles/Mixins/cover.css'; +} + +.overlay { + composes: linkOverlay from 'Styles/Mixins/linkOverlay.css'; + + padding: 10px; +} + +.name { + text-align: center; + font-weight: lighter; + font-size: 24px; +} + +.actions { + margin-top: 20px; + text-align: right; +} + +.presetsMenu { + composes: menu from 'Components/Menu/Menu.css'; + + display: inline-block; + margin: 0 5px; +} + +.presetsMenuButton { + composes: button from 'Components/Link/Button.css'; + + &::after { + margin-left: 5px; + content: '\25BE'; + } +} diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js new file mode 100644 index 000000000..6d90961b0 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationItem.js @@ -0,0 +1,110 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Link from 'Components/Link/Link'; +import Menu from 'Components/Menu/Menu'; +import MenuContent from 'Components/Menu/MenuContent'; +import AddNotificationPresetMenuItem from './AddNotificationPresetMenuItem'; +import styles from './AddNotificationItem.css'; + +class AddNotificationItem extends Component { + + // + // Listeners + + onNotificationSelect = () => { + const { + implementation + } = this.props; + + this.props.onNotificationSelect({ implementation }); + } + + // + // Render + + render() { + const { + implementation, + implementationName, + infoLink, + presets, + onNotificationSelect + } = this.props; + + const hasPresets = !!presets && !!presets.length; + + return ( +
+ + +
+
+ {implementationName} +
+ +
+ { + hasPresets && + + + + + + + + { + presets.map((preset) => { + return ( + + ); + }) + } + + + + } + + +
+
+
+ ); + } +} + +AddNotificationItem.propTypes = { + implementation: PropTypes.string.isRequired, + implementationName: PropTypes.string.isRequired, + infoLink: PropTypes.string.isRequired, + presets: PropTypes.arrayOf(PropTypes.object), + onNotificationSelect: PropTypes.func.isRequired +}; + +export default AddNotificationItem; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js new file mode 100644 index 000000000..45f5e14b6 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import AddNotificationModalContentConnector from './AddNotificationModalContentConnector'; + +function AddNotificationModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +AddNotificationModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNotificationModal; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css new file mode 100644 index 000000000..8744e516c --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.css @@ -0,0 +1,5 @@ +.notifications { + display: flex; + justify-content: center; + flex-wrap: wrap; +} diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js new file mode 100644 index 000000000..fa5a4af4f --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContent.js @@ -0,0 +1,85 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Button from 'Components/Link/Button'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import AddNotificationItem from './AddNotificationItem'; +import styles from './AddNotificationModalContent.css'; + +class AddNotificationModalContent extends Component { + + // + // Render + + render() { + const { + isFetching, + error, + isPopulated, + schema, + onNotificationSelect, + onModalClose + } = this.props; + + return ( + + + Add Notification + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new notification, please try again.
+ } + + { + !isFetching && !error && +
+
+ { + schema.map((notification) => { + return ( + + ); + }) + } +
+
+ } +
+ + + +
+ ); + } +} + +AddNotificationModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isPopulated: PropTypes.bool.isRequired, + schema: PropTypes.arrayOf(PropTypes.object).isRequired, + onNotificationSelect: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default AddNotificationModalContent; diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js new file mode 100644 index 000000000..a724ba35b --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationModalContentConnector.js @@ -0,0 +1,71 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchNotificationSchema, selectNotificationSchema } from 'Store/Actions/settingsActions'; +import AddNotificationModalContent from './AddNotificationModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.notifications, + (notifications) => { + const { + isFetching, + error, + isPopulated, + schema + } = notifications; + + return { + isFetching, + error, + isPopulated, + schema + }; + } + ); +} + +const mapDispatchToProps = { + fetchNotificationSchema, + selectNotificationSchema +}; + +class AddNotificationModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchNotificationSchema(); + } + + // + // Listeners + + onNotificationSelect = ({ implementation, name }) => { + this.props.selectNotificationSchema({ implementation, presetName: name }); + this.props.onModalClose({ notificationSelected: true }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AddNotificationModalContentConnector.propTypes = { + fetchNotificationSchema: PropTypes.func.isRequired, + selectNotificationSchema: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AddNotificationModalContentConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js new file mode 100644 index 000000000..e4df85b8a --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/AddNotificationPresetMenuItem.js @@ -0,0 +1,49 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import MenuItem from 'Components/Menu/MenuItem'; + +class AddNotificationPresetMenuItem extends Component { + + // + // Listeners + + onPress = () => { + const { + name, + implementation + } = this.props; + + this.props.onPress({ + name, + implementation + }); + } + + // + // Render + + render() { + const { + name, + implementation, + ...otherProps + } = this.props; + + return ( + + {name} + + ); + } +} + +AddNotificationPresetMenuItem.propTypes = { + name: PropTypes.string.isRequired, + implementation: PropTypes.string.isRequired, + onPress: PropTypes.func.isRequired +}; + +export default AddNotificationPresetMenuItem; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js new file mode 100644 index 000000000..91d9f67cc --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditNotificationModalContentConnector from './EditNotificationModalContentConnector'; + +function EditNotificationModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditNotificationModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditNotificationModal; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js new file mode 100644 index 000000000..3e47e7384 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalConnector.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditNotificationModal from './EditNotificationModal'; + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditNotificationModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'notifications' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditNotificationModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(null, mapDispatchToProps)(EditNotificationModalConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css new file mode 100644 index 000000000..c73406b57 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.css @@ -0,0 +1,11 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} + +.message { + composes: alert from 'Components/Alert.css'; + + margin-bottom: 30px; +} diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js new file mode 100644 index 000000000..f3af135f2 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContent.js @@ -0,0 +1,235 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; +import styles from './EditNotificationModalContent.css'; + +function EditNotificationModalContent(props) { + const { + advancedSettings, + isFetching, + error, + isSaving, + isTesting, + saveError, + item, + onInputChange, + onFieldChange, + onModalClose, + onSavePress, + onTestPress, + onDeleteNotificationPress, + ...otherProps + } = props; + + const { + id, + name, + onGrab, + onDownload, + onUpgrade, + onRename, + supportsOnGrab, + supportsOnDownload, + supportsOnUpgrade, + supportsOnRename, + tags, + fields, + message + } = item; + + return ( + + + {id ? 'Edit Notification' : 'Add Notification'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new notification, please try again.
+ } + + { + !isFetching && !error && +
+ { + !!message && + + {message.value.message} + + } + + + Name + + + + + + On Grab + + + + + + On Download + + + + + { + onDownload.value && + + On Upgrade + + + + } + + + On Rename + + + + + + Tags + + + + + { + fields.map((field) => { + return ( + + ); + }) + } + + + } +
+ + { + id && + + } + + + Test + + + + + + Save + + +
+ ); +} + +EditNotificationModalContent.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + isTesting: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onFieldChange: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onTestPress: PropTypes.func.isRequired, + onDeleteNotificationPress: PropTypes.func +}; + +export default EditNotificationModalContent; diff --git a/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js new file mode 100644 index 000000000..bca296315 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/EditNotificationModalContentConnector.js @@ -0,0 +1,94 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { setNotificationValue, setNotificationFieldValue, saveNotification, testNotification } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditNotificationModalContent from './EditNotificationModalContent'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + createProviderSettingsSelector(), + (advancedSettings, notification) => { + return { + advancedSettings, + ...notification + }; + } + ); +} + +const mapDispatchToProps = { + setNotificationValue, + setNotificationFieldValue, + saveNotification, + testNotification +}; + +class EditNotificationModalContentConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setNotificationValue({ name, value }); + } + + onFieldChange = ({ name, value }) => { + this.props.setNotificationFieldValue({ name, value }); + } + + onSavePress = () => { + this.props.saveNotification({ id: this.props.id }); + } + + onTestPress = () => { + this.props.testNotification({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditNotificationModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setNotificationValue: PropTypes.func.isRequired, + setNotificationFieldValue: PropTypes.func.isRequired, + saveNotification: PropTypes.func.isRequired, + testNotification: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'notifications' } +)(EditNotificationModalContentConnector); diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.css b/frontend/src/Settings/Notifications/Notifications/Notification.css new file mode 100644 index 000000000..8d28cef8e --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notification.css @@ -0,0 +1,19 @@ +.notification { + composes: card from 'Components/Card.css'; + + width: 290px; +} + +.name { + composes: truncate from 'Styles/Mixins/truncate.css'; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.enabled { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Notifications/Notifications/Notification.js b/frontend/src/Settings/Notifications/Notifications/Notification.js new file mode 100644 index 000000000..cca3761fc --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notification.js @@ -0,0 +1,151 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditNotificationModalConnector from './EditNotificationModalConnector'; +import styles from './Notification.css'; + +function getLabelKind(supports, enabled) { + if (!supports) { + return kinds.DEFAULT; + } + + if (!enabled) { + return kinds.DANGER; + } + + return kinds.SUCCESS; +} + +class Notification extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditNotificationModalOpen: false, + isDeleteNotificationModalOpen: false + }; + } + + // + // Listeners + + onEditNotificationPress = () => { + this.setState({ isEditNotificationModalOpen: true }); + } + + onEditNotificationModalClose = () => { + this.setState({ isEditNotificationModalOpen: false }); + } + + onDeleteNotificationPress = () => { + this.setState({ + isEditNotificationModalOpen: false, + isDeleteNotificationModalOpen: true + }); + } + + onDeleteNotificationModalClose= () => { + this.setState({ isDeleteNotificationModalOpen: false }); + } + + onConfirmDeleteNotification = () => { + this.props.onConfirmDeleteNotification(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + onGrab, + onDownload, + onUpgrade, + onRename, + supportsOnGrab, + supportsOnDownload, + supportsOnUpgrade, + supportsOnRename + } = this.props; + + return ( + +
+ {name} +
+ + + + + + + + + + + + +
+ ); + } +} + +Notification.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + onGrab: PropTypes.bool.isRequired, + onDownload: PropTypes.bool.isRequired, + onUpgrade: PropTypes.bool.isRequired, + onRename: PropTypes.bool.isRequired, + supportsOnGrab: PropTypes.bool.isRequired, + supportsOnDownload: PropTypes.bool.isRequired, + supportsOnUpgrade: PropTypes.bool.isRequired, + supportsOnRename: PropTypes.bool.isRequired, + onConfirmDeleteNotification: PropTypes.func.isRequired +}; + +export default Notification; diff --git a/frontend/src/Settings/Notifications/Notifications/Notifications.css b/frontend/src/Settings/Notifications/Notifications/Notifications.css new file mode 100644 index 000000000..26b890e88 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notifications.css @@ -0,0 +1,20 @@ +.notifications { + display: flex; + flex-wrap: wrap; +} + +.addNotification { + composes: notification from './Notification.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Notifications/Notifications/Notifications.js b/frontend/src/Settings/Notifications/Notifications/Notifications.js new file mode 100644 index 000000000..38d017062 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/Notifications.js @@ -0,0 +1,117 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import Notification from './Notification'; +import AddNotificationModal from './AddNotificationModal'; +import EditNotificationModalConnector from './EditNotificationModalConnector'; +import styles from './Notifications.css'; + +class Notifications extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddNotificationModalOpen: false, + isEditNotificationModalOpen: false + }; + } + + // + // Listeners + + onAddNotificationPress = () => { + this.setState({ isAddNotificationModalOpen: true }); + } + + onAddNotificationModalClose = ({ notificationSelected = false } = {}) => { + this.setState({ + isAddNotificationModalOpen: false, + isEditNotificationModalOpen: notificationSelected + }); + } + + onEditNotificationModalClose = () => { + this.setState({ isEditNotificationModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteNotification, + ...otherProps + } = this.props; + + const { + isAddNotificationModalOpen, + isEditNotificationModalOpen + } = this.state; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + + + +
+
+ ); + } +} + +Notifications.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteNotification: PropTypes.func.isRequired +}; + +export default Notifications; diff --git a/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js new file mode 100644 index 000000000..b2b5e5166 --- /dev/null +++ b/frontend/src/Settings/Notifications/Notifications/NotificationsConnector.js @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchNotifications, deleteNotification } from 'Store/Actions/settingsActions'; +import Notifications from './Notifications'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.notifications, + (notifications) => { + return { + ...notifications + }; + } + ); +} + +const mapDispatchToProps = { + fetchNotifications, + deleteNotification +}; + +class NotificationsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchNotifications(); + } + + // + // Listeners + + onConfirmDeleteNotification = (id) => { + this.props.deleteNotification({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +NotificationsConnector.propTypes = { + fetchNotifications: PropTypes.func.isRequired, + deleteNotification: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(NotificationsConnector); diff --git a/frontend/src/Settings/PendingChangesModal.js b/frontend/src/Settings/PendingChangesModal.js new file mode 100644 index 000000000..5754cb657 --- /dev/null +++ b/frontend/src/Settings/PendingChangesModal.js @@ -0,0 +1,64 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; + +function PendingChangesModal(props) { + const { + isOpen, + size, + onConfirm, + onCancel + } = props; + + return ( + + + Unsaved Changes + + + You have unsaved changes, are you sure you want to leave this page? + + + + + + + + + + ); +} + +PendingChangesModal.propTypes = { + className: PropTypes.string, + isOpen: PropTypes.bool.isRequired, + kind: PropTypes.oneOf(kinds.all), + size: PropTypes.oneOf(sizes.all), + onConfirm: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired +}; + +PendingChangesModal.defaultProps = { + kind: kinds.PRIMARY +}; + +export default PendingChangesModal; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfile.css b/frontend/src/Settings/Profiles/Delay/DelayProfile.css new file mode 100644 index 000000000..238742efd --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfile.css @@ -0,0 +1,40 @@ +.delayProfile { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.column { + flex: 0 0 200px; +} + +.actions { + display: flex; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} + +.editButton { + width: $dragHandleWidth; + text-align: center; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfile.js b/frontend/src/Settings/Profiles/Delay/DelayProfile.js new file mode 100644 index 000000000..b7be1622e --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfile.js @@ -0,0 +1,174 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import titleCase from 'Utilities/String/titleCase'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import TagList from 'Components/TagList'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditDelayProfileModalConnector from './EditDelayProfileModalConnector'; +import styles from './DelayProfile.css'; + +function getDelay(enabled, delay) { + if (!enabled) { + return '-'; + } + + if (!delay) { + return 'No Delay'; + } + + if (delay === 1) { + return '1 Minute'; + } + + // TODO: use better units of time than just minutes + return `${delay} Minutes`; +} + +class DelayProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditDelayProfileModalOpen: false, + isDeleteDelayProfileModalOpen: false + }; + } + + // + // Listeners + + onEditDelayProfilePress = () => { + this.setState({ isEditDelayProfileModalOpen: true }); + } + + onEditDelayProfileModalClose = () => { + this.setState({ isEditDelayProfileModalOpen: false }); + } + + onDeleteDelayProfilePress = () => { + this.setState({ + isEditDelayProfileModalOpen: false, + isDeleteDelayProfileModalOpen: true + }); + } + + onDeleteDelayProfileModalClose = () => { + this.setState({ isDeleteDelayProfileModalOpen: false }); + } + + onConfirmDeleteDelayProfile = () => { + this.props.onConfirmDeleteDelayProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + enableUsenet, + enableTorrent, + preferredProtocol, + usenetDelay, + torrentDelay, + order, + tags, + tagList, + isDragging, + connectDragSource + } = this.props; + + let preferred = titleCase(preferredProtocol); + + if (!enableUsenet) { + preferred = 'Only Torrent'; + } else if (!enableTorrent) { + preferred = 'Only Usenet'; + } + + return ( +
+
{preferred}
+
{getDelay(enableUsenet, usenetDelay)}
+
{getDelay(enableTorrent, torrentDelay)}
+ + + +
+ + + + + { + id !== 1 && + connectDragSource( +
+ +
+ ) + } +
+ + + + +
+ ); + } +} + +DelayProfile.propTypes = { + id: PropTypes.number.isRequired, + enableUsenet: PropTypes.bool.isRequired, + enableTorrent: PropTypes.bool.isRequired, + preferredProtocol: PropTypes.string.isRequired, + usenetDelay: PropTypes.number.isRequired, + torrentDelay: PropTypes.number.isRequired, + order: PropTypes.number.isRequired, + tags: PropTypes.arrayOf(PropTypes.number).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onConfirmDeleteDelayProfile: PropTypes.func.isRequired +}; + +DelayProfile.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default DelayProfile; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css new file mode 100644 index 000000000..cc5a92830 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.css @@ -0,0 +1,3 @@ +.dragPreview { + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js new file mode 100644 index 000000000..402ddcc13 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragPreview.js @@ -0,0 +1,78 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { DELAY_PROFILE } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import DelayProfile from './DelayProfile'; +import styles from './DelayProfileDragPreview.css'; + +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class DelayProfileDragPreview extends Component { + + // + // Render + + render() { + const { + width, + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== DELAY_PROFILE) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = width - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + width, + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + return ( + +
+ +
+
+ ); + } +} + +DelayProfileDragPreview.propTypes = { + width: PropTypes.number.isRequired, + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(DelayProfileDragPreview); diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css new file mode 100644 index 000000000..835250678 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.css @@ -0,0 +1,17 @@ +.delayProfileDragSource { + padding: 4px 0; +} + +.delayProfilePlaceholder { + width: 100%; + height: 30px; + border-bottom: 1px dotted #aaa; +} + +.delayProfilePlaceholderBefore { + margin-bottom: 8px; +} + +.delayProfilePlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js new file mode 100644 index 000000000..5c1c565e0 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfileDragSource.js @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { DELAY_PROFILE } from 'Helpers/dragTypes'; +import DelayProfile from './DelayProfile'; +import styles from './DelayProfileDragSource.css'; + +const delayProfileDragSource = { + beginDrag(item) { + return item; + }, + + endDrag(props, monitor, component) { + props.onDelayProfileDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const delayProfileDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().order; + const hoverIndex = props.order; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + if (dragIndex === hoverIndex) { + return; + } + + // When moving up, only trigger if drag position is above 50% and + // when moving down, only trigger if drag position is below 50%. + // If we're moving down the hoverIndex needs to be increased + // by one so it's ordered properly. Otherwise the hoverIndex will work. + + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + props.onDelayProfileDragMove(dragIndex, hoverIndex + 1); + } else if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + props.onDelayProfileDragMove(dragIndex, hoverIndex); + } + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class DelayProfileDragSource extends Component { + + // + // Render + + render() { + const { + id, + order, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + ...otherProps + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +DelayProfileDragSource.propTypes = { + id: PropTypes.number.isRequired, + order: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onDelayProfileDragMove: PropTypes.func.isRequired, + onDelayProfileDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + DELAY_PROFILE, + delayProfileDropTarget, + collectDropTarget +)(DragSource( + DELAY_PROFILE, + delayProfileDragSource, + collectDragSource +)(DelayProfileDragSource)); diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfiles.css b/frontend/src/Settings/Profiles/Delay/DelayProfiles.css new file mode 100644 index 000000000..3cf3e9020 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfiles.css @@ -0,0 +1,27 @@ +.delayProfiles { + user-select: none; +} + +.delayProfilesHeader { + display: flex; + margin-bottom: 10px; + font-weight: bold; +} + +.column { + flex: 0 0 200px; +} + +.tags { + flex: 1 0 auto; +} + +.addDelayProfile { + display: flex; + justify-content: flex-end; +} + +.addButton { + width: $dragHandleWidth; + text-align: center; +} diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfiles.js b/frontend/src/Settings/Profiles/Delay/DelayProfiles.js new file mode 100644 index 000000000..7fc0cab2a --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfiles.js @@ -0,0 +1,150 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import DelayProfileDragSource from './DelayProfileDragSource'; +import DelayProfileDragPreview from './DelayProfileDragPreview'; +import DelayProfile from './DelayProfile'; +import EditDelayProfileModalConnector from './EditDelayProfileModalConnector'; +import styles from './DelayProfiles.css'; + +class DelayProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddDelayProfileModalOpen: false, + width: 0 + }; + } + + // + // Listeners + + onAddDelayProfilePress = () => { + this.setState({ isAddDelayProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isAddDelayProfileModalOpen: false }); + } + + onMeasure = ({ width }) => { + this.setState({ width }); + } + + // + // Render + + render() { + const { + defaultProfile, + items, + tagList, + dragIndex, + dropIndex, + onConfirmDeleteDelayProfile, + ...otherProps + } = this.props; + + const { + isAddDelayProfileModalOpen, + width + } = this.state; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex < dragIndex; + const isDraggingDown = isDragging && dropIndex > dragIndex; + + return ( + +
+ +
+
Protocol
+
Usenet Delay
+
Torrent Delay
+
Tags
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } + + +
+ + { + defaultProfile && +
+ +
+ } + +
+ + + +
+ + +
+
+
+ ); + } +} + +DelayProfiles.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + defaultProfile: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + tagList: PropTypes.arrayOf(PropTypes.object).isRequired, + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + onConfirmDeleteDelayProfile: PropTypes.func.isRequired +}; + +export default DelayProfiles; diff --git a/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js b/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js new file mode 100644 index 000000000..16fe5718c --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/DelayProfilesConnector.js @@ -0,0 +1,105 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDelayProfiles, deleteDelayProfile, reorderDelayProfile } from 'Store/Actions/settingsActions'; +import createTagsSelector from 'Store/Selectors/createTagsSelector'; +import DelayProfiles from './DelayProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.delayProfiles, + createTagsSelector(), + (delayProfiles, tagList) => { + const defaultProfile = _.find(delayProfiles.items, { id: 1 }); + const items = _.sortBy(_.reject(delayProfiles.items, { id: 1 }), ['order']); + + return { + defaultProfile, + ...delayProfiles, + items, + tagList + }; + } + ); +} + +const mapDispatchToProps = { + fetchDelayProfiles, + deleteDelayProfile, + reorderDelayProfile +}; + +class DelayProfilesConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + this.props.fetchDelayProfiles(); + } + + // + // Listeners + + onConfirmDeleteDelayProfile = (id) => { + this.props.deleteDelayProfile({ id }); + } + + onDelayProfileDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onDelayProfileDragEnd = ({ id }, didDrop) => { + const { + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + this.props.reorderDelayProfile({ id, moveIndex: dropIndex - 1 }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DelayProfilesConnector.propTypes = { + fetchDelayProfiles: PropTypes.func.isRequired, + deleteDelayProfile: PropTypes.func.isRequired, + reorderDelayProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DelayProfilesConnector); diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js new file mode 100644 index 000000000..a593471ac --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditDelayProfileModalContentConnector from './EditDelayProfileModalContentConnector'; + +function EditDelayProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditDelayProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditDelayProfileModal; diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js new file mode 100644 index 000000000..1f696c846 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditDelayProfileModal from './EditDelayProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditDelayProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'delayProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDelayProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditDelayProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css new file mode 100644 index 000000000..a3c7f464c --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.css @@ -0,0 +1,5 @@ +.deleteButton { + composes: button from 'Components/Link/Button.css'; + + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js new file mode 100644 index 000000000..42c6bece0 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js @@ -0,0 +1,186 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Alert from 'Components/Alert'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import styles from './EditDelayProfileModalContent.css'; + +function EditDelayProfileModalContent(props) { + const { + id, + isFetching, + error, + isSaving, + saveError, + item, + protocol, + protocolOptions, + onInputChange, + onProtocolChange, + onSavePress, + onModalClose, + onDeleteDelayProfilePress, + ...otherProps + } = props; + + const { + enableUsenet, + enableTorrent, + usenetDelay, + torrentDelay, + tags + } = item; + + return ( + + + {id ? 'Edit Delay Profile' : 'Add Delay Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new quality profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Protocol + + + + + { + enableUsenet.value && + + Usenet Delay + + + + } + + { + enableTorrent.value && + + Torrent Delay + + + + } + + { + id === 1 ? + + This is the default profile. It applies to all series that don't have an explicit profile. + : + + + Tags + + + + } +
+ } +
+ + { + id && id > 1 && + + } + + + + + Save + + +
+ ); +} + +const delayProfileShape = { + enableUsenet: PropTypes.shape(boolSettingShape).isRequired, + enableTorrent: PropTypes.shape(boolSettingShape).isRequired, + usenetDelay: PropTypes.shape(numberSettingShape).isRequired, + torrentDelay: PropTypes.shape(numberSettingShape).isRequired, + order: PropTypes.shape(numberSettingShape), + tags: PropTypes.shape(tagSettingShape).isRequired +}; + +EditDelayProfileModalContent.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.shape(delayProfileShape).isRequired, + protocol: PropTypes.string.isRequired, + protocolOptions: PropTypes.arrayOf(PropTypes.object).isRequired, + onInputChange: PropTypes.func.isRequired, + onProtocolChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteDelayProfilePress: PropTypes.func +}; + +export default EditDelayProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js new file mode 100644 index 000000000..8cd001950 --- /dev/null +++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContentConnector.js @@ -0,0 +1,178 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; +import { setDelayProfileValue, saveDelayProfile } from 'Store/Actions/settingsActions'; +import EditDelayProfileModalContent from './EditDelayProfileModalContent'; + +const newDelayProfile = { + enableUsenet: true, + enableTorrent: true, + preferredProtocol: 'usenet', + usenetDelay: 0, + torrentDelay: 0, + tags: [] +}; + +function createDelayProfileSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.delayProfiles, + (id, delayProfiles) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = delayProfiles; + + const profile = id ? _.find(items, { id }) : newDelayProfile; + const settings = selectSettings(profile, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createDelayProfileSelector(), + (delayProfile) => { + const protocolOptions = [ + { key: 'preferUsenet', value: 'Prefer Usenet' }, + { key: 'preferTorrent', value: 'Prefer Torrent' }, + { key: 'onlyUsenet', value: 'Only Usenet' }, + { key: 'onlyTorrent', value: 'Only Torrent' } + ]; + + const enableUsenet = delayProfile.item.enableUsenet.value; + const enableTorrent = delayProfile.item.enableTorrent.value; + const preferredProtocol = delayProfile.item.preferredProtocol.value; + let protocol = 'preferUsenet'; + + if (preferredProtocol === 'usenet') { + protocol = 'preferUsenet'; + } else { + protocol = 'preferTorrent'; + } + + if (!enableUsenet) { + protocol = 'onlyTorrent'; + } + + if (!enableTorrent) { + protocol = 'onlyUsenet'; + } + + return { + protocol, + protocolOptions, + ...delayProfile + }; + } + ); +} + +const mapDispatchToProps = { + setDelayProfileValue, + saveDelayProfile +}; + +class EditDelayProfileModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newDelayProfile).forEach((name) => { + this.props.setDelayProfileValue({ + name, + value: newDelayProfile[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setDelayProfileValue({ name, value }); + } + + onProtocolChange = ({ value }) => { + switch (value) { + case 'preferUsenet': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); + break; + case 'preferTorrent': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' }); + break; + case 'onlyUsenet': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: true }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: false }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'usenet' }); + break; + case 'onlyTorrent': + this.props.setDelayProfileValue({ name: 'enableUsenet', value: false }); + this.props.setDelayProfileValue({ name: 'enableTorrent', value: true }); + this.props.setDelayProfileValue({ name: 'preferredProtocol', value: 'torrent' }); + break; + default: + throw Error(`Unknown protocol option: ${value}`); + } + } + + onSavePress = () => { + this.props.saveDelayProfile({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditDelayProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setDelayProfileValue: PropTypes.func.isRequired, + saveDelayProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditDelayProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js new file mode 100644 index 000000000..7fe5a1823 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditLanguageProfileModalContentConnector from './EditLanguageProfileModalContentConnector'; + +function EditLanguageProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditLanguageProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditLanguageProfileModal; diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js new file mode 100644 index 000000000..44866e2a3 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditLanguageProfileModal from './EditLanguageProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditLanguageProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'languageProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditLanguageProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditLanguageProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css new file mode 100644 index 000000000..74dd1c8b7 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.css @@ -0,0 +1,3 @@ +.deleteButtonContainer { + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js new file mode 100644 index 000000000..aab5146be --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import LanguageProfileItems from './LanguageProfileItems'; +import styles from './EditLanguageProfileModalContent.css'; + +function EditLanguageProfileModalContent(props) { + const { + isFetching, + error, + isSaving, + saveError, + languages, + item, + isInUse, + onInputChange, + onCutoffChange, + onSavePress, + onModalClose, + onDeleteLanguageProfilePress, + ...otherProps + } = props; + + const { + id, + name, + cutoff, + languages: itemLanguages + } = item; + + return ( + + + {id ? 'Edit Language Profile' : 'Add Language Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new language profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Cutoff + + + + + + + + } +
+ + { + id && +
+ +
+ } + + + + + Save + +
+
+ ); +} + +EditLanguageProfileModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, + isInUse: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onCutoffChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteLanguageProfilePress: PropTypes.func +}; + +export default EditLanguageProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js new file mode 100644 index 000000000..13f72f623 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/EditLanguageProfileModalContentConnector.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { fetchLanguageProfileSchema, setLanguageProfileValue, saveLanguageProfile } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditLanguageProfileModalContent from './EditLanguageProfileModalContent'; + +function createLanguagesSelector() { + return createSelector( + createProviderSettingsSelector(), + (languageProfile) => { + const languages = languageProfile.item.languages; + if (!languages || !languages.value) { + return []; + } + + return _.reduceRight(languages.value, (result, { allowed, language }) => { + if (allowed) { + result.push({ + key: language.id, + value: language.name + }); + } + + return result; + }, []); + } + ); +} + +function createMapStateToProps() { + return createSelector( + createProviderSettingsSelector(), + createLanguagesSelector(), + createProfileInUseSelector('languageProfileId'), + (languageProfile, languages, isInUse) => { + return { + languages, + ...languageProfile, + isInUse + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfileSchema, + setLanguageProfileValue, + saveLanguageProfile +}; + +class EditLanguageProfileModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + if (!this.props.id) { + this.props.fetchLanguageProfileSchema(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setLanguageProfileValue({ name, value }); + } + + onCutoffChange = ({ name, value }) => { + const id = parseInt(value); + const item = _.find(this.props.item.languages.value, (i) => i.language.id === id); + + this.props.setLanguageProfileValue({ name, value: item.language }); + } + + onSavePress = () => { + this.props.saveLanguageProfile({ id: this.props.id }); + } + + onLanguageProfileItemAllowedChange = (id, allowed) => { + const languageProfile = _.cloneDeep(this.props.item); + + const item = _.find(languageProfile.languages.value, (i) => i.language.id === id); + item.allowed = allowed; + + this.props.setLanguageProfileValue({ + name: 'languages', + value: languageProfile.languages.value + }); + + const cutoff = languageProfile.cutoff.value; + + // If the cutoff isn't allowed anymore or there isn't a cutoff set one + if (!cutoff || !_.find(languageProfile.languages.value, (i) => i.language.id === cutoff.id).allowed) { + const firstAllowed = _.find(languageProfile.languages.value, { allowed: true }); + + this.props.setLanguageProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.language : null }); + } + } + + onLanguageProfileItemDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onLanguageProfileItemDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const languageProfile = _.cloneDeep(this.props.item); + + const languages = languageProfile.languages.value.splice(dragIndex, 1); + languageProfile.languages.value.splice(dropIndex, 0, languages[0]); + + this.props.setLanguageProfileValue({ + name: 'languages', + value: languageProfile.languages.value + }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + if (_.isEmpty(this.props.item.languages) && !this.props.isFetching) { + return null; + } + + return ( + + ); + } +} + +EditLanguageProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setLanguageProfileValue: PropTypes.func.isRequired, + fetchLanguageProfileSchema: PropTypes.func.isRequired, + saveLanguageProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'languageProfiles' } +)(EditLanguageProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfile.css b/frontend/src/Settings/Profiles/Language/LanguageProfile.css new file mode 100644 index 000000000..f94aef43a --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfile.css @@ -0,0 +1,19 @@ +.languageProfile { + composes: card from 'Components/Card.css'; + + width: 300px; +} + +.name { + composes: truncate from 'Styles/Mixins/truncate.css'; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.languages { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfile.js b/frontend/src/Settings/Profiles/Language/LanguageProfile.js new file mode 100644 index 000000000..a13a5509d --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfile.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector'; +import styles from './LanguageProfile.css'; + +class LanguageProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditLanguageProfileModalOpen: false, + isDeleteLanguageProfileModalOpen: false + }; + } + + // + // Listeners + + onEditLanguageProfilePress = () => { + this.setState({ isEditLanguageProfileModalOpen: true }); + } + + onEditLanguageProfileModalClose = () => { + this.setState({ isEditLanguageProfileModalOpen: false }); + } + + onDeleteLanguageProfilePress = () => { + this.setState({ + isEditLanguageProfileModalOpen: false, + isDeleteLanguageProfileModalOpen: true + }); + } + + onDeleteLanguageProfileModalClose = () => { + this.setState({ isDeleteLanguageProfileModalOpen: false }); + } + + onConfirmDeleteLanguageProfile = () => { + this.props.onConfirmDeleteLanguageProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + cutoff, + languages, + isDeleting + } = this.props; + + return ( + +
+ {name} +
+ +
+ { + languages.map((item) => { + if (!item.allowed) { + return null; + } + + const isCutoff = item.language.id === cutoff.id; + + return ( + + ); + }) + } +
+ + + + +
+ ); + } +} + +LanguageProfile.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + cutoff: PropTypes.object.isRequired, + languages: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteLanguageProfile: PropTypes.func.isRequired +}; + +export default LanguageProfile; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css new file mode 100644 index 000000000..a10233929 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.css @@ -0,0 +1,44 @@ +.languageProfileItem { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.languageName { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js new file mode 100644 index 000000000..2a3671268 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItem.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './LanguageProfileItem.css'; + +class LanguageProfileItem extends Component { + + // + // Listeners + + onAllowedChange = ({ value }) => { + const { + languageId, + onLanguageProfileItemAllowedChange + } = this.props; + + onLanguageProfileItemAllowedChange(languageId, value); + } + + // + // Render + + render() { + const { + name, + allowed, + isDragging, + connectDragSource + } = this.props; + + return ( +
+ + + { + connectDragSource( +
+ +
+ ) + } +
+ ); + } +} + +LanguageProfileItem.propTypes = { + languageId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onLanguageProfileItemAllowedChange: PropTypes.func +}; + +LanguageProfileItem.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default LanguageProfileItem; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js new file mode 100644 index 000000000..0b6b27d8e --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragPreview.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import LanguageProfileItem from './LanguageProfileItem'; +import styles from './LanguageProfileItemDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class LanguageProfileItemDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== QUALITY_PROFILE_ITEM) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + const { + languageId, + name, + allowed, + sortIndex + } = item; + + return ( + +
+ +
+
+ ); + } +} + +LanguageProfileItemDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(LanguageProfileItemDragPreview); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css new file mode 100644 index 000000000..f59379129 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.css @@ -0,0 +1,18 @@ +.languageProfileItemDragSource { + padding: 4px 0; +} + +.languageProfileItemPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.languageProfileItemPlaceholderBefore { + margin-bottom: 8px; +} + +.languageProfileItemPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js new file mode 100644 index 000000000..304363726 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItemDragSource.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import LanguageProfileItem from './LanguageProfileItem'; +import styles from './LanguageProfileItemDragSource.css'; + +const languageProfileItemDragSource = { + beginDrag({ languageId, name, allowed, sortIndex }) { + return { + languageId, + name, + allowed, + sortIndex + }; + }, + + endDrag(props, monitor, component) { + props.onLanguageProfileItemDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const languageProfileItemDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().sortIndex; + const hoverIndex = props.sortIndex; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + // Moving up, only trigger if drag position is above 50% + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + // Moving down, only trigger if drag position is below 50% + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + props.onLanguageProfileItemDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class LanguageProfileItemDragSource extends Component { + + // + // Render + + render() { + const { + languageId, + name, + allowed, + sortIndex, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onLanguageProfileItemAllowedChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +LanguageProfileItemDragSource.propTypes = { + languageId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onLanguageProfileItemAllowedChange: PropTypes.func.isRequired, + onLanguageProfileItemDragMove: PropTypes.func.isRequired, + onLanguageProfileItemDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + QUALITY_PROFILE_ITEM, + languageProfileItemDropTarget, + collectDropTarget +)(DragSource( + QUALITY_PROFILE_ITEM, + languageProfileItemDragSource, + collectDragSource +)(LanguageProfileItemDragSource)); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css new file mode 100644 index 000000000..48b30f326 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.css @@ -0,0 +1,6 @@ +.languages { + margin-top: 10px; + /* TODO: This should consider the number of languages in the list */ + min-height: 550px; + user-select: none; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js new file mode 100644 index 000000000..831743cbe --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileItems.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import LanguageProfileItemDragSource from './LanguageProfileItemDragSource'; +import LanguageProfileItemDragPreview from './LanguageProfileItemDragPreview'; +import styles from './LanguageProfileItems.css'; + +class LanguageProfileItems extends Component { + + // + // Render + + render() { + const { + dragIndex, + dropIndex, + languageProfileItems, + errors, + warnings, + ...otherProps + } = this.props; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex > dragIndex; + const isDraggingDown = isDragging && dropIndex < dragIndex; + + return ( + + Languages +
+ + + { + errors.map((error, index) => { + return ( + + ); + }) + } + + { + warnings.map((warning, index) => { + return ( + + ); + }) + } + +
+ { + languageProfileItems.map(({ allowed, language }, index) => { + return ( + + ); + }).reverse() + } + + +
+
+
+ ); + } +} + +LanguageProfileItems.propTypes = { + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + languageProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, + errors: PropTypes.arrayOf(PropTypes.object), + warnings: PropTypes.arrayOf(PropTypes.object) +}; + +LanguageProfileItems.defaultProps = { + errors: [], + warnings: [] +}; + +export default LanguageProfileItems; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js b/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js new file mode 100644 index 000000000..61a7153b5 --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfileNameConnector.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector'; + +function createMapStateToProps() { + return createSelector( + createLanguageProfileSelector(), + (languageProfile) => { + return { + name: languageProfile.name + }; + } + ); +} + +function LanguageProfileNameConnector({ name, ...otherProps }) { + return ( + + {name} + + ); +} + +LanguageProfileNameConnector.propTypes = { + languageProfileId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(LanguageProfileNameConnector); diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfiles.css b/frontend/src/Settings/Profiles/Language/LanguageProfiles.css new file mode 100644 index 000000000..5a2fb73bd --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfiles.css @@ -0,0 +1,21 @@ +.languageProfiles { + display: flex; + flex-wrap: wrap; +} + +.addLanguageProfile { + composes: languageProfile from './LanguageProfile.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; + font-size: 45px; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfiles.js b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js new file mode 100644 index 000000000..2c2df29aa --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfiles.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import LanguageProfile from './LanguageProfile'; +import EditLanguageProfileModalConnector from './EditLanguageProfileModalConnector'; +import styles from './LanguageProfiles.css'; + +class LanguageProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isLanguageProfileModalOpen: false + }; + } + + // + // Listeners + + onEditLanguageProfilePress = () => { + this.setState({ isLanguageProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isLanguageProfileModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + isDeleting, + onConfirmDeleteLanguageProfile, + ...otherProps + } = this.props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + +
+
+ ); + } +} + +LanguageProfiles.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteLanguageProfile: PropTypes.func.isRequired +}; + +export default LanguageProfiles; diff --git a/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js b/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js new file mode 100644 index 000000000..2dc8967eb --- /dev/null +++ b/frontend/src/Settings/Profiles/Language/LanguageProfilesConnector.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchLanguageProfiles, deleteLanguageProfile } from 'Store/Actions/settingsActions'; +import LanguageProfiles from './LanguageProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.languageProfiles, + (advancedSettings, languageProfiles) => { + return { + advancedSettings, + ...languageProfiles + }; + } + ); +} + +const mapDispatchToProps = { + fetchLanguageProfiles, + deleteLanguageProfile +}; + +class LanguageProfilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLanguageProfiles(); + } + + // + // Listeners + + onConfirmDeleteLanguageProfile = (id) => { + this.props.deleteLanguageProfile({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LanguageProfilesConnector.propTypes = { + fetchLanguageProfiles: PropTypes.func.isRequired, + deleteLanguageProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LanguageProfilesConnector); diff --git a/frontend/src/Settings/Profiles/Profiles.js b/frontend/src/Settings/Profiles/Profiles.js new file mode 100644 index 000000000..ed288ca9b --- /dev/null +++ b/frontend/src/Settings/Profiles/Profiles.js @@ -0,0 +1,36 @@ +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import HTML5Backend from 'react-dnd-html5-backend'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import QualityProfilesConnector from './Quality/QualityProfilesConnector'; +import LanguageProfilesConnector from './Language/LanguageProfilesConnector'; +import DelayProfilesConnector from './Delay/DelayProfilesConnector'; + +class Profiles extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + + ); + } +} + +// Only a single DragDropContext can exist so it's done here to allow editing +// quality profiles and reordering delay profiles to work. + +export default DragDropContext(HTML5Backend)(Profiles); diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js new file mode 100644 index 000000000..13539e302 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js @@ -0,0 +1,25 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector'; + +function EditQualityProfileModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditQualityProfileModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditQualityProfileModal; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js new file mode 100644 index 000000000..5ec77950f --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalConnector.js @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import EditQualityProfileModal from './EditQualityProfileModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditQualityProfileModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'qualityProfiles' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditQualityProfileModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditQualityProfileModalConnector); diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css new file mode 100644 index 000000000..74dd1c8b7 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.css @@ -0,0 +1,3 @@ +.deleteButtonContainer { + margin-right: auto; +} diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js new file mode 100644 index 000000000..921aa94a8 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -0,0 +1,149 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import QualityProfileItems from './QualityProfileItems'; +import styles from './EditQualityProfileModalContent.css'; + +function EditQualityProfileModalContent(props) { + const { + isFetching, + error, + isSaving, + saveError, + qualities, + item, + isInUse, + onInputChange, + onCutoffChange, + onSavePress, + onModalClose, + onDeleteQualityProfilePress, + ...otherProps + } = props; + + const { + id, + name, + cutoff, + items + } = item; + + return ( + + + {id ? 'Edit Quality Profile' : 'Add Quality Profile'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new quality profile, please try again.
+ } + + { + !isFetching && !error && +
+ + Name + + + + + + Cutoff + + + + + + + + } +
+ + { + id && +
+ +
+ } + + + + + Save + +
+
+ ); +} + +EditQualityProfileModalContent.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + qualities: PropTypes.arrayOf(PropTypes.object).isRequired, + item: PropTypes.object.isRequired, + isInUse: PropTypes.bool.isRequired, + onInputChange: PropTypes.func.isRequired, + onCutoffChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteQualityProfilePress: PropTypes.func +}; + +export default EditQualityProfileModalContent; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js new file mode 100644 index 000000000..9de5080ca --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js @@ -0,0 +1,194 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { createSelector } from 'reselect'; +import createProfileInUseSelector from 'Store/Selectors/createProfileInUseSelector'; +import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector'; +import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile } from 'Store/Actions/settingsActions'; +import connectSection from 'Store/connectSection'; +import EditQualityProfileModalContent from './EditQualityProfileModalContent'; + +function createQualitiesSelector() { + return createSelector( + createProviderSettingsSelector(), + (qualityProfile) => { + const items = qualityProfile.item.items; + if (!items || !items.value) { + return []; + } + + return _.reduceRight(items.value, (result, { allowed, quality }) => { + if (allowed) { + result.push({ + key: quality.id, + value: quality.name + }); + } + + return result; + }, []); + } + ); +} + +function createMapStateToProps() { + return createSelector( + createProviderSettingsSelector(), + createQualitiesSelector(), + createProfileInUseSelector('qualityProfileId'), + (qualityProfile, qualities, isInUse) => { + return { + qualities, + ...qualityProfile, + isInUse + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfileSchema, + setQualityProfileValue, + saveQualityProfile +}; + +class EditQualityProfileModalContentConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + dragIndex: null, + dropIndex: null + }; + } + + componentDidMount() { + if (!this.props.id) { + this.props.fetchQualityProfileSchema(); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setQualityProfileValue({ name, value }); + } + + onCutoffChange = ({ name, value }) => { + const id = parseInt(value); + const item = _.find(this.props.item.items.value, (i) => i.quality.id === id); + + this.props.setQualityProfileValue({ name, value: item.quality }); + } + + onSavePress = () => { + this.props.saveQualityProfile({ id: this.props.id }); + } + + onQualityProfileItemAllowedChange = (id, allowed) => { + const qualityProfile = _.cloneDeep(this.props.item); + + const item = _.find(qualityProfile.items.value, (i) => i.quality.id === id); + item.allowed = allowed; + + this.props.setQualityProfileValue({ + name: 'items', + value: qualityProfile.items.value + }); + + const cutoff = qualityProfile.cutoff.value; + + // If the cutoff isn't allowed anymore or there isn't a cutoff set one + if (!cutoff || !_.find(qualityProfile.items.value, (i) => i.quality.id === cutoff.id).allowed) { + const firstAllowed = _.find(qualityProfile.items.value, { allowed: true }); + + this.props.setQualityProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.quality : null }); + } + } + + onQualityProfileItemDragMove = (dragIndex, dropIndex) => { + if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) { + this.setState({ + dragIndex, + dropIndex + }); + } + } + + onQualityProfileItemDragEnd = ({ id }, didDrop) => { + const { + dragIndex, + dropIndex + } = this.state; + + if (didDrop && dropIndex !== null) { + const qualityProfile = _.cloneDeep(this.props.item); + + const items = qualityProfile.items.value.splice(dragIndex, 1); + qualityProfile.items.value.splice(dropIndex, 0, items[0]); + + this.props.setQualityProfileValue({ + name: 'items', + value: qualityProfile.items.value + }); + } + + this.setState({ + dragIndex: null, + dropIndex: null + }); + } + + // + // Render + + render() { + if (_.isEmpty(this.props.item.items) && !this.props.isFetching) { + return null; + } + + return ( + + ); + } +} + +EditQualityProfileModalContentConnector.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setQualityProfileValue: PropTypes.func.isRequired, + fetchQualityProfileSchema: PropTypes.func.isRequired, + saveQualityProfile: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connectSection( + createMapStateToProps, + mapDispatchToProps, + undefined, + undefined, + { section: 'qualityProfiles' } +)(EditQualityProfileModalContentConnector); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.css b/frontend/src/Settings/Profiles/Quality/QualityProfile.css new file mode 100644 index 000000000..785047cd4 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.css @@ -0,0 +1,19 @@ +.qualityProfile { + composes: card from 'Components/Card.css'; + + width: 300px; +} + +.name { + composes: truncate from 'Styles/Mixins/truncate.css'; + + margin-bottom: 20px; + font-weight: 300; + font-size: 24px; +} + +.qualities { + display: flex; + flex-wrap: wrap; + margin-top: 5px; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js new file mode 100644 index 000000000..211656ab1 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.js @@ -0,0 +1,124 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import Card from 'Components/Card'; +import Label from 'Components/Label'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import EditQualityProfileModalConnector from './EditQualityProfileModalConnector'; +import styles from './QualityProfile.css'; + +class QualityProfile extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditQualityProfileModalOpen: false, + isDeleteQualityProfileModalOpen: false + }; + } + + // + // Listeners + + onEditQualityProfilePress = () => { + this.setState({ isEditQualityProfileModalOpen: true }); + } + + onEditQualityProfileModalClose = () => { + this.setState({ isEditQualityProfileModalOpen: false }); + } + + onDeleteQualityProfilePress = () => { + this.setState({ + isEditQualityProfileModalOpen: false, + isDeleteQualityProfileModalOpen: true + }); + } + + onDeleteQualityProfileModalClose = () => { + this.setState({ isDeleteQualityProfileModalOpen: false }); + } + + onConfirmDeleteQualityProfile = () => { + this.props.onConfirmDeleteQualityProfile(this.props.id); + } + + // + // Render + + render() { + const { + id, + name, + cutoff, + items, + isDeleting + } = this.props; + + return ( + +
+ {name} +
+ +
+ { + items.map((item) => { + if (!item.allowed) { + return null; + } + + const isCutoff = item.quality.id === cutoff.id; + + return ( + + ); + }) + } +
+ + + + +
+ ); + } +} + +QualityProfile.propTypes = { + id: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + cutoff: PropTypes.object.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + isDeleting: PropTypes.bool.isRequired, + onConfirmDeleteQualityProfile: PropTypes.func.isRequired +}; + +export default QualityProfile; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css new file mode 100644 index 000000000..90d48a2c5 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css @@ -0,0 +1,44 @@ +.qualityProfileItem { + display: flex; + align-items: stretch; + width: 100%; + border: 1px solid #aaa; + border-radius: 4px; + background: #fafafa; +} + +.checkContainer { + position: relative; + margin-right: 4px; + margin-bottom: 7px; + margin-left: 8px; +} + +.qualityName { + display: flex; + flex-grow: 1; + margin-bottom: 0; + margin-left: 2px; + font-weight: normal; + line-height: 36px; + cursor: pointer; +} + +.dragHandle { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-left: auto; + width: $dragHandleWidth; + text-align: center; + cursor: grab; +} + +.dragIcon { + top: 0; +} + +.isDragging { + opacity: 0.25; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js new file mode 100644 index 000000000..684b0c905 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js @@ -0,0 +1,83 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import CheckInput from 'Components/Form/CheckInput'; +import styles from './QualityProfileItem.css'; + +class QualityProfileItem extends Component { + + // + // Listeners + + onAllowedChange = ({ value }) => { + const { + qualityId, + onQualityProfileItemAllowedChange + } = this.props; + + onQualityProfileItemAllowedChange(qualityId, value); + } + + // + // Render + + render() { + const { + name, + allowed, + isDragging, + connectDragSource + } = this.props; + + return ( +
+ + + { + connectDragSource( +
+ +
+ ) + } +
+ ); + } +} + +QualityProfileItem.propTypes = { + qualityId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool.isRequired, + connectDragSource: PropTypes.func, + onQualityProfileItemAllowedChange: PropTypes.func +}; + +QualityProfileItem.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default QualityProfileItem; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css new file mode 100644 index 000000000..b927d9bce --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.css @@ -0,0 +1,4 @@ +.dragPreview { + width: 380px; + opacity: 0.75; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js new file mode 100644 index 000000000..1fd249714 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DragLayer } from 'react-dnd'; +import dimensions from 'Styles/Variables/dimensions.js'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import DragPreviewLayer from 'Components/DragPreviewLayer'; +import QualityProfileItem from './QualityProfileItem'; +import styles from './QualityProfileItemDragPreview.css'; + +const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth); +const formLabelWidth = parseInt(dimensions.formLabelWidth); +const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth); +const dragHandleWidth = parseInt(dimensions.dragHandleWidth); + +function collectDragLayer(monitor) { + return { + item: monitor.getItem(), + itemType: monitor.getItemType(), + currentOffset: monitor.getSourceClientOffset() + }; +} + +class QualityProfileItemDragPreview extends Component { + + // + // Render + + render() { + const { + item, + itemType, + currentOffset + } = this.props; + + if (!currentOffset || itemType !== QUALITY_PROFILE_ITEM) { + return null; + } + + // The offset is shifted because the drag handle is on the right edge of the + // list item and the preview is wider than the drag handle. + + const { x, y } = currentOffset; + const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth; + const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`; + + const style = { + position: 'absolute', + WebkitTransform: transform, + msTransform: transform, + transform + }; + + const { + qualityId, + name, + allowed, + sortIndex + } = item; + + return ( + +
+ +
+
+ ); + } +} + +QualityProfileItemDragPreview.propTypes = { + item: PropTypes.object, + itemType: PropTypes.string, + currentOffset: PropTypes.shape({ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired + }) +}; + +export default DragLayer(collectDragLayer)(QualityProfileItemDragPreview); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css new file mode 100644 index 000000000..5b9f36fe9 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css @@ -0,0 +1,18 @@ +.qualityProfileItemDragSource { + padding: 4px 0; +} + +.qualityProfileItemPlaceholder { + width: 100%; + height: 36px; + border: 1px dotted #aaa; + border-radius: 4px; +} + +.qualityProfileItemPlaceholderBefore { + margin-bottom: 8px; +} + +.qualityProfileItemPlaceholderAfter { + margin-top: 8px; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js new file mode 100644 index 000000000..ed8adc107 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { findDOMNode } from 'react-dom'; +import { DragSource, DropTarget } from 'react-dnd'; +import classNames from 'classnames'; +import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes'; +import QualityProfileItem from './QualityProfileItem'; +import styles from './QualityProfileItemDragSource.css'; + +const qualityProfileItemDragSource = { + beginDrag({ qualityId, name, allowed, sortIndex }) { + return { + qualityId, + name, + allowed, + sortIndex + }; + }, + + endDrag(props, monitor, component) { + props.onQualityProfileItemDragEnd(monitor.getItem(), monitor.didDrop()); + } +}; + +const qualityProfileItemDropTarget = { + hover(props, monitor, component) { + const dragIndex = monitor.getItem().sortIndex; + const hoverIndex = props.sortIndex; + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect(); + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientY = clientOffset.y - hoverBoundingRect.top; + + // Moving up, only trigger if drag position is above 50% + if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) { + return; + } + + // Moving down, only trigger if drag position is below 50% + if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) { + return; + } + + props.onQualityProfileItemDragMove(dragIndex, hoverIndex); + } +}; + +function collectDragSource(connect, monitor) { + return { + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging() + }; +} + +function collectDropTarget(connect, monitor) { + return { + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver() + }; +} + +class QualityProfileItemDragSource extends Component { + + // + // Render + + render() { + const { + qualityId, + name, + allowed, + sortIndex, + isDragging, + isDraggingUp, + isDraggingDown, + isOver, + connectDragSource, + connectDropTarget, + onQualityProfileItemAllowedChange + } = this.props; + + const isBefore = !isDragging && isDraggingUp && isOver; + const isAfter = !isDragging && isDraggingDown && isOver; + + // if (isDragging && !isOver) { + // return null; + // } + + return connectDropTarget( +
+ { + isBefore && +
+ } + + + + { + isAfter && +
+ } +
+ ); + } +} + +QualityProfileItemDragSource.propTypes = { + qualityId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired, + allowed: PropTypes.bool.isRequired, + sortIndex: PropTypes.number.isRequired, + isDragging: PropTypes.bool, + isDraggingUp: PropTypes.bool, + isDraggingDown: PropTypes.bool, + isOver: PropTypes.bool, + connectDragSource: PropTypes.func, + connectDropTarget: PropTypes.func, + onQualityProfileItemAllowedChange: PropTypes.func.isRequired, + onQualityProfileItemDragMove: PropTypes.func.isRequired, + onQualityProfileItemDragEnd: PropTypes.func.isRequired +}; + +export default DropTarget( + QUALITY_PROFILE_ITEM, + qualityProfileItemDropTarget, + collectDropTarget +)(DragSource( + QUALITY_PROFILE_ITEM, + qualityProfileItemDragSource, + collectDragSource +)(QualityProfileItemDragSource)); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css new file mode 100644 index 000000000..344df5b08 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css @@ -0,0 +1,6 @@ +.qualities { + margin-top: 10px; + /* TODO: This should consider the number of qualities in the list */ + min-height: 550px; + user-select: none; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js new file mode 100644 index 000000000..5a58da630 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputHelpText from 'Components/Form/FormInputHelpText'; +import QualityProfileItemDragSource from './QualityProfileItemDragSource'; +import QualityProfileItemDragPreview from './QualityProfileItemDragPreview'; +import styles from './QualityProfileItems.css'; + +class QualityProfileItems extends Component { + + // + // Render + + render() { + const { + dragIndex, + dropIndex, + qualityProfileItems, + errors, + warnings, + ...otherProps + } = this.props; + + const isDragging = dropIndex !== null; + const isDraggingUp = isDragging && dropIndex > dragIndex; + const isDraggingDown = isDragging && dropIndex < dragIndex; + + return ( + + Qualities +
+ + + { + errors.map((error, index) => { + return ( + + ); + }) + } + + { + warnings.map((warning, index) => { + return ( + + ); + }) + } + +
+ { + qualityProfileItems.map(({ allowed, quality }, index) => { + return ( + + ); + }).reverse() + } + + +
+
+
+ ); + } +} + +QualityProfileItems.propTypes = { + dragIndex: PropTypes.number, + dropIndex: PropTypes.number, + qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired, + errors: PropTypes.arrayOf(PropTypes.object), + warnings: PropTypes.arrayOf(PropTypes.object) +}; + +QualityProfileItems.defaultProps = { + errors: [], + warnings: [] +}; + +export default QualityProfileItems; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js new file mode 100644 index 000000000..bf13815ff --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileNameConnector.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector'; + +function createMapStateToProps() { + return createSelector( + createQualityProfileSelector(), + (qualityProfile) => { + return { + name: qualityProfile.name + }; + } + ); +} + +function QualityProfileNameConnector({ name, ...otherProps }) { + return ( + + {name} + + ); +} + +QualityProfileNameConnector.propTypes = { + qualityProfileId: PropTypes.number.isRequired, + name: PropTypes.string.isRequired +}; + +export default connect(createMapStateToProps)(QualityProfileNameConnector); diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.css b/frontend/src/Settings/Profiles/Quality/QualityProfiles.css new file mode 100644 index 000000000..9644a7c2d --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.css @@ -0,0 +1,21 @@ +.qualityProfiles { + display: flex; + flex-wrap: wrap; +} + +.addQualityProfile { + composes: qualityProfile from './QualityProfile.css'; + + background-color: $cardAlternateBackgroundColor; + color: $gray; + text-align: center; + font-size: 45px; +} + +.center { + display: inline-block; + padding: 5px 20px 0; + border: 1px solid $borderColor; + border-radius: 4px; + background-color: $white; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js new file mode 100644 index 000000000..75f049695 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js @@ -0,0 +1,102 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import sortByName from 'Utilities/Array/sortByName'; +import { icons } from 'Helpers/Props'; +import FieldSet from 'Components/FieldSet'; +import Card from 'Components/Card'; +import Icon from 'Components/Icon'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import QualityProfile from './QualityProfile'; +import EditQualityProfileModalConnector from './EditQualityProfileModalConnector'; +import styles from './QualityProfiles.css'; + +class QualityProfiles extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isQualityProfileModalOpen: false + }; + } + + // + // Listeners + + onEditQualityProfilePress = () => { + this.setState({ isQualityProfileModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isQualityProfileModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + isDeleting, + onConfirmDeleteQualityProfile, + ...otherProps + } = this.props; + + return ( +
+ +
+ { + items.sort(sortByName).map((item) => { + return ( + + ); + }) + } + + +
+ +
+
+
+ + +
+
+ ); + } +} + +QualityProfiles.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isDeleting: PropTypes.bool.isRequired, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteQualityProfile: PropTypes.func.isRequired +}; + +export default QualityProfiles; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js new file mode 100644 index 000000000..4bb1529ee --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js @@ -0,0 +1,60 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityProfiles, deleteQualityProfile } from 'Store/Actions/settingsActions'; +import QualityProfiles from './QualityProfiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.advancedSettings, + (state) => state.settings.qualityProfiles, + (advancedSettings, qualityProfiles) => { + return { + advancedSettings, + ...qualityProfiles + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityProfiles, + deleteQualityProfile +}; + +class QualityProfilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchQualityProfiles(); + } + + // + // Listeners + + onConfirmDeleteQualityProfile = (id) => { + this.props.deleteQualityProfile({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityProfilesConnector.propTypes = { + fetchQualityProfiles: PropTypes.func.isRequired, + deleteQualityProfile: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(QualityProfilesConnector); diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.css b/frontend/src/Settings/Quality/Definition/QualityDefinition.css new file mode 100644 index 000000000..5eced5bf4 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.css @@ -0,0 +1,95 @@ +.qualityDefinition { + display: flex; + align-content: stretch; + margin: 5px 0; + padding-top: 5px; + height: 45px; + border-top: 1px solid $borderColor; +} + +.quality, +.title { + flex: 0 1 250px; + padding-right: 20px; + line-height: 40px; +} + +.sizeLimit { + flex: 0 1 500px; + padding-right: 30px; +} + +.slider { + width: 100%; + height: 20px; +} + +.bar { + top: 6px; + margin: 0 5px; + height: 10px; + border: 1px solid $sliderAccentColor; + border-radius: 4px; + background-color: $sliderAccentColor; + box-shadow: 0 0 0 #000; + + &:nth-child(odd) { + background-color: $white; + } +} + +.handle { + top: 1px; + z-index: 0 !important; + width: 20px; + height: 20px; + border: 1px solid $sliderAccentColor; + border-radius: 50%; + background-color: $white; + text-align: center; + cursor: pointer; +} + +.sizes { + display: flex; + justify-content: space-between; +} + +.megabytesPerMinute { + display: flex; + justify-content: space-between; + flex: 0 0 250px; +} + +.sizeInput { + composes: text from 'Components/Form/TextInput.css'; + + display: inline-block; + margin-left: 5px; + padding: 6px; + width: 75px; +} + +@media only screen and (max-width: $breakpointSmall) { + .qualityDefinition { + flex-wrap: wrap; + height: auto; + + &:first-child { + border-top: none; + } + } + + .qualityDefinition:first-child { + border-top: none; + } + + .quality { + font-weight: bold; + line-height: inherit; + } + + .sizeLimit { + margin-top: 10px; + } +} diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.js b/frontend/src/Settings/Quality/Definition/QualityDefinition.js new file mode 100644 index 000000000..b1c715d8c --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.js @@ -0,0 +1,166 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import ReactSlider from 'react-slider'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { kinds } from 'Helpers/Props'; +import Label from 'Components/Label'; +import NumberInput from 'Components/Form/NumberInput'; +import TextInput from 'Components/Form/TextInput'; +import styles from './QualityDefinition.css'; + +const slider = { + min: 0, + max: 200, + step: 0.1 +}; + +function getValue(value) { + if (value < slider.min) { + return slider.min; + } + + if (value > slider.max) { + return slider.max; + } + + return value; +} + +class QualityDefinition extends Component { + + // + // Listeners + + onSizeChange = ([minSize, maxSize]) => { + maxSize = maxSize === slider.max ? null : maxSize; + + this.props.onSizeChange({ minSize, maxSize }); + } + + onMinSizeChange = ({ value }) => { + const minSize = getValue(value); + + this.props.onSizeChange({ + minSize, + maxSize: this.props.maxSize + }); + } + + onMaxSizeChange = ({ value }) => { + const maxSize = value === slider.max ? null : getValue(value); + + this.props.onSizeChange({ + minSize: this.props.minSize, + maxSize + }); + } + + // + // Render + + render() { + const { + id, + quality, + title, + minSize, + maxSize, + advancedSettings, + onTitleChange + } = this.props; + + const minBytes = minSize * 1024 * 1024; + const minThirty = formatBytes(minBytes * 30, 2); + const minSixty = formatBytes(minBytes * 60, 2); + + const maxBytes = maxSize && maxSize * 1024 * 1024; + const maxThirty = maxBytes ? formatBytes(maxBytes * 30, 2) : 'Unlimited'; + const maxSixty = maxBytes ? formatBytes(maxBytes * 60, 2) : 'Unlimited'; + + return ( +
+
+ {quality.name} +
+ +
+ +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+
+ + { + advancedSettings && +
+
+ Min + + +
+ +
+ Max + + +
+
+ } +
+ ); + } +} + +QualityDefinition.propTypes = { + id: PropTypes.number.isRequired, + quality: PropTypes.object.isRequired, + title: PropTypes.string.isRequired, + minSize: PropTypes.number, + maxSize: PropTypes.number, + advancedSettings: PropTypes.bool.isRequired, + onTitleChange: PropTypes.func.isRequired, + onSizeChange: PropTypes.func.isRequired +}; + +export default QualityDefinition; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js new file mode 100644 index 000000000..be83cc069 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js @@ -0,0 +1,70 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { setQualityDefinitionValue } from 'Store/Actions/settingsActions'; +import { clearPendingChanges } from 'Store/Actions/baseActions'; +import QualityDefinition from './QualityDefinition'; + +function mapStateToProps(state) { + return { + advancedSettings: state.settings.advancedSettings + }; +} + +const mapDispatchToProps = { + setQualityDefinitionValue, + clearPendingChanges +}; + +class QualityDefinitionConnector extends Component { + + componentWillUnmount() { + this.props.clearPendingChanges({ section: 'qualityDefinitions' }); + } + + // + // Listeners + + onTitleChange = ({ value }) => { + this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value }); + } + + onSizeChange = ({ minSize, maxSize }) => { + const { + id, + minSize: currentMinSize, + maxSize: currentMaxSize + } = this.props; + + if (minSize !== currentMinSize) { + this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize }); + } + + if (minSize !== currentMaxSize) { + this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize }); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityDefinitionConnector.propTypes = { + id: PropTypes.number.isRequired, + minSize: PropTypes.number, + maxSize: PropTypes.number, + setQualityDefinitionValue: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(QualityDefinitionConnector); diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css new file mode 100644 index 000000000..689017684 --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css @@ -0,0 +1,41 @@ +.header { + display: flex; + font-weight: bold; +} + +.quality, +.title { + flex: 0 1 250px; +} + +.sizeLimit { + flex: 0 1 500px; +} + +.megabytesPerMinute { + flex: 0 0 250px; +} + +.sizeLimitHelpTextContainer { + display: flex; + justify-content: flex-end; + margin-top: 20px; + max-width: 1000px; +} + +.sizeLimitHelpText { + max-width: 500px; + color: $helpTextColor; +} + +@media only screen and (max-width: $breakpointSmall) { + .header { + display: none; + } + + .definitions { + &:first-child { + border-top: none; + } + } +} diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js new file mode 100644 index 000000000..8eac34c8b --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js @@ -0,0 +1,65 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import FieldSet from 'Components/FieldSet'; +import PageSectionContent from 'Components/Page/PageSectionContent'; +import QualityDefinitionConnector from './QualityDefinitionConnector'; +import styles from './QualityDefinitions.css'; + +class QualityDefinitions extends Component { + + // + // Render + + render() { + const { + items, + ...otherProps + } = this.props; + + return ( +
+ +
+
Quality
+
Title
+
Size Limit
+
Megabytes Per Minute
+
+ +
+ { + items.map((item) => { + return ( + + ); + }) + } +
+ +
+
+ Limits are automatically adjusted for the series runtime and number of episodes in the file. +
+
+
+
+ ); + } +} + +QualityDefinitions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + defaultProfile: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default QualityDefinitions; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js new file mode 100644 index 000000000..2170919be --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js @@ -0,0 +1,78 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchQualityDefinitions, saveQualityDefinitions } from 'Store/Actions/settingsActions'; +import QualityDefinitions from './QualityDefinitions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.qualityDefinitions, + (qualityDefinitions) => { + const items = qualityDefinitions.items.map((item) => { + const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {}; + + return Object.assign({}, item, pendingChanges); + }); + + return { + ...qualityDefinitions, + items, + hasPendingChanges: !_.isEmpty(qualityDefinitions.pendingChanges) + }; + } + ); +} + +const mapDispatchToProps = { + fetchQualityDefinitions, + saveQualityDefinitions +}; + +class QualityDefinitionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchQualityDefinitions(); + } + + componentDidUpdate(prevProps) { + const { + hasPendingChanges + } = this.props; + + if (hasPendingChanges !== prevProps.hasPendingChanges) { + this.props.onHasPendingChange(hasPendingChanges); + } + } + + // + // Control + + save = () => { + this.props.saveQualityDefinitions(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +QualityDefinitionsConnector.propTypes = { + hasPendingChanges: PropTypes.bool.isRequired, + fetchQualityDefinitions: PropTypes.func.isRequired, + saveQualityDefinitions: PropTypes.func.isRequired, + onHasPendingChange: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector); diff --git a/frontend/src/Settings/Quality/Quality.js b/frontend/src/Settings/Quality/Quality.js new file mode 100644 index 000000000..ed58dac67 --- /dev/null +++ b/frontend/src/Settings/Quality/Quality.js @@ -0,0 +1,59 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; +import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; + +class Quality extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + hasPendingChanges: false + }; + } + + // + // Listeners + + setQualityDefinitionsRef = (ref) => { + this._qualityDefinitions = ref; + } + + onHasPendingChange = (hasPendingChanges) => { + this.setState({ + hasPendingChanges + }); + } + + onSavePress = () => { + this._qualityDefinitions.getWrappedInstance().save(); + } + + // + // Render + + render() { + return ( + + + + + + + + ); + } +} + +export default Quality; diff --git a/frontend/src/Settings/SettingsToolbar.css b/frontend/src/Settings/SettingsToolbar.css new file mode 100644 index 000000000..2d3aa1c6f --- /dev/null +++ b/frontend/src/Settings/SettingsToolbar.css @@ -0,0 +1,7 @@ +.advancedSettings { + composes: toolbarButton from 'Components/Page/Toolbar/PageToolbarButton.css'; +} + +.advancedSettingsEnabled { + color: $toobarButtonHoverColor; +} diff --git a/frontend/src/Settings/SettingsToolbar.js b/frontend/src/Settings/SettingsToolbar.js new file mode 100644 index 000000000..0d758ce23 --- /dev/null +++ b/frontend/src/Settings/SettingsToolbar.js @@ -0,0 +1,104 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import classNames from 'classnames'; +import { icons } from 'Helpers/Props'; +import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import PendingChangesModal from './PendingChangesModal'; +import styles from './SettingsToolbar.css'; + +class SettingsToolbar extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.bindShortcut(shortcuts.SAVE_SETTINGS.key, this.saveSettings, { isGlobal: true }); + } + + // + // Control + + saveSettings = (event) => { + event.preventDefault(); + + const { + hasPendingChanges, + onSavePress + } = this.props; + + if (hasPendingChanges) { + onSavePress(); + } + } + + // + // Render + + render() { + const { + advancedSettings, + showSave, + isSaving, + hasPendingChanges, + hasPendingLocation, + onSavePress, + onConfirmNavigation, + onCancelNavigation, + onAdvancedSettingsPress + } = this.props; + + return ( + + + + + { + showSave && + + } + + + + ); + } +} + +SettingsToolbar.propTypes = { + advancedSettings: PropTypes.bool.isRequired, + showSave: PropTypes.bool.isRequired, + isSaving: PropTypes.bool, + hasPendingLocation: PropTypes.bool.isRequired, + hasPendingChanges: PropTypes.bool, + onSavePress: PropTypes.func, + onAdvancedSettingsPress: PropTypes.func.isRequired, + onConfirmNavigation: PropTypes.func.isRequired, + onCancelNavigation: PropTypes.func.isRequired, + bindShortcut: PropTypes.func.isRequired +}; + +SettingsToolbar.defaultProps = { + showSave: true +}; + +export default keyboardShortcuts(SettingsToolbar); diff --git a/frontend/src/Settings/SettingsToolbarConnector.js b/frontend/src/Settings/SettingsToolbarConnector.js new file mode 100644 index 000000000..8bfb3dad5 --- /dev/null +++ b/frontend/src/Settings/SettingsToolbarConnector.js @@ -0,0 +1,147 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import { toggleAdvancedSettings } from 'Store/Actions/settingsActions'; +import SettingsToolbar from './SettingsToolbar'; + +function mapStateToProps(state) { + return { + advancedSettings: state.settings.advancedSettings + }; +} + +const mapDispatchToProps = { + toggleAdvancedSettings +}; + +class SettingsToolbarConnector extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + nextLocation: null, + nextLocationAction: null, + confirmed: false + }; + + this._unblock = null; + } + + componentDidMount() { + this._unblock = this.props.history.block(this.routerWillLeave); + } + + componentWillUnmount() { + if (this._unblock) { + this._unblock(); + } + } + + // + // Control + + routerWillLeave = (nextLocation, nextLocationAction) => { + if (this.state.confirmed) { + this.setState({ + nextLocation: null, + nextLocationAction: null, + confirmed: false + }); + + return true; + } + + if (this.props.hasPendingChanges ) { + this.setState({ + nextLocation, + nextLocationAction + }); + + return false; + } + + return true; + } + + // + // Listeners + + onAdvancedSettingsPress = () => { + this.props.toggleAdvancedSettings(); + } + + onConfirmNavigation = () => { + const { + nextLocation, + nextLocationAction + } = this.state; + + const history = this.props.history; + + const path = `${nextLocation.pathname}${nextLocation.search}`; + + this.setState({ + confirmed: true + }, () => { + if (nextLocationAction === 'PUSH') { + history.push(path); + } else { + // Unfortunately back and forward both use POP, + // which means we don't actually know which direction + // the user wanted to go, assuming back. + + history.goBack(); + } + }); + } + + onCancelNavigation = () => { + this.setState({ + nextLocation: null, + nextLocationAction: null, + confirmed: false + }); + } + + // + // Render + + render() { + const hasPendingLocation = this.state.nextLocation !== null; + + return ( + + ); + } +} + +const historyShape = { + block: PropTypes.func.isRequired, + goBack: PropTypes.func.isRequired, + push: PropTypes.func.isRequired +}; + +SettingsToolbarConnector.propTypes = { + hasPendingChanges: PropTypes.bool.isRequired, + history: PropTypes.shape(historyShape).isRequired, + onSavePress: PropTypes.func, + toggleAdvancedSettings: PropTypes.func.isRequired +}; + +SettingsToolbarConnector.defaultProps = { + hasPendingChanges: false +}; + +export default withRouter(connect(mapStateToProps, mapDispatchToProps)(SettingsToolbarConnector)); diff --git a/frontend/src/Shared/piwikCheck.js b/frontend/src/Shared/piwikCheck.js new file mode 100644 index 000000000..4bc7d0ed1 --- /dev/null +++ b/frontend/src/Shared/piwikCheck.js @@ -0,0 +1,10 @@ +if (window.Sonarr.analytics) { + var d = document; + var g = d.createElement('script'); + var s = d.getElementsByTagName('script')[0]; + g.type = 'text/javascript'; + g.async = true; + g.defer = true; + g.src = '//piwik.sonarr.tv/piwik.js'; + s.parentNode.insertBefore(g, s); +} diff --git a/frontend/src/Shims/jquery.js b/frontend/src/Shims/jquery.js new file mode 100644 index 000000000..1d32f35f3 --- /dev/null +++ b/frontend/src/Shims/jquery.js @@ -0,0 +1,8 @@ +var jquery = require('JsLibraries/jquery'); +var ajax = require('jQuery/jquery.ajax'); + +ajax(jquery); + +window.$ = jquery; +window.jQuery = jquery; +module.exports = jquery; diff --git a/frontend/src/Shims/signalR.js b/frontend/src/Shims/signalR.js new file mode 100644 index 000000000..22fb16b91 --- /dev/null +++ b/frontend/src/Shims/signalR.js @@ -0,0 +1,4 @@ +require('jquery'); +const signalR = require('JsLibraries/jquery.signalR'); + +module.exports = signalR; diff --git a/frontend/src/Store/Actions/Creators/createBatchToggleEpisodeMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createBatchToggleEpisodeMonitoredHandler.js new file mode 100644 index 000000000..1017d261d --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createBatchToggleEpisodeMonitoredHandler.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import updateEpisodes from 'Utilities/Episode/updateEpisodes'; + +function createBatchToggleEpisodeMonitoredHandler(section, getFromState) { + return function(payload) { + return function(dispatch, getState) { + const { + episodeIds, + monitored + } = payload; + + const state = getFromState(getState()); + + updateEpisodes(dispatch, section, state.items, episodeIds, { + isSaving: true + }); + + const promise = $.ajax({ + url: '/episode/monitor', + method: 'PUT', + data: JSON.stringify({ episodeIds, monitored }), + dataType: 'json' + }); + + promise.done(() => { + updateEpisodes(dispatch, section, state.items, episodeIds, { + isSaving: false, + monitored + }); + }); + + promise.fail(() => { + updateEpisodes(dispatch, section, state.items, episodeIds, { + isSaving: false + }); + }); + }; + }; +} + +export default createBatchToggleEpisodeMonitoredHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchHandler.js b/frontend/src/Store/Actions/Creators/createFetchHandler.js new file mode 100644 index 000000000..5bf31b92e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchHandler.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, update, updateItem } from '../baseActions'; + +function createFetchHandler(section, url) { + return function(payload = {}) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const { + id, + ...otherPayload + } = payload; + + const promise = $.ajax({ + url: id == null ? url : `${url}/${id}`, + data: otherPayload, + traditional: true + }); + + promise.done((data) => { + dispatch(batchActions([ + id == null ? update({ section, data }) : updateItem({ section, ...data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }; +} + +export default createFetchHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js new file mode 100644 index 000000000..e58811ee4 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchSchemaHandler.js @@ -0,0 +1,35 @@ +import $ from 'jquery'; +import { set } from '../baseActions'; + +function createFetchSchemaHandler(section, url) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isFetchingSchema: true })); + + const promise = $.ajax({ + url + }); + + promise.done((data) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: null, + schema: data + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetchingSchema: false, + schemaPopulated: true, + schemaError: xhr + })); + }); + }; + }; +} + +export default createFetchSchemaHandler; diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js new file mode 100644 index 000000000..4f8d779a8 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -0,0 +1,54 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, updateServerSideCollection } from '../baseActions'; + +function createFetchServerSideCollectionHandler(section, url, getFromState) { + return function(payload = {}) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const page = payload.page || sectionState.page || 1; + + const data = Object.assign({ page }, + _.pick(sectionState, [ + 'pageSize', + 'sortDirection', + 'sortKey', + 'filterKey', + 'filterValue' + ])); + + const promise = $.ajax({ + url, + data + }); + + promise.done((response) => { + dispatch(batchActions([ + updateServerSideCollection({ section, data: response }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }; +} + +export default createFetchServerSideCollectionHandler; diff --git a/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js new file mode 100644 index 000000000..f09a05948 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createRemoveItemHandler.js @@ -0,0 +1,47 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, removeItem } from '../baseActions'; + +function createRemoveItemHandler(section, url) { + return function(payload) { + return function(dispatch, getState) { + const { + id, + ...queryParms + } = payload; + + dispatch(set({ section, isDeleting: true })); + + const ajaxOptions = { + url: `${url}/${id}?${$.param(queryParms, true)}`, + method: 'DELETE' + }; + + const promise = $.ajax(ajaxOptions); + + promise.done((data) => { + dispatch(batchActions([ + removeItem({ section, id }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + + return promise; + }; + }; +} + +export default createRemoveItemHandler; diff --git a/frontend/src/Store/Actions/Creators/createSaveHandler.js b/frontend/src/Store/Actions/Creators/createSaveHandler.js new file mode 100644 index 000000000..76048192e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSaveHandler.js @@ -0,0 +1,44 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { set, update } from '../baseActions'; + +function createSaveHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isSaving: true })); + + const state = getFromState(getState()); + const saveData = Object.assign({}, state.item, state.pendingChanges); + + const promise = $.ajax({ + url, + method: 'PUT', + dataType: 'json', + data: JSON.stringify(saveData) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isSaving: false, + saveError: null, + pendingChanges: {} + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + }; +} + +export default createSaveHandler; diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js new file mode 100644 index 000000000..3a0148c71 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js @@ -0,0 +1,53 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import getProviderState from 'Utilities/State/getProviderState'; +import { set, updateItem } from '../baseActions'; + +function createSaveProviderHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isSaving: true })); + + const id = payload.id; + const saveData = getProviderState(payload, getState, getFromState); + + const ajaxOptions = { + url, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(saveData) + }; + + if (id) { + ajaxOptions.url = `${url}/${id}`; + ajaxOptions.method = 'PUT'; + } + + const promise = $.ajax(ajaxOptions); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ section, ...data }), + + set({ + section, + isSaving: false, + saveError: null, + pendingChanges: {} + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + }; +} + +export default createSaveProviderHandler; diff --git a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js new file mode 100644 index 000000000..91cef5d5e --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js @@ -0,0 +1,52 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import pages from 'Utilities/pages'; +import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler'; +import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler'; +import createSetServerSideCollectionSortHandler from './createSetServerSideCollectionSortHandler'; +import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; + +function createServerSideCollectionHandlers(section, url, getFromState, handlers) { + const actionHandlers = {}; + const fetchHandlerType = handlers[serverSideCollectionHandlers.FETCH]; + const fetchHandler = createFetchServerSideCollectionHandler(section, url, getFromState); + actionHandlers[fetchHandlerType] = fetchHandler; + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.FIRST_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.FIRST_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.FIRST, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.PREVIOUS_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.PREVIOUS_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.PREVIOUS, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.NEXT_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.NEXT_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.NEXT, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.LAST_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.LAST_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.LAST, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.EXACT_PAGE)) { + const handlerType = handlers[serverSideCollectionHandlers.EXACT_PAGE]; + actionHandlers[handlerType] = createSetServerSideCollectionPageHandler(section, pages.EXACT, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.SORT)) { + const handlerType = handlers[serverSideCollectionHandlers.SORT]; + actionHandlers[handlerType] = createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler); + } + + if (handlers.hasOwnProperty(serverSideCollectionHandlers.FILTER)) { + const handlerType = handlers[serverSideCollectionHandlers.FILTER]; + actionHandlers[handlerType] = createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler); + } + + return actionHandlers; +} + +export default createServerSideCollectionHandlers; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js new file mode 100644 index 000000000..0aaa342db --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionFilterHandler.js @@ -0,0 +1,12 @@ +import { set } from '../baseActions'; + +function createSetServerSideCollectionFilterHandler(section, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, ...payload })); + dispatch(fetchHandler({ page: 1 })); + }; + }; +} + +export default createSetServerSideCollectionFilterHandler; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js new file mode 100644 index 000000000..88682f118 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js @@ -0,0 +1,37 @@ +import pages from 'Utilities/pages'; + +function createSetServerSideCollectionPageHandler(section, page, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const currentPage = sectionState.page || 1; + let nextPage = 0; + + switch (page) { + case pages.FIRST: + nextPage = 1; + break; + case pages.PREVIOUS: + nextPage = currentPage - 1; + break; + case pages.NEXT: + nextPage = currentPage + 1; + break; + case pages.LAST: + nextPage = sectionState.totalPages; + break; + default: + nextPage = payload.page; + } + + // If we prefer to update the page immediately we should + // set the page and not pass a page to the fetch handler. + + // dispatch(set({ section, page: nextPage })); + dispatch(fetchHandler({ page: nextPage })); + }; + }; +} + +export default createSetServerSideCollectionPageHandler; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js new file mode 100644 index 000000000..457108894 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionSortHandler.js @@ -0,0 +1,28 @@ +import { sortDirections } from 'Helpers/Props'; +import { set } from '../baseActions'; + +function createSetServerSideCollectionSortHandler(section, getFromState, fetchHandler) { + return function(payload) { + return function(dispatch, getState) { + const state = getFromState(getState()); + const sectionState = state.hasOwnProperty(section) ? state[section] : state; + const sortKey = payload.sortKey || sectionState.sortKey; + let sortDirection = payload.sortDirection; + + if (!sortDirection) { + if (payload.sortKey === sectionState.sortKey) { + sortDirection = sectionState.sortDirection === sortDirections.ASCENDING ? + sortDirections.DESCENDING : + sortDirections.ASCENDING; + } else { + sortDirection = sectionState.sortDirection; + } + } + + dispatch(set({ section, sortKey, sortDirection })); + dispatch(fetchHandler()); + }; + }; +} + +export default createSetServerSideCollectionSortHandler; diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js new file mode 100644 index 000000000..3dbc1a113 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import getProviderState from 'Utilities/State/getProviderState'; +import { set } from '../baseActions'; + +function createTestProviderHandler(section, url, getFromState) { + return function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isTesting: true })); + + const testData = getProviderState(payload, getState, getFromState); + + const ajaxOptions = { + url: `${url}/test`, + method: 'POST', + contentType: 'application/json', + dataType: 'json', + data: JSON.stringify(testData) + }; + + const promise = $.ajax(ajaxOptions); + + promise.done((data) => { + dispatch(set({ + section, + isTesting: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isTesting: false, + saveError: xhr + })); + }); + }; + }; +} + +export default createTestProviderHandler; diff --git a/frontend/src/Store/Actions/Creators/createToggleEpisodeMonitoredHandler.js b/frontend/src/Store/Actions/Creators/createToggleEpisodeMonitoredHandler.js new file mode 100644 index 000000000..8480ed067 --- /dev/null +++ b/frontend/src/Store/Actions/Creators/createToggleEpisodeMonitoredHandler.js @@ -0,0 +1,41 @@ +import $ from 'jquery'; +import updateEpisodes from 'Utilities/Episode/updateEpisodes'; + +function createToggleEpisodeMonitoredHandler(section, getFromState) { + return function(payload) { + return function(dispatch, getState) { + const { + episodeId, + monitored + } = payload; + + const state = getFromState(getState()); + + updateEpisodes(dispatch, section, state.items, [episodeId], { + isSaving: true + }); + + const promise = $.ajax({ + url: `/episode/${episodeId}`, + method: 'PUT', + data: JSON.stringify({ monitored }), + dataType: 'json' + }); + + promise.done(() => { + updateEpisodes(dispatch, section, state.items, [episodeId], { + isSaving: false, + monitored + }); + }); + + promise.fail(() => { + updateEpisodes(dispatch, section, state.items, [episodeId], { + isSaving: false + }); + }); + }; + }; +} + +export default createToggleEpisodeMonitoredHandler; diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js new file mode 100644 index 000000000..5d00df4f3 --- /dev/null +++ b/frontend/src/Store/Actions/actionTypes.js @@ -0,0 +1,394 @@ +// +// BASE + +export const SET = 'SET'; + +export const UPDATE = 'UPDATE'; +export const UPDATE_ITEM = 'UPDATE_ITEM'; +export const UPDATE_SERVER_SIDE_COLLECTION = 'UPDATE_SERVER_SIDE_COLLECTION'; + +export const SET_SETTING_VALUE = 'SET_SETTING_VALUE'; +export const CLEAR_PENDING_CHANGES = 'CLEAR_PENDING_CHANGES'; +export const SAVE_SETTINGS = 'SAVE_SETTINGS'; + +export const REMOVE_ITEM = 'REMOVE_ITEM'; + +// +// App + +export const SHOW_MESSAGE = 'SHOW_MESSAGE'; +export const HIDE_MESSAGE = 'HIDE_MESSAGE'; +export const SAVE_DIMENSIONS = 'SAVE_DIMENSIONS'; +export const SET_VERSION = 'SET_VERSION'; +export const SET_APP_VALUE = 'SET_APP_VALUE'; +export const SET_IS_SIDEBAR_VISIBLE = 'SET_IS_SIDEBAR_VISIBLE'; + +// +// Add Series + +export const LOOKUP_SERIES = 'LOOKUP_SERIES'; +export const ADD_SERIES = 'ADD_SERIES'; +export const SET_ADD_SERIES_VALUE = 'SET_ADD_SERIES_VALUE'; +export const CLEAR_ADD_SERIES = 'CLEAR_ADD_SERIES'; +export const SET_ADD_SERIES_DEFAULT = 'SET_ADD_SERIES_DEFAULT'; + +// +// Import Series + +export const QUEUE_LOOKUP_SERIES = 'QUEUE_LOOKUP_SERIES'; +export const START_LOOKUP_SERIES = 'START_LOOKUP_SERIES'; +export const CLEAR_IMPORT_SERIES = 'CLEAR_IMPORT_SERIES'; +export const SET_IMPORT_SERIES_VALUE = 'SET_IMPORT_SERIES_VALUE'; +export const IMPORT_SERIES = 'IMPORT_SERIES'; + +// +// Series + +export const FETCH_ARTIST = 'FETCH_ARTIST'; +export const SET_ARTIST_VALUE = 'SET_ARTIST_VALUE'; +export const SAVE_ARTIST = 'SAVE_ARTIST'; +export const DELETE_ARTIST = 'DELETE_ARTIST'; + +export const SET_ARTIST_SORT = 'SET_ARTIST_SORT'; +export const SET_ARTIST_FILTER = 'SET_ARTIST_FILTER'; +export const SET_ARTIST_VIEW = 'SET_ARTIST_VIEW'; +export const SET_ARTIST_TABLE_OPTION = 'SET_ARTIST_TABLE_OPTION'; +export const SET_ARTIST_POSTER_OPTION = 'SET_ARTIST_POSTER_OPTION'; + +export const TOGGLE_ARTIST_MONITORED = 'TOGGLE_ARTIST_MONITORED'; +export const TOGGLE_ALBUM_MONITORED = 'TOGGLE_ALBUM_MONITORED'; + +// +// Series Editor + +export const SET_SERIES_EDITOR_SORT = 'SET_SERIES_EDITOR_SORT'; +export const SET_SERIES_EDITOR_FILTER = 'SET_SERIES_EDITOR_FILTER'; +export const SAVE_ARTIST_EDITOR = 'SAVE_ARTIST_EDITOR'; +export const BULK_DELETE_ARTIST = 'BULK_DELETE_ARTIST'; + +// +// Season Pass + +export const SET_SEASON_PASS_SORT = 'SET_SEASON_PASS_SORT'; +export const SET_SEASON_PASS_FILTER = 'SET_SEASON_PASS_FILTER'; +export const SAVE_SEASON_PASS = 'SAVE_SEASON_PASS'; + +// +// Episodes + +export const FETCH_EPISODES = 'FETCH_EPISODES'; +export const SET_EPISODES_SORT = 'SET_EPISODES_SORT'; +export const SET_EPISODES_TABLE_OPTION = 'SET_EPISODES_TABLE_OPTION'; +export const CLEAR_EPISODES = 'CLEAR_EPISODES'; +export const TOGGLE_EPISODE_MONITORED = 'TOGGLE_EPISODE_MONITORED'; +export const TOGGLE_EPISODES_MONITORED = 'TOGGLE_EPISODES_MONITORED'; + +// +// Episode Files + +export const FETCH_EPISODE_FILES = 'FETCH_EPISODE_FILES'; +export const CLEAR_EPISODE_FILES = 'CLEAR_EPISODE_FILES'; +export const DELETE_EPISODE_FILE = 'DELETE_EPISODE_FILE'; +export const DELETE_EPISODE_FILES = 'DELETE_EPISODE_FILES'; +export const UPDATE_EPISODE_FILES = 'UPDATE_EPISODE_FILES'; + +// +// Episode History + +export const FETCH_EPISODE_HISTORY = 'FETCH_EPISODE_HISTORY'; +export const CLEAR_EPISODE_HISTORY = 'CLEAR_EPISODE_HISTORY'; +export const EPISODE_HISTORY_MARK_AS_FAILED = 'EPISODE_HISTORY_MARK_AS_FAILED'; + +// +// Releases + +export const FETCH_RELEASES = 'FETCH_RELEASES'; +export const SET_RELEASES_SORT = 'SET_RELEASES_SORT'; +export const CLEAR_RELEASES = 'CLEAR_RELEASES'; +export const GRAB_RELEASE = 'GRAB_RELEASE'; +export const UPDATE_RELEASE = 'UPDATE_RELEASE'; + +// +// Calendar + +export const FETCH_CALENDAR = 'FETCH_CALENDAR'; +export const SET_CALENDAR_DAYS_COUNT = 'SET_CALENDAR_DAYS_COUNT'; +export const SET_CALENDAR_INCLUDE_UNMONITORED = 'SET_CALENDAR_INCLUDE_UNMONITORED'; +export const SET_CALENDAR_VIEW = 'SET_CALENDAR_VIEW'; +export const GOTO_CALENDAR_TODAY = 'GOTO_CALENDAR_TODAY'; +export const GOTO_CALENDAR_PREVIOUS_RANGE = 'GOTO_CALENDAR_PREVIOUS_RANGE'; +export const GOTO_CALENDAR_NEXT_RANGE = 'GOTO_CALENDAR_NEXT_RANGE'; +export const CLEAR_CALENDAR = 'CLEAR_CALENDAR'; + +// +// History + +export const FETCH_HISTORY = 'FETCH_HISTORY'; +export const GOTO_FIRST_HISTORY_PAGE = 'GOTO_FIRST_HISTORY_PAGE'; +export const GOTO_PREVIOUS_HISTORY_PAGE = 'GOTO_PREVIOUS_HISTORY_PAGE'; +export const GOTO_NEXT_HISTORY_PAGE = 'GOTO_NEXT_HISTORY_PAGE'; +export const GOTO_LAST_HISTORY_PAGE = 'GOTO_LAST_HISTORY_PAGE'; +export const GOTO_HISTORY_PAGE = 'GOTO_HISTORY_PAGE'; +export const SET_HISTORY_SORT = 'SET_HISTORY_SORT'; +export const SET_HISTORY_FILTER = 'SET_HISTORY_FILTER'; +export const SET_HISTORY_TABLE_OPTION = 'SET_HISTORY_TABLE_OPTION'; +export const CLEAR_HISTORY = 'CLEAR_HISTORY'; + +export const MARK_AS_FAILED = 'MARK_AS_FAILED'; + +// +// Queue + +export const FETCH_QUEUE_STATUS = 'FETCH_QUEUE_STATUS'; + +export const FETCH_QUEUE_DETAILS = 'FETCH_QUEUE_DETAILS'; +export const CLEAR_QUEUE_DETAILS = 'CLEAR_QUEUE_DETAILS'; + +export const FETCH_QUEUE = 'FETCH_QUEUE'; +export const GOTO_FIRST_QUEUE_PAGE = 'GOTO_FIRST_QUEUE_PAGE'; +export const GOTO_PREVIOUS_QUEUE_PAGE = 'GOTO_PREVIOUS_QUEUE_PAGE'; +export const GOTO_NEXT_QUEUE_PAGE = 'GOTO_NEXT_QUEUE_PAGE'; +export const GOTO_LAST_QUEUE_PAGE = 'GOTO_LAST_QUEUE_PAGE'; +export const GOTO_QUEUE_PAGE = 'GOTO_QUEUE_PAGE'; +export const SET_QUEUE_SORT = 'SET_QUEUE_SORT'; +export const SET_QUEUE_TABLE_OPTION = 'SET_QUEUE_TABLE_OPTION'; +export const CLEAR_QUEUE = 'CLEAR_QUEUE'; + +export const SET_QUEUE_EPISODES = 'SET_QUEUE_EPISODES'; +export const GRAB_QUEUE_ITEM = 'GRAB_QUEUE_ITEM'; +export const GRAB_QUEUE_ITEMS = 'GRAB_QUEUE_ITEMS'; +export const REMOVE_QUEUE_ITEM = 'REMOVE_QUEUE_ITEM'; +export const REMOVE_QUEUE_ITEMS = 'REMOVE_QUEUE_ITEMS'; + +// +// Blacklist + +export const FETCH_BLACKLIST = 'FETCH_BLACKLIST'; +export const GOTO_FIRST_BLACKLIST_PAGE = 'GOTO_FIRST_BLACKLIST_PAGE'; +export const GOTO_PREVIOUS_BLACKLIST_PAGE = 'GOTO_PREVIOUS_BLACKLIST_PAGE'; +export const GOTO_NEXT_BLACKLIST_PAGE = 'GOTO_NEXT_BLACKLIST_PAGE'; +export const GOTO_LAST_BLACKLIST_PAGE = 'GOTO_LAST_BLACKLIST_PAGE'; +export const GOTO_BLACKLIST_PAGE = 'GOTO_BLACKLIST_PAGE'; +export const SET_BLACKLIST_SORT = 'SET_BLACKLIST_SORT'; +export const SET_BLACKLIST_TABLE_OPTION = 'SET_BLACKLIST_TABLE_OPTION'; + +// +// Wanted + +export const FETCH_MISSING = 'FETCH_MISSING'; +export const GOTO_FIRST_MISSING_PAGE = 'GOTO_FIRST_MISSING_PAGE'; +export const GOTO_PREVIOUS_MISSING_PAGE = 'GOTO_PREVIOUS_MISSING_PAGE'; +export const GOTO_NEXT_MISSING_PAGE = 'GOTO_NEXT_MISSING_PAGE'; +export const GOTO_LAST_MISSING_PAGE = 'GOTO_LAST_MISSING_PAGE'; +export const GOTO_MISSING_PAGE = 'GOTO_MISSING_PAGE'; +export const SET_MISSING_SORT = 'SET_MISSING_SORT'; +export const SET_MISSING_FILTER = 'SET_MISSING_FILTER'; +export const SET_MISSING_TABLE_OPTION = 'SET_MISSING_TABLE_OPTION'; +export const CLEAR_MISSING = 'CLEAR_MISSING'; + +export const BATCH_TOGGLE_MISSING_EPISODES = 'BATCH_TOGGLE_MISSING_EPISODES'; + +export const FETCH_CUTOFF_UNMET = 'FETCH_CUTOFF_UNMET'; +export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'GOTO_FIRST_CUTOFF_UNMET_PAGE'; +export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'GOTO_PREVIOUS_CUTOFF_UNMET_PAGE'; +export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'GOTO_NEXT_CUTOFF_UNMET_PAGE'; +export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'GOTO_LAST_CUTOFF_UNMET_PAGE'; +export const GOTO_CUTOFF_UNMET_PAGE = 'GOTO_CUTOFF_UNMET_PAGE'; +export const SET_CUTOFF_UNMET_SORT = 'SET_CUTOFF_UNMET_SORT'; +export const SET_CUTOFF_UNMET_FILTER = 'SET_CUTOFF_UNMET_FILTER'; +export const SET_CUTOFF_UNMET_TABLE_OPTION = 'SET_CUTOFF_UNMET_TABLE_OPTION'; +export const CLEAR_CUTOFF_UNMET = 'CLEAR_CUTOFF_UNMET'; + +export const BATCH_TOGGLE_CUTOFF_UNMET_EPISODES = 'BATCH_TOGGLE_CUTOFF_UNMET_EPISODES'; + +// +// Settings + +export const TOGGLE_ADVANCED_SETTINGS = 'TOGGLE_ADVANCED_SETTINGS'; + +export const FETCH_UI_SETTINGS = 'FETCH_UI_SETTINGS'; +export const SET_UI_SETTINGS_VALUE = 'SET_UI_SETTINGS_VALUE'; +export const SAVE_UI_SETTINGS = 'SAVE_UI_SETTINGS'; + +export const FETCH_MEDIA_MANAGEMENT_SETTINGS = 'FETCH_MEDIA_MANAGEMENT_SETTINGS'; +export const SET_MEDIA_MANAGEMENT_SETTINGS_VALUE = 'SET_MEDIA_MANAGEMENT_SETTINGS_VALUE'; +export const SAVE_MEDIA_MANAGEMENT_SETTINGS = 'SAVE_MEDIA_MANAGEMENT_SETTINGS'; + +export const FETCH_NAMING_SETTINGS = 'FETCH_NAMING_SETTINGS'; +export const SET_NAMING_SETTINGS_VALUE = 'SET_NAMING_SETTINGS_VALUE'; +export const SAVE_NAMING_SETTINGS = 'SAVE_NAMING_SETTINGS'; +export const FETCH_NAMING_EXAMPLES = 'FETCH_NAMING_EXAMPLES'; + +export const FETCH_QUALITY_PROFILES = 'FETCH_QUALITY_PROFILES'; +export const FETCH_QUALITY_PROFILE_SCHEMA = 'FETCH_QUALITY_PROFILE_SCHEMA'; +export const SET_QUALITY_PROFILE_VALUE = 'SET_QUALITY_PROFILE_VALUE'; +export const SAVE_QUALITY_PROFILE = 'SAVE_QUALITY_PROFILE'; +export const DELETE_QUALITY_PROFILE = 'DELETE_QUALITY_PROFILE'; + +export const FETCH_LANGUAGE_PROFILES = 'FETCH_LANGUAGE_PROFILES'; +export const FETCH_LANGUAGE_PROFILE_SCHEMA = 'FETCH_LANGUAGE_PROFILE_SCHEMA'; +export const SET_LANGUAGE_PROFILE_VALUE = 'SET_LANGUAGE_PROFILE_VALUE'; +export const SAVE_LANGUAGE_PROFILE = 'SAVE_LANGUAGE_PROFILE'; +export const DELETE_LANGUAGE_PROFILE = 'DELETE_LANGUAGE_PROFILE'; + +export const FETCH_DELAY_PROFILES = 'FETCH_DELAY_PROFILES'; +export const SET_DELAY_PROFILE_VALUE = 'SET_DELAY_PROFILE_VALUE'; +export const SAVE_DELAY_PROFILE = 'SAVE_DELAY_PROFILE'; +export const DELETE_DELAY_PROFILE = 'DELETE_DELAY_PROFILE'; +export const REORDER_DELAY_PROFILE = 'REORDER_DELAY_PROFILE'; + +export const FETCH_QUALITY_DEFINITIONS = 'FETCH_QUALITY_DEFINITIONS'; +export const SET_QUALITY_DEFINITION_VALUE = 'SET_QUALITY_DEFINITION_VALUE'; +export const SAVE_QUALITY_DEFINITIONS = 'SAVE_QUALITY_DEFINITIONS'; + +export const FETCH_INDEXERS = 'FETCH_INDEXERS'; +export const FETCH_INDEXER_SCHEMA = 'FETCH_INDEXER_SCHEMA'; +export const SELECT_INDEXER_SCHEMA = 'SELECT_INDEXER_SCHEMA'; +export const SET_INDEXER_VALUE = 'SET_INDEXER_VALUE'; +export const SET_INDEXER_FIELD_VALUE = 'SET_INDEXER_FIELD_VALUE'; +export const SAVE_INDEXER = 'SAVE_INDEXER'; +export const DELETE_INDEXER = 'DELETE_INDEXER'; +export const TEST_INDEXER = 'TEST_INDEXER'; + +export const FETCH_INDEXER_OPTIONS = 'FETCH_INDEXER_OPTIONS'; +export const SET_INDEXER_OPTIONS_VALUE = 'SET_INDEXER_OPTIONS_VALUE'; +export const SAVE_INDEXER_OPTIONS = 'SAVE_INDEXER_OPTIONS'; + +export const FETCH_RESTRICTIONS = 'FETCH_RESTRICTIONS'; +export const SET_RESTRICTION_VALUE = 'SET_RESTRICTION_VALUE'; +export const SAVE_RESTRICTION = 'SAVE_RESTRICTION'; +export const DELETE_RESTRICTION = 'DELETE_RESTRICTION'; + +export const FETCH_DOWNLOAD_CLIENTS = 'FETCH_DOWNLOAD_CLIENTS'; +export const FETCH_DOWNLOAD_CLIENT_SCHEMA = 'FETCH_DOWNLOAD_CLIENT_SCHEMA'; +export const SELECT_DOWNLOAD_CLIENT_SCHEMA = 'SELECT_DOWNLOAD_CLIENT_SCHEMA'; +export const SET_DOWNLOAD_CLIENT_VALUE = 'SET_DOWNLOAD_CLIENT_VALUE'; +export const SET_DOWNLOAD_CLIENT_FIELD_VALUE = 'SET_DOWNLOAD_CLIENT_FIELD_VALUE'; +export const SAVE_DOWNLOAD_CLIENT = 'SAVE_DOWNLOAD_CLIENT'; +export const DELETE_DOWNLOAD_CLIENT = 'DELETE_DOWNLOAD_CLIENT'; +export const TEST_DOWNLOAD_CLIENT = 'TEST_DOWNLOAD_CLIENT'; + +export const FETCH_DOWNLOAD_CLIENT_OPTIONS = 'FETCH_DOWNLOAD_CLIENT_OPTIONS'; +export const SET_DOWNLOAD_CLIENT_OPTIONS_VALUE = 'SET_DOWNLOAD_CLIENT_OPTIONS_VALUE'; +export const SAVE_DOWNLOAD_CLIENT_OPTIONS = 'SAVE_DOWNLOAD_CLIENT_OPTIONS'; + +export const FETCH_REMOTE_PATH_MAPPINGS = 'FETCH_REMOTE_PATH_MAPPINGS'; +export const SET_REMOTE_PATH_MAPPING_VALUE = 'SET_REMOTE_PATH_MAPPING_VALUE'; +export const SAVE_REMOTE_PATH_MAPPING = 'SAVE_REMOTE_PATH_MAPPING'; +export const DELETE_REMOTE_PATH_MAPPING = 'DELETE_REMOTE_PATH_MAPPING'; + +export const FETCH_NOTIFICATIONS = 'FETCH_NOTIFICATIONS'; +export const FETCH_NOTIFICATION_SCHEMA = 'FETCH_NOTIFICATION_SCHEMA'; +export const SELECT_NOTIFICATION_SCHEMA = 'SELECT_NOTIFICATION_SCHEMA'; +export const SET_NOTIFICATION_VALUE = 'SET_NOTIFICATION_VALUE'; +export const SET_NOTIFICATION_FIELD_VALUE = 'SET_NOTIFICATION_FIELD_VALUE'; +export const SAVE_NOTIFICATION = 'SAVE_NOTIFICATION'; +export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'; +export const TEST_NOTIFICATION = 'TEST_NOTIFICATION'; + +export const FETCH_METADATA = 'FETCH_METADATA'; +export const SET_METADATA_VALUE = 'SET_METADATA_VALUE'; +export const SET_METADATA_FIELD_VALUE = 'SET_METADATA_FIELD_VALUE'; +export const SAVE_METADATA = 'SAVE_METADATA'; + +// +// System + +export const FETCH_STATUS = 'FETCH_STATUS'; +export const FETCH_HEALTH = 'FETCH_HEALTH'; +export const FETCH_DISK_SPACE = 'FETCH_DISK_SPACE'; + +export const FETCH_TASK = 'FETCH_TASK'; +export const FETCH_TASKS = 'FETCH_TASKS'; +export const FETCH_BACKUPS = 'FETCH_BACKUPS'; +export const FETCH_UPDATES = 'FETCH_UPDATES'; + +export const FETCH_LOGS = 'FETCH_LOGS'; +export const GOTO_FIRST_LOGS_PAGE = 'GOTO_FIRST_LOGS_PAGE'; +export const GOTO_PREVIOUS_LOGS_PAGE = 'GOTO_PREVIOUS_LOGS_PAGE'; +export const GOTO_NEXT_LOGS_PAGE = 'GOTO_NEXT_LOGS_PAGE'; +export const GOTO_LAST_LOGS_PAGE = 'GOTO_LAST_LOGS_PAGE'; +export const GOTO_LOGS_PAGE = 'GOTO_LOGS_PAGE'; +export const SET_LOGS_SORT = 'SET_LOGS_SORT'; +export const SET_LOGS_FILTER = 'SET_LOGS_FILTER'; +export const SET_LOGS_TABLE_OPTION = 'SET_LOGS_TABLE_OPTION'; + +export const FETCH_LOG_FILES = 'FETCH_LOG_FILES'; +export const FETCH_UPDATE_LOG_FILES = 'FETCH_UPDATE_LOG_FILES'; + +export const FETCH_GENERAL_SETTINGS = 'FETCH_GENERAL_SETTINGS'; +export const SET_GENERAL_SETTINGS_VALUE = 'SET_GENERAL_SETTINGS_VALUE'; +export const SAVE_GENERAL_SETTINGS = 'SAVE_GENERAL_SETTINGS'; + +export const RESTART = 'RESTART'; +export const SHUTDOWN = 'SHUTDOWN'; + +// +// Commands + +export const FETCH_COMMANDS = 'FETCH_COMMANDS'; +export const EXECUTE_COMMAND = 'EXECUTE_COMMAND'; +export const ADD_COMMAND = 'ADD_COMMAND'; +export const UPDATE_COMMAND = 'UPDATE_COMMAND'; +export const FINISH_COMMAND = 'FINISH_COMMAND'; +export const REMOVE_COMMAND = 'REMOVE_COMMAND'; +export const REGISTER_FINISH_COMMAND_HANDLER = 'REGISTER_FINISH_COMMAND_HANDLER'; +export const UNREGISTER_FINISH_COMMAND_HANDLER = 'UNREGISTER_FINISH_COMMAND_HANDLER'; + +// +// Paths + +export const FETCH_PATHS = 'FETCH_PATHS'; +export const UPDATE_PATHS = 'UPDATE_PATHS'; +export const CLEAR_PATHS = 'CLEAR_PATHS'; + +// +// Languages + +export const FETCH_LANGUAGES = 'FETCH_LANGUAGES'; + +// +// Tags + +export const FETCH_TAGS = 'FETCH_TAGS'; +export const ADD_TAG = 'ADD_TAG'; + +// +// Captcha + +export const REFRESH_CAPTCHA = 'REFRESH_CAPTCHA'; +export const GET_CAPTCHA_COOKIE = 'GET_CAPTCHA_COOKIE'; +export const SET_CAPTCHA_VALUE = 'SET_CAPTCHA_VALUE'; +export const RESET_CAPTCHA = 'RESET_CAPTCHA'; + +// +// OAuth + +export const START_OAUTH = 'START_OAUTH'; +export const GET_OAUTH_TOKEN = 'GET_OAUTH_TOKEN'; +export const SET_OAUTH_VALUE = 'SET_OAUTH_VALUE'; +export const RESET_OAUTH = 'RESET_OAUTH'; + +// +// Interactive Import + +export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'FETCH_INTERACTIVE_IMPORT_ITEMS'; +export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'UPDATE_INTERACTIVE_IMPORT_ITEM'; +export const SET_INTERACTIVE_IMPORT_SORT = 'SET_INTERACTIVE_IMPORT_SORT'; +export const CLEAR_INTERACTIVE_IMPORT = 'CLEAR_INTERACTIVE_IMPORT'; +export const ADD_RECENT_FOLDER = 'ADD_RECENT_FOLDER'; +export const REMOVE_RECENT_FOLDER = 'REMOVE_RECENT_FOLDER'; +export const SET_INTERACTIVE_IMPORT_MODE = 'SET_INTERACTIVE_IMPORT_MODE'; + +// +// Root Folders + +export const FETCH_ROOT_FOLDERS = 'FETCH_ROOT_FOLDERS'; +export const ADD_ROOT_FOLDER = 'ADD_ROOT_FOLDER'; +export const DELETE_ROOT_FOLDER = 'DELETE_ROOT_FOLDER'; + +// +// Organize Preview + +export const FETCH_ORGANIZE_PREVIEW = 'FETCH_ORGANIZE_PREVIEW'; +export const CLEAR_ORGANIZE_PREVIEW = 'CLEAR_ORGANIZE_PREVIEW'; diff --git a/frontend/src/Store/Actions/addSeriesActionHandlers.js b/frontend/src/Store/Actions/addSeriesActionHandlers.js new file mode 100644 index 000000000..27d08a8a6 --- /dev/null +++ b/frontend/src/Store/Actions/addSeriesActionHandlers.js @@ -0,0 +1,98 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import getNewSeries from 'Utilities/Series/getNewSeries'; +import * as types from './actionTypes'; +import { set, update, updateItem } from './baseActions'; + +let currentXHR = null; +let xhrCancelled = false; +const section = 'addSeries'; + +const addSeriesActionHandlers = { + [types.LOOKUP_SERIES]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + if (currentXHR) { + xhrCancelled = true; + currentXHR.abort(); + currentXHR = null; + } + + currentXHR = new window.XMLHttpRequest(); + xhrCancelled = false; + + const promise = $.ajax({ + url: '/artist/lookup', + xhr: () => currentXHR, + data: { + term: payload.term + } + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhrCancelled ? null : xhr + })); + }); + }; + }, + + [types.ADD_SERIES]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isAdding: true })); + + const foreignArtistId = payload.foreignArtistId; + const items = getState().addSeries.items; + const newSeries = getNewSeries(_.cloneDeep(_.find(items, { foreignArtistId })), payload); + + const promise = $.ajax({ + url: '/artist', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(newSeries) + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ section: 'series', ...data }), + + set({ + section, + isAdding: false, + isAdded: true, + addError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isAdding: false, + isAdded: false, + addError: xhr + })); + }); + }; + } +}; + +export default addSeriesActionHandlers; diff --git a/frontend/src/Store/Actions/addSeriesActions.js b/frontend/src/Store/Actions/addSeriesActions.js new file mode 100644 index 000000000..aeb2c1db2 --- /dev/null +++ b/frontend/src/Store/Actions/addSeriesActions.js @@ -0,0 +1,15 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import addSeriesActionHandlers from './addSeriesActionHandlers'; + +export const lookupSeries = addSeriesActionHandlers[types.LOOKUP_SERIES]; +export const addSeries = addSeriesActionHandlers[types.ADD_SERIES]; +export const clearAddSeries = createAction(types.CLEAR_ADD_SERIES); +export const setAddSeriesDefault = createAction(types.SET_ADD_SERIES_DEFAULT); + +export const setAddSeriesValue = createAction(types.SET_ADD_SERIES_VALUE, (payload) => { + return { + section: 'addSeries', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/appActions.js b/frontend/src/Store/Actions/appActions.js new file mode 100644 index 000000000..3d0191a21 --- /dev/null +++ b/frontend/src/Store/Actions/appActions.js @@ -0,0 +1,27 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const saveDimensions = createAction(types.SAVE_DIMENSIONS); +export const setVersion = createAction(types.SET_VERSION); +export const setIsSidebarVisible = createAction(types.SET_IS_SIDEBAR_VISIBLE); + +export const setAppValue = createAction(types.SET_APP_VALUE, (payload) => { + return { + section: 'app', + ...payload + }; +}); + +export const showMessage = createAction(types.SHOW_MESSAGE, (payload) => { + return { + section: 'messages', + ...payload + }; +}); + +export const hideMessage = createAction(types.HIDE_MESSAGE, (payload) => { + return { + section: 'messages', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/artistActionHandlers.js b/frontend/src/Store/Actions/artistActionHandlers.js new file mode 100644 index 000000000..6e4f1d231 --- /dev/null +++ b/frontend/src/Store/Actions/artistActionHandlers.js @@ -0,0 +1,130 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { updateItem } from './baseActions'; + +const section = 'series'; + +const artistActionHandlers = { + [types.FETCH_ARTIST]: createFetchHandler(section, '/artist'), + + [types.SAVE_ARTIST]: createSaveProviderHandler(section, + '/artist', + (state) => state.series), + + [types.DELETE_ARTIST]: createRemoveItemHandler(section, + '/artist', + (state) => state.series), + + [types.TOGGLE_ARTIST_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + artistId: id, + monitored + } = payload; + + const series = _.find(getState().series.items, { id }); + + dispatch(updateItem({ + id, + section, + isSaving: true + })); + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...series, + monitored + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id, + section, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + isSaving: false + })); + }); + }; + }, + + [types.TOGGLE_ALBUM_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + artistId: id, + seasonNumber, + monitored + } = payload; + + const series = _.find(getState().series.items, { id }); + const seasons = _.cloneDeep(series.seasons); + const season = _.find(seasons, { seasonNumber }); + + season.isSaving = true; + + dispatch(updateItem({ + id, + section, + seasons + })); + + season.monitored = monitored; + + const promise = $.ajax({ + url: `/artist/${id}`, + method: 'PUT', + data: JSON.stringify({ + ...series, + seasons + }), + dataType: 'json' + }); + + promise.done((data) => { + const episodes = _.filter(getState().episodes.items, { artistId: id, seasonNumber }); + + dispatch(batchActions([ + updateItem({ + id, + section, + ...data + }), + + ...episodes.map((episode) => { + return updateItem({ + id: episode.id, + section: 'episodes', + monitored + }); + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section, + seasons: series.seasons + })); + }); + }; + } +}; + +export default artistActionHandlers; diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js new file mode 100644 index 000000000..0f7ba43bf --- /dev/null +++ b/frontend/src/Store/Actions/artistIndexActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const setArtistSort = createAction(types.SET_ARTIST_SORT); +export const setArtistFilter = createAction(types.SET_ARTIST_FILTER); +export const setArtistView = createAction(types.SET_ARTIST_VIEW); +export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION); +export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION); diff --git a/frontend/src/Store/Actions/baseActions.js b/frontend/src/Store/Actions/baseActions.js new file mode 100644 index 000000000..e2d7e9d7e --- /dev/null +++ b/frontend/src/Store/Actions/baseActions.js @@ -0,0 +1,13 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; + +export const set = createAction(types.SET); + +export const update = createAction(types.UPDATE); +export const updateItem = createAction(types.UPDATE_ITEM); +export const updateServerSideCollection = createAction(types.UPDATE_SERVER_SIDE_COLLECTION); + +export const setSettingValue = createAction(types.SET_SETTING_VALUE); +export const clearPendingChanges = createAction(types.CLEAR_PENDING_CHANGES); + +export const removeItem = createAction(types.REMOVE_ITEM); diff --git a/frontend/src/Store/Actions/blacklistActionHandlers.js b/frontend/src/Store/Actions/blacklistActionHandlers.js new file mode 100644 index 000000000..0a4e4a1f6 --- /dev/null +++ b/frontend/src/Store/Actions/blacklistActionHandlers.js @@ -0,0 +1,17 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import * as types from './actionTypes'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; + +const blacklistActionHandlers = { + ...createServerSideCollectionHandlers('blacklist', '/blacklist', (state) => state, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_BLACKLIST, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_BLACKLIST_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_BLACKLIST_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_BLACKLIST_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_BLACKLIST_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_BLACKLIST_SORT + }) +}; + +export default blacklistActionHandlers; diff --git a/frontend/src/Store/Actions/blacklistActions.js b/frontend/src/Store/Actions/blacklistActions.js new file mode 100644 index 000000000..d931842d1 --- /dev/null +++ b/frontend/src/Store/Actions/blacklistActions.js @@ -0,0 +1,12 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import blacklistActionHandlers from './blacklistActionHandlers'; + +export const fetchBlacklist = blacklistActionHandlers[types.FETCH_BLACKLIST]; +export const gotoBlacklistFirstPage = blacklistActionHandlers[types.GOTO_FIRST_BLACKLIST_PAGE]; +export const gotoBlacklistPreviousPage = blacklistActionHandlers[types.GOTO_PREVIOUS_BLACKLIST_PAGE]; +export const gotoBlacklistNextPage = blacklistActionHandlers[types.GOTO_NEXT_BLACKLIST_PAGE]; +export const gotoBlacklistLastPage = blacklistActionHandlers[types.GOTO_LAST_BLACKLIST_PAGE]; +export const gotoBlacklistPage = blacklistActionHandlers[types.GOTO_BLACKLIST_PAGE]; +export const setBlacklistSort = blacklistActionHandlers[types.SET_BLACKLIST_SORT]; +export const setBlacklistTableOption = createAction(types.SET_BLACKLIST_TABLE_OPTION); diff --git a/frontend/src/Store/Actions/calendarActionHandlers.js b/frontend/src/Store/Actions/calendarActionHandlers.js new file mode 100644 index 000000000..2054a64a0 --- /dev/null +++ b/frontend/src/Store/Actions/calendarActionHandlers.js @@ -0,0 +1,264 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import moment from 'moment'; +import { batchActions } from 'redux-batched-actions'; +import * as calendarViews from 'Calendar/calendarViews'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; +import { fetchCalendar } from './calendarActions'; + +const viewRanges = { + [calendarViews.DAY]: 'day', + [calendarViews.WEEK]: 'week', + [calendarViews.MONTH]: 'month', + [calendarViews.FORECAST]: 'day' +}; + +function getDays(start, end) { + const startTime = moment(start); + const endTime = moment(end); + const difference = endTime.diff(startTime, 'days'); + + // Difference is one less than the number of days we need to account for. + return _.times(difference + 1, (i) => { + return startTime.clone().add(i, 'days').toISOString(); + }); +} + +function getDates(time, view, firstDayOfWeek, dayCount) { + const weekName = firstDayOfWeek === 0 ? 'week' : 'isoWeek'; + + let start = time.clone().startOf('day'); + let end = time.clone().endOf('day'); + + if (view === calendarViews.WEEK) { + start = time.clone().startOf(weekName); + end = time.clone().endOf(weekName); + } + + if (view === calendarViews.FORECAST) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(dayCount - 2, 'days').endOf('day'); + } + + if (view === calendarViews.MONTH) { + start = time.clone().startOf('month').startOf(weekName); + end = time.clone().endOf('month').endOf(weekName); + } + + if (view === calendarViews.AGENDA) { + start = time.clone().subtract(1, 'day').startOf('day'); + end = time.clone().add(1, 'month').endOf('day'); + } + + return { + start: start.toISOString(), + end: end.toISOString(), + time: time.toISOString(), + dates: getDays(start, end) + }; +} + +function getPopulatableRange(startDate, endDate, view) { + switch (view) { + case calendarViews.DAY: + return { + start: moment(startDate).subtract(1, 'day').toISOString(), + end: moment(endDate).add(1, 'day').toISOString() + }; + case calendarViews.WEEK: + case calendarViews.FORECAST: + return { + start: moment(startDate).subtract(1, 'week').toISOString(), + end: moment(endDate).add(1, 'week').toISOString() + }; + default: + return { + start: startDate, + end: endDate + }; + } +} + +function isRangePopulated(start, end, state) { + const { + start: currentStart, + end: currentEnd, + view: currentView + } = state; + + if (!currentStart || !currentEnd) { + return false; + } + + const { + start: currentPopulatedStart, + end: currentPopulatedEnd + } = getPopulatableRange(currentStart, currentEnd, currentView); + + if ( + moment(start).isAfter(currentPopulatedStart) && + moment(start).isBefore(currentPopulatedEnd) + ) { + return true; + } + + return false; +} + +const section = 'calendar'; + +const calendarActionHandlers = { + [types.FETCH_CALENDAR]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const unmonitored = state.calendar.unmonitored; + + const { + time, + view + } = payload; + + const dayCount = state.calendar.dayCount; + const dates = getDates(moment(time), view, state.settings.ui.item.firstDayOfWeek, dayCount); + const { start, end } = getPopulatableRange(dates.start, dates.end, view); + const isPrePopulated = isRangePopulated(start, end, state.calendar); + + const basesAttrs = { + section, + isFetching: true + }; + + const attrs = isPrePopulated ? + { + view, + ...basesAttrs, + ...dates + } : + basesAttrs; + + dispatch(set(attrs)); + + const promise = $.ajax({ + url: '/calendar', + data: { + unmonitored, + start, + end + } + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + view, + ...dates, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.SET_CALENDAR_DAYS_COUNT]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + dayCount: payload.dayCount + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ + time, + view + })); + }; + }, + + [types.SET_CALENDAR_INCLUDE_UNMONITORED]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + unmonitored: payload.unmonitored + })); + + const state = getState(); + const { time, view } = state.calendar; + + dispatch(fetchCalendar({ + time, + view + })); + }; + }, + + [types.SET_CALENDAR_VIEW]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const view = payload.view; + const time = view === calendarViews.FORECAST ? moment() : state.calendar.time; + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_TODAY]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + const view = state.calendar.view; + const time = moment(); + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_PREVIOUS_RANGE]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).subtract(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + }; + }, + + [types.GOTO_CALENDAR_NEXT_RANGE]: function(payload) { + return function(dispatch, getState) { + const state = getState(); + + const { + view, + dayCount + } = state.calendar; + + const amount = view === calendarViews.FORECAST ? dayCount : 1; + const time = moment(state.calendar.time).add(amount, viewRanges[view]); + + dispatch(fetchCalendar({ time, view })); + }; + } +}; + +export default calendarActionHandlers; diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js new file mode 100644 index 000000000..452769343 --- /dev/null +++ b/frontend/src/Store/Actions/calendarActions.js @@ -0,0 +1,12 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import calendarActionHandlers from './calendarActionHandlers'; + +export const fetchCalendar = calendarActionHandlers[types.FETCH_CALENDAR]; +export const setCalendarDaysCount = calendarActionHandlers[types.SET_CALENDAR_DAYS_COUNT]; +export const setCalendarIncludeUnmonitored = calendarActionHandlers[types.SET_CALENDAR_INCLUDE_UNMONITORED]; +export const setCalendarView = calendarActionHandlers[types.SET_CALENDAR_VIEW]; +export const gotoCalendarToday = calendarActionHandlers[types.GOTO_CALENDAR_TODAY]; +export const gotoCalendarPreviousRange = calendarActionHandlers[types.GOTO_CALENDAR_PREVIOUS_RANGE]; +export const gotoCalendarNextRange = calendarActionHandlers[types.GOTO_CALENDAR_NEXT_RANGE]; +export const clearCalendar = createAction(types.CLEAR_CALENDAR); diff --git a/frontend/src/Store/Actions/captchaActionHandlers.js b/frontend/src/Store/Actions/captchaActionHandlers.js new file mode 100644 index 000000000..6e00a840e --- /dev/null +++ b/frontend/src/Store/Actions/captchaActionHandlers.js @@ -0,0 +1,67 @@ +import requestAction from 'Utilities/requestAction'; +import * as types from './actionTypes'; +import { setCaptchaValue } from './captchaActions'; + +const captchaActionHandlers = { + [types.REFRESH_CAPTCHA]: function(payload) { + return (dispatch, getState) => { + const actionPayload = { + action: 'checkCaptcha', + ...payload + }; + + dispatch(setCaptchaValue({ + refreshing: true + })); + + const promise = requestAction(actionPayload); + + promise.done((data) => { + if (!data.captchaRequest) { + dispatch(setCaptchaValue({ + refreshing: false + })); + } + + dispatch(setCaptchaValue({ + refreshing: false, + ...data.captchaRequest + })); + }); + + promise.fail(() => { + dispatch(setCaptchaValue({ + refreshing: false + })); + }); + }; + }, + + [types.GET_CAPTCHA_COOKIE]: function(payload) { + return (dispatch, getState) => { + const state = getState().captcha; + + const queryParams = { + responseUrl: state.responseUrl, + ray: state.ray, + captchaResponse: payload.captchaResponse + }; + + const actionPayload = { + action: 'getCaptchaCookie', + queryParams, + ...payload + }; + + const promise = requestAction(actionPayload); + + promise.done((data) => { + dispatch(setCaptchaValue({ + token: data.captchaToken + })); + }); + }; + } +}; + +export default captchaActionHandlers; diff --git a/frontend/src/Store/Actions/captchaActions.js b/frontend/src/Store/Actions/captchaActions.js new file mode 100644 index 000000000..e87a2a088 --- /dev/null +++ b/frontend/src/Store/Actions/captchaActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import captchaActionHandlers from './captchaActionHandlers'; + +export const refreshCaptcha = captchaActionHandlers[types.REFRESH_CAPTCHA]; +export const getCaptchaCookie = captchaActionHandlers[types.GET_CAPTCHA_COOKIE]; +export const setCaptchaValue = createAction(types.SET_CAPTCHA_VALUE); +export const resetCaptcha = createAction(types.RESET_CAPTCHA); diff --git a/frontend/src/Store/Actions/commandActionHandlers.js b/frontend/src/Store/Actions/commandActionHandlers.js new file mode 100644 index 000000000..e0f8baea0 --- /dev/null +++ b/frontend/src/Store/Actions/commandActionHandlers.js @@ -0,0 +1,141 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { isSameCommand } from 'Utilities/Command'; +import { messageTypes } from 'Helpers/Props'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import { showMessage, hideMessage } from './appActions'; +import { updateItem } from './baseActions'; +import { addCommand, removeCommand } from './commandActions'; + +let lastCommand = null; +let lastCommandTimeout = null; +const removeCommandTimeoutIds = {}; + +function showCommandMessage(payload, dispatch) { + const { + id, + name, + manual, + message, + body = {}, + state + } = payload; + + const { + sendUpdatesToClient, + suppressMessages + } = body; + + if (!message || !body || !sendUpdatesToClient || suppressMessages) { + return; + } + + let type = messageTypes.INFO; + let hideAfter = 0; + + if (state === 'completed') { + type = messageTypes.SUCCESS; + hideAfter = 4; + } else if (state === 'failed') { + type = messageTypes.ERROR; + hideAfter = manual ? 10 : 4; + } + + dispatch(showMessage({ + id, + name, + message, + type, + hideAfter + })); +} + +function scheduleRemoveCommand(command, dispatch) { + const { + id, + state + } = command; + + if (state === 'queued') { + return; + } + + const timeoutId = removeCommandTimeoutIds[id]; + + if (timeoutId) { + clearTimeout(timeoutId); + } + + removeCommandTimeoutIds[id] = setTimeout(() => { + dispatch(batchActions([ + removeCommand({ section: 'commands', id }), + hideMessage({ id }) + ])); + + delete removeCommandTimeoutIds[id]; + }, 30000); +} + +const commandActionHandlers = { + [types.FETCH_COMMANDS]: createFetchHandler('commands', '/command'), + + [types.EXECUTE_COMMAND](payload) { + return (dispatch, getState) => { + // TODO: show a message for the user + if (lastCommand && isSameCommand(lastCommand, payload)) { + console.warn('Please wait at least 5 seconds before running this command again'); + } + + lastCommand = payload; + + // clear last command after 5 seconds. + if (lastCommandTimeout) { + clearTimeout(lastCommandTimeout); + } + + lastCommandTimeout = setTimeout(() => { + lastCommand = null; + }, 5000); + + const promise = $.ajax({ + url: '/command', + method: 'POST', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(addCommand(data)); + }); + }; + }, + + [types.UPDATE_COMMAND](payload) { + return (dispatch, getState) => { + dispatch(updateItem({ section: 'commands', ...payload })); + + showCommandMessage(payload, dispatch); + scheduleRemoveCommand(payload, dispatch); + }; + }, + + [types.FINISH_COMMAND](payload) { + return (dispatch, getState) => { + const state = getState(); + const handlers = state.commands.handlers; + Object.keys(handlers).forEach((key) => { + const handler = handlers[key]; + + if (handler.name === payload.name) { + dispatch(handler.handler(payload)); + } + }); + + dispatch(removeCommand({ section: 'commands', ...payload })); + showCommandMessage(payload, dispatch); + }; + } + +}; + +export default commandActionHandlers; diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js new file mode 100644 index 000000000..84b6d4fdb --- /dev/null +++ b/frontend/src/Store/Actions/commandActions.js @@ -0,0 +1,14 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import commandActionHandlers from './commandActionHandlers'; + +export const fetchCommands = commandActionHandlers[types.FETCH_COMMANDS]; +export const executeCommand = commandActionHandlers[types.EXECUTE_COMMAND]; +export const updateCommand = commandActionHandlers[types.UPDATE_COMMAND]; +export const finishCommand = commandActionHandlers[types.FINISH_COMMAND]; + +export const addCommand = createAction(types.ADD_COMMAND); +export const removeCommand = createAction(types.REMOVE_COMMAND); + +export const registerFinishCommandHandler = createAction(types.REGISTER_FINISH_COMMAND_HANDLER); +export const unregisterFinishCommandHandler = createAction(types.UNREGISTER_FINISH_COMMAND_HANDLER); diff --git a/frontend/src/Store/Actions/episodeActionHandlers.js b/frontend/src/Store/Actions/episodeActionHandlers.js new file mode 100644 index 000000000..2d2c3ed65 --- /dev/null +++ b/frontend/src/Store/Actions/episodeActionHandlers.js @@ -0,0 +1,111 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import episodeEntities from 'Episode/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; +import { updateItem } from './baseActions'; + +const section = 'episodes'; + +const episodeActionHandlers = { + [types.FETCH_EPISODES]: createFetchHandler(section, '/episode'), + + [types.TOGGLE_EPISODE_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + episodeId: id, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(updateItem({ + id, + section: episodeSection, + isSaving: true + })); + + const promise = $.ajax({ + url: `/episode/${id}`, + method: 'PUT', + data: JSON.stringify({ monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(updateItem({ + id, + section: episodeSection, + isSaving: false, + monitored + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + id, + section: episodeSection, + isSaving: false + })); + }); + }; + }, + + [types.TOGGLE_EPISODES_MONITORED]: function(payload) { + return function(dispatch, getState) { + const { + episodeIds, + episodeEntity = episodeEntities.EPISODES, + monitored + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + dispatch(batchActions( + episodeIds.map((episodeId) => { + return updateItem({ + id: episodeId, + section: episodeSection, + isSaving: true + }); + }) + )); + + const promise = $.ajax({ + url: '/episode/monitor', + method: 'PUT', + data: JSON.stringify({ episodeIds, monitored }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions( + episodeIds.map((episodeId) => { + return updateItem({ + id: episodeId, + section: episodeSection, + isSaving: false, + monitored + }); + }) + )); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + episodeIds.map((episodeId) => { + return updateItem({ + id: episodeId, + section: episodeSection, + isSaving: false + }); + }) + )); + }); + }; + } +}; + +export default episodeActionHandlers; diff --git a/frontend/src/Store/Actions/episodeActions.js b/frontend/src/Store/Actions/episodeActions.js new file mode 100644 index 000000000..b0abe85eb --- /dev/null +++ b/frontend/src/Store/Actions/episodeActions.js @@ -0,0 +1,10 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import episodeActionHandlers from './episodeActionHandlers'; + +export const fetchEpisodes = episodeActionHandlers[types.FETCH_EPISODES]; +export const setEpisodesSort = createAction(types.SET_EPISODES_SORT); +export const setEpisodesTableOption = createAction(types.SET_EPISODES_TABLE_OPTION); +export const clearEpisodes = createAction(types.CLEAR_EPISODES); +export const toggleEpisodeMonitored = episodeActionHandlers[types.TOGGLE_EPISODE_MONITORED]; +export const toggleEpisodesMonitored = episodeActionHandlers[types.TOGGLE_EPISODES_MONITORED]; diff --git a/frontend/src/Store/Actions/episodeFileActionHandlers.js b/frontend/src/Store/Actions/episodeFileActionHandlers.js new file mode 100644 index 000000000..a0a661f03 --- /dev/null +++ b/frontend/src/Store/Actions/episodeFileActionHandlers.js @@ -0,0 +1,164 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import episodeEntities from 'Episode/episodeEntities'; +import createFetchHandler from './Creators/createFetchHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import * as types from './actionTypes'; +import { set, removeItem, updateItem } from './baseActions'; + +const section = 'episodeFiles'; +const deleteEpisodeFile = createRemoveItemHandler(section, '/episodeFile'); + +const episodeFileActionHandlers = { + [types.FETCH_EPISODE_FILES]: createFetchHandler(section, '/episodeFile'), + + [types.DELETE_EPISODE_FILE]: function(payload) { + return function(dispatch, getState) { + const { + id: episodeFileId, + episodeEntity = episodeEntities.EPISODES + } = payload; + + const episodeSection = _.last(episodeEntity.split('.')); + + const deletePromise = deleteEpisodeFile(payload)(dispatch, getState); + + deletePromise.done(() => { + const episodes = getState().episodes.items; + const episodesWithRemovedFiles = _.filter(episodes, { episodeFileId }); + + dispatch(batchActions([ + ...episodesWithRemovedFiles.map((episode) => { + return updateItem({ + section: episodeSection, + ...episode, + episodeFileId: 0, + hasFile: false + }); + }) + ])); + }); + }; + }, + + [types.DELETE_EPISODE_FILES]: function(payload) { + return function(dispatch, getState) { + const { + episodeFileIds + } = payload; + + dispatch(set({ section, isDeleting: true })); + + const promise = $.ajax({ + url: '/episodeFile/bulk', + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ episodeFileIds }) + }); + + promise.done(() => { + const episodes = getState().episodes.items; + const episodesWithRemovedFiles = episodeFileIds.reduce((acc, episodeFileId) => { + acc.push(..._.filter(episodes, { episodeFileId })); + + return acc; + }, []); + + dispatch(batchActions([ + ...episodeFileIds.map((id) => { + return removeItem({ section, id }); + }), + + ...episodesWithRemovedFiles.map((episode) => { + return updateItem({ + section: 'episodes', + ...episode, + episodeFileId: 0, + hasFile: false + }); + }), + + set({ + section, + isDeleting: false, + deleteError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + }; + }, + + [types.UPDATE_EPISODE_FILES]: function(payload) { + return function(dispatch, getState) { + const { + episodeFileIds, + language, + quality + } = payload; + + dispatch(set({ section, isSaving: true })); + + const data = { + episodeFileIds + }; + + if (language) { + data.language = language; + } + + if (quality) { + data.quality = quality; + } + + const promise = $.ajax({ + url: '/episodeFile/editor', + method: 'PUT', + dataType: 'json', + data: JSON.stringify(data) + }); + + promise.done(() => { + dispatch(batchActions([ + ...episodeFileIds.map((id) => { + const props = {}; + + if (language) { + props.language = language; + } + + if (quality) { + props.quality = quality; + } + + return updateItem({ section, id, ...props }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default episodeFileActionHandlers; diff --git a/frontend/src/Store/Actions/episodeFileActions.js b/frontend/src/Store/Actions/episodeFileActions.js new file mode 100644 index 000000000..f8087afa8 --- /dev/null +++ b/frontend/src/Store/Actions/episodeFileActions.js @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import episodeFileActionHandlers from './episodeFileActionHandlers'; + +export const fetchEpisodeFiles = episodeFileActionHandlers[types.FETCH_EPISODE_FILES]; +export const deleteEpisodeFile = episodeFileActionHandlers[types.DELETE_EPISODE_FILE]; +export const deleteEpisodeFiles = episodeFileActionHandlers[types.DELETE_EPISODE_FILES]; +export const updateEpisodeFiles = episodeFileActionHandlers[types.UPDATE_EPISODE_FILES]; +export const clearEpisodeFiles = createAction(types.CLEAR_EPISODE_FILES); diff --git a/frontend/src/Store/Actions/episodeHistoryActionHandlers.js b/frontend/src/Store/Actions/episodeHistoryActionHandlers.js new file mode 100644 index 000000000..530eade2b --- /dev/null +++ b/frontend/src/Store/Actions/episodeHistoryActionHandlers.js @@ -0,0 +1,75 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import { sortDirections } from 'Helpers/Props'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; +import { fetchEpisodeHistory } from './episodeHistoryActions'; + +const episodeHistoryActionHandlers = { + [types.FETCH_EPISODE_HISTORY]: function(payload) { + const section = 'episodeHistory'; + + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const queryParams = { + pageSize: 1000, + page: 1, + filterKey: 'episodeId', + filterValue: payload.episodeId, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING + }; + + const promise = $.ajax({ + url: '/history', + data: queryParams + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data: data.records }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.EPISODE_HISTORY_MARK_AS_FAILED]: function(payload) { + return function(dispatch, getState) { + const { + historyId, + episodeId + } = payload; + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id: historyId + } + }); + + promise.done(() => { + dispatch(fetchEpisodeHistory({ episodeId })); + }); + }; + } +}; + +export default episodeHistoryActionHandlers; diff --git a/frontend/src/Store/Actions/episodeHistoryActions.js b/frontend/src/Store/Actions/episodeHistoryActions.js new file mode 100644 index 000000000..6c0c90770 --- /dev/null +++ b/frontend/src/Store/Actions/episodeHistoryActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import episodeHistoryActionHandlers from './episodeHistoryActionHandlers'; + +export const fetchEpisodeHistory = episodeHistoryActionHandlers[types.FETCH_EPISODE_HISTORY]; +export const clearEpisodeHistory = createAction(types.CLEAR_EPISODE_HISTORY); +export const episodeHistoryMarkAsFailed = episodeHistoryActionHandlers[types.EPISODE_HISTORY_MARK_AS_FAILED]; diff --git a/frontend/src/Store/Actions/historyActionHandlers.js b/frontend/src/Store/Actions/historyActionHandlers.js new file mode 100644 index 000000000..44cff0fc3 --- /dev/null +++ b/frontend/src/Store/Actions/historyActionHandlers.js @@ -0,0 +1,60 @@ +import $ from 'jquery'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; +import { updateItem } from './baseActions'; + +const section = 'history'; + +const historyActionHandlers = { + ...createServerSideCollectionHandlers('history', '/history', (state) => state.history, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_HISTORY, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_HISTORY_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_HISTORY_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_HISTORY_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_HISTORY_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_HISTORY_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_HISTORY_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_HISTORY_FILTER + }), + + [types.MARK_AS_FAILED]: function(payload) { + return function(dispatch, getState) { + const id = payload.id; + + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: true + })); + + const promise = $.ajax({ + url: '/history/failed', + method: 'POST', + data: { + id + } + }); + + promise.done(() => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: null + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id, + isMarkingAsFailed: false, + markAsFailedError: xhr + })); + }); + }; + } +}; + +export default historyActionHandlers; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js new file mode 100644 index 000000000..d4fe71be9 --- /dev/null +++ b/frontend/src/Store/Actions/historyActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import historyActionHandlers from './historyActionHandlers'; + +export const fetchHistory = historyActionHandlers[types.FETCH_HISTORY]; +export const gotoHistoryFirstPage = historyActionHandlers[types.GOTO_FIRST_HISTORY_PAGE]; +export const gotoHistoryPreviousPage = historyActionHandlers[types.GOTO_PREVIOUS_HISTORY_PAGE]; +export const gotoHistoryNextPage = historyActionHandlers[types.GOTO_NEXT_HISTORY_PAGE]; +export const gotoHistoryLastPage = historyActionHandlers[types.GOTO_LAST_HISTORY_PAGE]; +export const gotoHistoryPage = historyActionHandlers[types.GOTO_HISTORY_PAGE]; +export const setHistorySort = historyActionHandlers[types.SET_HISTORY_SORT]; +export const setHistoryFilter = historyActionHandlers[types.SET_HISTORY_FILTER]; +export const setHistoryTableOption = createAction(types.SET_HISTORY_TABLE_OPTION); +export const clearHistory = createAction(types.CLEAR_HISTORY); + +export const markAsFailed = historyActionHandlers[types.MARK_AS_FAILED]; diff --git a/frontend/src/Store/Actions/importSeriesActionHandlers.js b/frontend/src/Store/Actions/importSeriesActionHandlers.js new file mode 100644 index 000000000..bbf3523dc --- /dev/null +++ b/frontend/src/Store/Actions/importSeriesActionHandlers.js @@ -0,0 +1,172 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import getNewSeries from 'Utilities/Series/getNewSeries'; +import * as types from './actionTypes'; +import { set, updateItem, removeItem } from './baseActions'; +import { startLookupSeries } from './importSeriesActions'; +import { fetchRootFolders } from './rootFolderActions'; + +const section = 'importSeries'; +let concurrentLookups = 0; + +const importSeriesActionHandlers = { + [types.QUEUE_LOOKUP_SERIES]: function(payload) { + return function(dispatch, getState) { + const { + name, + path, + term + } = payload; + + const state = getState().importSeries; + const item = _.find(state.items, { id: name }) || { + id: name, + term, + path, + isFetching: false, + isPopulated: false, + error: null + }; + + dispatch(updateItem({ + section, + ...item, + term, + queued: true, + items: [] + })); + + if (term && term.length > 2) { + dispatch(startLookupSeries()); + } + }; + }, + + [types.START_LOOKUP_SERIES]: function(payload) { + return function(dispatch, getState) { + if (concurrentLookups >= 1) { + return; + } + + const state = getState().importSeries; + const queued = _.find(state.items, { queued: true }); + + if (!queued) { + return; + } + + concurrentLookups++; + + dispatch(updateItem({ + section, + id: queued.id, + isFetching: true + })); + + const promise = $.ajax({ + url: '/series/lookup', + data: { + term: queued.term + } + }); + + promise.done((data) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: true, + error: null, + items: data, + queued: false, + selectedSeries: queued.selectedSeries || data[0] + })); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id: queued.id, + isFetching: false, + isPopulated: false, + error: xhr, + queued: false + })); + }); + + promise.always(() => { + concurrentLookups--; + dispatch(startLookupSeries()); + }); + }; + }, + + [types.IMPORT_SERIES]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ section, isImporting: true })); + + const ids = payload.ids; + const items = getState().importSeries.items; + const addedIds = []; + + const allNewSeries = ids.reduce((acc, id) => { + const item = _.find(items, { id }); + const selectedSeries = item.selectedSeries; + + // Make sure we have a selected series and + // the same series hasn't been added yet. + if (selectedSeries && !_.some(acc, { tvdbId: selectedSeries.tvdbId })) { + const newSeries = getNewSeries(_.cloneDeep(selectedSeries), item); + newSeries.path = item.path; + + addedIds.push(id); + acc.push(newSeries); + } + + return acc; + }, []); + + const promise = $.ajax({ + url: '/series/import', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(allNewSeries) + }); + + promise.done((data) => { + dispatch(batchActions([ + set({ + section, + isImporting: false, + isImported: true + }), + + ...data.map((series) => updateItem({ section: 'series', ...series })), + + ...addedIds.map((id) => removeItem({ section, id })) + ])); + + dispatch(fetchRootFolders()); + }); + + promise.fail((xhr) => { + dispatch(batchActions( + set({ + section, + isImporting: false, + isImported: true + }), + + addedIds.map((id) => updateItem({ + section, + id, + importError: xhr + })) + )); + }); + }; + } +}; + +export default importSeriesActionHandlers; diff --git a/frontend/src/Store/Actions/importSeriesActions.js b/frontend/src/Store/Actions/importSeriesActions.js new file mode 100644 index 000000000..6b119368b --- /dev/null +++ b/frontend/src/Store/Actions/importSeriesActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import importSeriesActionHandlers from './importSeriesActionHandlers'; + +export const queueLookupSeries = importSeriesActionHandlers[types.QUEUE_LOOKUP_SERIES]; +export const startLookupSeries = importSeriesActionHandlers[types.START_LOOKUP_SERIES]; +export const importSeries = importSeriesActionHandlers[types.IMPORT_SERIES]; +export const clearImportSeries = createAction(types.CLEAR_IMPORT_SERIES); + +export const setImportSeriesValue = createAction(types.SET_IMPORT_SERIES_VALUE, (payload) => { + return { + + section: 'importSeries', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/interactiveImportActionHandlers.js b/frontend/src/Store/Actions/interactiveImportActionHandlers.js new file mode 100644 index 000000000..a46ce5702 --- /dev/null +++ b/frontend/src/Store/Actions/interactiveImportActionHandlers.js @@ -0,0 +1,48 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import { set, update } from './baseActions'; + +const section = 'interactiveImport'; + +const interactiveImportActionHandlers = { + [types.FETCH_INTERACTIVE_IMPORT_ITEMS]: function(payload) { + return function(dispatch, getState) { + if (!payload.downloadId && !payload.folder) { + dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } })); + return; + } + + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/manualimport', + data: payload + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + } +}; + +export default interactiveImportActionHandlers; diff --git a/frontend/src/Store/Actions/interactiveImportActions.js b/frontend/src/Store/Actions/interactiveImportActions.js new file mode 100644 index 000000000..880cd0ade --- /dev/null +++ b/frontend/src/Store/Actions/interactiveImportActions.js @@ -0,0 +1,11 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import interactiveImportActionHandlers from './interactiveImportActionHandlers'; + +export const fetchInteractiveImportItems = interactiveImportActionHandlers[types.FETCH_INTERACTIVE_IMPORT_ITEMS]; +export const setInteractiveImportSort = createAction(types.SET_INTERACTIVE_IMPORT_SORT); +export const updateInteractiveImportItem = createAction(types.UPDATE_INTERACTIVE_IMPORT_ITEM); +export const clearInteractiveImport = createAction(types.CLEAR_INTERACTIVE_IMPORT); +export const addRecentFolder = createAction(types.ADD_RECENT_FOLDER); +export const removeRecentFolder = createAction(types.REMOVE_RECENT_FOLDER); +export const setInteractiveImportMode = createAction(types.SET_INTERACTIVE_IMPORT_MODE); diff --git a/frontend/src/Store/Actions/oAuthActionHandlers.js b/frontend/src/Store/Actions/oAuthActionHandlers.js new file mode 100644 index 000000000..f65584015 --- /dev/null +++ b/frontend/src/Store/Actions/oAuthActionHandlers.js @@ -0,0 +1,80 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import requestAction from 'Utilities/requestAction'; +import * as types from './actionTypes'; +import { setOAuthValue } from './oAuthActions'; + +function showOAuthWindow(url) { + const deferred = $.Deferred(); + const selfWindow = window; + + window.open(url); + + selfWindow.onCompleteOauth = function(query, callback) { + delete selfWindow.onCompleteOauth; + + const queryParams = {}; + const splitQuery = query.substring(1).split('&'); + + _.each(splitQuery, (param) => { + const paramSplit = param.split('='); + + queryParams[paramSplit[0]] = paramSplit[1]; + }); + + callback(); + deferred.resolve(queryParams); + }; + + return deferred.promise(); +} + +const oAuthActionHandlers = { + + [types.START_OAUTH]: function(payload) { + return (dispatch, getState) => { + const actionPayload = { + action: 'startOAuth', + queryParams: { callbackUrl: `${window.location.origin}/oauth.html` }, + ...payload + }; + + dispatch(setOAuthValue({ + authorizing: true + })); + + const promise = requestAction(actionPayload) + .then((response) => { + return showOAuthWindow(response.oauthUrl); + }) + .then((queryParams) => { + return requestAction({ + action: 'getOAuthToken', + queryParams, + ...payload + }); + }) + .then((response) => { + const { + accessToken, + accessTokenSecret + } = response; + + dispatch(setOAuthValue({ + authorizing: false, + accessToken, + accessTokenSecret + })); + }); + + promise.fail(() => { + dispatch(setOAuthValue({ + authorizing: false + })); + }); + }; + } + +}; + +export default oAuthActionHandlers; diff --git a/frontend/src/Store/Actions/oAuthActions.js b/frontend/src/Store/Actions/oAuthActions.js new file mode 100644 index 000000000..93de6f11f --- /dev/null +++ b/frontend/src/Store/Actions/oAuthActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import oAuthActionHandlers from './oAuthActionHandlers'; + +export const startOAuth = oAuthActionHandlers[types.START_OAUTH]; +export const setOAuthValue = createAction(types.SET_OAUTH_VALUE); +export const resetOAuth = createAction(types.RESET_OAUTH); diff --git a/frontend/src/Store/Actions/organizePreviewActionHandlers.js b/frontend/src/Store/Actions/organizePreviewActionHandlers.js new file mode 100644 index 000000000..d0901b5ea --- /dev/null +++ b/frontend/src/Store/Actions/organizePreviewActionHandlers.js @@ -0,0 +1,8 @@ +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; + +const organizePreviewActionHandlers = { + [types.FETCH_ORGANIZE_PREVIEW]: createFetchHandler('organizePreview', '/rename') +}; + +export default organizePreviewActionHandlers; diff --git a/frontend/src/Store/Actions/organizePreviewActions.js b/frontend/src/Store/Actions/organizePreviewActions.js new file mode 100644 index 000000000..602028ff4 --- /dev/null +++ b/frontend/src/Store/Actions/organizePreviewActions.js @@ -0,0 +1,6 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import organizePreviewActionHandlers from './organizePreviewActionHandlers'; + +export const fetchOrganizePreview = organizePreviewActionHandlers[types.FETCH_ORGANIZE_PREVIEW]; +export const clearOrganizePreview = createAction(types.CLEAR_ORGANIZE_PREVIEW); diff --git a/frontend/src/Store/Actions/pathActionHandlers.js b/frontend/src/Store/Actions/pathActionHandlers.js new file mode 100644 index 000000000..11ebfaf21 --- /dev/null +++ b/frontend/src/Store/Actions/pathActionHandlers.js @@ -0,0 +1,43 @@ +import $ from 'jquery'; +import * as types from './actionTypes'; +import { set } from './baseActions'; +import { updatePaths } from './pathActions'; + +const section = 'paths'; + +const pathActionHandlers = { + [types.FETCH_PATHS](payload) { + return (dispatch, getState) => { + dispatch(set({ section, isFetching: true })); + + const promise = $.ajax({ + url: '/filesystem', + data: { + path: payload.path + } + }); + + promise.done((data) => { + dispatch(updatePaths({ path: payload.path, ...data })); + + dispatch(set({ + section, + isFetching: false, + isPopulated: true, + error: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + } +}; + +export default pathActionHandlers; diff --git a/frontend/src/Store/Actions/pathActions.js b/frontend/src/Store/Actions/pathActions.js new file mode 100644 index 000000000..fd4e76f02 --- /dev/null +++ b/frontend/src/Store/Actions/pathActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import pathActionHandlers from './pathActionHandlers'; + +export const fetchPaths = pathActionHandlers[types.FETCH_PATHS]; +export const updatePaths = createAction(types.UPDATE_PATHS); +export const clearPaths = createAction(types.CLEAR_PATHS); diff --git a/frontend/src/Store/Actions/queueActionHandlers.js b/frontend/src/Store/Actions/queueActionHandlers.js new file mode 100644 index 000000000..b5b44e8ec --- /dev/null +++ b/frontend/src/Store/Actions/queueActionHandlers.js @@ -0,0 +1,230 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createFetchHandler from './Creators/createFetchHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; +import { set, updateItem } from './baseActions'; +import { fetchQueue } from './queueActions'; + +const fetchQueueDetailsHandler = createFetchHandler('details', '/queue/details'); + +const queueActionHandlers = { + [types.FETCH_QUEUE_STATUS]: createFetchHandler('queueStatus', '/queue/status'), + + [types.FETCH_QUEUE_DETAILS]: function(payload) { + return function(dispatch, getState) { + let params = payload; + + // If the payload params are empty try to get params from state. + + if (params && !_.isEmpty(params)) { + dispatch(set({ section: 'details', params })); + } else { + params = getState().queue.details.params; + } + + // Ensure there are params before trying to fetch the queue + // so we don't make a bad request to the server. + + if (params && !_.isEmpty(params)) { + const fetchFunction = fetchQueueDetailsHandler(params); + fetchFunction(dispatch, getState); + } + }; + }, + + ...createServerSideCollectionHandlers('paged', '/queue', (state) => state.queue, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_QUEUE, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_QUEUE_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_QUEUE_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_QUEUE_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_QUEUE_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_QUEUE_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_QUEUE_SORT + }), + + [types.GRAB_QUEUE_ITEM]: function(payload) { + const section = 'paged'; + + const { + id + } = payload; + + return function(dispatch, getState) { + dispatch(updateItem({ section, id, isGrabbing: true })); + + const promise = $.ajax({ + url: `/queue/grab/${id}`, + method: 'POST' + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + set({ + section, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ + section, + id, + isGrabbing: false, + grabError: xhr + })); + }); + }; + }, + + [types.GRAB_QUEUE_ITEMS]: function(payload) { + const section = 'paged'; + + const { + ids + } = payload; + + return function(dispatch, getState) { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: true + }); + }), + + set({ + section, + isGrabbing: true + }) + ])); + + const promise = $.ajax({ + url: '/queue/grab/bulk', + method: 'POST', + dataType: 'json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(batchActions([ + dispatch(fetchQueue()), + + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ + section, + isGrabbing: false, + grabError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isGrabbing: false, + grabError: null + }); + }), + + set({ section, isGrabbing: false }) + ])); + }); + }; + }, + + [types.REMOVE_QUEUE_ITEM]: function(payload) { + const section = 'paged'; + + const { + id, + blacklist + } = payload; + + return function(dispatch, getState) { + dispatch(updateItem({ section, id, isRemoving: true })); + + const promise = $.ajax({ + url: `/queue/${id}?blacklist=${blacklist}`, + method: 'DELETE' + }); + + promise.done((data) => { + dispatch(fetchQueue()); + }); + + promise.fail((xhr) => { + dispatch(updateItem({ section, id, isRemoving: false })); + }); + }; + }, + + [types.REMOVE_QUEUE_ITEMS]: function(payload) { + const section = 'paged'; + + const { + ids, + blacklist + } = payload; + + return function(dispatch, getState) { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isRemoving: true + }); + }), + + set({ section, isRemoving: true }) + ])); + + const promise = $.ajax({ + url: `/queue/bulk?blacklist=${blacklist}`, + method: 'DELETE', + dataType: 'json', + data: JSON.stringify({ ids }) + }); + + promise.done((data) => { + dispatch(fetchQueue()); + }); + + promise.fail((xhr) => { + dispatch(batchActions([ + ...ids.map((id) => { + return updateItem({ + section, + id, + isRemoving: false + }); + }), + + set({ section, isRemoving: false }) + ])); + }); + }; + } + +}; + +export default queueActionHandlers; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js new file mode 100644 index 000000000..9ef9b6a32 --- /dev/null +++ b/frontend/src/Store/Actions/queueActions.js @@ -0,0 +1,24 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import queueActionHandlers from './queueActionHandlers'; + +export const fetchQueueStatus = queueActionHandlers[types.FETCH_QUEUE_STATUS]; + +export const fetchQueueDetails = queueActionHandlers[types.FETCH_QUEUE_DETAILS]; +export const clearQueueDetails = createAction(types.CLEAR_QUEUE_DETAILS); + +export const fetchQueue = queueActionHandlers[types.FETCH_QUEUE]; +export const gotoQueueFirstPage = queueActionHandlers[types.GOTO_FIRST_QUEUE_PAGE]; +export const gotoQueuePreviousPage = queueActionHandlers[types.GOTO_PREVIOUS_QUEUE_PAGE]; +export const gotoQueueNextPage = queueActionHandlers[types.GOTO_NEXT_QUEUE_PAGE]; +export const gotoQueueLastPage = queueActionHandlers[types.GOTO_LAST_QUEUE_PAGE]; +export const gotoQueuePage = queueActionHandlers[types.GOTO_QUEUE_PAGE]; +export const setQueueSort = queueActionHandlers[types.SET_QUEUE_SORT]; +export const setQueueTableOption = createAction(types.SET_QUEUE_TABLE_OPTION); +export const clearQueue = createAction(types.CLEAR_QUEUE); + +export const setQueueEpisodes = createAction(types.SET_QUEUE_EPISODES); +export const grabQueueItem = queueActionHandlers[types.GRAB_QUEUE_ITEM]; +export const grabQueueItems = queueActionHandlers[types.GRAB_QUEUE_ITEMS]; +export const removeQueueItem = queueActionHandlers[types.REMOVE_QUEUE_ITEM]; +export const removeQueueItems = queueActionHandlers[types.REMOVE_QUEUE_ITEMS]; diff --git a/frontend/src/Store/Actions/releaseActionHandlers.js b/frontend/src/Store/Actions/releaseActionHandlers.js new file mode 100644 index 000000000..15ebaa2b0 --- /dev/null +++ b/frontend/src/Store/Actions/releaseActionHandlers.js @@ -0,0 +1,47 @@ +import $ from 'jquery'; +import createFetchHandler from './Creators/createFetchHandler'; +import * as types from './actionTypes'; +import { updateRelease } from './releaseActions'; + +const section = 'releases'; + +const releaseActionHandlers = { + [types.FETCH_RELEASES]: createFetchHandler(section, '/release'), + + [types.GRAB_RELEASE]: function(payload) { + return function(dispatch, getState) { + const guid = payload.guid; + + dispatch(updateRelease({ guid, isGrabbing: true })); + + const promise = $.ajax({ + url: '/release', + method: 'POST', + contentType: 'application/json', + data: JSON.stringify(payload) + }); + + promise.done((data) => { + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: true, + grabError: null + })); + }); + + promise.fail((xhr) => { + const grabError = xhr.responseJSON && xhr.responseJSON.message || 'Failed to add to download queue'; + + dispatch(updateRelease({ + guid, + isGrabbing: false, + isGrabbed: false, + grabError + })); + }); + }; + } +}; + +export default releaseActionHandlers; diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js new file mode 100644 index 000000000..580ffe804 --- /dev/null +++ b/frontend/src/Store/Actions/releaseActions.js @@ -0,0 +1,9 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import releaseActionHandlers from './releaseActionHandlers'; + +export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES]; +export const setReleasesSort = createAction(types.SET_RELEASES_SORT); +export const clearReleases = createAction(types.CLEAR_RELEASES); +export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE]; +export const updateRelease = createAction(types.UPDATE_RELEASE); diff --git a/frontend/src/Store/Actions/rootFolderActionHandlers.js b/frontend/src/Store/Actions/rootFolderActionHandlers.js new file mode 100644 index 000000000..f437721b7 --- /dev/null +++ b/frontend/src/Store/Actions/rootFolderActionHandlers.js @@ -0,0 +1,59 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import { set, updateItem } from './baseActions'; + +const section = 'rootFolders'; + +const rootFolderActionHandlers = { + [types.FETCH_ROOT_FOLDERS]: createFetchHandler('rootFolders', '/rootFolder'), + + [types.DELETE_ROOT_FOLDER]: createRemoveItemHandler('rootFolders', + '/rootFolder', + (state) => state.rootFolders), + + [types.ADD_ROOT_FOLDER]: function(payload) { + return function(dispatch, getState) { + const path = payload.path; + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/rootFolder', + method: 'POST', + data: JSON.stringify({ path }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + updateItem({ + section, + ...data + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default rootFolderActionHandlers; diff --git a/frontend/src/Store/Actions/rootFolderActions.js b/frontend/src/Store/Actions/rootFolderActions.js new file mode 100644 index 000000000..0d0b8112a --- /dev/null +++ b/frontend/src/Store/Actions/rootFolderActions.js @@ -0,0 +1,6 @@ +import * as types from './actionTypes'; +import rootFolderActionHandlers from './rootFolderActionHandlers'; + +export const fetchRootFolders = rootFolderActionHandlers[types.FETCH_ROOT_FOLDERS]; +export const addRootFolder = rootFolderActionHandlers[types.ADD_ROOT_FOLDER]; +export const deleteRootFolder = rootFolderActionHandlers[types.DELETE_ROOT_FOLDER]; diff --git a/frontend/src/Store/Actions/seasonPassActionHandlers.js b/frontend/src/Store/Actions/seasonPassActionHandlers.js new file mode 100644 index 000000000..8c9fd9b55 --- /dev/null +++ b/frontend/src/Store/Actions/seasonPassActionHandlers.js @@ -0,0 +1,83 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import getMonitoringOptions from 'Utilities/Series/getMonitoringOptions'; +import * as types from './actionTypes'; +import { set } from './baseActions'; +import { fetchArtist } from './seriesActions'; + +const section = 'seasonPass'; + +const seasonPassActionHandlers = { + [types.SAVE_SEASON_PASS]: function(payload) { + return function(dispatch, getState) { + const { + artistIds, + monitored, + monitor + } = payload; + + let monitoringOptions = null; + const series = []; + const allSeries = getState().series.items; + + artistIds.forEach((id) => { + const s = _.find(allSeries, { id }); + const seriesToUpdate = { id }; + + if (payload.hasOwnProperty('monitored')) { + seriesToUpdate.monitored = monitored; + } + + if (monitor) { + const { + seasons, + options: seriesMonitoringOptions + } = getMonitoringOptions(_.cloneDeep(s.seasons), monitor); + + if (!monitoringOptions) { + monitoringOptions = seriesMonitoringOptions; + } + + seriesToUpdate.seasons = seasons; + } + + series.push(seriesToUpdate); + }); + + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/seasonPass', + method: 'POST', + data: JSON.stringify({ + series, + monitoringOptions + }), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(fetchArtist()); + + dispatch(set({ + section, + isSaving: false, + saveError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + } +}; + +export default seasonPassActionHandlers; diff --git a/frontend/src/Store/Actions/seasonPassActions.js b/frontend/src/Store/Actions/seasonPassActions.js new file mode 100644 index 000000000..34851458c --- /dev/null +++ b/frontend/src/Store/Actions/seasonPassActions.js @@ -0,0 +1,7 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import seasonPassActionHandlers from './seasonPassActionHandlers'; + +export const setSeasonPassSort = createAction(types.SET_SEASON_PASS_SORT); +export const setSeasonPassFilter = createAction(types.SET_SEASON_PASS_FILTER); +export const saveSeasonPass = seasonPassActionHandlers[types.SAVE_SEASON_PASS]; diff --git a/frontend/src/Store/Actions/seriesActions.js b/frontend/src/Store/Actions/seriesActions.js new file mode 100644 index 000000000..21f97a129 --- /dev/null +++ b/frontend/src/Store/Actions/seriesActions.js @@ -0,0 +1,16 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import artistActionHandlers from './artistActionHandlers'; + +export const fetchArtist = artistActionHandlers[types.FETCH_ARTIST]; +export const saveArtist = artistActionHandlers[types.SAVE_ARTIST]; +export const deleteArtist = artistActionHandlers[types.DELETE_ARTIST]; +export const toggleSeriesMonitored = artistActionHandlers[types.TOGGLE_ARTIST_MONITORED]; +export const toggleSeasonMonitored = artistActionHandlers[types.TOGGLE_ALBUM_MONITORED]; + +export const setSeriesValue = createAction(types.SET_ARTIST_VALUE, (payload) => { + return { + section: 'series', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/seriesEditorActionHandlers.js b/frontend/src/Store/Actions/seriesEditorActionHandlers.js new file mode 100644 index 000000000..07b577723 --- /dev/null +++ b/frontend/src/Store/Actions/seriesEditorActionHandlers.js @@ -0,0 +1,86 @@ +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import { set, updateItem } from './baseActions'; + +const section = 'seriesEditor'; + +const seriesEditorActionHandlers = { + [types.SAVE_ARTIST_EDITOR]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + isSaving: true + })); + + const promise = $.ajax({ + url: '/series/editor', + method: 'PUT', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done((data) => { + dispatch(batchActions([ + ...data.map((series) => { + return updateItem({ + id: series.id, + section: 'series', + ...series + }); + }), + + set({ + section, + isSaving: false, + saveError: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isSaving: false, + saveError: xhr + })); + }); + }; + }, + + [types.BULK_DELETE_ARTIST]: function(payload) { + return function(dispatch, getState) { + dispatch(set({ + section, + isDeleting: true + })); + + const promise = $.ajax({ + url: '/series/editor', + method: 'DELETE', + data: JSON.stringify(payload), + dataType: 'json' + }); + + promise.done(() => { + // SignaR will take care of removing the serires from the collection + + dispatch(set({ + section, + isDeleting: false, + deleteError: null + })); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isDeleting: false, + deleteError: xhr + })); + }); + }; + } +}; + +export default seriesEditorActionHandlers; diff --git a/frontend/src/Store/Actions/seriesEditorActions.js b/frontend/src/Store/Actions/seriesEditorActions.js new file mode 100644 index 000000000..efd632d72 --- /dev/null +++ b/frontend/src/Store/Actions/seriesEditorActions.js @@ -0,0 +1,8 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import seriesEditorActionHandlers from './seriesEditorActionHandlers'; + +export const setSeriesEditorSort = createAction(types.SET_SERIES_EDITOR_SORT); +export const setSeriesEditorFilter = createAction(types.SET_SERIES_EDITOR_FILTER); +export const saveArtistEditor = seriesEditorActionHandlers[types.SAVE_ARTIST_EDITOR]; +export const bulkDeleteArtist = seriesEditorActionHandlers[types.BULK_DELETE_ARTIST]; diff --git a/frontend/src/Store/Actions/settingsActionHandlers.js b/frontend/src/Store/Actions/settingsActionHandlers.js new file mode 100644 index 000000000..4d7d6d4c1 --- /dev/null +++ b/frontend/src/Store/Actions/settingsActionHandlers.js @@ -0,0 +1,238 @@ +import _ from 'lodash'; +import $ from 'jquery'; +import { batchActions } from 'redux-batched-actions'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createFetchSchemaHandler from './Creators/createFetchSchemaHandler'; +import createSaveHandler from './Creators/createSaveHandler'; +import createSaveProviderHandler from './Creators/createSaveProviderHandler'; +import createRemoveItemHandler from './Creators/createRemoveItemHandler'; +import createTestProviderHandler from './Creators/createTestProviderHandler'; +import { set, update, clearPendingChanges } from './baseActions'; + +const settingsActionHandlers = { + [types.FETCH_UI_SETTINGS]: createFetchHandler('ui', '/config/ui'), + [types.SAVE_UI_SETTINGS]: createSaveHandler('ui', '/config/ui', (state) => state.settings.ui), + + [types.FETCH_MEDIA_MANAGEMENT_SETTINGS]: createFetchHandler('mediaManagement', '/config/mediamanagement'), + [types.SAVE_MEDIA_MANAGEMENT_SETTINGS]: createSaveHandler('mediaManagement', '/config/mediamanagement', (state) => state.settings.mediaManagement), + + [types.FETCH_NAMING_SETTINGS]: createFetchHandler('naming', '/config/naming'), + [types.SAVE_NAMING_SETTINGS]: createSaveHandler('naming', '/config/naming', (state) => state.settings.naming), + + [types.FETCH_NAMING_EXAMPLES]: function(payload) { + const section = 'namingExamples'; + + return function(dispatch, getState) { + dispatch(set({ section, isFetching: true })); + + const naming = getState().settings.naming; + + const promise = $.ajax({ + url: '/config/naming/examples', + data: Object.assign({}, naming.item, naming.pendingChanges) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + + set({ + section, + isFetching: false, + isPopulated: true, + error: null + }) + ])); + }); + + promise.fail((xhr) => { + dispatch(set({ + section, + isFetching: false, + isPopulated: false, + error: xhr + })); + }); + }; + }, + + [types.REORDER_DELAY_PROFILE]: function(payload) { + const section = 'delayProfiles'; + + return function(dispatch, getState) { + const { id, moveIndex } = payload; + const moveOrder = moveIndex + 1; + const delayProfiles = getState().settings.delayProfiles.items; + const moving = _.find(delayProfiles, { id }); + + // Don't move if the order hasn't changed + if (moving.order === moveOrder) { + return; + } + + const after = moveIndex > 0 ? _.find(delayProfiles, { order: moveIndex }) : null; + const afterQueryParam = after ? `after=${after.id}` : ''; + + const promise = $.ajax({ + method: 'PUT', + url: `/delayprofile/reorder/${id}?${afterQueryParam}` + }); + + promise.done((data) => { + dispatch(update({ section, data })); + }); + }; + }, + + [types.FETCH_QUALITY_PROFILES]: createFetchHandler('qualityProfiles', '/qualityprofile'), + [types.FETCH_QUALITY_PROFILE_SCHEMA]: createFetchSchemaHandler('qualityProfiles', '/qualityprofile/schema'), + + [types.SAVE_QUALITY_PROFILE]: createSaveProviderHandler('qualityProfiles', + '/qualityprofile', + (state) => state.settings.qualityProfiles), + + [types.DELETE_QUALITY_PROFILE]: createRemoveItemHandler('qualityProfiles', + '/qualityprofile', + (state) => state.settings.qualityProfiles), + + [types.FETCH_LANGUAGE_PROFILES]: createFetchHandler('languageProfiles', '/languageprofile'), + [types.FETCH_LANGUAGE_PROFILE_SCHEMA]: createFetchSchemaHandler('languageProfiles', '/languageprofile/schema'), + + [types.SAVE_LANGUAGE_PROFILE]: createSaveProviderHandler('languageProfiles', + '/languageprofile', + (state) => state.settings.languageProfiles), + + [types.DELETE_LANGUAGE_PROFILE]: createRemoveItemHandler('languageProfiles', + '/languageprofile', + (state) => state.settings.languageProfiles), + + [types.FETCH_DELAY_PROFILES]: createFetchHandler('delayProfiles', '/delayprofile'), + + [types.SAVE_DELAY_PROFILE]: createSaveProviderHandler('delayProfiles', + '/delayprofile', + (state) => state.settings.delayProfiles), + + [types.DELETE_DELAY_PROFILE]: createRemoveItemHandler('delayProfiles', + '/delayprofile', + (state) => state.settings.delayProfiles), + + [types.FETCH_QUALITY_DEFINITIONS]: createFetchHandler('qualityDefinitions', '/qualitydefinition'), + [types.SAVE_QUALITY_DEFINITIONS]: createSaveHandler('qualityDefinitions', '/qualitydefinition', (state) => state.settings.qualitydefinitions), + + [types.SAVE_QUALITY_DEFINITIONS]: function() { + const section = 'qualityDefinitions'; + + return function(dispatch, getState) { + const qualityDefinitions = getState().settings.qualityDefinitions; + + const upatedDefinitions = Object.keys(qualityDefinitions.pendingChanges).map((key) => { + const id = parseInt(key); + const pendingChanges = qualityDefinitions.pendingChanges[id] || {}; + const item = _.find(qualityDefinitions.items, { id }); + + return Object.assign({}, item, pendingChanges); + }); + + // If there is nothing to save don't bother isSaving + if (!upatedDefinitions || !upatedDefinitions.length) { + return; + } + + const promise = $.ajax({ + method: 'PUT', + url: '/qualityDefinition/update', + data: JSON.stringify(upatedDefinitions) + }); + + promise.done((data) => { + dispatch(batchActions([ + update({ section, data }), + clearPendingChanges({ section: 'qualityDefinitions' }) + ])); + }); + }; + }, + + [types.FETCH_INDEXERS]: createFetchHandler('indexers', '/indexer'), + [types.FETCH_INDEXER_SCHEMA]: createFetchSchemaHandler('indexers', '/indexer/schema'), + + [types.SAVE_INDEXER]: createSaveProviderHandler('indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.DELETE_INDEXER]: createRemoveItemHandler('indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.TEST_INDEXER]: createTestProviderHandler('indexers', + '/indexer', + (state) => state.settings.indexers), + + [types.FETCH_INDEXER_OPTIONS]: createFetchHandler('indexerOptions', '/config/indexer'), + [types.SAVE_INDEXER_OPTIONS]: createSaveHandler('indexerOptions', '/config/indexer', (state) => state.settings.indexerOptions), + + [types.FETCH_RESTRICTIONS]: createFetchHandler('restrictions', '/restriction'), + + [types.SAVE_RESTRICTION]: createSaveProviderHandler('restrictions', + '/restriction', + (state) => state.settings.restrictions), + + [types.DELETE_RESTRICTION]: createRemoveItemHandler('restrictions', + '/restriction', + (state) => state.settings.restrictions), + + [types.FETCH_DOWNLOAD_CLIENTS]: createFetchHandler('downloadClients', '/downloadclient'), + [types.FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler('downloadClients', '/downloadclient/schema'), + + [types.SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler('downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler('downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.TEST_DOWNLOAD_CLIENT]: createTestProviderHandler('downloadClients', + '/downloadclient', + (state) => state.settings.downloadClients), + + [types.FETCH_DOWNLOAD_CLIENT_OPTIONS]: createFetchHandler('downloadClientOptions', '/config/downloadclient'), + [types.SAVE_DOWNLOAD_CLIENT_OPTIONS]: createSaveHandler('downloadClientOptions', '/config/downloadclient', (state) => state.settings.downloadClientOptions), + + [types.FETCH_REMOTE_PATH_MAPPINGS]: createFetchHandler('remotePathMappings', '/remotepathmapping'), + + [types.SAVE_REMOTE_PATH_MAPPING]: createSaveProviderHandler('remotePathMappings', + '/remotepathmapping', + (state) => state.settings.remotePathMappings), + + [types.DELETE_REMOTE_PATH_MAPPING]: createRemoveItemHandler('remotePathMappings', + '/remotepathmapping', + (state) => state.settings.remotePathMappings), + + [types.FETCH_NOTIFICATIONS]: createFetchHandler('notifications', '/notification'), + [types.FETCH_NOTIFICATION_SCHEMA]: createFetchSchemaHandler('notifications', '/notification/schema'), + + [types.SAVE_NOTIFICATION]: createSaveProviderHandler('notifications', + '/notification', + (state) => state.settings.notifications), + + [types.DELETE_NOTIFICATION]: createRemoveItemHandler('notifications', + '/notification', + (state) => state.settings.notifications), + + [types.TEST_NOTIFICATION]: createTestProviderHandler('notifications', + '/notification', + (state) => state.settings.notifications), + + [types.FETCH_METADATA]: createFetchHandler('metadata', '/metadata'), + + [types.SAVE_METADATA]: createSaveProviderHandler('metadata', + '/metadata', + (state) => state.settings.metadata), + + [types.FETCH_GENERAL_SETTINGS]: createFetchHandler('general', '/config/host'), + [types.SAVE_GENERAL_SETTINGS]: createSaveHandler('general', '/config/host', (state) => state.settings.general) +}; + +export default settingsActionHandlers; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js new file mode 100644 index 000000000..394d25013 --- /dev/null +++ b/frontend/src/Store/Actions/settingsActions.js @@ -0,0 +1,207 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import settingsActionHandlers from './settingsActionHandlers'; + +export const toggleAdvancedSettings = createAction(types.TOGGLE_ADVANCED_SETTINGS); + +export const fetchUISettings = settingsActionHandlers[types.FETCH_UI_SETTINGS]; +export const saveUISettings = settingsActionHandlers[types.SAVE_UI_SETTINGS]; +export const setUISettingsValue = createAction(types.SET_UI_SETTINGS_VALUE, (payload) => { + return { + section: 'ui', + ...payload + }; +}); + +export const fetchMediaManagementSettings = settingsActionHandlers[types.FETCH_MEDIA_MANAGEMENT_SETTINGS]; +export const saveMediaManagementSettings = settingsActionHandlers[types.SAVE_MEDIA_MANAGEMENT_SETTINGS]; +export const setMediaManagementSettingsValue = createAction(types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE, (payload) => { + return { + section: 'mediaManagement', + ...payload + }; +}); + +export const fetchNamingSettings = settingsActionHandlers[types.FETCH_NAMING_SETTINGS]; +export const saveNamingSettings = settingsActionHandlers[types.SAVE_NAMING_SETTINGS]; +export const setNamingSettingsValue = createAction(types.SET_NAMING_SETTINGS_VALUE, (payload) => { + return { + section: 'naming', + ...payload + }; +}); + +export const fetchNamingExamples = settingsActionHandlers[types.FETCH_NAMING_EXAMPLES]; + +export const fetchQualityProfiles = settingsActionHandlers[types.FETCH_QUALITY_PROFILES]; +export const fetchQualityProfileSchema = settingsActionHandlers[types.FETCH_QUALITY_PROFILE_SCHEMA]; +export const saveQualityProfile = settingsActionHandlers[types.SAVE_QUALITY_PROFILE]; +export const deleteQualityProfile = settingsActionHandlers[types.DELETE_QUALITY_PROFILE]; + +export const setQualityProfileValue = createAction(types.SET_QUALITY_PROFILE_VALUE, (payload) => { + return { + section: 'qualityProfiles', + ...payload + }; +}); + +export const fetchLanguageProfiles = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILES]; +export const fetchLanguageProfileSchema = settingsActionHandlers[types.FETCH_LANGUAGE_PROFILE_SCHEMA]; +export const saveLanguageProfile = settingsActionHandlers[types.SAVE_LANGUAGE_PROFILE]; +export const deleteLanguageProfile = settingsActionHandlers[types.DELETE_LANGUAGE_PROFILE]; + +export const setLanguageProfileValue = createAction(types.SET_LANGUAGE_PROFILE_VALUE, (payload) => { + return { + section: 'languageProfiles', + ...payload + }; +}); + +export const fetchDelayProfiles = settingsActionHandlers[types.FETCH_DELAY_PROFILES]; +export const saveDelayProfile = settingsActionHandlers[types.SAVE_DELAY_PROFILE]; +export const deleteDelayProfile = settingsActionHandlers[types.DELETE_DELAY_PROFILE]; +export const reorderDelayProfile = settingsActionHandlers[types.REORDER_DELAY_PROFILE]; + +export const setDelayProfileValue = createAction(types.SET_DELAY_PROFILE_VALUE, (payload) => { + return { + section: 'delayProfiles', + ...payload + }; +}); + +export const fetchQualityDefinitions = settingsActionHandlers[types.FETCH_QUALITY_DEFINITIONS]; +export const saveQualityDefinitions = settingsActionHandlers[types.SAVE_QUALITY_DEFINITIONS]; + +export const setQualityDefinitionValue = createAction(types.SET_QUALITY_DEFINITION_VALUE); + +export const fetchIndexers = settingsActionHandlers[types.FETCH_INDEXERS]; +export const fetchIndexerSchema = settingsActionHandlers[types.FETCH_INDEXER_SCHEMA]; +export const selectIndexerSchema = createAction(types.SELECT_INDEXER_SCHEMA); + +export const saveIndexer = settingsActionHandlers[types.SAVE_INDEXER]; +export const deleteIndexer = settingsActionHandlers[types.DELETE_INDEXER]; +export const testIndexer = settingsActionHandlers[types.TEST_INDEXER]; + +export const setIndexerValue = createAction(types.SET_INDEXER_VALUE, (payload) => { + return { + section: 'indexers', + ...payload + }; +}); + +export const setIndexerFieldValue = createAction(types.SET_INDEXER_FIELD_VALUE, (payload) => { + return { + section: 'indexers', + ...payload + }; +}); + +export const fetchIndexerOptions = settingsActionHandlers[types.FETCH_INDEXER_OPTIONS]; +export const saveIndexerOptions = settingsActionHandlers[types.SAVE_INDEXER_OPTIONS]; +export const setIndexerOptionsValue = createAction(types.SET_INDEXER_OPTIONS_VALUE, (payload) => { + return { + section: 'indexerOptions', + ...payload + }; +}); + +export const fetchRestrictions = settingsActionHandlers[types.FETCH_RESTRICTIONS]; +export const saveRestriction = settingsActionHandlers[types.SAVE_RESTRICTION]; +export const deleteRestriction = settingsActionHandlers[types.DELETE_RESTRICTION]; + +export const setRestrictionValue = createAction(types.SET_RESTRICTION_VALUE, (payload) => { + return { + section: 'restrictions', + ...payload + }; +}); + +export const fetchDownloadClients = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENTS]; +export const fetchDownloadClientSchema = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_SCHEMA]; +export const selectDownloadClientSchema = createAction(types.SELECT_DOWNLOAD_CLIENT_SCHEMA); + +export const saveDownloadClient = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT]; +export const deleteDownloadClient = settingsActionHandlers[types.DELETE_DOWNLOAD_CLIENT]; +export const testDownloadClient = settingsActionHandlers[types.TEST_DOWNLOAD_CLIENT]; + +export const setDownloadClientValue = createAction(types.SET_DOWNLOAD_CLIENT_VALUE, (payload) => { + return { + section: 'downloadClients', + ...payload + }; +}); + +export const setDownloadClientFieldValue = createAction(types.SET_DOWNLOAD_CLIENT_FIELD_VALUE, (payload) => { + return { + section: 'downloadClients', + ...payload + }; +}); + +export const fetchDownloadClientOptions = settingsActionHandlers[types.FETCH_DOWNLOAD_CLIENT_OPTIONS]; +export const saveDownloadClientOptions = settingsActionHandlers[types.SAVE_DOWNLOAD_CLIENT_OPTIONS]; +export const setDownloadClientOptionsValue = createAction(types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE, (payload) => { + return { + section: 'downloadClientOptions', + ...payload + }; +}); + +export const fetchRemotePathMappings = settingsActionHandlers[types.FETCH_REMOTE_PATH_MAPPINGS]; +export const saveRemotePathMapping = settingsActionHandlers[types.SAVE_REMOTE_PATH_MAPPING]; +export const deleteRemotePathMapping = settingsActionHandlers[types.DELETE_REMOTE_PATH_MAPPING]; + +export const setRemotePathMappingValue = createAction(types.SET_REMOTE_PATH_MAPPING_VALUE, (payload) => { + return { + section: 'remotePathMappings', + ...payload + }; +}); + +export const fetchNotifications = settingsActionHandlers[types.FETCH_NOTIFICATIONS]; +export const fetchNotificationSchema = settingsActionHandlers[types.FETCH_NOTIFICATION_SCHEMA]; +export const selectNotificationSchema = createAction(types.SELECT_NOTIFICATION_SCHEMA); + +export const saveNotification = settingsActionHandlers[types.SAVE_NOTIFICATION]; +export const deleteNotification = settingsActionHandlers[types.DELETE_NOTIFICATION]; +export const testNotification = settingsActionHandlers[types.TEST_NOTIFICATION]; + +export const setNotificationValue = createAction(types.SET_NOTIFICATION_VALUE, (payload) => { + return { + section: 'notifications', + ...payload + }; +}); + +export const setNotificationFieldValue = createAction(types.SET_NOTIFICATION_FIELD_VALUE, (payload) => { + return { + section: 'notifications', + ...payload + }; +}); + +export const fetchMetadata = settingsActionHandlers[types.FETCH_METADATA]; +export const saveMetadata = settingsActionHandlers[types.SAVE_METADATA]; + +export const setMetadataValue = createAction(types.SET_METADATA_VALUE, (payload) => { + return { + section: 'metadata', + ...payload + }; +}); + +export const setMetadataFieldValue = createAction(types.SET_METADATA_FIELD_VALUE, (payload) => { + return { + section: 'metadata', + ...payload + }; +}); + +export const fetchGeneralSettings = settingsActionHandlers[types.FETCH_GENERAL_SETTINGS]; +export const saveGeneralSettings = settingsActionHandlers[types.SAVE_GENERAL_SETTINGS]; +export const setGeneralSettingsValue = createAction(types.SET_GENERAL_SETTINGS_VALUE, (payload) => { + return { + section: 'general', + ...payload + }; +}); diff --git a/frontend/src/Store/Actions/systemActionHandlers.js b/frontend/src/Store/Actions/systemActionHandlers.js new file mode 100644 index 000000000..d40674da3 --- /dev/null +++ b/frontend/src/Store/Actions/systemActionHandlers.js @@ -0,0 +1,48 @@ +import $ from 'jquery'; +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import * as types from './actionTypes'; +import createFetchHandler from './Creators/createFetchHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; + +const systemActionHandlers = { + [types.FETCH_STATUS]: createFetchHandler('status', '/system/status'), + [types.FETCH_HEALTH]: createFetchHandler('health', '/health'), + [types.FETCH_DISK_SPACE]: createFetchHandler('diskSpace', '/diskspace'), + [types.FETCH_TASK]: createFetchHandler('tasks', '/system/task'), + [types.FETCH_TASKS]: createFetchHandler('tasks', '/system/task'), + [types.FETCH_BACKUPS]: createFetchHandler('backups', '/system/backup'), + [types.FETCH_UPDATES]: createFetchHandler('updates', '/update'), + [types.FETCH_LOG_FILES]: createFetchHandler('logFiles', '/log/file'), + [types.FETCH_UPDATE_LOG_FILES]: createFetchHandler('updateLogFiles', '/log/file/update'), + + ...createServerSideCollectionHandlers('logs', '/log', (state) => state.system, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_LOGS, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_LOGS_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_LOGS_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_LOGS_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_LOGS_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_LOGS_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_LOGS_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_LOGS_FILTER + }), + + [types.RESTART]: function() { + return function() { + $.ajax({ + url: '/system/restart', + method: 'POST' + }); + }; + }, + + [types.SHUTDOWN]: function() { + return function() { + $.ajax({ + url: '/system/shutdown', + method: 'POST' + }); + }; + } +}; + +export default systemActionHandlers; diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js new file mode 100644 index 000000000..b5614d2d3 --- /dev/null +++ b/frontend/src/Store/Actions/systemActions.js @@ -0,0 +1,28 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import systemActionHandlers from './systemActionHandlers'; + +export const fetchStatus = systemActionHandlers[types.FETCH_STATUS]; +export const fetchHealth = systemActionHandlers[types.FETCH_HEALTH]; +export const fetchDiskSpace = systemActionHandlers[types.FETCH_DISK_SPACE]; + +export const fetchTask = systemActionHandlers[types.FETCH_TASK]; +export const fetchTasks = systemActionHandlers[types.FETCH_TASKS]; +export const fetchBackups = systemActionHandlers[types.FETCH_BACKUPS]; +export const fetchUpdates = systemActionHandlers[types.FETCH_UPDATES]; + +export const fetchLogs = systemActionHandlers[types.FETCH_LOGS]; +export const gotoLogsFirstPage = systemActionHandlers[types.GOTO_FIRST_LOGS_PAGE]; +export const gotoLogsPreviousPage = systemActionHandlers[types.GOTO_PREVIOUS_LOGS_PAGE]; +export const gotoLogsNextPage = systemActionHandlers[types.GOTO_NEXT_LOGS_PAGE]; +export const gotoLogsLastPage = systemActionHandlers[types.GOTO_LAST_LOGS_PAGE]; +export const gotoLogsPage = systemActionHandlers[types.GOTO_LOGS_PAGE]; +export const setLogsSort = systemActionHandlers[types.SET_LOGS_SORT]; +export const setLogsFilter = systemActionHandlers[types.SET_LOGS_FILTER]; +export const setLogsTableOption = createAction(types.SET_LOGS_TABLE_OPTION); + +export const fetchLogFiles = systemActionHandlers[types.FETCH_LOG_FILES]; +export const fetchUpdateLogFiles = systemActionHandlers[types.FETCH_UPDATE_LOG_FILES]; + +export const restart = systemActionHandlers[types.RESTART]; +export const shutdown = systemActionHandlers[types.SHUTDOWN]; diff --git a/frontend/src/Store/Actions/tagActionHandlers.js b/frontend/src/Store/Actions/tagActionHandlers.js new file mode 100644 index 000000000..c4e007f6c --- /dev/null +++ b/frontend/src/Store/Actions/tagActionHandlers.js @@ -0,0 +1,28 @@ +import $ from 'jquery'; +import * as types from './actionTypes'; +import { update } from './baseActions'; +import createFetchHandler from './Creators/createFetchHandler'; + +const tagActionHandlers = { + [types.FETCH_TAGS]: createFetchHandler('tags', '/tag'), + + [types.ADD_TAG]: function(payload) { + return (dispatch, getState) => { + const promise = $.ajax({ + url: '/tag', + method: 'POST', + data: JSON.stringify(payload.tag) + }); + + promise.done((data) => { + const tags = getState().tags.items.slice(); + tags.push(data); + + dispatch(update({ section: 'tags', data: tags })); + payload.onTagCreated(data); + }); + }; + } +}; + +export default tagActionHandlers; diff --git a/frontend/src/Store/Actions/tagActions.js b/frontend/src/Store/Actions/tagActions.js new file mode 100644 index 000000000..45f0141ce --- /dev/null +++ b/frontend/src/Store/Actions/tagActions.js @@ -0,0 +1,5 @@ +import * as types from './actionTypes'; +import tagActionHandlers from './tagActionHandlers'; + +export const fetchTags = tagActionHandlers[types.FETCH_TAGS]; +export const addTag = tagActionHandlers[types.ADD_TAG]; diff --git a/frontend/src/Store/Actions/wantedActionHandlers.js b/frontend/src/Store/Actions/wantedActionHandlers.js new file mode 100644 index 000000000..ad560b088 --- /dev/null +++ b/frontend/src/Store/Actions/wantedActionHandlers.js @@ -0,0 +1,34 @@ +import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; +import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler'; +import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers'; +import * as types from './actionTypes'; + +const wantedActionHandlers = { + ...createServerSideCollectionHandlers('missing', '/wanted/missing', (state) => state.wanted, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_MISSING, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_MISSING_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_MISSING_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_MISSING_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_MISSING_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_MISSING_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_MISSING_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_MISSING_FILTER + }), + + [types.BATCH_TOGGLE_MISSING_EPISODES]: createBatchToggleEpisodeMonitoredHandler('missing', (state) => state.wanted.missing), + + ...createServerSideCollectionHandlers('cutoffUnmet', '/wanted/cutoff', (state) => state.wanted, { + [serverSideCollectionHandlers.FETCH]: types.FETCH_CUTOFF_UNMET, + [serverSideCollectionHandlers.FIRST_PAGE]: types.GOTO_FIRST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.PREVIOUS_PAGE]: types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.NEXT_PAGE]: types.GOTO_NEXT_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.LAST_PAGE]: types.GOTO_LAST_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.EXACT_PAGE]: types.GOTO_CUTOFF_UNMET_PAGE, + [serverSideCollectionHandlers.SORT]: types.SET_CUTOFF_UNMET_SORT, + [serverSideCollectionHandlers.FILTER]: types.SET_CUTOFF_UNMET_FILTER + }), + + [types.BATCH_TOGGLE_CUTOFF_UNMET_EPISODES]: createBatchToggleEpisodeMonitoredHandler('cutoffUnmet', (state) => state.wanted.cutoffUnmet) +}; + +export default wantedActionHandlers; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js new file mode 100644 index 000000000..9baac1cd5 --- /dev/null +++ b/frontend/src/Store/Actions/wantedActions.js @@ -0,0 +1,35 @@ +import { createAction } from 'redux-actions'; +import * as types from './actionTypes'; +import wantedActionHandlers from './wantedActionHandlers'; + +// +// Missing + +export const fetchMissing = wantedActionHandlers[types.FETCH_MISSING]; +export const gotoMissingFirstPage = wantedActionHandlers[types.GOTO_FIRST_MISSING_PAGE]; +export const gotoMissingPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_MISSING_PAGE]; +export const gotoMissingNextPage = wantedActionHandlers[types.GOTO_NEXT_MISSING_PAGE]; +export const gotoMissingLastPage = wantedActionHandlers[types.GOTO_LAST_MISSING_PAGE]; +export const gotoMissingPage = wantedActionHandlers[types.GOTO_MISSING_PAGE]; +export const setMissingSort = wantedActionHandlers[types.SET_MISSING_SORT]; +export const setMissingFilter = wantedActionHandlers[types.SET_MISSING_FILTER]; +export const setMissingTableOption = createAction(types.SET_MISSING_TABLE_OPTION); +export const clearMissing = createAction(types.CLEAR_MISSING); + +export const batchToggleMissingEpisodes = wantedActionHandlers[types.BATCH_TOGGLE_MISSING_EPISODES]; + +// +// Cutoff Unmet + +export const fetchCutoffUnmet = wantedActionHandlers[types.FETCH_CUTOFF_UNMET]; +export const gotoCutoffUnmetFirstPage = wantedActionHandlers[types.GOTO_FIRST_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetPreviousPage = wantedActionHandlers[types.GOTO_PREVIOUS_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetNextPage = wantedActionHandlers[types.GOTO_NEXT_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetLastPage = wantedActionHandlers[types.GOTO_LAST_CUTOFF_UNMET_PAGE]; +export const gotoCutoffUnmetPage = wantedActionHandlers[types.GOTO_CUTOFF_UNMET_PAGE]; +export const setCutoffUnmetSort = wantedActionHandlers[types.SET_CUTOFF_UNMET_SORT]; +export const setCutoffUnmetFilter = wantedActionHandlers[types.SET_CUTOFF_UNMET_FILTER]; +export const setCutoffUnmetTableOption= createAction(types.SET_CUTOFF_UNMET_TABLE_OPTION); +export const clearCutoffUnmet= createAction(types.CLEAR_CUTOFF_UNMET); + +export const batchToggleCutoffUnmetEpisodes = wantedActionHandlers[types.BATCH_TOGGLE_CUTOFF_UNMET_EPISODES]; diff --git a/frontend/src/Store/Middleware/middlewares.js b/frontend/src/Store/Middleware/middlewares.js new file mode 100644 index 000000000..0367e06e1 --- /dev/null +++ b/frontend/src/Store/Middleware/middlewares.js @@ -0,0 +1,39 @@ +import { applyMiddleware, compose } from 'redux'; +import ravenMiddleware from 'redux-raven-middleware'; +import thunk from 'redux-thunk'; +import { routerMiddleware } from 'react-router-redux'; +import persistState from './persistState'; + +export default function(history) { + const { + analytics, + branch, + version, + release, + isProduction + } = window.Sonarr; + + const dsn = isProduction ? 'https://b80ca60625b443c38b242e0d21681eb7@sentry.sonarr.tv/13' : + 'https://8dbaacdfe2ff4caf97dc7945aecf9ace@sentry.sonarr.tv/12'; + + const middlewares = []; + + if (analytics) { + middlewares.push(ravenMiddleware(dsn, { + environment: isProduction ? 'production' : 'development', + release, + tags: { + branch, + version + } + })); + } + + middlewares.push(routerMiddleware(history)); + middlewares.push(thunk); + + return compose( + applyMiddleware(...middlewares), + persistState + ); +} diff --git a/frontend/src/Store/Middleware/persistState.js b/frontend/src/Store/Middleware/persistState.js new file mode 100644 index 000000000..30bd47ace --- /dev/null +++ b/frontend/src/Store/Middleware/persistState.js @@ -0,0 +1,119 @@ +import _ from 'lodash'; +import persistState from 'redux-localstorage'; +import * as addSeriesReducers from 'Store/Reducers/addSeriesReducers'; +import * as episodeReducers from 'Store/Reducers/episodeReducers'; +import * as artistIndexReducers from 'Store/Reducers/artistIndexReducers'; +import * as seriesEditorReducers from 'Store/Reducers/seriesEditorReducers'; +import * as seasonPassReducers from 'Store/Reducers/seasonPassReducers'; +import * as calendarReducers from 'Store/Reducers/calendarReducers'; +import * as historyReducers from 'Store/Reducers/historyReducers'; +import * as blacklistReducers from 'Store/Reducers/blacklistReducers'; +import * as wantedReducers from 'Store/Reducers/wantedReducers'; +import * as settingsReducers from 'Store/Reducers/settingsReducers'; +import * as systemReducers from 'Store/Reducers/systemReducers'; +import * as interactiveImportReducers from 'Store/Reducers/interactiveImportReducers'; +import * as queueReducers from 'Store/Reducers/queueReducers'; + +const reducers = [ + addSeriesReducers, + episodeReducers, + artistIndexReducers, + seriesEditorReducers, + seasonPassReducers, + calendarReducers, + historyReducers, + blacklistReducers, + wantedReducers, + settingsReducers, + systemReducers, + interactiveImportReducers, + queueReducers +]; + +const columnPaths = []; + +const paths = _.reduce(reducers, (acc, reducer) => { + reducer.persistState.forEach((path) => { + if (path.match(/\.columns$/)) { + columnPaths.push(path); + } + + acc.push(path); + }); + + return acc; +}, []); + +function mergeColumns(path, initialState, persistedState, computedState) { + const initialColumns = _.get(initialState, path); + const persistedColumns = _.get(persistedState, path); + + if (!persistedColumns || !persistedColumns.length) { + return; + } + + const columns = []; + + initialColumns.forEach((initialColumn) => { + const persistedColumnIndex = _.findIndex(persistedColumns, { name: initialColumn.name }); + const column = Object.assign({}, initialColumn); + const persistedColumn = persistedColumnIndex > -1 ? persistedColumns[persistedColumnIndex] : undefined; + + if (persistedColumn) { + column.isVisible = persistedColumn.isVisible; + } + + // If there is a persisted column, it's index doesn't exceed the column list + // and it's modifiable, insert it in the proper position. + + if (persistedColumn && columns.length - 1 > persistedColumnIndex && persistedColumn.isModifiable !== false) { + columns.splice(persistedColumnIndex, 0, column); + } else { + columns.push(column); + } + + // Set the columns in the persisted state + _.set(computedState, path, columns); + }); +} + +function slicer(paths_) { + return (state) => { + const subset = {}; + + paths_.forEach((path) => { + _.set(subset, path, _.get(state, path)); + }); + + return subset; + }; +} + +function serialize(obj) { + return JSON.stringify(obj, null, 2); +} + +function merge(initialState, persistedState) { + if (!persistedState) { + return initialState; + } + + const computedState = {}; + + _.merge(computedState, initialState, persistedState); + + columnPaths.forEach((columnPath) => { + mergeColumns(columnPath, initialState, persistedState, computedState); + }); + + return computedState; +} + +const config = { + slicer, + serialize, + merge, + key: 'sonarr' +}; + +export default persistState(paths, config); diff --git a/frontend/src/Store/Reducers/Creators/createAddItemReducer.js b/frontend/src/Store/Reducers/Creators/createAddItemReducer.js new file mode 100644 index 000000000..d0e75c758 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createAddItemReducer.js @@ -0,0 +1,23 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createAddItemReducer(section) { + return (state, { payload }) => { + const { + section: payloadSection, + ...otherProps + } = payload; + + if (section === payloadSection) { + const newState = getSectionState(state, section); + + newState.items = [...newState.items, { ...otherProps }]; + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createAddItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js b/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js new file mode 100644 index 000000000..6ff6e7b25 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createClearPendingChangesReducer.js @@ -0,0 +1,21 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createClearPendingChangesReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + newState.pendingChanges = {}; + + if (newState.hasOwnProperty('saveError')) { + newState.saveError = null; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createClearPendingChangesReducer; diff --git a/frontend/src/Store/Reducers/Creators/createClearReducer.js b/frontend/src/Store/Reducers/Creators/createClearReducer.js new file mode 100644 index 000000000..2952973a9 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createClearReducer.js @@ -0,0 +1,12 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createClearReducer(section, defaultState) { + return (state) => { + const newState = Object.assign(getSectionState(state, section), defaultState); + + return updateSectionState(state, section, newState); + }; +} + +export default createClearReducer; diff --git a/frontend/src/Store/Reducers/Creators/createReducers.js b/frontend/src/Store/Reducers/Creators/createReducers.js new file mode 100644 index 000000000..13ed584e8 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createReducers.js @@ -0,0 +1,20 @@ +function createReducers(sections, createReducer) { + const reducers = {}; + + sections.forEach((section) => { + reducers[section] = createReducer(section); + }); + + return (state, action) => { + const section = action.payload.section; + const reducer = reducers[section]; + + if (reducer) { + return reducer(state, action); + } + + return state; + }; +} + +export default createReducers; diff --git a/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js b/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js new file mode 100644 index 000000000..c09655b0c --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createRemoveItemReducer.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createRemoveItemReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + + newState.items = [...newState.items]; + _.remove(newState.items, { id: payload.id }); + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createRemoveItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js new file mode 100644 index 000000000..d756a736e --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionFilterReducer.js @@ -0,0 +1,17 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { filterTypes } from 'Helpers/Props'; + +function createSetClientSideCollectionFilterReducer(section) { + return (state, { payload }) => { + const newState = getSectionState(state, section); + + newState.filterKey = payload.filterKey; + newState.filterValue = payload.filterValue; + newState.filterType = payload.filterType || filterTypes.EQUAL; + + return updateSectionState(state, section, newState); + }; +} + +export default createSetClientSideCollectionFilterReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js new file mode 100644 index 000000000..07f57e970 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetClientSideCollectionSortReducer.js @@ -0,0 +1,29 @@ +import { sortDirections } from 'Helpers/Props'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetClientSideCollectionSortReducer(section) { + return (state, { payload }) => { + const newState = getSectionState(state, section); + + const sortKey = payload.sortKey || newState.sortKey; + let sortDirection = payload.sortDirection; + + if (!sortDirection) { + if (payload.sortKey === newState.sortKey) { + sortDirection = newState.sortDirection === sortDirections.ASCENDING ? + sortDirections.DESCENDING : + sortDirections.ASCENDING; + } else { + sortDirection = newState.sortDirection; + } + } + + newState.sortKey = sortKey; + newState.sortDirection = sortDirection; + + return updateSectionState(state, section, newState); + }; +} + +export default createSetClientSideCollectionSortReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js b/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js new file mode 100644 index 000000000..3af58dd3b --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetProviderFieldValueReducer.js @@ -0,0 +1,23 @@ +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetProviderFieldValueReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const { name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = Object.assign({}, newState.pendingChanges); + const fields = Object.assign({}, newState.pendingChanges.fields || {}); + + fields[name] = value; + + newState.pendingChanges.fields = fields; + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetProviderFieldValueReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetReducer.js b/frontend/src/Store/Reducers/Creators/createSetReducer.js new file mode 100644 index 000000000..2c673b927 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetReducer.js @@ -0,0 +1,45 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +const whitelistedProperties = [ + 'isFetching', + 'isPopulated', + 'error', + 'isFetchingSchema', + 'schemaPopulated', + 'schemaError', + 'schema', + 'selectedSchema', + 'isSaving', + 'saveError', + 'isTesting', + 'isDeleting', + 'deleteError', + 'pendingChanges', + 'filterKey', + 'filterValue', + 'page', + 'sortKey', + 'sortDirection' +]; + +const blacklistedProperties = [ + 'section', + 'id' +]; + +function createSetReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = Object.assign(getSectionState(state, section), + _.omit(payload, blacklistedProperties)); + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js b/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js new file mode 100644 index 000000000..33ac23044 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetSettingValueReducer.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createSetSettingValueReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const { name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = Object.assign({}, newState.pendingChanges); + + const currentValue = newState.item ? newState.item[name] : null; + const pendingState = newState.pendingChanges; + + let parsedValue = null; + + if (_.isNumber(currentValue)) { + parsedValue = parseInt(value); + } else { + parsedValue = value; + } + + if (currentValue === parsedValue) { + delete pendingState[name]; + } else { + pendingState[name] = parsedValue; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createSetSettingValueReducer; diff --git a/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js b/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js new file mode 100644 index 000000000..5f36e7940 --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createSetTableOptionReducer.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +const whitelistedProperties = [ + 'pageSize', + 'columns' +]; + +function createSetTableOptionReducer(section) { + return (state, { payload }) => { + const newState = Object.assign(getSectionState(state, section), + _.pick(payload, whitelistedProperties)); + + return updateSectionState(state, section, newState); + }; +} + +export default createSetTableOptionReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js new file mode 100644 index 000000000..aba730afd --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateItemReducer.js @@ -0,0 +1,36 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateItemReducer(section, idProp = 'id') { + return (state, { payload }) => { + const { + section: payloadSection, + updateOnly = false, + ...otherProps + } = payload; + + if (section === payloadSection) { + const newState = getSectionState(state, section); + const items = newState.items; + const index = _.findIndex(items, { [idProp]: payload[idProp] }); + + newState.items = [...items]; + + // TODO: Move adding to it's own reducer + if (index >= 0) { + const item = items[index]; + + newState.items.splice(index, 1, { ...item, ...otherProps }); + } else if (!updateOnly) { + newState.items.push({ ...otherProps }); + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createUpdateItemReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateReducer.js new file mode 100644 index 000000000..ea566ad9b --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateReducer.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const newState = getSectionState(state, section); + + if (_.isArray(payload.data)) { + newState.items = payload.data; + } else { + newState.item = payload.data; + } + + return updateSectionState(state, section, newState); + } + + return state; + }; +} + +export default createUpdateReducer; diff --git a/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js b/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js new file mode 100644 index 000000000..235a1016a --- /dev/null +++ b/frontend/src/Store/Reducers/Creators/createUpdateServerSideCollectionReducer.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function createUpdateServerSideCollectionReducer(section) { + return (state, { payload }) => { + if (section === payload.section) { + const data = payload.data; + const newState = getSectionState(state, section); + + const serverState = _.omit(data, ['records']); + const calculatedState = { + totalPages: Math.max(Math.ceil(data.totalRecords / data.pageSize), 1), + items: data.records + }; + + return updateSectionState(state, section, Object.assign(newState, serverState, calculatedState)); + } + + return state; + }; +} + +export default createUpdateServerSideCollectionReducer; diff --git a/frontend/src/Store/Reducers/addSeriesReducers.js b/frontend/src/Store/Reducers/addSeriesReducers.js new file mode 100644 index 000000000..1432840dc --- /dev/null +++ b/frontend/src/Store/Reducers/addSeriesReducers.js @@ -0,0 +1,68 @@ +import { handleActions } from 'redux-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isAdding: false, + isAdded: false, + addError: null, + items: [], + + defaults: { + rootFolderPath: '', + monitor: 'allEpisodes', + qualityProfileId: 0, + languageProfileId: 0, + seriesType: 'standard', + albumFolder: true, + tags: [] + } +}; + +export const persistState = [ + 'addSeries.defaults' +]; + +const reducerSection = 'addSeries'; + +const addSeriesReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.SET_ADD_SERIES_VALUE]: createSetSettingValueReducer(reducerSection), + + [types.SET_ADD_SERIES_DEFAULT]: function(state, { payload }) { + const newState = getSectionState(state, reducerSection); + + newState.defaults = { + ...newState.defaults, + ...payload + }; + + return updateSectionState(state, reducerSection, newState); + }, + + [types.CLEAR_ADD_SERIES]: function(state) { + const { + defaults, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState); + +export default addSeriesReducers; diff --git a/frontend/src/Store/Reducers/appReducers.js b/frontend/src/Store/Reducers/appReducers.js new file mode 100644 index 000000000..f574495e3 --- /dev/null +++ b/frontend/src/Store/Reducers/appReducers.js @@ -0,0 +1,74 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +function getDimensions(width, height) { + const dimensions = { + width, + height, + isExtraSmallScreen: width <= 480, + isSmallScreen: width <= 768, + isMediumScreen: width <= 992, + isLargeScreen: width <= 1200 + }; + + return dimensions; +} + +export const defaultState = { + dimensions: getDimensions(window.innerWidth, window.innerHeight), + messages: { + items: [] + }, + version: window.Sonarr.version, + isUpdated: false, + isConnected: true, + isReconnecting: false, + isDisconnected: false, + isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen +}; + +const appReducers = handleActions({ + + [types.SAVE_DIMENSIONS]: function(state, { payload }) { + const { + width, + height + } = payload; + + const dimensions = getDimensions(width, height); + + return Object.assign({}, state, { dimensions }); + }, + + [types.SHOW_MESSAGE]: createUpdateItemReducer('messages'), + [types.HIDE_MESSAGE]: createRemoveItemReducer('messages'), + + [types.SET_APP_VALUE]: createSetReducer('app'), + [types.SET_VERSION]: function(state, { payload }) { + const version = payload.version; + + const newState = { + version + }; + + if (state.version !== version) { + newState.isUpdated = true; + } + + return Object.assign({}, state, newState); + }, + + [types.SET_IS_SIDEBAR_VISIBLE]: function(state, { payload }) { + const newState = { + isSidebarVisible: payload.isSidebarVisible + }; + + return Object.assign({}, state, newState); + } + +}, defaultState); + +export default appReducers; diff --git a/frontend/src/Store/Reducers/artistIndexReducers.js b/frontend/src/Store/Reducers/artistIndexReducers.js new file mode 100644 index 000000000..9a5e14e03 --- /dev/null +++ b/frontend/src/Store/Reducers/artistIndexReducers.js @@ -0,0 +1,213 @@ +import moment from 'moment'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortTitle', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL, + view: 'posters', + + posterOptions: { + detailedProgressBar: false, + size: 'large', + showTitle: false, + showQualityProfile: false + }, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'sortName', + label: 'Artist Name', + isSortable: true, + isVisible: true, + isModifiable: false + }, + { + name: 'network', + label: 'Network', + isSortable: true, + isVisible: true + }, + { + name: 'qualityProfileId', + label: 'Quality Profile', + isSortable: true, + isVisible: true + }, + { + name: 'languageProfileId', + label: 'Language Profile', + isSortable: true, + isVisible: false + }, + { + name: 'nextAiring', + label: 'Next Airing', + isSortable: true, + isVisible: true + }, + { + name: 'previousAiring', + label: 'Previous Airing', + isSortable: true, + isVisible: false + }, + { + name: 'added', + label: 'Added', + isSortable: true, + isVisible: false + }, + { + name: 'albumCount', + label: 'Albums', + isSortable: true, + isVisible: true + }, + { + name: 'trackProgress', + label: 'Tracks', + isSortable: true, + isVisible: true + }, + { + name: 'trackCount', + label: 'Track Count', + isSortable: true, + isVisible: false + }, + { + name: 'latestSeason', + label: 'Latest Season', + isSortable: true, + isVisible: false + }, + { + name: 'path', + label: 'Path', + isSortable: true, + isVisible: false + }, + { + name: 'sizeOnDisk', + label: 'Size on Disk', + isSortable: true, + isVisible: false + }, + { + name: 'tags', + label: 'Tags', + isSortable: false, + isVisible: false + }, + { + name: 'useSceneNumbering', + label: 'Scene Numbering', + isSortable: true, + isVisible: false + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ], + + sortPredicates: { + network: function(item) { + const network = item.network; + + return network ? network.toLowerCase() : ''; + }, + + nextAiring: function(item, direction) { + const nextAiring = item.nextAiring; + + if (nextAiring) { + return moment(nextAiring).unix(); + } + + if (direction === sortDirections.DESCENDING) { + return 0; + } + + return Number.MAX_VALUE; + }, + + episodeProgress: function(item) { + const { + episodeCount = 0, + episodeFileCount + } = item; + + const progress = episodeCount ? episodeFileCount / episodeCount * 100 : 100; + + return progress + episodeCount / 1000000; + } + }, + + filterPredicates: { + missing: function(item) { + return item.episodeCount - item.episodeFileCount > 0; + } + } +}; + +export const persistState = [ + 'seriesIndex.sortKey', + 'seriesIndex.sortDirection', + 'seriesIndex.filterKey', + 'seriesIndex.filterValue', + 'seriesIndex.filterType', + 'seriesIndex.view', + 'seriesIndex.columns', + 'seriesIndex.posterOptions' +]; + +const reducerSection = 'seriesIndex'; + +const artistIndexReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_ARTIST_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_ARTIST_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection), + + [types.SET_ARTIST_VIEW]: function(state, { payload }) { + return Object.assign({}, state, { view: payload.view }); + }, + + [types.SET_ARTIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), + + [types.SET_ARTIST_POSTER_OPTION]: function(state, { payload }) { + const posterOptions = state.posterOptions; + + return { + ...state, + posterOptions: { + ...posterOptions, + ...payload + } + }; + } + +}, defaultState); + +export default artistIndexReducers; diff --git a/frontend/src/Store/Reducers/blacklistReducers.js b/frontend/src/Store/Reducers/blacklistReducers.js new file mode 100644 index 000000000..e7cab587d --- /dev/null +++ b/frontend/src/Store/Reducers/blacklistReducers.js @@ -0,0 +1,80 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +const reducerSection = 'blacklist'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + error: null, + items: [], + + columns: [ + { + name: 'series.sortTitle', + label: 'Series Title', + isSortable: true, + isVisible: true + }, + { + name: 'sourceTitle', + label: 'Source Title', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'indexer', + label: 'Indexer', + isSortable: true, + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'blacklist.pageSize', + 'blacklist.sortKey', + 'blacklist.sortDirection', + 'blacklist.columns' +]; + +const blacklistReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(reducerSection), + [types.SET_BLACKLIST_TABLE_OPTION]: createSetTableOptionReducer(reducerSection) + +}, defaultState); + +export default blacklistReducers; diff --git a/frontend/src/Store/Reducers/calendarReducers.js b/frontend/src/Store/Reducers/calendarReducers.js new file mode 100644 index 000000000..4dea88e97 --- /dev/null +++ b/frontend/src/Store/Reducers/calendarReducers.js @@ -0,0 +1,48 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + start: null, + end: null, + dates: [], + dayCount: 7, + view: window.innerWidth > 768 ? 'week' : 'day', + unmonitored: false, + showUpcoming: true, + error: null, + items: [] +}; + +export const persistState = [ + 'calendar.view', + 'calendar.unmonitored', + 'calendar.showUpcoming' +]; + +const section = 'calendar'; + +const calendarReducers = handleActions({ + + [types.SET]: createSetReducer(section), + [types.UPDATE]: createUpdateReducer(section), + [types.UPDATE_ITEM]: createUpdateItemReducer(section), + + [types.CLEAR_CALENDAR]: (state) => { + const { + view, + unmonitored, + showUpcoming, + ...otherDefaultState + } = defaultState; + + return Object.assign({}, state, otherDefaultState); + } + +}, defaultState); + +export default calendarReducers; diff --git a/frontend/src/Store/Reducers/captchaReducers.js b/frontend/src/Store/Reducers/captchaReducers.js new file mode 100644 index 000000000..67372839f --- /dev/null +++ b/frontend/src/Store/Reducers/captchaReducers.js @@ -0,0 +1,32 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +export const defaultState = { + refreshing: false, + token: null, + siteKey: null, + secretToken: null, + ray: null, + stoken: null, + responseUrl: null +}; + +const section = 'captcha'; + +const captchaReducers = handleActions({ + + [types.SET_CAPTCHA_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [types.RESET_CAPTCHA]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState); + +export default captchaReducers; diff --git a/frontend/src/Store/Reducers/commandReducers.js b/frontend/src/Store/Reducers/commandReducers.js new file mode 100644 index 000000000..b2b474e65 --- /dev/null +++ b/frontend/src/Store/Reducers/commandReducers.js @@ -0,0 +1,64 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + handlers: {} +}; + +const reducerSection = 'commands'; + +const commandReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + + [types.ADD_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items, payload]; + + return newState; + }, + + [types.REMOVE_COMMAND]: (state, { payload }) => { + const newState = Object.assign({}, state); + newState.items = [...state.items]; + + const index = _.findIndex(newState.items, { id: payload.id }); + + if (index > -1) { + newState.items.splice(index, 1); + } + + return newState; + }, + + [types.REGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.handlers[payload.key] = { + name: payload.name, + handler: payload.handler + }; + + return newState; + }, + + [types.UNREGISTER_FINISH_COMMAND_HANDLER]: (state, { payload }) => { + const newState = Object.assign({}, state); + delete newState.handlers[payload.key]; + + return newState; + } + +}, defaultState); + +export default commandReducers; diff --git a/frontend/src/Store/Reducers/episodeFileReducers.js b/frontend/src/Store/Reducers/episodeFileReducers.js new file mode 100644 index 000000000..904175863 --- /dev/null +++ b/frontend/src/Store/Reducers/episodeFileReducers.js @@ -0,0 +1,34 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isSaving: false, + saveError: null, + items: [] +}; + +const reducerSection = 'episodeFiles'; + +const episodeFileReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.CLEAR_EPISODE_FILES]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default episodeFileReducers; diff --git a/frontend/src/Store/Reducers/episodeHistoryReducers.js b/frontend/src/Store/Reducers/episodeHistoryReducers.js new file mode 100644 index 000000000..6bf246cb1 --- /dev/null +++ b/frontend/src/Store/Reducers/episodeHistoryReducers.js @@ -0,0 +1,26 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'episodeHistory'; + +const episodeHistoryReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_EPISODE_HISTORY]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default episodeHistoryReducers; diff --git a/frontend/src/Store/Reducers/episodeReducers.js b/frontend/src/Store/Reducers/episodeReducers.js new file mode 100644 index 000000000..4c21e4fc3 --- /dev/null +++ b/frontend/src/Store/Reducers/episodeReducers.js @@ -0,0 +1,106 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + sortKey: 'episodeNumber', + sortDirection: sortDirections.DESCENDING, + items: [], + + columns: [ + { + name: 'monitored', + columnLabel: 'Monitored', + isVisible: true, + isModifiable: false + }, + { + name: 'episodeNumber', + label: '#', + isVisible: true + }, + { + name: 'title', + label: 'Title', + isVisible: true + }, + { + name: 'path', + label: 'Path', + isVisible: false + }, + { + name: 'relativePath', + label: 'Relative Path', + isVisible: false + }, + { + name: 'airDateUtc', + label: 'Air Date', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'audioInfo', + label: 'Audio Info', + isVisible: false + }, + { + name: 'videoCodec', + label: 'Video Codec', + isVisible: false + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'episodes.columns' +]; + +const reducerSection = 'episodes'; + +const episodeReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + + [types.SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(reducerSection), + + [types.CLEAR_EPISODES]: (state) => { + return Object.assign({}, state, { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }); + }, + + [types.SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) + +}, defaultState); + +export default episodeReducers; diff --git a/frontend/src/Store/Reducers/historyReducers.js b/frontend/src/Store/Reducers/historyReducers.js new file mode 100644 index 000000000..7ebca5212 --- /dev/null +++ b/frontend/src/Store/Reducers/historyReducers.js @@ -0,0 +1,113 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + pageSize: 20, + sortKey: 'date', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + items: [], + + columns: [ + { + name: 'eventType', + columnLabel: 'Event Type', + isVisible: true, + isModifiable: false + }, + { + name: 'series.sortTitle', + label: 'Series', + isSortable: true, + isVisible: true + }, + { + name: 'episode', + label: 'Episode', + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Episode Title', + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'quality', + label: 'Quality', + isVisible: true + }, + { + name: 'date', + label: 'Date', + isSortable: true, + isVisible: true + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'releaseGroup', + label: 'Release Group', + isVisible: false + }, + { + name: 'details', + columnLabel: 'Details', + isVisible: true, + isModifiable: false + } + ] +}; + +export const persistState = [ + 'history.pageSize', + 'history.sortKey', + 'history.sortDirection', + 'history.filterKey', + 'history.filterValue' +]; + +const serverSideCollectionName = 'history'; + +const historyReducers = handleActions({ + + [types.SET]: createSetReducer(serverSideCollectionName), + [types.UPDATE]: createUpdateReducer(serverSideCollectionName), + [types.UPDATE_ITEM]: createUpdateItemReducer(serverSideCollectionName), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(serverSideCollectionName), + + [types.SET_HISTORY_TABLE_OPTION]: createSetTableOptionReducer(serverSideCollectionName), + + [types.CLEAR_HISTORY]: createClearReducer('history', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState); + +export default historyReducers; diff --git a/frontend/src/Store/Reducers/importSeriesReducers.js b/frontend/src/Store/Reducers/importSeriesReducers.js new file mode 100644 index 000000000..9aa86697b --- /dev/null +++ b/frontend/src/Store/Reducers/importSeriesReducers.js @@ -0,0 +1,35 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isImporting: false, + isImported: false, + importError: null, + items: [] +}; + +const reducerSection = 'importSeries'; + +const importSeriesReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.CLEAR_IMPORT_SERIES]: function(state) { + return Object.assign({}, state, defaultState); + }, + + [types.SET_IMPORT_SERIES_VALUE]: createUpdateItemReducer(reducerSection) + +}, defaultState); + +export default importSeriesReducers; diff --git a/frontend/src/Store/Reducers/index.js b/frontend/src/Store/Reducers/index.js new file mode 100644 index 000000000..4d6f91942 --- /dev/null +++ b/frontend/src/Store/Reducers/index.js @@ -0,0 +1,88 @@ +import { combineReducers } from 'redux'; +import { enableBatching } from 'redux-batched-actions'; +import { routerReducer } from 'react-router-redux'; +import app, { defaultState as defaultappState } from './appReducers'; +import addSeries, { defaultState as defaultAddSeriesState } from './addSeriesReducers'; +import importSeries, { defaultState as defaultImportSeriesState } from './importSeriesReducers'; +import series, { defaultState as defaultSeriesState } from './seriesReducers'; +import seriesIndex, { defaultState as defaultSeriesIndexState } from './artistIndexReducers'; +import seriesEditor, { defaultState as defaultSeriesEditorState } from './seriesEditorReducers'; +import seasonPass, { defaultState as defaultSeasonPassState } from './seasonPassReducers'; +import calendar, { defaultState as defaultCalendarState } from './calendarReducers'; +import history, { defaultState as defaultHistoryState } from './historyReducers'; +import queue, { defaultState as defaultQueueState } from './queueReducers'; +import blacklist, { defaultState as defaultBlacklistState } from './blacklistReducers'; +import episodes, { defaultState as defaultEpisodesState } from './episodeReducers'; +import episodeFiles, { defaultState as defaultEpisodeFilesState } from './episodeFileReducers'; +import episodeHistory, { defaultState as defaultEpisodeHistoryState } from './episodeHistoryReducers'; +import releases, { defaultState as defaultReleasesState } from './releaseReducers'; +import wanted, { defaultState as defaultWantedState } from './wantedReducers'; +import settings, { defaultState as defaultSettingsState } from './settingsReducers'; +import system, { defaultState as defaultSystemState } from './systemReducers'; +import commands, { defaultState as defaultCommandsState } from './commandReducers'; +import paths, { defaultState as defaultPathsState } from './pathReducers'; +import tags, { defaultState as defaultTagsState } from './tagReducers'; +import captcha, { defaultState as defaultCaptchaState } from './captchaReducers'; +import oAuth, { defaultState as defaultOAuthState } from './oAuthReducers'; +import interactiveImport, { defaultState as defaultInteractiveImportState } from './interactiveImportReducers'; +import rootFolders, { defaultState as defaultRootFoldersState } from './rootFolderReducers'; +import organizePreview, { defaultState as defaultOrganizePreviewState } from './organizePreviewReducers'; + +export const defaultState = { + app: defaultappState, + addSeries: defaultAddSeriesState, + importSeries: defaultImportSeriesState, + series: defaultSeriesState, + seriesIndex: defaultSeriesIndexState, + seriesEditor: defaultSeriesEditorState, + seasonPass: defaultSeasonPassState, + calendar: defaultCalendarState, + history: defaultHistoryState, + queue: defaultQueueState, + blacklist: defaultBlacklistState, + episodes: defaultEpisodesState, + episodeFiles: defaultEpisodeFilesState, + episodeHistory: defaultEpisodeHistoryState, + releases: defaultReleasesState, + wanted: defaultWantedState, + settings: defaultSettingsState, + system: defaultSystemState, + commands: defaultCommandsState, + paths: defaultPathsState, + tags: defaultTagsState, + captcha: defaultCaptchaState, + oAuth: defaultOAuthState, + interactiveImport: defaultInteractiveImportState, + rootFolders: defaultRootFoldersState, + organizePreview: defaultOrganizePreviewState +}; + +export default enableBatching(combineReducers({ + app, + addSeries, + importSeries, + series, + seriesIndex, + seriesEditor, + seasonPass, + calendar, + history, + queue, + blacklist, + episodes, + episodeFiles, + episodeHistory, + releases, + wanted, + settings, + system, + commands, + paths, + tags, + captcha, + oAuth, + interactiveImport, + rootFolders, + organizePreview, + routing: routerReducer +})); diff --git a/frontend/src/Store/Reducers/interactiveImportReducers.js b/frontend/src/Store/Reducers/interactiveImportReducers.js new file mode 100644 index 000000000..d0eea157d --- /dev/null +++ b/frontend/src/Store/Reducers/interactiveImportReducers.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; +import moment from 'moment'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'quality', + sortDirection: sortDirections.DESCENDING, + recentFolders: [], + importMode: 'move', + sortPredicates: { + series: function(item, direction) { + const series = item.series; + + return series ? series.sortTitle : ''; + }, + + quality: function(item, direction) { + return item.quality.qualityWeight; + } + } +}; + +export const persistState = [ + 'interactiveImport.recentFolders', + 'interactiveImport.importMode' +]; + +const reducerSection = 'interactiveImport'; + +const interactiveImportReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.UPDATE_INTERACTIVE_IMPORT_ITEM]: (state, { payload }) => { + const id = payload.id; + const newState = Object.assign({}, state); + const items = newState.items; + const index = _.findIndex(items, { id }); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [types.ADD_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolder = { folder, lastUsed: moment().toISOString() }; + const recentFolders = [...state.recentFolders]; + const index = _.findIndex(recentFolders, { folder }); + + if (index > -1) { + recentFolders.splice(index, 1, recentFolder); + } else { + recentFolders.push(recentFolder); + } + + return Object.assign({}, state, { recentFolders }); + }, + + [types.REMOVE_RECENT_FOLDER]: function(state, { payload }) { + const folder = payload.folder; + const recentFolders = _.remove([...state.recentFolders], { folder }); + + return Object.assign({}, state, { recentFolders }); + }, + + [types.CLEAR_INTERACTIVE_IMPORT]: function(state) { + const newState = { + ...defaultState, + recentFolders: state.recentFolders, + importMode: state.importMode + }; + + return newState; + }, + + [types.SET_INTERACTIVE_IMPORT_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + + [types.SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) { + return Object.assign({}, state, { importMode: payload.importMode }); + } + +}, defaultState); + +export default interactiveImportReducers; diff --git a/frontend/src/Store/Reducers/oAuthReducers.js b/frontend/src/Store/Reducers/oAuthReducers.js new file mode 100644 index 000000000..291faf285 --- /dev/null +++ b/frontend/src/Store/Reducers/oAuthReducers.js @@ -0,0 +1,28 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +export const defaultState = { + authorizing: false, + accessToken: null, + accessTokenSecret: null +}; + +const section = 'oAuth'; + +const oAuthReducers = handleActions({ + + [types.SET_OAUTH_VALUE]: function(state, { payload }) { + const newState = Object.assign(getSectionState(state, section), payload); + + return updateSectionState(state, section, newState); + }, + + [types.RESET_OAUTH]: function(state) { + return updateSectionState(state, section, defaultState); + } + +}, defaultState); + +export default oAuthReducers; diff --git a/frontend/src/Store/Reducers/organizePreviewReducers.js b/frontend/src/Store/Reducers/organizePreviewReducers.js new file mode 100644 index 000000000..c4e182c09 --- /dev/null +++ b/frontend/src/Store/Reducers/organizePreviewReducers.js @@ -0,0 +1,26 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'organizePreview'; + +const organizePreviewReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_ORGANIZE_PREVIEW]: (state) => { + return Object.assign({}, state, defaultState); + } + +}, defaultState); + +export default organizePreviewReducers; diff --git a/frontend/src/Store/Reducers/pathReducers.js b/frontend/src/Store/Reducers/pathReducers.js new file mode 100644 index 000000000..c10ad89c7 --- /dev/null +++ b/frontend/src/Store/Reducers/pathReducers.js @@ -0,0 +1,45 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; + +export const defaultState = { + currentPath: '', + isPopulated: false, + isFetching: false, + error: null, + directories: [], + files: [], + parent: null +}; + +const reducerSection = 'paths'; + +const pathReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.UPDATE_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.currentPath = payload.path; + newState.directories = payload.directories; + newState.files = payload.files; + newState.parent = payload.parent; + + return newState; + }, + + [types.CLEAR_PATHS]: (state, { payload }) => { + const newState = Object.assign({}, state); + + newState.path = ''; + newState.directories = []; + newState.files = []; + newState.parent = ''; + + return newState; + } + +}, defaultState); + +export default pathReducers; diff --git a/frontend/src/Store/Reducers/queueReducers.js b/frontend/src/Store/Reducers/queueReducers.js new file mode 100644 index 000000000..b313193c2 --- /dev/null +++ b/frontend/src/Store/Reducers/queueReducers.js @@ -0,0 +1,165 @@ +import { handleActions } from 'redux-actions'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import { sortDirections } from 'Helpers/Props'; +import * as types from 'Store/Actions/actionTypes'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createReducers from './Creators/createReducers'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; + +export const defaultState = { + queueStatus: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + details: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + params: {} + }, + + paged: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'timeleft', + sortDirection: sortDirections.ASCENDING, + error: null, + items: [], + isGrabbing: false, + isRemoving: false, + + columns: [ + { + name: 'status', + columnLabel: 'Status', + isVisible: true, + isModifiable: false + }, + { + name: 'series.sortTitle', + label: 'Series', + isSortable: true, + isVisible: true + }, + { + name: 'episode', + label: 'Episode', + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Episode Title', + isVisible: true + }, + { + name: 'quality', + label: 'Quality', + isSortable: true, + isVisible: true + }, + { + name: 'protocol', + label: 'Protocol', + isVisible: false + }, + { + name: 'indexer', + label: 'Indexer', + isVisible: false + }, + { + name: 'downloadClient', + label: 'Download Client', + isVisible: false + }, + { + name: 'estimatedCompletionTime', + label: 'Timeleft', + isSortable: true, + isVisible: true + }, + { + name: 'progress', + label: 'Progress', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + }, + + queueEpisodes: { + isPopulated: false, + items: [] + } +}; + +export const persistState = [ + 'queue.paged.pageSize', + 'queue.paged.sortKey', + 'queue.paged.sortDirection', + 'queue.paged.columns' +]; + +const propertyNames = [ + 'queueStatus', + 'details', + 'episodes' +]; + +const paged = 'paged'; + +const queueReducers = handleActions({ + + [types.SET]: createReducers([...propertyNames, paged], createSetReducer), + [types.UPDATE]: createReducers([...propertyNames, paged], createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(['queueEpisodes', paged], createUpdateItemReducer), + + [types.CLEAR_QUEUE_DETAILS]: createClearReducer('details', defaultState.details), + + [types.UPDATE_SERVER_SIDE_COLLECTION]: createUpdateServerSideCollectionReducer(paged), + + [types.SET_QUEUE_TABLE_OPTION]: createSetTableOptionReducer(paged), + + [types.CLEAR_QUEUE]: createClearReducer('paged', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }), + + [types.SET_QUEUE_EPISODES]: function(state, { payload }) { + const section = 'queueEpisodes'; + + return updateSectionState(state, section, { + isPopulated: true, + items: payload.episodes + }); + }, + + [types.CLEAR_EPISODES]: (state) => { + const section = 'queueEpisodes'; + + return updateSectionState(state, section, { + isPopulated: false, + items: [] + }); + } + +}, defaultState); + +export default queueReducers; diff --git a/frontend/src/Store/Reducers/releaseReducers.js b/frontend/src/Store/Reducers/releaseReducers.js new file mode 100644 index 000000000..2f55929d6 --- /dev/null +++ b/frontend/src/Store/Reducers/releaseReducers.js @@ -0,0 +1,65 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [], + sortKey: 'releaseWeight', + sortDirection: sortDirections.ASCENDING, + sortPredicates: { + peers: function(item, direction) { + const seeders = item.seeders || 0; + const leechers = item.leechers || 0; + + return seeders * 1000000 + leechers; + }, + + rejections: function(item, direction) { + const rejections = item.rejections; + const releaseWeight = item.releaseWeight; + + if (rejections.length !== 0) { + return releaseWeight + 1000000; + } + + return releaseWeight; + } + } +}; + +const reducerSection = 'releases'; + +const releaseReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + + [types.CLEAR_RELEASES]: (state) => { + return Object.assign({}, state, defaultState); + }, + + [types.UPDATE_RELEASE]: (state, { payload }) => { + const guid = payload.guid; + const newState = Object.assign({}, state); + const items = newState.items; + const index = _.findIndex(items, { guid }); + const item = Object.assign({}, items[index], payload); + + newState.items = [...items]; + newState.items.splice(index, 1, item); + + return newState; + }, + + [types.SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(reducerSection) + +}, defaultState); + +export default releaseReducers; diff --git a/frontend/src/Store/Reducers/rootFolderReducers.js b/frontend/src/Store/Reducers/rootFolderReducers.js new file mode 100644 index 000000000..bd5c18bfa --- /dev/null +++ b/frontend/src/Store/Reducers/rootFolderReducers.js @@ -0,0 +1,28 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [] +}; + +const reducerSection = 'rootFolders'; + +const rootFolderReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection) + +}, defaultState); + +export default rootFolderReducers; diff --git a/frontend/src/Store/Reducers/seasonPassReducers.js b/frontend/src/Store/Reducers/seasonPassReducers.js new file mode 100644 index 000000000..c09a776b4 --- /dev/null +++ b/frontend/src/Store/Reducers/seasonPassReducers.js @@ -0,0 +1,39 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + isSaving: false, + saveError: null, + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortTitle', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'seasonPass.sortKey', + 'seasonPass.sortDirection', + 'seasonPass.filterKey', + 'seasonPass.filterValue', + 'seasonPass.filterType' +]; + +const reducerSection = 'seasonPass'; + +const seasonPassReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_SEASON_PASS_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_SEASON_PASS_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) + +}, defaultState); + +export default seasonPassReducers; diff --git a/frontend/src/Store/Reducers/seriesEditorReducers.js b/frontend/src/Store/Reducers/seriesEditorReducers.js new file mode 100644 index 000000000..8b6e81411 --- /dev/null +++ b/frontend/src/Store/Reducers/seriesEditorReducers.js @@ -0,0 +1,41 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { filterTypes, sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetClientSideCollectionSortReducer from './Creators/createSetClientSideCollectionSortReducer'; +import createSetClientSideCollectionFilterReducer from './Creators/createSetClientSideCollectionFilterReducer'; + +export const defaultState = { + isSaving: false, + saveError: null, + isDeleting: false, + deleteError: null, + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + secondarySortKey: 'sortTitle', + secondarySortDirection: sortDirections.ASCENDING, + filterKey: null, + filterValue: null, + filterType: filterTypes.EQUAL +}; + +export const persistState = [ + 'seriesEditor.sortKey', + 'seriesEditor.sortDirection', + 'seriesEditor.filterKey', + 'seriesEditor.filterValue', + 'seriesEditor.filterType' +]; + +const reducerSection = 'seriesEditor'; + +const seriesEditorReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + + [types.SET_SERIES_EDITOR_SORT]: createSetClientSideCollectionSortReducer(reducerSection), + [types.SET_SERIES_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(reducerSection) + +}, defaultState); + +export default seriesEditorReducers; diff --git a/frontend/src/Store/Reducers/seriesReducers.js b/frontend/src/Store/Reducers/seriesReducers.js new file mode 100644 index 000000000..79f274373 --- /dev/null +++ b/frontend/src/Store/Reducers/seriesReducers.js @@ -0,0 +1,37 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + sortKey: 'sortTitle', + sortDirection: sortDirections.ASCENDING, + pendingChanges: {} +}; + +const reducerSection = 'series'; + +const seriesReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection), + [types.UPDATE_ITEM]: createUpdateItemReducer(reducerSection), + [types.REMOVE_ITEM]: createRemoveItemReducer(reducerSection), + + [types.SET_ARTIST_VALUE]: createSetSettingValueReducer(reducerSection), + [types.CLEAR_PENDING_CHANGES]: createClearPendingChangesReducer(reducerSection) + +}, defaultState); + +export default seriesReducers; diff --git a/frontend/src/Store/Reducers/settingsReducers.js b/frontend/src/Store/Reducers/settingsReducers.js new file mode 100644 index 000000000..73b4b0055 --- /dev/null +++ b/frontend/src/Store/Reducers/settingsReducers.js @@ -0,0 +1,337 @@ +import _ from 'lodash'; +import { handleActions } from 'redux-actions'; +import getSectionState from 'Utilities/State/getSectionState'; +import selectProviderSchema from 'Utilities/State/selectProviderSchema'; +import updateSectionState from 'Utilities/State/updateSectionState'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createSetSettingValueReducer from './Creators/createSetSettingValueReducer'; +import createSetProviderFieldValueReducer from './Creators/createSetProviderFieldValueReducer'; +import createClearPendingChangesReducer from './Creators/createClearPendingChangesReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createRemoveItemReducer from './Creators/createRemoveItemReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + ui: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + mediaManagement: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + naming: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + namingExamples: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + qualityProfiles: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + languageProfiles: { + isFetching: false, + isPopulated: false, + error: null, + isDeleting: false, + deleteError: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: {}, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + delayProfiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + qualityDefinitions: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + indexers: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + indexerOptions: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + restrictions: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + downloadClients: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + downloadClientOptions: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + remotePathMappings: { + isFetching: false, + isPopulated: false, + error: null, + items: [], + isSaving: false, + saveError: null, + pendingChanges: {} + }, + + notifications: { + isFetching: false, + isPopulated: false, + error: null, + isFetchingSchema: false, + schemaPopulated: false, + schemaError: null, + schema: [], + selectedSchema: {}, + isSaving: false, + saveError: null, + isTesting: false, + items: [], + pendingChanges: {} + }, + + metadata: { + isFetching: false, + isPopulated: false, + error: null, + isSaving: false, + saveError: null, + items: [], + pendingChanges: {} + }, + + general: { + isFetching: false, + isPopulated: false, + error: null, + pendingChanges: {}, + isSaving: false, + saveError: null, + item: {} + }, + + advancedSettings: false +}; + +export const persistState = [ + 'settings.advancedSettings' +]; + +const propertyNames = [ + 'ui', + 'mediaManagement', + 'naming', + 'namingExamples', + 'qualityDefinitions', + 'indexerOptions', + 'downloadClientOptions', + 'general' +]; + +const providerPropertyNames = [ + 'qualityProfiles', + 'languageProfiles', + 'delayProfiles', + 'indexers', + 'restrictions', + 'downloadClients', + 'remotePathMappings', + 'notifications', + 'metadata' +]; + +const settingsReducers = handleActions({ + + [types.TOGGLE_ADVANCED_SETTINGS]: (state, { payload }) => { + return Object.assign({}, state, { advancedSettings: !state.advancedSettings }); + }, + + [types.SET]: createReducers([...propertyNames, ...providerPropertyNames], createSetReducer), + [types.UPDATE]: createReducers([...propertyNames, ...providerPropertyNames], createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(providerPropertyNames, createUpdateItemReducer), + [types.CLEAR_PENDING_CHANGES]: createReducers([...propertyNames, ...providerPropertyNames], createClearPendingChangesReducer), + + [types.REMOVE_ITEM]: createReducers(providerPropertyNames, createRemoveItemReducer), + + [types.SET_UI_SETTINGS_VALUE]: createSetSettingValueReducer('ui'), + [types.SET_MEDIA_MANAGEMENT_SETTINGS_VALUE]: createSetSettingValueReducer('mediaManagement'), + [types.SET_NAMING_SETTINGS_VALUE]: createSetSettingValueReducer('naming'), + [types.SET_QUALITY_PROFILE_VALUE]: createSetSettingValueReducer('qualityProfiles'), + [types.SET_LANGUAGE_PROFILE_VALUE]: createSetSettingValueReducer('languageProfiles'), + [types.SET_DELAY_PROFILE_VALUE]: createSetSettingValueReducer('delayProfiles'), + + [types.SET_QUALITY_DEFINITION_VALUE]: function(state, { payload }) { + const section = 'qualityDefinitions'; + const { id, name, value } = payload; + const newState = getSectionState(state, section); + newState.pendingChanges = _.cloneDeep(newState.pendingChanges); + + const pendingState = newState.pendingChanges[id] || {}; + const currentValue = _.find(newState.items, { id })[name]; + + if (currentValue === value) { + delete pendingState[name]; + } else { + pendingState[name] = value; + } + + if (_.isEmpty(pendingState)) { + delete newState.pendingChanges[id]; + } else { + newState.pendingChanges[id] = pendingState; + } + + return updateSectionState(state, section, newState); + }, + + [types.SET_INDEXER_VALUE]: createSetSettingValueReducer('indexers'), + [types.SET_INDEXER_FIELD_VALUE]: createSetProviderFieldValueReducer('indexers'), + [types.SET_INDEXER_OPTIONS_VALUE]: createSetSettingValueReducer('indexerOptions'), + [types.SET_RESTRICTION_VALUE]: createSetSettingValueReducer('restrictions'), + + [types.SELECT_INDEXER_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'indexers', payload, (selectedSchema) => { + selectedSchema.enableRss = selectedSchema.supportsRss; + selectedSchema.enableSearch = selectedSchema.supportsSearch; + + return selectedSchema; + }); + }, + + [types.SET_DOWNLOAD_CLIENT_VALUE]: createSetSettingValueReducer('downloadClients'), + [types.SET_DOWNLOAD_CLIENT_FIELD_VALUE]: createSetProviderFieldValueReducer('downloadClients'), + + [types.SELECT_DOWNLOAD_CLIENT_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'downloadClients', payload, (selectedSchema) => { + selectedSchema.enable = true; + + return selectedSchema; + }); + }, + + [types.SET_DOWNLOAD_CLIENT_OPTIONS_VALUE]: createSetSettingValueReducer('downloadClientOptions'), + [types.SET_REMOTE_PATH_MAPPING_VALUE]: createSetSettingValueReducer('remotePathMappings'), + + [types.SET_NOTIFICATION_VALUE]: createSetSettingValueReducer('notifications'), + [types.SET_NOTIFICATION_FIELD_VALUE]: createSetProviderFieldValueReducer('notifications'), + + [types.SELECT_NOTIFICATION_SCHEMA]: function(state, { payload }) { + return selectProviderSchema(state, 'notifications', payload, (selectedSchema) => { + selectedSchema.onGrab = selectedSchema.supportsOnGrab; + selectedSchema.onDownload = selectedSchema.supportsOnDownload; + selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade; + selectedSchema.onRename = selectedSchema.supportsOnRename; + + return selectedSchema; + }); + }, + + [types.SET_METADATA_VALUE]: createSetSettingValueReducer('metadata'), + [types.SET_METADATA_FIELD_VALUE]: createSetProviderFieldValueReducer('metadata'), + + [types.SET_GENERAL_SETTINGS_VALUE]: createSetSettingValueReducer('general') + +}, defaultState); + +export default settingsReducers; diff --git a/frontend/src/Store/Reducers/systemReducers.js b/frontend/src/Store/Reducers/systemReducers.js new file mode 100644 index 000000000..31aa2ab08 --- /dev/null +++ b/frontend/src/Store/Reducers/systemReducers.js @@ -0,0 +1,146 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + status: { + isFetching: false, + isPopulated: false, + error: null, + item: {} + }, + + health: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + diskSpace: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + tasks: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + backups: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updates: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + logs: { + isFetching: false, + isPopulated: false, + pageSize: 50, + sortKey: 'time', + sortDirection: sortDirections.DESCENDING, + filterKey: null, + filterValue: null, + error: null, + items: [], + + columns: [ + { + name: 'level', + isSortable: true, + isVisible: true + }, + { + name: 'logger', + label: 'Component', + isSortable: true, + isVisible: true + }, + { + name: 'message', + label: 'Message', + isVisible: true + }, + { + name: 'time', + label: 'Time', + isSortable: true, + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isSortable: true, + isVisible: true, + isModifiable: false + } + ] + }, + + logFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }, + + updateLogFiles: { + isFetching: false, + isPopulated: false, + error: null, + items: [] + } +}; + +export const persistState = [ + 'system.logs.pageSize', + 'system.logs.sortKey', + 'system.logs.sortDirection', + 'system.logs.filterKey', + 'system.logs.filterValue' +]; + +const collectionNames = [ + 'health', + 'diskSpace', + 'tasks', + 'backups', + 'updates', + 'logFiles', + 'updateLogFiles' +]; + +const serverSideCollectionNames = [ + 'logs' +]; + +const systemReducers = handleActions({ + + [types.SET]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createSetReducer), + [types.UPDATE]: createReducers(['status', ...collectionNames, ...serverSideCollectionNames], createUpdateReducer), + [types.UPDATE_ITEM]: createUpdateItemReducer('tasks'), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), + + [types.SET_LOGS_TABLE_OPTION]: createSetTableOptionReducer('logs') + +}, defaultState); + +export default systemReducers; diff --git a/frontend/src/Store/Reducers/tagReducers.js b/frontend/src/Store/Reducers/tagReducers.js new file mode 100644 index 000000000..6aa822fd7 --- /dev/null +++ b/frontend/src/Store/Reducers/tagReducers.js @@ -0,0 +1,22 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import createSetReducer from './Creators/createSetReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; + +export const defaultState = { + isFetching: false, + isPopulated: false, + error: null, + items: [] +}; + +const reducerSection = 'tags'; + +const tagReducers = handleActions({ + + [types.SET]: createSetReducer(reducerSection), + [types.UPDATE]: createUpdateReducer(reducerSection) + +}, defaultState); + +export default tagReducers; diff --git a/frontend/src/Store/Reducers/wantedReducers.js b/frontend/src/Store/Reducers/wantedReducers.js new file mode 100644 index 000000000..950d0f994 --- /dev/null +++ b/frontend/src/Store/Reducers/wantedReducers.js @@ -0,0 +1,161 @@ +import { handleActions } from 'redux-actions'; +import * as types from 'Store/Actions/actionTypes'; +import { sortDirections } from 'Helpers/Props'; +import createClearReducer from './Creators/createClearReducer'; +import createSetReducer from './Creators/createSetReducer'; +import createSetTableOptionReducer from './Creators/createSetTableOptionReducer'; +import createUpdateReducer from './Creators/createUpdateReducer'; +import createUpdateItemReducer from './Creators/createUpdateItemReducer'; +import createUpdateServerSideCollectionReducer from './Creators/createUpdateServerSideCollectionReducer'; +import createReducers from './Creators/createReducers'; + +export const defaultState = { + missing: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'airDateUtc', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: 'true', + error: null, + items: [], + + columns: [ + { + name: 'series.sortTitle', + label: 'Series Title', + isSortable: true, + isVisible: true + }, + { + name: 'episode', + label: 'Episode', + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Episode Title', + isVisible: true + }, + { + name: 'airDateUtc', + label: 'Air Date', + isSortable: true, + isVisible: true + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + }, + + cutoffUnmet: { + isFetching: false, + isPopulated: false, + pageSize: 20, + sortKey: 'airDateUtc', + sortDirection: sortDirections.DESCENDING, + filterKey: 'monitored', + filterValue: true, + error: null, + items: [], + + columns: [ + { + name: 'series.sortTitle', + label: 'Series Title', + isSortable: true, + isVisible: true + }, + { + name: 'episode', + label: 'Episode', + isVisible: true + }, + { + name: 'episodeTitle', + label: 'Episode Title', + isVisible: true + }, + { + name: 'airDateUtc', + label: 'Air Date', + isSortable: true, + isVisible: true + }, + { + name: 'language', + label: 'Language', + isVisible: false + }, + { + name: 'status', + label: 'Status', + isVisible: true + }, + { + name: 'actions', + columnLabel: 'Actions', + isVisible: true, + isModifiable: false + } + ] + } +}; + +export const persistState = [ + 'wanted.missing.pageSize', + 'wanted.missing.sortKey', + 'wanted.missing.sortDirection', + 'wanted.missing.filterKey', + 'wanted.missing.filterValue', + 'wanted.missing.columns', + 'wanted.cutoffUnmet.pageSize', + 'wanted.cutoffUnmet.sortKey', + 'wanted.cutoffUnmet.sortDirection', + 'wanted.cutoffUnmet.filterKey', + 'wanted.cutoffUnmet.filterValue', + 'wanted.cutoffUnmet.columns' +]; + +const serverSideCollectionNames = [ + 'missing', + 'cutoffUnmet' +]; + +const wantedReducers = handleActions({ + + [types.SET]: createReducers(serverSideCollectionNames, createSetReducer), + [types.UPDATE]: createReducers(serverSideCollectionNames, createUpdateReducer), + [types.UPDATE_ITEM]: createReducers(serverSideCollectionNames, createUpdateItemReducer), + [types.UPDATE_SERVER_SIDE_COLLECTION]: createReducers(serverSideCollectionNames, createUpdateServerSideCollectionReducer), + + [types.SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('missing'), + [types.SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('cutoffUnmet'), + + [types.CLEAR_MISSING]: createClearReducer('missing', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }), + + [types.CLEAR_CUTOFF_UNMET]: createClearReducer('cutoffUnmet', { + isFetching: false, + isPopulated: false, + error: null, + items: [] + }) + +}, defaultState); + +export default wantedReducers; diff --git a/frontend/src/Store/Selectors/createAllSeriesSelector.js b/frontend/src/Store/Selectors/createAllSeriesSelector.js new file mode 100644 index 000000000..6a1abdac4 --- /dev/null +++ b/frontend/src/Store/Selectors/createAllSeriesSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createAllSeriesSelector() { + return createSelector( + (state) => state.series, + (series) => { + return series.items; + } + ); +} + +export default createAllSeriesSelector; diff --git a/frontend/src/Store/Selectors/createArtistSelector.js b/frontend/src/Store/Selectors/createArtistSelector.js new file mode 100644 index 000000000..5ed1a473f --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from './createAllSeriesSelector'; + +function createArtistSelector() { + return createSelector( + (state, { artistId }) => artistId, + createAllSeriesSelector(), + (artistId, series) => { + return _.find(series, { id: artistId }); + } + ); +} + +export default createArtistSelector; diff --git a/frontend/src/Store/Selectors/createClientSideCollectionSelector.js b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js new file mode 100644 index 000000000..e5b740afc --- /dev/null +++ b/frontend/src/Store/Selectors/createClientSideCollectionSelector.js @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import { filterTypes, sortDirections } from 'Helpers/Props'; + +const filterTypePredicates = { + [filterTypes.CONTAINS]: function(value, filterValue) { + return value.toLowerCase().indexOf(filterValue.toLowerCase()) > -1; + }, + + [filterTypes.EQUAL]: function(value, filterValue) { + return value === filterValue; + }, + + [filterTypes.GREATER_THAN]: function(value, filterValue) { + return value > filterValue; + }, + + [filterTypes.GREATER_THAN_OR_EQUAL]: function(value, filterValue) { + return value >= filterValue; + }, + + [filterTypes.LESS_THAN]: function(value, filterValue) { + return value < filterValue; + }, + + [filterTypes.LESS_THAN_OR_EQUAL]: function(value, filterValue) { + return value <= filterValue; + }, + + [filterTypes.NOT_EQUAL]: function(value, filterValue) { + return value !== filterValue; + } +}; + +function getSortClause(sortKey, sortDirection, sortPredicates) { + if (sortPredicates && sortPredicates.hasOwnProperty(sortKey)) { + return function(item) { + return sortPredicates[sortKey](item, sortDirection); + }; + } + + return function(item) { + return item[sortKey]; + }; +} + +function filter(items, state) { + const { + filterKey, + filterValue, + filterType, + filterPredicates + } = state; + + if (!filterKey || !filterValue) { + return items; + } + + return _.filter(items, (item) => { + if (filterPredicates && filterPredicates.hasOwnProperty(filterKey)) { + return filterPredicates[filterKey](item); + } + + if (item.hasOwnProperty(filterKey)) { + return filterTypePredicates[filterType](item[filterKey], filterValue); + } + + return false; + }); +} + +function sort(items, state) { + const { + sortKey, + sortDirection, + sortPredicates, + secondarySortKey, + secondarySortDirection + } = state; + + const clauses = []; + const orders = []; + + clauses.push(getSortClause(sortKey, sortDirection, sortPredicates)); + orders.push(sortDirection === sortDirections.ASCENDING ? 'asc' : 'desc'); + + if (secondarySortKey && + secondarySortDirection && + (sortKey !== secondarySortKey || + sortDirection !== secondarySortDirection)) { + clauses.push(getSortClause(secondarySortKey, secondarySortDirection, sortPredicates)); + orders.push(secondarySortDirection === sortDirections.ASCENDING ? 'asc' : 'desc'); + } + + return _.orderBy(items, clauses, orders); +} + +function createClientSideCollectionSelector() { + return createSelector( + (state, { section }) => state[section], + (state, { uiSection }) => state[uiSection], + (sectionState, uiSectionState = {}) => { + const state = Object.assign({}, sectionState, uiSectionState); + + const filtered = filter(state.items, state); + const sorted = sort(filtered, state); + + return { + ...sectionState, + ...uiSectionState, + items: sorted + }; + } + ); +} + +export default createClientSideCollectionSelector; diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.js b/frontend/src/Store/Selectors/createCommandExecutingSelector.js new file mode 100644 index 000000000..ac79194ad --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandExecutingSelector(name) { + return createSelector( + createCommandsSelector(), + (commands) => { + return _.some(commands, { name }); + } + ); +} + +export default createCommandExecutingSelector; diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js new file mode 100644 index 000000000..ff5bfe50a --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createCommandsSelector from './createCommandsSelector'; + +function createCommandSelector(name, contraints = {}) { + return createSelector( + createCommandsSelector(), + (commands) => { + return _.some(commands, { name, ...contraints }); + } + ); +} + +export default createCommandSelector; diff --git a/frontend/src/Store/Selectors/createCommandsSelector.js b/frontend/src/Store/Selectors/createCommandsSelector.js new file mode 100644 index 000000000..7b9edffd9 --- /dev/null +++ b/frontend/src/Store/Selectors/createCommandsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createCommandsSelector() { + return createSelector( + (state) => state.commands, + (commands) => { + return commands.items; + } + ); +} + +export default createCommandsSelector; diff --git a/frontend/src/Store/Selectors/createDimensionsSelector.js b/frontend/src/Store/Selectors/createDimensionsSelector.js new file mode 100644 index 000000000..ce26b2e2c --- /dev/null +++ b/frontend/src/Store/Selectors/createDimensionsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createDimensionsSelector() { + return createSelector( + (state) => state.app.dimensions, + (dimensions) => { + return dimensions; + } + ); +} + +export default createDimensionsSelector; diff --git a/frontend/src/Store/Selectors/createEpisodeFileSelector.js b/frontend/src/Store/Selectors/createEpisodeFileSelector.js new file mode 100644 index 000000000..156c48d95 --- /dev/null +++ b/frontend/src/Store/Selectors/createEpisodeFileSelector.js @@ -0,0 +1,18 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createEpisodeFileSelector() { + return createSelector( + (state, { episodeFileId }) => episodeFileId, + (state) => state.episodeFiles, + (episodeFileId, episodeFiles) => { + if (!episodeFileId) { + return null; + } + + return _.find(episodeFiles.items, { id: episodeFileId }); + } + ); +} + +export default createEpisodeFileSelector; diff --git a/frontend/src/Store/Selectors/createEpisodeSelector.js b/frontend/src/Store/Selectors/createEpisodeSelector.js new file mode 100644 index 000000000..6725cadd9 --- /dev/null +++ b/frontend/src/Store/Selectors/createEpisodeSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import episodeEntities from 'Episode/episodeEntities'; + +function createEpisodeSelector() { + return createSelector( + (state, { episodeId }) => episodeId, + (state, { episodeEntity = episodeEntities.EPISODES }) => _.get(state, episodeEntity, { items: [] }), + (episodeId, episodes) => { + return _.find(episodes.items, { id: episodeId }); + } + ); +} + +export default createEpisodeSelector; diff --git a/frontend/src/Store/Selectors/createExistingSeriesSelector.js b/frontend/src/Store/Selectors/createExistingSeriesSelector.js new file mode 100644 index 000000000..77d18acee --- /dev/null +++ b/frontend/src/Store/Selectors/createExistingSeriesSelector.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from './createAllSeriesSelector'; + +function createExistingSeriesSelector() { + return createSelector( + (state, { tvdbId }) => tvdbId, + createAllSeriesSelector(), + (tvdbId, series) => { + return _.some(series, { tvdbId }); + } + ); +} + +export default createExistingSeriesSelector; diff --git a/frontend/src/Store/Selectors/createImportSeriesItemSelector.js b/frontend/src/Store/Selectors/createImportSeriesItemSelector.js new file mode 100644 index 000000000..dc6c28a05 --- /dev/null +++ b/frontend/src/Store/Selectors/createImportSeriesItemSelector.js @@ -0,0 +1,28 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from './createAllSeriesSelector'; + +function createImportSeriesItemSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.addSeries, + (state) => state.importSeries, + createAllSeriesSelector(), + (id, addSeries, importSeries, series) => { + const item = _.find(importSeries.items, { id }) || {}; + const selectedSeries = item && item.selectedSeries; + const isExistingSeries = !!selectedSeries && _.some(series, { tvdbId: selectedSeries.tvdbId }); + + return { + defaultMonitor: addSeries.defaults.monitor, + defaultQualityProfileId: addSeries.defaults.qualityProfileId, + defaultSeriesType: addSeries.defaults.seriesType, + defaultSeasonFolder: addSeries.defaults.seasonFolder, + ...item, + isExistingSeries + }; + } + ); +} + +export default createImportSeriesItemSelector; diff --git a/frontend/src/Store/Selectors/createLanguageProfileSelector.js b/frontend/src/Store/Selectors/createLanguageProfileSelector.js new file mode 100644 index 000000000..2ad04d506 --- /dev/null +++ b/frontend/src/Store/Selectors/createLanguageProfileSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createLanguageProfileSelector() { + return createSelector( + (state, { languageProfileId }) => languageProfileId, + (state) => state.settings.languageProfiles.items, + (languageProfileId, languageProfiles) => { + return _.find(languageProfiles, { id: languageProfileId }); + } + ); +} + +export default createLanguageProfileSelector; diff --git a/frontend/src/Store/Selectors/createProfileInUseSelector.js b/frontend/src/Store/Selectors/createProfileInUseSelector.js new file mode 100644 index 000000000..540b61d26 --- /dev/null +++ b/frontend/src/Store/Selectors/createProfileInUseSelector.js @@ -0,0 +1,19 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import createAllSeriesSelector from './createAllSeriesSelector'; + +function createProfileInUseSelector(profileProp) { + return createSelector( + (state, { id }) => id, + createAllSeriesSelector(), + (id, series) => { + if (!id) { + return false; + } + + return _.some(series, { [profileProp]: id }); + } + ); +} + +export default createProfileInUseSelector; diff --git a/frontend/src/Store/Selectors/createProviderSettingsSelector.js b/frontend/src/Store/Selectors/createProviderSettingsSelector.js new file mode 100644 index 000000000..c1018b661 --- /dev/null +++ b/frontend/src/Store/Selectors/createProviderSettingsSelector.js @@ -0,0 +1,59 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; + +function createProviderSettingsSelector() { + return createSelector( + (state, { id }) => id, + (state, { section }) => state.settings[section], + (id, section) => { + if (!id) { + const item = _.isArray(section.schema) ? section.selectedSchema : section.schema; + const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError); + + const { + isFetchingSchema: isFetching, + schemaError: error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + return { + isFetching, + error, + isSaving, + saveError, + isTesting, + pendingChanges, + ...settings, + item: settings.settings + }; + } + + const { + isFetching, + error, + isSaving, + saveError, + isTesting, + pendingChanges + } = section; + + const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError); + + return { + isFetching, + error, + isSaving, + saveError, + isTesting, + item: settings.settings, + ...settings + }; + } + ); +} + +export default createProviderSettingsSelector; diff --git a/frontend/src/Store/Selectors/createQualityProfileSelector.js b/frontend/src/Store/Selectors/createQualityProfileSelector.js new file mode 100644 index 000000000..9308d63ac --- /dev/null +++ b/frontend/src/Store/Selectors/createQualityProfileSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createQualityProfileSelector() { + return createSelector( + (state, { qualityProfileId }) => qualityProfileId, + (state) => state.settings.qualityProfiles.items, + (qualityProfileId, qualityProfiles) => { + return _.find(qualityProfiles, { id: qualityProfileId }); + } + ); +} + +export default createQualityProfileSelector; diff --git a/frontend/src/Store/Selectors/createQueueItemSelector.js b/frontend/src/Store/Selectors/createQueueItemSelector.js new file mode 100644 index 000000000..3d1eeb766 --- /dev/null +++ b/frontend/src/Store/Selectors/createQueueItemSelector.js @@ -0,0 +1,20 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createQueueItemSelector() { + return createSelector( + (state, { episodeId }) => episodeId, + (state) => state.queue.details, + (episodeId, details) => { + if (!episodeId) { + return null; + } + + return _.find(details.items, (item) => { + return item.episode.id === episodeId; + }); + } + ); +} + +export default createQueueItemSelector; diff --git a/frontend/src/Store/Selectors/createSettingsSectionSelector.js b/frontend/src/Store/Selectors/createSettingsSectionSelector.js new file mode 100644 index 000000000..7ff0d2708 --- /dev/null +++ b/frontend/src/Store/Selectors/createSettingsSectionSelector.js @@ -0,0 +1,32 @@ +import { createSelector } from 'reselect'; +import selectSettings from 'Store/Selectors/selectSettings'; + +function createSettingsSectionSelector() { + return createSelector( + (state, { section }) => state.settings[section], + (sectionSettings) => { + const { + isFetching, + isPopulated, + error, + item, + pendingChanges, + isSaving, + saveError + } = sectionSettings; + + const settings = selectSettings(item, pendingChanges, saveError); + + return { + isFetching, + isPopulated, + error, + isSaving, + saveError, + ...settings + }; + } + ); +} + +export default createSettingsSectionSelector; diff --git a/frontend/src/Store/Selectors/createSystemStatusSelector.js b/frontend/src/Store/Selectors/createSystemStatusSelector.js new file mode 100644 index 000000000..df586bbb9 --- /dev/null +++ b/frontend/src/Store/Selectors/createSystemStatusSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createSystemStatusSelector() { + return createSelector( + (state) => state.system.status, + (status) => { + return status.item; + } + ); +} + +export default createSystemStatusSelector; diff --git a/frontend/src/Store/Selectors/createTagsSelector.js b/frontend/src/Store/Selectors/createTagsSelector.js new file mode 100644 index 000000000..fbfd91cdb --- /dev/null +++ b/frontend/src/Store/Selectors/createTagsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createTagsSelector() { + return createSelector( + (state) => state.tags.items, + (tags) => { + return tags; + } + ); +} + +export default createTagsSelector; diff --git a/frontend/src/Store/Selectors/createUISettingsSelector.js b/frontend/src/Store/Selectors/createUISettingsSelector.js new file mode 100644 index 000000000..b256d0e98 --- /dev/null +++ b/frontend/src/Store/Selectors/createUISettingsSelector.js @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +function createUISettingsSelector() { + return createSelector( + (state) => state.settings.ui, + (ui) => { + return ui.item; + } + ); +} + +export default createUISettingsSelector; diff --git a/frontend/src/Store/Selectors/selectSettings.js b/frontend/src/Store/Selectors/selectSettings.js new file mode 100644 index 000000000..74e5444c9 --- /dev/null +++ b/frontend/src/Store/Selectors/selectSettings.js @@ -0,0 +1,97 @@ +import _ from 'lodash'; + +function getValidationFailures(saveError) { + if (!saveError || saveError.status !== 400) { + return []; + } + + return _.cloneDeep(saveError.responseJSON); +} + +function mapFailure(failure) { + return { + message: failure.errorMessage, + link: failure.infoLink, + detailedMessage: failure.detailedDescription + }; +} + +function selectSettings(item, pendingChanges, saveError) { + const validationFailures = getValidationFailures(saveError); + + // Merge all settings from the item along with pending + // changes to ensure any settings that were not included + // with the item are included. + const allSettings = Object.assign({}, item, pendingChanges); + + const settings = _.reduce(allSettings, (result, value, key) => { + if (key === 'fields') { + return result; + } + + const setting = { + value: item[key], + errors: _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning; + }), mapFailure), + + warnings: _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning; + }), mapFailure) + }; + + if (pendingChanges.hasOwnProperty(key)) { + setting.previousValue = setting.value; + setting.value = pendingChanges[key]; + setting.pending = true; + } + + result[key] = setting; + return result; + }, {}); + + const fields = _.reduce(item.fields, (result, f) => { + const field = Object.assign({ pending: false }, f); + const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name); + + if (hasPendingFieldChange) { + field.previousValue = field.value; + field.value = pendingChanges.fields[field.name]; + field.pending = true; + } + + field.errors = _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning; + }), mapFailure); + + field.warnings = _.map(_.remove(validationFailures, (failure) => { + return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning; + }), mapFailure); + + result.push(field); + return result; + }, []); + + if (fields.length) { + settings.fields = fields; + } + + const validationErrors = _.filter(validationFailures, (failure) => { + return !failure.isWarning; + }); + + const validationWarnings = _.filter(validationFailures, (failure) => { + return failure.isWarning; + }); + + return { + settings, + validationErrors, + validationWarnings, + hasPendingChanges: !_.isEmpty(pendingChanges), + hasSettings: !_.isEmpty(settings), + pendingChanges + }; +} + +export default selectSettings; diff --git a/frontend/src/Store/connectSection.js b/frontend/src/Store/connectSection.js new file mode 100644 index 000000000..5d309cc5e --- /dev/null +++ b/frontend/src/Store/connectSection.js @@ -0,0 +1,57 @@ +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import getDisplayName from 'Helpers/getDisplayName'; + +function connectSection(mapStateToProps, mapDispatchToProps, mergeProps, options = {}, sectionOptions = {}) { + return function wrap(WrappedComponent) { + const ConnectedComponent = connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(WrappedComponent); + + class Section extends Component { + + // + // Control + + getWrappedInstance = () => { + if (this._wrappedInstance) { + return this._wrappedInstance.getWrappedInstance(); + } + } + + // + // Listeners + + setWrappedInstanceRef = (ref) => { + this._wrappedInstance = ref; + } + + // + // Render + + render() { + if (options.withRef) { + return ( + + ); + } + + return ( + + ); + } + } + + Section.displayName = `Section(${getDisplayName(WrappedComponent)})`; + Section.WrappedComponent = WrappedComponent; + + return Section; + }; +} + +export default connectSection; diff --git a/frontend/src/Store/createAppStore.js b/frontend/src/Store/createAppStore.js new file mode 100644 index 000000000..168bbc954 --- /dev/null +++ b/frontend/src/Store/createAppStore.js @@ -0,0 +1,15 @@ +import { createStore } from 'redux'; +import reducers, { defaultState } from 'Store/Reducers'; +import middlewares from 'Store/Middleware/middlewares'; + +function createAppStore(history) { + const appStore = createStore( + reducers, + defaultState, + middlewares(history) + ); + + return appStore; +} + +export default createAppStore; diff --git a/frontend/src/Store/scrollPositions.js b/frontend/src/Store/scrollPositions.js new file mode 100644 index 000000000..39648c008 --- /dev/null +++ b/frontend/src/Store/scrollPositions.js @@ -0,0 +1,5 @@ +const scrollPositions = { + seriesIndex: 0 +}; + +export default scrollPositions; diff --git a/src/UI/Shared/Styles/clickable.less b/frontend/src/Styles/Mixins/clickable.css similarity index 100% rename from src/UI/Shared/Styles/clickable.less rename to frontend/src/Styles/Mixins/clickable.css diff --git a/frontend/src/Styles/Mixins/cover.css b/frontend/src/Styles/Mixins/cover.css new file mode 100644 index 000000000..b266846d4 --- /dev/null +++ b/frontend/src/Styles/Mixins/cover.css @@ -0,0 +1,8 @@ +.cover { + position: absolute; + top: 0; + left: 0; + display: block; + width: 100%; + height: 100%; +} diff --git a/frontend/src/Styles/Mixins/linkOverlay.css b/frontend/src/Styles/Mixins/linkOverlay.css new file mode 100644 index 000000000..971f96b13 --- /dev/null +++ b/frontend/src/Styles/Mixins/linkOverlay.css @@ -0,0 +1,11 @@ +.linkOverlay { + composes: cover from 'Styles/Mixins/cover.css'; + + pointer-events: none; + user-select: none; + + a, + button { + pointer-events: all; + } +} diff --git a/frontend/src/Styles/Mixins/scroller.css b/frontend/src/Styles/Mixins/scroller.css new file mode 100644 index 000000000..6a982646a --- /dev/null +++ b/frontend/src/Styles/Mixins/scroller.css @@ -0,0 +1,26 @@ +.scrollbar { + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } +} + +.scrollbarTrack { + &&::-webkit-scrollbar-track { + background-color: transparent; + } +} + +.scrollbarThumb { + &::-webkit-scrollbar-thumb { + min-height: 50px; + border: 1px solid transparent; + border-radius: 5px; + background-color: $scrollbarBackgroundColor; + background-clip: padding-box; + + &:hover { + background-color: $scrollbarHoverBackgroundColor; + } + } +} diff --git a/frontend/src/Styles/Mixins/truncate.css b/frontend/src/Styles/Mixins/truncate.css new file mode 100644 index 000000000..aa157178f --- /dev/null +++ b/frontend/src/Styles/Mixins/truncate.css @@ -0,0 +1,18 @@ +/** + * From: https://github.com/suitcss/utils-text/blob/master/lib/text.css + * + * Text truncation + * + * Prevent text from wrapping onto multiple lines, and truncate with an + * ellipsis. + * + * 1. Ensure that the node has a maximum width after which truncation can + * occur. + */ + +.truncate { + overflow: hidden !important; + max-width: 100%; /* 1 */ + text-overflow: ellipsis !important; + white-space: nowrap !important; +} diff --git a/frontend/src/Styles/Variables/animations.js b/frontend/src/Styles/Variables/animations.js new file mode 100644 index 000000000..52d12827a --- /dev/null +++ b/frontend/src/Styles/Variables/animations.js @@ -0,0 +1,8 @@ +// Use CommonJS since this is consumed by PostCSS via webpack (node.js). + +module.exports = { + // Durations + defaultSpeed: '0.2s', + slowSpeed: '0.6s', + fastSpeed: '0.1s' +}; diff --git a/frontend/src/Styles/Variables/colors.js b/frontend/src/Styles/Variables/colors.js new file mode 100644 index 000000000..d045f2c30 --- /dev/null +++ b/frontend/src/Styles/Variables/colors.js @@ -0,0 +1,170 @@ +module.exports = { + defaultColor: '#333', + disabledColor: '#999', + black: '#000', + white: '#fff', + primaryColor: '#0b8750', + selectedColor: '#f9be03', + successColor: '#27c24c', + dangerColor: '#f05050', + warningColor: '#ffa500', + infoColor: '#00A65B', + purple: '#7a43b6', + nzbdronePurple: '#7932ea', + nzbdronePink: '#f43565', + sonarrBlue: '#00A65B', + helpTextColor: '#909293', + gray: '#adadad', + + // Theme Colors + + themeBlue: '#00A65B', + themeRed: '#c4273c', + themeDarkColor: '#216044', + themeLightColor: '#216044', + + torrentColor: '#00853d', + usenetColor: '#17b1d9', + + // Links + defaultLinkHoverColor: '#fff', + linkColor: '#0b8750', + linkHoverColor: '#1b72e2', + + // Sidebar + + sidebarColor: '#e1e2e3', + sidebarBackgroundColor: '#216044', + sidebarActiveBackgroundColor: '#353535', + + // Toolbar + toolbarColor: '#e1e2e3', + toolbarBackgroundColor: '#216044', + toolbarMenuItemBackgroundColor: '#4D8069', + toolbarMenuItemHoverBackgroundColor: '#216044', + toolbarLabelColor: '#8895aa', + + // Accents + borderColor: '#e5e5e5', + inputBorderColor: '#dde6e9', + inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)', + inputFocusBorderColor: '#66afe9', + inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)', + inputErrorBorderColor: '#f05050', + inputErrorBoxShadowColor: 'rgba(240, 80, 80, 0.6)', + inputWarningBorderColor: '#ffa500', + inputWarningBoxShadowColor: 'rgba(255, 165, 0, 0.6)', + + // + // Buttons + + defaultBackgroundColor: '#fff', + defaultBorderColor: '#eaeaea', + defaultHoverBackgroundColor: '#f5f5f5', + defaultHoverBorderColor: '#d6d6d6;', + + primaryBackgroundColor: '#0b8750', + primaryBorderColor: '#216044', + primaryHoverBackgroundColor: '#097948', + primaryHoverBorderColor: '#1D563D;', + + successBackgroundColor: '#27c24c', + successBorderColor: '#26be4a', + successHoverBackgroundColor: '#24b145', + successHoverBorderColor: '#1f9c3d;', + + warningBackgroundColor: '#ff902b', + warningBorderColor: '#ff8d26', + warningHoverBackgroundColor: '#ff8517', + warningHoverBorderColor: '#fc7800;', + + dangerBackgroundColor: '#f05050', + dangerBorderColor: '#f04b4b', + dangerHoverBackgroundColor: '#ee3d3d', + dangerHoverBorderColor: '#ec2626;', + + iconButtonHoverColor: '#666', + + // + // Modal + + modalBackdropBackgroundColor: 'rgba(0, 0, 0, 0.6)', + modalBackgroundColor: '#fff', + modalCloseButtonHoverColor: '#888', + + // + // Menu + menuItemColor: '#e1e2e3', + menuItemHoverColor: '#fbfcfc', + + // + // Toolbar + + toobarButtonHoverColor: '#00A65B', + toobarButtonSelectedColor: '#00A65B', + + // + // Scroller + + scrollbarBackgroundColor: '#9ea4b9', + scrollbarHoverBackgroundColor: '#656d8c', + + // + // Card + + cardShadowColor: '#e1e1e1', + cardAlternateBackgroundColor: '#f5f5f5', + + // + // Alert + + alertDangerBorderColor: '#ebccd1', + alertDangerBackgroundColor: '#f2dede', + alertDangerColor: '#a94442', + + alertInfoBorderColor: '#bce8f1', + alertInfoBackgroundColor: '#d9edf7', + alertInfoColor: '#31708f', + + alertSuccessBorderColor: '#d6e9c6', + alertSuccessBackgroundColor: '#dff0d8', + alertSuccessColor: '#3c763d', + + alertWarningBorderColor: '#faebcc', + alertWarningBackgroundColor: '#fcf8e3', + alertWarningColor: '#8a6d3b', + + // + // Slider + + sliderAccentColor: '#0b8750', + + // + // Form + + advancedFormLabelColor: '#ff902b', + disabledCheckInputColor: '#ddd', + + // + // Popover + + popoverTitleBackgroundColor: '#f7f7f7', + popoverTitleBorderColor: '#ebebeb', + popoverShadowColor: 'rgba(0, 0, 0, 0.2)', + popoverArrowBorderColor: 'rgba(0, 0, 0, 0.25)', + + popoverTitleBackgroundInverseColor: '#3a3f51', + popoverTitleBorderInverseColor: '#216044', + popoverShadowInverseColor: 'rgba(0, 0, 0, 0.2)', + popoverArrowBorderInverseColor: 'rgba(58, 63, 81, 0.75)', + + // + // Calendar + + calendarTodayBackgroundColor: '#ddd', + + // + // Table + + tableRowHoverBackgroundColor: '#fafbfc' +}; diff --git a/frontend/src/Styles/Variables/dimensions.js b/frontend/src/Styles/Variables/dimensions.js new file mode 100644 index 000000000..fed51a1df --- /dev/null +++ b/frontend/src/Styles/Variables/dimensions.js @@ -0,0 +1,43 @@ +module.exports = { + // Page + pageContentBodyPadding: '20px', + pageContentBodyPaddingSmallScreen: '10px', + + // Header + headerHeight: '60px', + + // Sidebar + sidebarWidth: '210px', + + // Toolbar + toolbarHeight: '60px', + toolbarButtonWidth: '60px', + toolbarSeparatorMargin: '20px', + + // Break Points + breakpointExtraSmall: '480px', + breakpointSmall: '768px', + breakpointMedium: '992px', + breakpointLarge: '1200px', + + // Form + formGroupSmallWidth: '650px', + formGroupMediumWidth: '800px', + formGroupLargeWidth: '1200px', + formLabelWidth: '250px', + formLabelRightMarginWidth: '20px', + + // Drag + dragHandleWidth: '40px', + + // Progress Bar + progressBarSmallHeight: '5px', + progressBarMediumHeight: '15px', + progressBarLargeHeight: '20px', + + // Jump Bar + jumpBarItemHeight: '25px' + + // Series + +}; diff --git a/frontend/src/Styles/Variables/fonts.js b/frontend/src/Styles/Variables/fonts.js new file mode 100644 index 000000000..c4060b661 --- /dev/null +++ b/frontend/src/Styles/Variables/fonts.js @@ -0,0 +1,11 @@ +module.exports = { + // Families + defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif', + monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;', + + // Sizes + extraSmallFontSize: '11px', + smallFontSize: '12px', + defaultFontSize: '14px', + largeFontSize: '16px' +}; diff --git a/frontend/src/Styles/globals.css b/frontend/src/Styles/globals.css new file mode 100644 index 000000000..1d77911f9 --- /dev/null +++ b/frontend/src/Styles/globals.css @@ -0,0 +1,4 @@ +@import '~normalize.css/normalize.css'; +@import 'scaffolding.css'; +@import '../Content/Fonts/fonts.css'; +@import '../Content/Fonts/font-awesome.css'; diff --git a/frontend/src/Styles/scaffolding.css b/frontend/src/Styles/scaffolding.css new file mode 100644 index 000000000..79786d5f9 --- /dev/null +++ b/frontend/src/Styles/scaffolding.css @@ -0,0 +1,42 @@ +* { + box-sizing: border-box; +} +*:before, +*:after { + box-sizing: border-box; +} + +*:focus { + outline: none; +} + +html, +body { + color: #515253; + font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +body { + font-size: 14px; + line-height: 1.528571429; /* 20/14 */ +} + +/* Override normalize */ + +button, +input, +optgroup, +select, +textarea { + margin: 0; + font-size: inherit; + font-family: inherit; + line-height: 1.528571429; /* 20/14 */ +} + +/* Better defaults for unordererd lists */ + +ul { + margin: 0; + padding-left: 20px; +} diff --git a/frontend/src/System/Backup/Backups.css b/frontend/src/System/Backup/Backups.css new file mode 100644 index 000000000..fb280312c --- /dev/null +++ b/frontend/src/System/Backup/Backups.css @@ -0,0 +1,5 @@ +.type { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} diff --git a/frontend/src/System/Backup/Backups.js b/frontend/src/System/Backup/Backups.js new file mode 100644 index 000000000..02b8c3166 --- /dev/null +++ b/frontend/src/System/Backup/Backups.js @@ -0,0 +1,148 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import styles from './Backups.css'; + +const columns = [ + { + name: 'type', + isVisible: true + }, + { + name: 'name', + label: 'Name', + isVisible: true + }, + { + name: 'time', + label: 'Time', + isVisible: true + } +]; + +class Backups extends Component { + + // + // Render + + render() { + const { + isFetching, + items, + backupExecuting, + onBackupPress + } = this.props; + + const hasBackups = !isFetching && items.length > 0; + const noBackups = !isFetching && !items.length; + + return ( + + + + + + + + + { + isFetching && + + } + + { + noBackups && +
No backups are available
+ } + + { + hasBackups && + + + { + items.map((item) => { + const { + id, + type, + name, + path, + time + } = item; + + let iconClassName = icons.SCHEDULED; + let iconTooltip = 'Scheduled'; + + if (type === 'manual') { + iconClassName = icons.INTERACTIVE; + iconTooltip = 'Manual'; + } else if (item === 'update') { + iconClassName = icons.UPDATE; + iconTooltip = 'Before update'; + } + + return ( + + + { + + } + + + + + {name} + + + + + + ); + }) + } + +
+ } +
+
+ ); + } + +} + +Backups.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, + backupExecuting: PropTypes.bool.isRequired, + onBackupPress: PropTypes.func.isRequired +}; + +export default Backups; diff --git a/frontend/src/System/Backup/BackupsConnector.js b/frontend/src/System/Backup/BackupsConnector.js new file mode 100644 index 000000000..f6d5469eb --- /dev/null +++ b/frontend/src/System/Backup/BackupsConnector.js @@ -0,0 +1,81 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { fetchBackups } from 'Store/Actions/systemActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as commandNames from 'Commands/commandNames'; +import Backups from './Backups'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.backups, + createCommandsSelector(), + (backups, commands) => { + const { + isFetching, + items + } = backups; + + const backupExecuting = _.some(commands, { name: commandNames.BACKUP }); + + return { + isFetching, + items, + backupExecuting + }; + } + ); +} + +const mapDispatchToProps = { + fetchBackups, + executeCommand +}; + +class BackupsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchBackups(); + } + + componentDidUpdate(prevProps) { + if (prevProps.backupExecuting && !this.props.backupExecuting) { + this.props.fetchBackups(); + } + } + + // + // Listeners + + onBackupPress = () => { + this.props.executeCommand({ + name: commandNames.BACKUP + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +BackupsConnector.propTypes = { + backupExecuting: PropTypes.bool.isRequired, + fetchBackups: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(BackupsConnector); diff --git a/frontend/src/System/Events/LogsTable.js b/frontend/src/System/Events/LogsTable.js new file mode 100644 index 000000000..7ed1f6cf1 --- /dev/null +++ b/frontend/src/System/Events/LogsTable.js @@ -0,0 +1,176 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { align, icons } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import MenuContent from 'Components/Menu/MenuContent'; +import LogsTableRow from './LogsTableRow'; + +class LogsTable extends Component { + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + filterKey, + filterValue, + totalRecords, + clearLogExecuting, + onRefreshPress, + onClearLogsPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + + + + + All + + + + Info + + + + Warn + + + + Error + + + + + + + + { + isFetching && !isPopulated && + + } + + { + isPopulated && !error && !items.length && +
+ No logs found +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + +
+ } +
+
+ ); + } + +} + +LogsTable.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.string, + totalRecords: PropTypes.number, + clearLogExecuting: PropTypes.bool.isRequired, + onFilterSelect: PropTypes.func.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onClearLogsPress: PropTypes.func.isRequired +}; + +export default LogsTable; diff --git a/frontend/src/System/Events/LogsTableConnector.js b/frontend/src/System/Events/LogsTableConnector.js new file mode 100644 index 000000000..428ea90fd --- /dev/null +++ b/frontend/src/System/Events/LogsTableConnector.js @@ -0,0 +1,130 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import * as systemActions from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogsTable from './LogsTable'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.logs, + createCommandsSelector(), + (logs, commands) => { + const clearLogExecuting = _.some(commands, { name: commandNames.CLEAR_LOGS }); + + return { + clearLogExecuting, + ...logs + }; + } + ); +} + +const mapDispatchToProps = { + executeCommand, + ...systemActions +}; + +class LogsTableConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLogs(); + } + + componentDidUpdate(prevProps) { + if (prevProps.clearLogExecuting && !this.props.clearLogExecuting) { + this.props.gotoLogsFirstPage(); + } + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoLogsFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoLogsPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoLogsNextPage(); + } + + onLastPagePress = () => { + this.props.gotoLogsLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoLogsPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setLogsSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setLogsFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setLogsTableOption(payload); + + if (payload.pageSize) { + this.props.gotoLogsFirstPage(); + } + } + + onRefreshPress = () => { + this.props.gotoLogsFirstPage(); + } + + onClearLogsPress = () => { + this.props.executeCommand({ name: commandNames.CLEAR_LOGS }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LogsTableConnector.propTypes = { + clearLogExecuting: PropTypes.bool.isRequired, + fetchLogs: PropTypes.func.isRequired, + gotoLogsFirstPage: PropTypes.func.isRequired, + gotoLogsPreviousPage: PropTypes.func.isRequired, + gotoLogsNextPage: PropTypes.func.isRequired, + gotoLogsLastPage: PropTypes.func.isRequired, + gotoLogsPage: PropTypes.func.isRequired, + setLogsSort: PropTypes.func.isRequired, + setLogsFilter: PropTypes.func.isRequired, + setLogsTableOption: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LogsTableConnector); diff --git a/frontend/src/System/Events/LogsTableDetailsModal.css b/frontend/src/System/Events/LogsTableDetailsModal.css new file mode 100644 index 000000000..127c1139f --- /dev/null +++ b/frontend/src/System/Events/LogsTableDetailsModal.css @@ -0,0 +1,17 @@ +.detailsText { + composes: scroller from 'Components/Scroller/Scroller.css'; + + display: block; + margin: 0 0 10.5px; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f5f5f5; + color: #3a3f51; + white-space: pre; + word-wrap: break-word; + word-break: break-all; + font-size: 13px; + font-family: $monoSpaceFontFamily; + line-height: 1.52857143; +} diff --git a/frontend/src/System/Events/LogsTableDetailsModal.js b/frontend/src/System/Events/LogsTableDetailsModal.js new file mode 100644 index 000000000..de6a881df --- /dev/null +++ b/frontend/src/System/Events/LogsTableDetailsModal.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { scrollDirections } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import Scroller from 'Components/Scroller/Scroller'; +import Modal from 'Components/Modal/Modal'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './LogsTableDetailsModal.css'; + +function LogsTableDetailsModal(props) { + const { + isOpen, + message, + exception, + onModalClose + } = props; + + return ( + + + + Details + + + +
Message
+ + + {message} + + + { + !!exception && +
+
Exception
+ + {exception} + +
+ } +
+ + + + +
+
+ ); +} + +LogsTableDetailsModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + message: PropTypes.string.isRequired, + exception: PropTypes.string, + onModalClose: PropTypes.func.isRequired +}; + +export default LogsTableDetailsModal; diff --git a/frontend/src/System/Events/LogsTableRow.css b/frontend/src/System/Events/LogsTableRow.css new file mode 100644 index 000000000..557d690f0 --- /dev/null +++ b/frontend/src/System/Events/LogsTableRow.css @@ -0,0 +1,35 @@ +.level { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} + +.info { + color: #1e90ff; +} + +.debug { + color: #808080; +} + +.trace { + color: #d3d3d3; +} + +.warn { + color: $warningColor; +} + +.error { + color: $dangerColor; +} + +.fatal { + color: $purple; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 45px; +} diff --git a/frontend/src/System/Events/LogsTableRow.js b/frontend/src/System/Events/LogsTableRow.js new file mode 100644 index 000000000..9c54e5289 --- /dev/null +++ b/frontend/src/System/Events/LogsTableRow.js @@ -0,0 +1,150 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRowButton from 'Components/Table/TableRowButton'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import LogsTableDetailsModal from './LogsTableDetailsModal'; +import styles from './LogsTableRow.css'; + +function getIconName(level) { + switch (level) { + case 'trace': + case 'debug': + case 'info': + return icons.INFO; + case 'warn': + return icons.DANGER; + case 'error': + return icons.BUG; + case 'fatal': + return icons.FATAL; + default: + return icons.UNKNOWN; + } +} + +class LogsTableRow extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isDetailsModalOpen: false + }; + } + + // + // Listeners + + onPress = () => { + this.setState({ isDetailsModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isDetailsModalOpen: false }); + } + + // + // Render + + render() { + const { + level, + logger, + message, + time, + exception, + columns + } = this.props; + + return ( + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'level') { + return ( + + + + ); + } + + if (name === 'logger') { + return ( + + {logger} + + ); + } + + if (name === 'message') { + return ( + + {message} + + ); + } + + if (name === 'time') { + return ( + + ); + } + + if (name === 'actions') { + return ( + + ); + } + }) + } + + + + ); + } + +} + +LogsTableRow.propTypes = { + level: PropTypes.string.isRequired, + logger: PropTypes.string.isRequired, + message: PropTypes.string.isRequired, + time: PropTypes.string.isRequired, + exception: PropTypes.string, + columns: PropTypes.arrayOf(PropTypes.object).isRequired +}; + +export default LogsTableRow; diff --git a/frontend/src/System/Logs/Files/LogFiles.js b/frontend/src/System/Logs/Files/LogFiles.js new file mode 100644 index 000000000..47482b3fe --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFiles.js @@ -0,0 +1,139 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { icons } from 'Helpers/Props'; +import Alert from 'Components/Alert'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import TableBody from 'Components/Table/TableBody'; +import LogsNavMenu from '../LogsNavMenu'; +import LogFilesTableRow from './LogFilesTableRow'; + +const columns = [ + { + name: 'filename', + label: 'Filename', + isVisible: true + }, + { + name: 'lastWriteTime', + label: 'Last Write Time', + isVisible: true + }, + { + name: 'download', + isVisible: true + } +]; + +class LogFiles extends Component { + + // + // Render + + render() { + const { + isFetching, + items, + deleteFilesExecuting, + currentLogView, + location, + onRefreshPress, + onDeleteFilesPress, + ...otherProps + } = this.props; + + return ( + + + + + + + + + + + + + + +
+ Log files are located in: {location} +
+ + { + currentLogView === 'Log Files' && +
+ The log level defaults to 'Info' and can be changed in General Settings +
+ } +
+ + { + isFetching && + + } + + { + !isFetching && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+
+ } + + { + !isFetching && !items.length && +
No log files
+ } +
+
+ ); + } + +} + +LogFiles.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired, + deleteFilesExecuting: PropTypes.bool.isRequired, + currentLogView: PropTypes.string.isRequired, + location: PropTypes.string.isRequired, + onRefreshPress: PropTypes.func.isRequired, + onDeleteFilesPress: PropTypes.func.isRequired +}; + +export default LogFiles; diff --git a/frontend/src/System/Logs/Files/LogFilesConnector.js b/frontend/src/System/Logs/Files/LogFilesConnector.js new file mode 100644 index 000000000..16d8d23b9 --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import combinePath from 'Utilities/String/combinePath'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchLogFiles } from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogFiles from './LogFiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.logFiles, + (state) => state.system.status.item, + createCommandsSelector(), + (logFiles, status, commands) => { + const { + isFetching, + items + } = logFiles; + + const { + appData, + isWindows + } = status; + + const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_LOG_FILES }); + + return { + isFetching, + items, + deleteFilesExecuting, + currentLogView: 'Log Files', + location: combinePath(isWindows, appData, ['logs']) + }; + } + ); +} + +const mapDispatchToProps = { + fetchLogFiles, + executeCommand +}; + +class LogFilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchLogFiles(); + } + + componentDidUpdate(prevProps) { + if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) { + this.props.fetchLogFiles(); + } + } + + // + // Listeners + + onRefreshPress = () => { + this.props.fetchLogFiles(); + } + + onDeleteFilesPress = () => { + this.props.executeCommand({ name: commandNames.DELETE_LOG_FILES }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +LogFilesConnector.propTypes = { + deleteFilesExecuting: PropTypes.bool.isRequired, + fetchLogFiles: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(LogFilesConnector); diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.css b/frontend/src/System/Logs/Files/LogFilesTableRow.css new file mode 100644 index 000000000..779794b7d --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesTableRow.css @@ -0,0 +1,5 @@ +.download { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/System/Logs/Files/LogFilesTableRow.js b/frontend/src/System/Logs/Files/LogFilesTableRow.js new file mode 100644 index 000000000..7ae61a531 --- /dev/null +++ b/frontend/src/System/Logs/Files/LogFilesTableRow.js @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './LogFilesTableRow.css'; + +class LogFilesTableRow extends Component { + + // + // Render + + render() { + const { + filename, + lastWriteTime, + downloadUrl + } = this.props; + + return ( + + {filename} + + + + + + Download + + + + ); + } + +} + +LogFilesTableRow.propTypes = { + filename: PropTypes.string.isRequired, + lastWriteTime: PropTypes.string.isRequired, + downloadUrl: PropTypes.string.isRequired +}; + +export default LogFilesTableRow; diff --git a/frontend/src/System/Logs/Logs.js b/frontend/src/System/Logs/Logs.js new file mode 100644 index 000000000..fa0be453e --- /dev/null +++ b/frontend/src/System/Logs/Logs.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react'; +import { Route } from 'react-router-dom'; +import Switch from 'Components/Router/Switch'; +import LogFilesConnector from './Files/LogFilesConnector'; +import UpdateLogFilesConnector from './Updates/UpdateLogFilesConnector'; + +class Logs extends Component { + + // + // Render + + render() { + return ( + + + + + + ); + } +} + +export default Logs; diff --git a/frontend/src/System/Logs/LogsNavMenu.js b/frontend/src/System/Logs/LogsNavMenu.js new file mode 100644 index 000000000..b69630248 --- /dev/null +++ b/frontend/src/System/Logs/LogsNavMenu.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import Menu from 'Components/Menu/Menu'; +import MenuButton from 'Components/Menu/MenuButton'; +import MenuContent from 'Components/Menu/MenuContent'; +import MenuItem from 'Components/Menu/MenuItem'; + +class LogsNavMenu extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isMenuOpen: false + }; + } + + // + // Listeners + + onMenuButtonPress = () => { + this.setState({ isMenuOpen: !this.state.isMenuOpen }); + } + + onMenuItemPress = () => { + this.setState({ isMenuOpen: false }); + } + + // + // Render + + render() { + const { + current + } = this.props; + + return ( + + + {current} + + + + Log Files + + + + Updater Log Files + + + + ); + } +} + +LogsNavMenu.propTypes = { + current: PropTypes.string.isRequired +}; + +export default LogsNavMenu; diff --git a/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js b/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js new file mode 100644 index 000000000..d49b82152 --- /dev/null +++ b/frontend/src/System/Logs/Updates/UpdateLogFilesConnector.js @@ -0,0 +1,93 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import combinePath from 'Utilities/String/combinePath'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchUpdateLogFiles } from 'Store/Actions/systemActions'; +import * as commandNames from 'Commands/commandNames'; +import LogFiles from '../Files/LogFiles'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.updateLogFiles, + (state) => state.system.status.item, + createCommandsSelector(), + (updateLogFiles, status, commands) => { + const { + isFetching, + items + } = updateLogFiles; + + const deleteFilesExecuting = _.some(commands, { name: commandNames.DELETE_UPDATE_LOG_FILES }); + + const { + appData, + isWindows + } = status; + + return { + isFetching, + items, + deleteFilesExecuting, + currentLogView: 'Updater Log Files', + location: combinePath(isWindows, appData, ['UpdateLogs']) + }; + } + ); +} + +const mapDispatchToProps = { + fetchUpdateLogFiles, + executeCommand +}; + +class UpdateLogFilesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchUpdateLogFiles(); + } + + componentDidUpdate(prevProps) { + if (prevProps.deleteFilesExecuting && !this.props.deleteFilesExecuting) { + this.props.fetchUpdateLogFiles(); + } + } + + // + // Listeners + + onRefreshPress = () => { + this.props.fetchUpdateLogFiles(); + } + + onDeleteFilesPress = () => { + this.props.executeCommand({ name: commandNames.DELETE_UPDATE_LOG_FILES }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +UpdateLogFilesConnector.propTypes = { + deleteFilesExecuting: PropTypes.bool.isRequired, + fetchUpdateLogFiles: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdateLogFilesConnector); diff --git a/frontend/src/System/Status/About/About.js b/frontend/src/System/Status/About/About.js new file mode 100644 index 000000000..51c0afde6 --- /dev/null +++ b/frontend/src/System/Status/About/About.js @@ -0,0 +1,71 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import FieldSet from 'Components/FieldSet'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; + +class About extends Component { + + // + // Render + + render() { + const { + version, + isMonoRuntime, + runtimeVersion, + appData, + startupPath, + mode + } = this.props; + + return ( +
+ + + + { + isMonoRuntime && + + } + + + + + + + +
+ ); + } + +} + +About.propTypes = { + version: PropTypes.string, + isMonoRuntime: PropTypes.bool, + runtimeVersion: PropTypes.string, + appData: PropTypes.string, + startupPath: PropTypes.string, + mode: PropTypes.string +}; + +export default About; diff --git a/frontend/src/System/Status/About/AboutConnector.js b/frontend/src/System/Status/About/AboutConnector.js new file mode 100644 index 000000000..8d5c2ce0f --- /dev/null +++ b/frontend/src/System/Status/About/AboutConnector.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchStatus } from 'Store/Actions/systemActions'; +import About from './About'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.status, + (status) => { + return { + ...status.item + }; + } + ); +} + +const mapDispatchToProps = { + fetchStatus +}; + +class AboutConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchStatus(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +AboutConnector.propTypes = { + fetchStatus: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(AboutConnector); diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.css b/frontend/src/System/Status/DiskSpace/DiskSpace.css new file mode 100644 index 000000000..70ef6f884 --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.css @@ -0,0 +1,5 @@ +.space { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} diff --git a/frontend/src/System/Status/DiskSpace/DiskSpace.js b/frontend/src/System/Status/DiskSpace/DiskSpace.js new file mode 100644 index 000000000..d2c10706e --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpace.js @@ -0,0 +1,122 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds, sizes } from 'Helpers/Props'; +import formatBytes from 'Utilities/Number/formatBytes'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import ProgressBar from 'Components/ProgressBar'; +import styles from './DiskSpace.css'; + +const columns = [ + { + name: 'path', + label: 'Location', + isVisible: true + }, + { + name: 'freeSpace', + label: 'Free Space', + isVisible: true + }, + { + name: 'totalSpace', + label: 'Total Space', + isVisible: true + }, + { + name: 'progress', + isVisible: true + } +]; + +class DiskSpace extends Component { + + // + // Render + + render() { + const { + isFetching, + items + } = this.props; + + return ( +
+ { + isFetching && + + } + + { + !isFetching && + + + { + items.map((item) => { + const { + freeSpace, + totalSpace + } = item; + + const diskUsage = (100 - freeSpace / totalSpace * 100); + let diskUsageKind = kinds.PRIMARY; + + if (diskUsage > 90) { + diskUsageKind = kinds.DANGER; + } else if (diskUsage > 80) { + diskUsageKind = kinds.WARNING; + } + + return ( + + + {item.path} + + { + item.label && + ` (${item.label})` + } + + + + {formatBytes(freeSpace)} + + + + {formatBytes(totalSpace)} + + + + + + + ); + }) + } + +
+ } +
+ ); + } + +} + +DiskSpace.propTypes = { + isFetching: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default DiskSpace; diff --git a/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js new file mode 100644 index 000000000..3049b2ead --- /dev/null +++ b/frontend/src/System/Status/DiskSpace/DiskSpaceConnector.js @@ -0,0 +1,54 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchDiskSpace } from 'Store/Actions/systemActions'; +import DiskSpace from './DiskSpace'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.diskSpace, + (diskSpace) => { + const { + isFetching, + items + } = diskSpace; + + return { + isFetching, + items + }; + } + ); +} + +const mapDispatchToProps = { + fetchDiskSpace +}; + +class DiskSpaceConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchDiskSpace(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DiskSpaceConnector.propTypes = { + fetchDiskSpace: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DiskSpaceConnector); diff --git a/frontend/src/System/Status/Health/Health.css b/frontend/src/System/Status/Health/Health.css new file mode 100644 index 000000000..1aad8ee77 --- /dev/null +++ b/frontend/src/System/Status/Health/Health.css @@ -0,0 +1,21 @@ +.legend { + display: flex; + justify-content: space-between; +} + +.loading { + composes: loading from 'Components/Loading/LoadingIndicator.css'; + + margin-top: 2px; + margin-left: 10px; + text-align: left; +} + +.status { + width: 20px; +} + +.healthOk { + margin-bottom: 25px; +} + diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js new file mode 100644 index 000000000..629971f6d --- /dev/null +++ b/frontend/src/System/Status/Health/Health.js @@ -0,0 +1,170 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import titleCase from 'Utilities/String/titleCase'; +import { icons, kinds } from 'Helpers/Props'; +import Icon from 'Components/Icon'; +import Link from 'Components/Link/Link'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FieldSet from 'Components/FieldSet'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './Health.css'; + +function getInternalLink(source) { + switch (source) { + case 'IndexerRssCheck': + case 'IndexerSearchCheck': + case 'IndexerStatusCheck': + return ( + + Settings + + ); + case 'DownloadClientCheck': + case 'ImportMechanismCheck': + return ( + + Settings + + ); + case 'RootFolderCheck': + return ( +
+ + Series Editor + +
+ ); + case 'UpdateCheck': + return ( + + Updates + + ); + default: + return; + } +} + +const columns = [ + { + className: styles.status, + name: 'type', + isVisible: true + }, + { + name: 'message', + label: 'Message', + isVisible: true + }, + { + name: 'wikiLink', + label: 'Wiki', + isVisible: true + }, + { + name: 'internalLink', + isVisible: true + } +]; + +class Health extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + items + } = this.props; + + const healthIssues = !!items.length; + + return ( +
+ Health + + { + isFetching && isPopulated && + + } +
+ } + > + { + isFetching && !isPopulated && + + } + + { + !healthIssues && +
+ No issues with your configuration +
+ } + + { + healthIssues && + + + { + items.map((item) => { + const internalLink = getInternalLink(item.source); + + return ( + + + + + + {item.message} + + + + Wiki + + + + + { + internalLink + } + + + ); + }) + } + +
+ } + + ); + } + +} + +Health.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default Health; diff --git a/frontend/src/System/Status/Health/HealthConnector.js b/frontend/src/System/Status/Health/HealthConnector.js new file mode 100644 index 000000000..67f6a39dc --- /dev/null +++ b/frontend/src/System/Status/Health/HealthConnector.js @@ -0,0 +1,56 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import Health from './Health'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.health, + (health) => { + const { + isFetching, + isPopulated, + items + } = health; + + return { + isFetching, + isPopulated, + items + }; + } + ); +} + +const mapDispatchToProps = { + fetchHealth +}; + +class HealthConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchHealth(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +HealthConnector.propTypes = { + fetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthConnector); diff --git a/frontend/src/System/Status/Health/HealthStatusConnector.js b/frontend/src/System/Status/Health/HealthStatusConnector.js new file mode 100644 index 000000000..181eae916 --- /dev/null +++ b/frontend/src/System/Status/Health/HealthStatusConnector.js @@ -0,0 +1,79 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchHealth } from 'Store/Actions/systemActions'; +import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus'; + +function createMapStateToProps() { + return createSelector( + (state) => state.app, + (state) => state.system.health, + (app, health) => { + const count = health.items.length; + let errors = false; + let warnings = false; + + health.items.forEach((item) => { + if (item.type === 'error') { + errors = true; + } + + if (item.type === 'warning') { + warnings = true; + } + }); + + return { + isConnected: app.isConnected, + isReconnecting: app.isReconnecting, + isPopulated: health.isPopulated, + count, + errors, + warnings + }; + } + ); +} + +const mapDispatchToProps = { + fetchHealth +}; + +class HealthStatusConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.isPopulated) { + this.props.fetchHealth(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.isConnected && prevProps.isReconnecting) { + this.props.fetchHealth(); + } + } + + // + // Render + + render() { + return ( + + ); + } +} + +HealthStatusConnector.propTypes = { + isConnected: PropTypes.bool.isRequired, + isReconnecting: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + fetchHealth: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(HealthStatusConnector); diff --git a/frontend/src/System/Status/MoreInfo/MoreInfo.js b/frontend/src/System/Status/MoreInfo/MoreInfo.js new file mode 100644 index 000000000..b3afec021 --- /dev/null +++ b/frontend/src/System/Status/MoreInfo/MoreInfo.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import Link from 'Components/Link/Link'; +import FieldSet from 'Components/FieldSet'; +import DescriptionList from 'Components/DescriptionList/DescriptionList'; +import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle'; +import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription'; + +class MoreInfo extends Component { + + // + // Render + + render() { + return ( +
+ + Home page + + lidarr.audio + + + Wiki + + wiki.lidarr.audio + + + Reddit + + Lidarr + + + Discord + + #lidarr on Discord + + + Donations + + Donate to Lidarr + + + Donations (Sonarr) + + Donate to Sonarr + + + Source + + github.com/Lidarr/Lidarr + + + Feature Requests + + github.com/Lidarr/Lidarr/issues + + + +
+ ); + } +} + +MoreInfo.propTypes = { + +}; + +export default MoreInfo; diff --git a/frontend/src/System/Status/Status.js b/frontend/src/System/Status/Status.js new file mode 100644 index 000000000..f0b157515 --- /dev/null +++ b/frontend/src/System/Status/Status.js @@ -0,0 +1,29 @@ +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import HealthConnector from './Health/HealthConnector'; +import DiskSpaceConnector from './DiskSpace/DiskSpaceConnector'; +import AboutConnector from './About/AboutConnector'; +import MoreInfo from './MoreInfo/MoreInfo'; + +class Status extends Component { + + // + // Render + + render() { + return ( + + + + + + + + + ); + } + +} + +export default Status; diff --git a/frontend/src/System/Tasks/TaskRow.css b/frontend/src/System/Tasks/TaskRow.css new file mode 100644 index 000000000..dc83cfd69 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRow.css @@ -0,0 +1,18 @@ +.interval { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 150px; +} + +.lastExecution, +.nextExecution { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 180px; +} + +.actions { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 20px; +} diff --git a/frontend/src/System/Tasks/TaskRow.js b/frontend/src/System/Tasks/TaskRow.js new file mode 100644 index 000000000..f118842a7 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRow.js @@ -0,0 +1,94 @@ +import moment from 'moment'; +import PropTypes from 'prop-types'; +import React from 'react'; +import formatDate from 'Utilities/Date/formatDate'; +import formatDateTime from 'Utilities/Date/formatDateTime'; +import { icons } from 'Helpers/Props'; +import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import styles from './TaskRow.css'; + +function TaskRow(props) { + const { + name, + interval, + lastExecution, + nextExecution, + isExecuting, + showRelativeDates, + shortDateFormat, + longDateFormat, + timeFormat, + onExecutePress + } = props; + + const disabled = interval === 0; + const executeNow = !disabled && moment().isAfter(nextExecution); + const hasNextExecutionTime = !disabled && !executeNow; + const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); + + return ( + + {name} + + {disabled ? 'disabled' : duration} + + + + {showRelativeDates ? moment(lastExecution).fromNow() : formatDate(lastExecution, shortDateFormat)} + + + { + disabled && + - + } + + { + executeNow && + now + } + + { + hasNextExecutionTime && + + {showRelativeDates ? moment(nextExecution).fromNow() : formatDate(nextExecution, shortDateFormat)} + + } + + + + + + ); +} + +TaskRow.propTypes = { + name: PropTypes.string.isRequired, + interval: PropTypes.number.isRequired, + lastExecution: PropTypes.string.isRequired, + nextExecution: PropTypes.string.isRequired, + isExecuting: PropTypes.bool.isRequired, + showRelativeDates: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + longDateFormat: PropTypes.string.isRequired, + timeFormat: PropTypes.string.isRequired, + onExecutePress: PropTypes.func.isRequired +}; + +export default TaskRow; diff --git a/frontend/src/System/Tasks/TaskRowConnector.js b/frontend/src/System/Tasks/TaskRowConnector.js new file mode 100644 index 000000000..035364034 --- /dev/null +++ b/frontend/src/System/Tasks/TaskRowConnector.js @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { findCommand } from 'Utilities/Command'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchTask } from 'Store/Actions/systemActions'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import TaskRow from './TaskRow'; + +function createMapStateToProps() { + return createSelector( + (state, { taskName }) => taskName, + createCommandsSelector(), + createUISettingsSelector(), + (taskName, commands, uiSettings) => { + const isExecuting = !!findCommand(commands, { name: taskName }); + + return { + isExecuting, + showRelativeDates: uiSettings.showRelativeDates, + shortDateFormat: uiSettings.shortDateFormat, + longDateFormat: uiSettings.longDateFormat, + timeFormat: uiSettings.timeFormat + }; + } + ); +} + +function createMapDispatchToProps(dispatch, props) { + const taskName = props.taskName; + + return { + dispatchFetchTask() { + dispatch(fetchTask({ + id: props.id + })); + }, + + onExecutePress() { + dispatch(executeCommand({ + name: taskName + })); + } + }; +} + +class TaskRowConnector extends Component { + + // + // Lifecycle + + componentDidUpdate(prevProps) { + const { + isExecuting, + dispatchFetchTask + } = this.props; + + if (!isExecuting && prevProps.isExecuting) { + // Give the host a moment to update after the command completes + setTimeout(() => { + dispatchFetchTask(); + }, 1000); + } + } + + // + // Render + + render() { + const { + dispatchFetchTask, + ...otherProps + } = this.props; + + return ( + + ); + } +} + +TaskRowConnector.propTypes = { + id: PropTypes.number.isRequired, + isExecuting: PropTypes.bool.isRequired, + dispatchFetchTask: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector); diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js new file mode 100644 index 000000000..ae2d75dbb --- /dev/null +++ b/frontend/src/System/Tasks/Tasks.js @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TaskRowConnector from './TaskRowConnector'; + +const columns = [ + { + name: 'name', + label: 'Name', + isVisible: true + }, + { + name: 'interval', + label: 'Interval', + isVisible: true + }, + { + name: 'lastExecution', + label: 'Last Execution', + isVisible: true + }, + { + name: 'nextExecution', + label: 'Next Execution', + isVisible: true + }, + { + name: 'actions', + isVisible: true + } +]; + +class Tasks extends Component { + + // + // Render + + render() { + const { + isFetching, + isPopulated, + items + } = this.props; + + return ( + + + { + isFetching && !isPopulated && + + } + + { + isPopulated && + + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ } +
+
+ ); + } + +} + +Tasks.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + items: PropTypes.array.isRequired +}; + +export default Tasks; diff --git a/frontend/src/System/Tasks/TasksConnector.js b/frontend/src/System/Tasks/TasksConnector.js new file mode 100644 index 000000000..492040674 --- /dev/null +++ b/frontend/src/System/Tasks/TasksConnector.js @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchTasks } from 'Store/Actions/systemActions'; +import Tasks from './Tasks'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.tasks, + (tasks) => { + return tasks; + } + ); +} + +const mapDispatchToProps = { + fetchTasks +}; + +class TasksConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchTasks(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +TasksConnector.propTypes = { + fetchTasks: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector); diff --git a/frontend/src/System/Updates/UpdateChanges.css b/frontend/src/System/Updates/UpdateChanges.css new file mode 100644 index 000000000..d21897373 --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.css @@ -0,0 +1,4 @@ +.title { + margin-top: 10px; + font-size: 16px; +} diff --git a/frontend/src/System/Updates/UpdateChanges.js b/frontend/src/System/Updates/UpdateChanges.js new file mode 100644 index 000000000..1e08c67af --- /dev/null +++ b/frontend/src/System/Updates/UpdateChanges.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import styles from './UpdateChanges.css'; + +class UpdateChanges extends Component { + + // + // Render + + render() { + const { + title, + changes + } = this.props; + + if (changes.length === 0) { + return null; + } + + return ( +
+
{title}
+
    + { + changes.map((change, index) => { + return ( +
  • + {change} +
  • + ) + }) + } +
+
+ ); + } + +} + +UpdateChanges.propTypes = { + title: PropTypes.string.isRequired, + changes: PropTypes.arrayOf(PropTypes.string) +}; + +export default UpdateChanges diff --git a/frontend/src/System/Updates/Updates.css b/frontend/src/System/Updates/Updates.css new file mode 100644 index 000000000..c7561fdd2 --- /dev/null +++ b/frontend/src/System/Updates/Updates.css @@ -0,0 +1,46 @@ +.upToDate { + display: flex; + margin-bottom: 20px; +} + +.upToDateIcon { + color: #37bc9b; + font-size: 30px; +} + +.upToDateMessage { + padding-left: 5px; + font-size: 18px; + line-height: 30px; +} + +.update { + margin-top: 20px; +} + +.info { + display: flex; + margin-bottom: 10px; + padding-bottom: 5px; + border-bottom: 1px solid #e5e5e5; + line-height: 21px; +} + +.version { + font-size: 21px; +} + +.space { + padding: 0 5px; +} + +.date { + font-size: 16px; +} + +.branch { + composes: label from 'Components/Label.css'; + + margin-left: 10px; + font-size: 14px; +} diff --git a/frontend/src/System/Updates/Updates.js b/frontend/src/System/Updates/Updates.js new file mode 100644 index 000000000..3aa2037a4 --- /dev/null +++ b/frontend/src/System/Updates/Updates.js @@ -0,0 +1,149 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { kinds } from 'Helpers/Props'; +import formatDate from 'Utilities/Date/formatDate'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import SpinnerButton from 'Components/Link/SpinnerButton'; +import Icon from 'Components/Icon'; +import Label from 'Components/Label'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import UpdateChanges from './UpdateChanges'; +import styles from './Updates.css'; + +class Updates extends Component { + + // + // Render + + render() { + const { + isPopulated, + error, + items, + isInstallingUpdate, + shortDateFormat, + onInstallLatestPress + } = this.props; + + const hasUpdates = isPopulated && !error && items.length > 0; + const noUpdates = isPopulated && !error && !items.length; + const hasUpdateToInstall = hasUpdates && _.some(items, { installable: true, latest: true }); + const noUpdateToInstall = hasUpdates && !hasUpdateToInstall; + + return ( + + + { + !isPopulated && + + } + + { + noUpdates && +
No updates are available
+ } + + { + hasUpdateToInstall && + + Install Latest + + } + + { + noUpdateToInstall && +
+ +
+ The latest version of Sonarr is already installed +
+
+ } + + { + hasUpdates && +
+ { + items.map((update) => { + const hasChanges = !!update.changes; + + return ( +
+
+
{update.version}
+
+
{formatDate(update.releaseDate, shortDateFormat)}
+ + { + update.branch !== 'master' && + + } +
+ + { + !hasChanges && +
Maintenance release
+ } + + { + hasChanges && +
+ + + +
+ } +
+ ); + }) + } +
+ } + + { + !!error && +
+ Failed to fetch updates +
+ } +
+
+ ); + } + +} + +Updates.propTypes = { + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object.isRequired, + items: PropTypes.array.isRequired, + isInstallingUpdate: PropTypes.bool.isRequired, + shortDateFormat: PropTypes.string.isRequired, + onInstallLatestPress: PropTypes.func.isRequired +}; + +export default Updates; diff --git a/frontend/src/System/Updates/UpdatesConnector.js b/frontend/src/System/Updates/UpdatesConnector.js new file mode 100644 index 000000000..0ac6cf239 --- /dev/null +++ b/frontend/src/System/Updates/UpdatesConnector.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchUpdates } from 'Store/Actions/systemActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; +import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; +import * as commandNames from 'Commands/commandNames'; +import Updates from './Updates'; + +function createMapStateToProps() { + return createSelector( + (state) => state.system.updates, + createUISettingsSelector(), + createCommandExecutingSelector(commandNames.APPLICATION_UPDATE), + (updates, uiSettings, isInstallingUpdate) => { + const { + isPopulated, + error, + items + } = updates; + + return { + isPopulated, + error, + items, + isInstallingUpdate, + shortDateFormat: uiSettings.shortDateFormat + }; + } + ); +} + +const mapDispatchToProps = { + fetchUpdates, + executeCommand +}; + +class UpdatesConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchUpdates(); + } + + // + // Listeners + + onInstallLatestPress = () => { + this.props.executeCommand({ name: commandNames.APPLICATION_UPDATE }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +UpdatesConnector.propTypes = { + fetchUpdates: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(UpdatesConnector); diff --git a/frontend/src/Utilities/Array/sortByName.js b/frontend/src/Utilities/Array/sortByName.js new file mode 100644 index 000000000..1956d3bac --- /dev/null +++ b/frontend/src/Utilities/Array/sortByName.js @@ -0,0 +1,5 @@ +function sortByName(a, b) { + return a.name.localeCompare(b.name); +} + +export default sortByName; diff --git a/frontend/src/Utilities/Command/findCommand.js b/frontend/src/Utilities/Command/findCommand.js new file mode 100644 index 000000000..cf7d5444a --- /dev/null +++ b/frontend/src/Utilities/Command/findCommand.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; +import isSameCommand from './isSameCommand'; + +function findCommand(commands, options) { + return _.findLast(commands, (command) => { + return isSameCommand(command.body, options); + }); +} + +export default findCommand; diff --git a/frontend/src/Utilities/Command/index.js b/frontend/src/Utilities/Command/index.js new file mode 100644 index 000000000..66043bf03 --- /dev/null +++ b/frontend/src/Utilities/Command/index.js @@ -0,0 +1,5 @@ +export { default as findCommand } from './findCommand'; +export { default as isCommandComplete } from './isCommandComplete'; +export { default as isCommandExecuting } from './isCommandExecuting'; +export { default as isCommandFailed } from './isCommandFailed'; +export { default as isSameCommand } from './isSameCommand'; diff --git a/frontend/src/Utilities/Command/isCommandComplete.js b/frontend/src/Utilities/Command/isCommandComplete.js new file mode 100644 index 000000000..e64737188 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandComplete.js @@ -0,0 +1,9 @@ +function isCommandComplete(command) { + if (!command) { + return false; + } + + return command.state === 'complete'; +} + +export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandExecuting.js b/frontend/src/Utilities/Command/isCommandExecuting.js new file mode 100644 index 000000000..4e2e6d8c4 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandExecuting.js @@ -0,0 +1,9 @@ +function isCommandExecuting(command) { + if (!command) { + return false; + } + + return command.state === 'queued' || command.state === 'started'; +} + +export default isCommandExecuting; diff --git a/frontend/src/Utilities/Command/isCommandFailed.js b/frontend/src/Utilities/Command/isCommandFailed.js new file mode 100644 index 000000000..f48d790e3 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandFailed.js @@ -0,0 +1,12 @@ +function isCommandFailed(command) { + if (!command) { + return false; + } + + return command.state === 'failed' || + command.state === 'aborted' || + command.state === 'cancelled' || + command.state === 'orphaned'; +} + +export default isCommandFailed; diff --git a/frontend/src/Utilities/Command/isSameCommand.js b/frontend/src/Utilities/Command/isSameCommand.js new file mode 100644 index 000000000..d0acb24b5 --- /dev/null +++ b/frontend/src/Utilities/Command/isSameCommand.js @@ -0,0 +1,24 @@ +import _ from 'lodash'; + +function isSameCommand(commandA, commandB) { + if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) { + return false; + } + + for (const key in commandB) { + if (key !== 'name') { + const value = commandB[key]; + if (Array.isArray(value)) { + if (_.difference(value, commandA[key]).length > 0) { + return false; + } + } else if (value !== commandA[key]) { + return false; + } + } + } + + return true; +} + +export default isSameCommand; diff --git a/frontend/src/Utilities/Constants/keyCodes.js b/frontend/src/Utilities/Constants/keyCodes.js new file mode 100644 index 000000000..9285b10fe --- /dev/null +++ b/frontend/src/Utilities/Constants/keyCodes.js @@ -0,0 +1,7 @@ +export const TAB = 9; +export const ENTER = 13; +export const SHIFT = 16; +export const CONTROL = 17; +export const ESCAPE = 27; +export const UP_ARROW = 38; +export const DOWN_ARROW = 40; diff --git a/frontend/src/Utilities/Date/formatDate.js b/frontend/src/Utilities/Date/formatDate.js new file mode 100644 index 000000000..92eb57840 --- /dev/null +++ b/frontend/src/Utilities/Date/formatDate.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function formatDate(date, dateFormat) { + if (!date) { + return ''; + } + + return moment(date).format(dateFormat); +} + +export default formatDate; diff --git a/frontend/src/Utilities/Date/formatDateTime.js b/frontend/src/Utilities/Date/formatDateTime.js new file mode 100644 index 000000000..f36f4f3e0 --- /dev/null +++ b/frontend/src/Utilities/Date/formatDateTime.js @@ -0,0 +1,39 @@ +import moment from 'moment'; +import formatTime from './formatTime'; +import isToday from './isToday'; +import isTomorrow from './isTomorrow'; +import isYesterday from './isYesterday'; + +function getRelativeDay(date, includeRelativeDate) { + if (!includeRelativeDate) { + return ''; + } + + if (isYesterday(date)) { + return 'Yesterday, '; + } + + if (isToday(date)) { + return 'Today, '; + } + + if (isTomorrow(date)) { + return 'Tomorrow, '; + } + + return ''; +} + +function formatDateTime(date, dateFormat, timeFormat, { includeSeconds = false, includeRelativeDay = false } = {}) { + if (!date) { + return ''; + } + + const relativeDay = getRelativeDay(date, includeRelativeDay); + const formattedDate = moment(date).format(dateFormat); + const formattedTime = formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); + + return `${relativeDay}${formattedDate} ${formattedTime}`; +} + +export default formatDateTime; diff --git a/frontend/src/Utilities/Date/formatTime.js b/frontend/src/Utilities/Date/formatTime.js new file mode 100644 index 000000000..89c908d1f --- /dev/null +++ b/frontend/src/Utilities/Date/formatTime.js @@ -0,0 +1,19 @@ +import moment from 'moment'; + +function formatTime(date, timeFormat, { includeMinuteZero = false, includeSeconds = false } = {}) { + if (!date) { + return ''; + } + + if (includeSeconds) { + timeFormat = timeFormat.replace(/\(?:mm\)?/, ':mm:ss'); + } else if (includeMinuteZero) { + timeFormat = timeFormat.replace('(:mm)', ':mm'); + } else { + timeFormat = timeFormat.replace('(:mm)', ''); + } + + return moment(date).format(timeFormat); +} + +export default formatTime; diff --git a/frontend/src/Utilities/Date/formatTimeSpan.js b/frontend/src/Utilities/Date/formatTimeSpan.js new file mode 100644 index 000000000..ef1a278e5 --- /dev/null +++ b/frontend/src/Utilities/Date/formatTimeSpan.js @@ -0,0 +1,24 @@ +import moment from 'moment'; +import padNumber from 'Utilities/Number/padNumber'; + +function formatTimeSpan(timeSpan) { + if (!timeSpan) { + return ''; + } + + const duration = moment.duration(timeSpan); + const days = duration.get('days'); + const hours = padNumber(duration.get('hours'), 2); + const minutes = padNumber(duration.get('minutes'), 2); + const seconds = padNumber(duration.get('seconds'), 2); + + const time = `${hours}:${minutes}:${seconds}`; + + if (days > 0) { + return `${days}d ${time}`; + } + + return time; +} + +export default formatTimeSpan; diff --git a/frontend/src/Utilities/Date/getRelativeDate.js b/frontend/src/Utilities/Date/getRelativeDate.js new file mode 100644 index 000000000..0d5ab4f58 --- /dev/null +++ b/frontend/src/Utilities/Date/getRelativeDate.js @@ -0,0 +1,40 @@ +import moment from 'moment'; +import formatTime from 'Utilities/Date/formatTime'; +import isInNextWeek from 'Utilities/Date/isInNextWeek'; +import isToday from 'Utilities/Date/isToday'; +import isTomorrow from 'Utilities/Date/isTomorrow'; +import isYesterday from 'Utilities/Date/isYesterday'; + +function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) { + if (!date) { + return null; + } + + if (!showRelativeDates) { + return moment(date).format(shortDateFormat); + } + + if (isYesterday(date)) { + return 'Yesterday'; + } + + if (isToday(date)) { + if (timeForToday && timeFormat) { + return formatTime(date, timeFormat, { includeMinuteZero: true, includeSeconds }); + } + + return 'Today'; + } + + if (isTomorrow(date)) { + return 'Tomorrow'; + } + + if (isInNextWeek(date)) { + return moment(date).format('dddd'); + } + + return moment(date).format(shortDateFormat); +} + +export default getRelativeDate; diff --git a/frontend/src/Utilities/Date/isAfter.js b/frontend/src/Utilities/Date/isAfter.js new file mode 100644 index 000000000..4bbd8660b --- /dev/null +++ b/frontend/src/Utilities/Date/isAfter.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +function isAfter(date, offsets = {}) { + if (!date) { + return false; + } + + const offsetTime = moment(); + + Object.keys(offsets).forEach((key) => { + offsetTime.add(offsets[key], key); + }); + + return moment(date).isAfter(offsetTime); +} + +export default isAfter; diff --git a/frontend/src/Utilities/Date/isBefore.js b/frontend/src/Utilities/Date/isBefore.js new file mode 100644 index 000000000..3e1e81f67 --- /dev/null +++ b/frontend/src/Utilities/Date/isBefore.js @@ -0,0 +1,17 @@ +import moment from 'moment'; + +function isBefore(date, offsets = {}) { + if (!date) { + return false; + } + + const offsetTime = moment(); + + Object.keys(offsets).forEach((key) => { + offsetTime.add(offsets[key], key); + }); + + return moment(date).isBefore(offsetTime); +} + +export default isBefore; diff --git a/frontend/src/Utilities/Date/isInNextWeek.js b/frontend/src/Utilities/Date/isInNextWeek.js new file mode 100644 index 000000000..7b5fd7cc7 --- /dev/null +++ b/frontend/src/Utilities/Date/isInNextWeek.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isInNextWeek(date) { + if (!date) { + return false; + } + const now = moment(); + return moment(date).isBetween(now, now.clone().add(6, 'days').endOf('day')); +} + +export default isInNextWeek; diff --git a/frontend/src/Utilities/Date/isSameWeek.js b/frontend/src/Utilities/Date/isSameWeek.js new file mode 100644 index 000000000..14b76ffb7 --- /dev/null +++ b/frontend/src/Utilities/Date/isSameWeek.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isSameWeek(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment(), 'week'); +} + +export default isSameWeek; diff --git a/frontend/src/Utilities/Date/isToday.js b/frontend/src/Utilities/Date/isToday.js new file mode 100644 index 000000000..31502951f --- /dev/null +++ b/frontend/src/Utilities/Date/isToday.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isToday(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment(), 'day'); +} + +export default isToday; diff --git a/frontend/src/Utilities/Date/isTomorrow.js b/frontend/src/Utilities/Date/isTomorrow.js new file mode 100644 index 000000000..1d83d1b72 --- /dev/null +++ b/frontend/src/Utilities/Date/isTomorrow.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isTomrrow(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment().add(1, 'day'), 'day'); +} + +export default isTomrrow; diff --git a/frontend/src/Utilities/Date/isYesterday.js b/frontend/src/Utilities/Date/isYesterday.js new file mode 100644 index 000000000..9de21d82a --- /dev/null +++ b/frontend/src/Utilities/Date/isYesterday.js @@ -0,0 +1,11 @@ +import moment from 'moment'; + +function isYesterday(date) { + if (!date) { + return false; + } + + return moment(date).isSame(moment().subtract(1, 'day'), 'day'); +} + +export default isYesterday; diff --git a/frontend/src/Utilities/Episode/updateEpisodes.js b/frontend/src/Utilities/Episode/updateEpisodes.js new file mode 100644 index 000000000..91cc61cd9 --- /dev/null +++ b/frontend/src/Utilities/Episode/updateEpisodes.js @@ -0,0 +1,21 @@ +import _ from 'lodash'; +import { update } from 'Store/Actions/baseActions'; + +function updateEpisodes(dispatch, section, episodes, episodeIds, options) { + const data = _.reduce(episodes, (result, item) => { + if (episodeIds.indexOf(item.id) > -1) { + result.push({ + ...item, + ...options + }); + } else { + result.push(item); + } + + return result; + }, []); + + dispatch(update({ section, data })); +} + +export default updateEpisodes; diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js new file mode 100644 index 000000000..b8a4aacc5 --- /dev/null +++ b/frontend/src/Utilities/Number/formatAge.js @@ -0,0 +1,17 @@ +function formatAge(age, ageHours, ageMinutes) { + age = Math.round(age); + ageHours = parseFloat(ageHours); + ageMinutes = ageMinutes && parseFloat(ageMinutes); + + if (age < 2 && ageHours) { + if (ageHours < 2 && !!ageMinutes) { + return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? 'minute' : 'minutes'}`; + } + + return `${ageHours.toFixed(1)} ${ageHours === 1 ? 'hour' : 'hours'}`; + } + + return `${age} ${age === 1 ? 'day' : 'days'}`; +} + +export default formatAge; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.js new file mode 100644 index 000000000..1ff1b5a97 --- /dev/null +++ b/frontend/src/Utilities/Number/formatBytes.js @@ -0,0 +1,16 @@ +import filesize from 'filesize'; + +function formatBytes(input) { + const size = Number(input); + + if (isNaN(size)) { + return ''; + } + + return filesize(size, { + base: 2, + round: 1 + }); +} + +export default formatBytes; diff --git a/frontend/src/Utilities/Number/padNumber.js b/frontend/src/Utilities/Number/padNumber.js new file mode 100644 index 000000000..53ae69cac --- /dev/null +++ b/frontend/src/Utilities/Number/padNumber.js @@ -0,0 +1,10 @@ +function padNumber(input, width, paddingCharacter = 0) { + if (input == null) { + return ''; + } + + input = `${input}`; + return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input; +} + +export default padNumber; diff --git a/frontend/src/Utilities/Object/getErrorMessage.js b/frontend/src/Utilities/Object/getErrorMessage.js new file mode 100644 index 000000000..1ba874660 --- /dev/null +++ b/frontend/src/Utilities/Object/getErrorMessage.js @@ -0,0 +1,11 @@ +function getErrorMessage(xhr, fallbackErrorMessage) { + if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) { + return fallbackErrorMessage; + } + + const message = xhr.responseJSON.message; + + return message || fallbackErrorMessage; +} + +export default getErrorMessage; diff --git a/frontend/src/Utilities/Object/hasDifferentItems.js b/frontend/src/Utilities/Object/hasDifferentItems.js new file mode 100644 index 000000000..f89c99a10 --- /dev/null +++ b/frontend/src/Utilities/Object/hasDifferentItems.js @@ -0,0 +1,10 @@ +import _ from 'lodash'; + +function hasDifferentItems(prevItems, currentItems, idProp = 'id') { + const diff1 = _.differenceBy(prevItems, currentItems, (item) => item[idProp]); + const diff2 = _.differenceBy(currentItems, prevItems, (item) => item[idProp]); + + return diff1.length > 0 || diff2.length > 0; +} + +export default hasDifferentItems; diff --git a/frontend/src/Utilities/Object/selectUniqueIds.js b/frontend/src/Utilities/Object/selectUniqueIds.js new file mode 100644 index 000000000..c2c0c17e3 --- /dev/null +++ b/frontend/src/Utilities/Object/selectUniqueIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function selectUniqueIds(items, idProp) { + const ids = _.reduce(items, (result, item) => { + if (item[idProp]) { + result.push(item[idProp]); + } + + return result; + }, []); + + return _.uniq(ids); +} + +export default selectUniqueIds; diff --git a/frontend/src/Utilities/ResolutionUtility.js b/frontend/src/Utilities/ResolutionUtility.js new file mode 100644 index 000000000..610d3fece --- /dev/null +++ b/frontend/src/Utilities/ResolutionUtility.js @@ -0,0 +1,26 @@ +var $ = require('jquery'); + +module.exports = { + resolutions: { + desktopLarge: 1200, + desktop: 992, + tablet: 768, + mobile: 480 + }, + + isDesktopLarge() { + return $(window).width() < this.resolutions.desktopLarge; + }, + + isDesktop() { + return $(window).width() < this.resolutions.desktop; + }, + + isTablet() { + return $(window).width() < this.resolutions.tablet; + }, + + isMobile() { + return $(window).width() < this.resolutions.mobile; + } +}; diff --git a/frontend/src/Utilities/Series/getMonitoringOptions.js b/frontend/src/Utilities/Series/getMonitoringOptions.js new file mode 100644 index 000000000..d36b7dcfd --- /dev/null +++ b/frontend/src/Utilities/Series/getMonitoringOptions.js @@ -0,0 +1,70 @@ +import _ from 'lodash'; + +function monitorSeasons(seasons, startingSeason) { + seasons.forEach((season) => { + if (season.seasonNumber >= startingSeason) { + season.monitored = true; + } else { + season.monitored = false; + } + }); +} + +function getMonitoringOptions(seasons, monitor) { + if (!seasons.length) { + return { + seasons: [], + options: { + ignoreEpisodesWithFiles: false, + ignoreEpisodesWithoutFiles: false + } + }; + } + + const firstSeason = _.minBy(_.reject(seasons, { seasonNumber: 0 }), 'seasonNumber').seasonNumber; + const lastSeason = _.maxBy(seasons, 'seasonNumber').seasonNumber; + + monitorSeasons(seasons, firstSeason); + + const monitoringOptions = { + ignoreEpisodesWithFiles: false, + ignoreEpisodesWithoutFiles: false + }; + + switch (monitor) { + case 'future': + monitoringOptions.ignoreEpisodesWithFiles = true; + monitoringOptions.ignoreEpisodesWithoutFiles = true; + break; + case 'latest': + monitorSeasons(seasons, lastSeason); + break; + case 'first': + monitorSeasons(seasons, lastSeason + 1); + _.find(seasons, { seasonNumber: firstSeason }).monitored = true; + break; + case 'missing': + monitoringOptions.ignoreEpisodesWithFiles = true; + break; + case 'existing': + monitoringOptions.ignoreEpisodesWithoutFiles = true; + break; + case 'none': + monitorSeasons(seasons, lastSeason + 1); + break; + default: + break; + } + + return { + seasons: _.map(seasons, (season) => { + return _.pick(season, [ + 'seasonNumber', + 'monitored' + ]); + }), + options: monitoringOptions + }; +} + +export default getMonitoringOptions; diff --git a/frontend/src/Utilities/Series/getNewSeries.js b/frontend/src/Utilities/Series/getNewSeries.js new file mode 100644 index 000000000..037827b50 --- /dev/null +++ b/frontend/src/Utilities/Series/getNewSeries.js @@ -0,0 +1,34 @@ +import getMonitoringOptions from 'Utilities/Series/getMonitoringOptions'; + +function getNewSeries(series, payload) { + const { + rootFolderPath, + monitor, + qualityProfileId, + languageProfileId, + seriesType, + albumFolder, + tags, + searchForMissingEpisodes = false + } = payload; + + //const { + //seasons, + //options: addOptions + //} = getMonitoringOptions(series.seasons, monitor); + + //addOptions.searchForMissingEpisodes = searchForMissingEpisodes; + //series.addOptions = addOptions; + //series.seasons = seasons; + series.monitored = true; + series.qualityProfileId = qualityProfileId; + series.languageProfileId = languageProfileId; + series.rootFolderPath = rootFolderPath; + //series.seriesType = seriesType; + series.albumFolder = albumFolder; + series.tags = tags; + + return series; +} + +export default getNewSeries; diff --git a/frontend/src/Utilities/Series/getProgressBarKind.js b/frontend/src/Utilities/Series/getProgressBarKind.js new file mode 100644 index 000000000..eb3b2dd6e --- /dev/null +++ b/frontend/src/Utilities/Series/getProgressBarKind.js @@ -0,0 +1,15 @@ +import { kinds } from 'Helpers/Props'; + +function getProgressBarKind(status, monitored, progress) { + if (progress === 100) { + return status === 'ended' ? kinds.SUCCESS : kinds.PRIMARY; + } + + if (monitored) { + return kinds.DANGER; + } + + return kinds.WARNING; +} + +export default getProgressBarKind; diff --git a/frontend/src/Utilities/State/getProviderState.js b/frontend/src/Utilities/State/getProviderState.js new file mode 100644 index 000000000..7d6e5cb1d --- /dev/null +++ b/frontend/src/Utilities/State/getProviderState.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; + +function getProviderState(payload, getState, getFromState) { + const id = payload.id; + const state = getFromState(getState()); + const pendingChanges = Object.assign({}, state.pendingChanges); + const pendingFields = state.pendingChanges.fields || {}; + delete pendingChanges.fields; + + const item = id ? _.find(state.items, { id }) : state.selectedSchema || state.schema || {}; + + if (item.fields) { + pendingChanges.fields = _.reduce(item.fields, (result, field) => { + const value = pendingFields.hasOwnProperty(field.name) ? + pendingFields[field.name] : + field.value; + + result.push({ + ...field, + value + }); + + return result; + }, []); + } + + return Object.assign({}, item, pendingChanges); +} + +export default getProviderState; diff --git a/frontend/src/Utilities/State/getSectionState.js b/frontend/src/Utilities/State/getSectionState.js new file mode 100644 index 000000000..c188d9eaa --- /dev/null +++ b/frontend/src/Utilities/State/getSectionState.js @@ -0,0 +1,9 @@ +function getSectionState(state, section) { + if (state.hasOwnProperty(section)) { + return Object.assign({}, state[section]); + } + + return Object.assign({}, state); +} + +export default getSectionState; diff --git a/frontend/src/Utilities/State/selectProviderSchema.js b/frontend/src/Utilities/State/selectProviderSchema.js new file mode 100644 index 000000000..c8a31760c --- /dev/null +++ b/frontend/src/Utilities/State/selectProviderSchema.js @@ -0,0 +1,34 @@ +import _ from 'lodash'; +import getSectionState from 'Utilities/State/getSectionState'; +import updateSectionState from 'Utilities/State/updateSectionState'; + +function applySchemaDefaults(selectedSchema, schemaDefaults) { + if (!schemaDefaults) { + return selectedSchema; + } else if (_.isFunction(schemaDefaults)) { + return schemaDefaults(selectedSchema); + } + + return Object.assign(selectedSchema, schemaDefaults); +} + +function selectProviderSchema(state, section, payload, schemaDefaults) { + const newState = getSectionState(state, section); + + const { + implementation, + presetName + } = payload; + + const selectedImplementation = _.find(newState.schema, { implementation }); + + const selectedSchema = presetName ? + _.find(selectedImplementation.presets, { name: presetName }) : + selectedImplementation; + + newState.selectedSchema = applySchemaDefaults(_.cloneDeep(selectedSchema), schemaDefaults); + + return updateSectionState(state, section, newState); +} + +export default selectProviderSchema; diff --git a/frontend/src/Utilities/State/updateSectionState.js b/frontend/src/Utilities/State/updateSectionState.js new file mode 100644 index 000000000..c7407257d --- /dev/null +++ b/frontend/src/Utilities/State/updateSectionState.js @@ -0,0 +1,9 @@ +function updateSectionState(state, section, newState) { + if (state.hasOwnProperty(section)) { + return Object.assign({}, state, { [section]: newState }); + } + + return Object.assign({}, state, newState); +} + +export default updateSectionState; diff --git a/frontend/src/Utilities/String/combinePath.js b/frontend/src/Utilities/String/combinePath.js new file mode 100644 index 000000000..9e4e9abe8 --- /dev/null +++ b/frontend/src/Utilities/String/combinePath.js @@ -0,0 +1,5 @@ +export default function combinePath(isWindows, basePath, paths = []) { + const slash = isWindows ? '\\' : '/'; + + return `${basePath}${slash}${paths.join(slash)}`; +} diff --git a/frontend/src/Utilities/String/split.js b/frontend/src/Utilities/String/split.js new file mode 100644 index 000000000..0e57e7545 --- /dev/null +++ b/frontend/src/Utilities/String/split.js @@ -0,0 +1,17 @@ +import _ from 'lodash'; + +function split(input, separator = ',') { + if (!input) { + return []; + } + + return _.reduce(input.split(separator), (result, s) => { + if (s) { + result.push(s); + } + + return result; + }, []); +} + +export default split; diff --git a/frontend/src/Utilities/String/titleCase.js b/frontend/src/Utilities/String/titleCase.js new file mode 100644 index 000000000..531e4df68 --- /dev/null +++ b/frontend/src/Utilities/String/titleCase.js @@ -0,0 +1,11 @@ +function titleCase(input) { + if (!input) { + return ''; + } + + return input.replace(/\w\S*/g, (match) => { + return match.charAt(0).toUpperCase() + match.substr(1).toLowerCase(); + }); +} + +export default titleCase; diff --git a/frontend/src/Utilities/Table/areAllSelected.js b/frontend/src/Utilities/Table/areAllSelected.js new file mode 100644 index 000000000..23bc8e20d --- /dev/null +++ b/frontend/src/Utilities/Table/areAllSelected.js @@ -0,0 +1,17 @@ +export default function aareAllSelected(selectedState) { + let allSelected = true; + let allUnselected = true; + + Object.keys(selectedState).forEach((key) => { + if (selectedState[key]) { + allUnselected = false; + } else { + allSelected = false; + } + }); + + return { + allSelected, + allUnselected + }; +} diff --git a/frontend/src/Utilities/Table/getSelectedIds.js b/frontend/src/Utilities/Table/getSelectedIds.js new file mode 100644 index 000000000..705f13a5d --- /dev/null +++ b/frontend/src/Utilities/Table/getSelectedIds.js @@ -0,0 +1,15 @@ +import _ from 'lodash'; + +function getSelectedIds(selectedState, { parseIds = true } = {}) { + return _.reduce(selectedState, (result, value, id) => { + if (value) { + const parsedId = parseIds ? parseInt(id) : id; + + result.push(parsedId); + } + + return result; + }, []); +} + +export default getSelectedIds; diff --git a/frontend/src/Utilities/Table/getToggledRange.js b/frontend/src/Utilities/Table/getToggledRange.js new file mode 100644 index 000000000..c0cc44fe5 --- /dev/null +++ b/frontend/src/Utilities/Table/getToggledRange.js @@ -0,0 +1,23 @@ +import _ from 'lodash'; + +function getToggledRange(items, id, lastToggled) { + const lastToggledIndex = _.findIndex(items, { id: lastToggled }); + const changedIndex = _.findIndex(items, { id }); + let lower = 0; + let upper = 0; + + if (lastToggledIndex > changedIndex) { + lower = changedIndex; + upper = lastToggledIndex + 1; + } else { + lower = lastToggledIndex; + upper = changedIndex; + } + + return { + lower, + upper + }; +} + +export default getToggledRange; diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.js b/frontend/src/Utilities/Table/removeOldSelectedState.js new file mode 100644 index 000000000..ff3a4fe11 --- /dev/null +++ b/frontend/src/Utilities/Table/removeOldSelectedState.js @@ -0,0 +1,16 @@ +import areAllSelected from './areAllSelected'; + +export default function removeOldSelectedState(state, prevItems) { + const selectedState = { + ...state.selectedState + }; + + prevItems.forEach((item) => { + delete selectedState[item.id]; + }); + + return { + ...areAllSelected(selectedState), + selectedState + }; +} diff --git a/frontend/src/Utilities/Table/selectAll.js b/frontend/src/Utilities/Table/selectAll.js new file mode 100644 index 000000000..ffaaeaddf --- /dev/null +++ b/frontend/src/Utilities/Table/selectAll.js @@ -0,0 +1,17 @@ +import _ from 'lodash'; + +function selectAll(selectedState, selected) { + const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => { + result[item] = selected; + return result; + }, {}); + + return { + allSelected: selected, + allUnselected: !selected, + lastToggled: null, + selectedState: newSelectedState + }; +} + +export default selectAll; diff --git a/frontend/src/Utilities/Table/toggleSelected.js b/frontend/src/Utilities/Table/toggleSelected.js new file mode 100644 index 000000000..4b19dc268 --- /dev/null +++ b/frontend/src/Utilities/Table/toggleSelected.js @@ -0,0 +1,26 @@ +import areAllSelected from './areAllSelected'; +import getToggledRange from './getToggledRange'; + +function toggleSelected(state, items, id, selected, shiftKey) { + const lastToggled = state.lastToggled; + const selectedState = { + ...state.selectedState, + [id]: selected + }; + + if (shiftKey && lastToggled) { + const { lower, upper } = getToggledRange(items, id, lastToggled); + + for (let i = lower; i < upper; i++) { + selectedState[items[i].id] = selected; + } + } + + return { + ...areAllSelected(selectedState), + lastToggled: id, + selectedState + }; +} + +export default toggleSelected; diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js new file mode 100644 index 000000000..60533d3d3 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.js @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path) { + return `${window.Sonarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.js new file mode 100644 index 000000000..dae5150b7 --- /dev/null +++ b/frontend/src/Utilities/getUniqueElementId.js @@ -0,0 +1,7 @@ +let i = 0; + +// returns a HTML 4.0 compliant element IDs (http://stackoverflow.com/a/79022) + +export default function getUniqueElementId() { + return `id-${i++}`; +} diff --git a/frontend/src/Utilities/isMobile.js b/frontend/src/Utilities/isMobile.js new file mode 100644 index 000000000..489020a23 --- /dev/null +++ b/frontend/src/Utilities/isMobile.js @@ -0,0 +1,7 @@ +import MobileDetect from 'mobile-detect'; + +export default function isMobile() { + const mobileDetect = new MobileDetect(window.navigator.userAgent); + + return mobileDetect.mobile() != null; +} diff --git a/frontend/src/Utilities/pages.js b/frontend/src/Utilities/pages.js new file mode 100644 index 000000000..1355442d9 --- /dev/null +++ b/frontend/src/Utilities/pages.js @@ -0,0 +1,9 @@ +const pages = { + FIRST: 'first', + PREVIOUS: 'previous', + NEXT: 'next', + LAST: 'last', + EXACT: 'exact' +}; + +export default pages; diff --git a/frontend/src/Utilities/requestAction.js b/frontend/src/Utilities/requestAction.js new file mode 100644 index 000000000..3f2564a7b --- /dev/null +++ b/frontend/src/Utilities/requestAction.js @@ -0,0 +1,40 @@ +import $ from 'jquery'; +import _ from 'lodash'; + +function flattenProviderData(providerData) { + return _.reduce(Object.keys(providerData), (result, key) => { + const property = providerData[key]; + + if (key === 'fields') { + result[key] = property; + } else { + result[key] = property.value; + } + + return result; + }, {}); +} + +function requestAction(payload) { + const { + provider, + action, + providerData, + queryParams + } = payload; + + const ajaxOptions = { + url: `/${provider}/action/${action}`, + contentType: 'application/json', + method: 'POST', + data: JSON.stringify(flattenProviderData(providerData)) + }; + + if (queryParams) { + ajaxOptions.url += `?${$.param(queryParams, true)}`; + } + + return $.ajax(ajaxOptions); +} + +export default requestAction; diff --git a/frontend/src/Utilities/sectionTypes.js b/frontend/src/Utilities/sectionTypes.js new file mode 100644 index 000000000..5479b32b9 --- /dev/null +++ b/frontend/src/Utilities/sectionTypes.js @@ -0,0 +1,6 @@ +const sectionTypes = { + COLLECTION: 'collection', + MODEL: 'model' +}; + +export default sectionTypes; diff --git a/frontend/src/Utilities/serverSideCollectionHandlers.js b/frontend/src/Utilities/serverSideCollectionHandlers.js new file mode 100644 index 000000000..03fa39c00 --- /dev/null +++ b/frontend/src/Utilities/serverSideCollectionHandlers.js @@ -0,0 +1,12 @@ +const serverSideCollectionHandlers = { + FETCH: 'fetch', + FIRST_PAGE: 'firstPage', + PREVIOUS_PAGE: 'previousPage', + NEXT_PAGE: 'nextPage', + LAST_PAGE: 'lastPage', + EXACT_PAGE: 'exactPage', + SORT: 'sort', + FILTER: 'filter' +}; + +export default serverSideCollectionHandlers; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js new file mode 100644 index 000000000..bb62d4575 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js @@ -0,0 +1,285 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, icons, kinds } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import CutoffUnmetRow from './CutoffUnmetRow'; + +class CutoffUnmet extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmSearchAllCutoffUnmetModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState((state) => { + return removeOldSelectedState(state, prevProps.items); + }); + }s; + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSearchSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onSearchSelectedPress(selected); + } + + onToggleSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onToggleSelectedPress(selected); + } + + onSearchAllCutoffUnmetPress = () => { + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true }); + } + + onSearchAllCutoffUnmetConfirmed = () => { + this.props.onSearchAllCutoffUnmetPress(); + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false }); + } + + onConfirmSearchAllCutoffUnmetModalClose = () => { + this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isSearchingForEpisodes, + isSearchingForCutoffUnmetEpisodes, + isSaving, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmSearchAllCutoffUnmetModalOpen + } = this.state; + + const itemsSelected = !!this.getSelectedIds().length; + + return ( + + + + + + + + + + + + + + + + + + + Monitored + + + + Unmonitored + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
+ Error fetching cutoff unmet +
+ } + + { + isPopulated && !error && !items.length && +
+ No cutoff unmet items +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + + + +
+ Are you sure you want to search for all {totalRecords} Cutoff Unmet episodes? +
+
+ This cannot be cancelled once started without restarting Sonarr. +
+
+ } + confirmLabel="Search" + onConfirm={this.onSearchAllCutoffUnmetConfirmed} + onCancel={this.onConfirmSearchAllCutoffUnmetModalClose} + /> +
+ } + + + ); + } +} + +CutoffUnmet.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isSearchingForEpisodes: PropTypes.bool.isRequired, + isSearchingForCutoffUnmetEpisodes: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired, + onSearchSelectedPress: PropTypes.func.isRequired, + onToggleSelectedPress: PropTypes.func.isRequired, + onSearchAllCutoffUnmetPress: PropTypes.func.isRequired +}; + +export default CutoffUnmet; diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js new file mode 100644 index 000000000..53ef15430 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js @@ -0,0 +1,180 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as wantedActions from 'Store/Actions/wantedActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileActions'; +import * as commandNames from 'Commands/commandNames'; +import CutoffUnmet from './CutoffUnmet'; + +function createMapStateToProps() { + return createSelector( + (state) => state.wanted.cutoffUnmet, + createCommandsSelector(), + (cutoffUnmet, commands) => { + const isSearchingForEpisodes = _.some(commands, { name: commandNames.EPISODE_SEARCH }); + const isSearchingForCutoffUnmetEpisodes = _.some(commands, { name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH }); + + return { + isSearchingForEpisodes, + isSearchingForCutoffUnmetEpisodes, + isSaving: _.some(cutoffUnmet.items, { isSaving: true }), + ...cutoffUnmet + }; + } + ); +} + +const mapDispatchToProps = { + ...wantedActions, + executeCommand, + fetchQueueDetails, + clearQueueDetails, + fetchEpisodeFiles, + clearEpisodeFiles +}; + +class CutoffUnmetConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.gotoCutoffUnmetFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const episodeIds = selectUniqueIds(this.props.items, 'id'); + const episodeFileIds = selectUniqueIds(this.props.items, 'episodeFileId'); + + this.props.fetchQueueDetails({ episodeIds }); + + if (episodeFileIds.length) { + this.props.fetchEpisodeFiles({ episodeFileIds }); + } + } + } + + componentWillUnmount() { + this.props.clearCutoffUnmet(); + this.props.clearQueueDetails(); + this.props.clearEpisodeFiles(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoCutoffUnmetFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoCutoffUnmetPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoCutoffUnmetNextPage(); + } + + onLastPagePress = () => { + this.props.gotoCutoffUnmetLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoCutoffUnmetPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setCutoffUnmetSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setCutoffUnmetFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setCutoffUnmetTableOption(payload); + + if (payload.pageSize) { + this.props.gotoCutoffUnmetFirstPage(); + } + } + + onSearchSelectedPress = (selected) => { + this.props.executeCommand({ + name: commandNames.EPISODE_SEARCH, + episodeIds: selected + }); + } + + onToggleSelectedPress = (selected) => { + const { + filterKey, + filterValue + } = this.props; + + this.props.batchToggleCutoffUnmetEpisodes({ + episodeIds: selected, + monitored: filterKey !== 'monitored' || !filterValue + }); + } + + onSearchAllCutoffUnmetPress = () => { + this.props.executeCommand({ + name: commandNames.CUTOFF_UNMET_EPISODE_SEARCH + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +CutoffUnmetConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string.isRequired, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + fetchCutoffUnmet: PropTypes.func.isRequired, + gotoCutoffUnmetFirstPage: PropTypes.func.isRequired, + gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired, + gotoCutoffUnmetNextPage: PropTypes.func.isRequired, + gotoCutoffUnmetLastPage: PropTypes.func.isRequired, + gotoCutoffUnmetPage: PropTypes.func.isRequired, + setCutoffUnmetSort: PropTypes.func.isRequired, + setCutoffUnmetFilter: PropTypes.func.isRequired, + setCutoffUnmetTableOption: PropTypes.func.isRequired, + batchToggleCutoffUnmetEpisodes: PropTypes.func.isRequired, + clearCutoffUnmet: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired, + fetchEpisodeFiles: PropTypes.func.isRequired, + clearEpisodeFiles: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector); diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css new file mode 100644 index 000000000..934076d15 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.css @@ -0,0 +1,7 @@ +.episode, +.language, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js new file mode 100644 index 000000000..32bc95650 --- /dev/null +++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetRow.js @@ -0,0 +1,169 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import episodeEntities from 'Episode/episodeEntities'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; +import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import styles from './CutoffUnmetRow.css'; + +function CutoffUnmetRow(props) { + const { + id, + episodeFileId, + series, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + airDateUtc, + title, + isSelected, + columns, + onSelectedChange + } = props; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'series.sortTitle') { + return ( + + + + ); + } + + if (name === 'episode') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'airDateUtc') { + return ( + + ); + } + + if (name === 'language') { + return ( + + + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + }) + } + + ); +} + +CutoffUnmetRow.propTypes = { + id: PropTypes.number.isRequired, + episodeFileId: PropTypes.number, + series: PropTypes.object.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + airDateUtc: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default CutoffUnmetRow; diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js new file mode 100644 index 000000000..35c2143de --- /dev/null +++ b/frontend/src/Wanted/Missing/Missing.js @@ -0,0 +1,307 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; +import selectAll from 'Utilities/Table/selectAll'; +import toggleSelected from 'Utilities/Table/toggleSelected'; +import { align, icons, kinds } from 'Helpers/Props'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import Table from 'Components/Table/Table'; +import TableBody from 'Components/Table/TableBody'; +import TablePager from 'Components/Table/TablePager'; +import PageContent from 'Components/Page/PageContent'; +import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; +import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; +import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; +import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; +import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; +import FilterMenu from 'Components/Menu/FilterMenu'; +import MenuContent from 'Components/Menu/MenuContent'; +import FilterMenuItem from 'Components/Menu/FilterMenuItem'; +import ConfirmModal from 'Components/Modal/ConfirmModal'; +import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; +import MissingRow from './MissingRow'; + +class Missing extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + allSelected: false, + allUnselected: false, + lastToggled: null, + selectedState: {}, + isConfirmSearchAllMissingModalOpen: false, + isInteractiveImportModalOpen: false + }; + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + this.setState((state) => { + return removeOldSelectedState(state, prevProps.items); + }); + } + } + + // + // Control + + getSelectedIds = () => { + return getSelectedIds(this.state.selectedState); + } + + // + // Listeners + + onFilterMenuItemPress = (filterKey, filterValue) => { + this.props.onFilterSelect(filterKey, filterValue); + } + + onSelectAllChange = ({ value }) => { + this.setState(selectAll(this.state.selectedState, value)); + } + + onSelectedChange = ({ id, value, shiftKey = false }) => { + this.setState((state) => { + return toggleSelected(state, this.props.items, id, value, shiftKey); + }); + } + + onSearchSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onSearchSelectedPress(selected); + } + + onToggleSelectedPress = () => { + const selected = this.getSelectedIds(); + + this.props.onToggleSelectedPress(selected); + } + + onSearchAllMissingPress = () => { + this.setState({ isConfirmSearchAllMissingModalOpen: true }); + } + + onSearchAllMissingConfirmed = () => { + this.props.onSearchAllMissingPress(); + this.setState({ isConfirmSearchAllMissingModalOpen: false }); + } + + onConfirmSearchAllMissingModalClose = () => { + this.setState({ isConfirmSearchAllMissingModalOpen: false }); + } + + onInteractiveImportPress = () => { + this.setState({ isInteractiveImportModalOpen: true }); + } + + onInteractiveImportModalClose = () => { + this.setState({ isInteractiveImportModalOpen: false }); + } + + // + // Render + + render() { + const { + isFetching, + isPopulated, + error, + items, + columns, + totalRecords, + isSearchingForEpisodes, + isSearchingForMissingEpisodes, + isSaving, + filterKey, + filterValue, + ...otherProps + } = this.props; + + const { + allSelected, + allUnselected, + selectedState, + isConfirmSearchAllMissingModalOpen, + isInteractiveImportModalOpen + } = this.state; + + const itemsSelected = !!this.getSelectedIds().length; + + return ( + + + + + + + + + + + + + + + + + + + + + + Monitored + + + + Unmonitored + + + + + + + + { + isFetching && !isPopulated && + + } + + { + !isFetching && error && +
+ Error fetching missing items +
+ } + + { + isPopulated && !error && !items.length && +
+ No missing items +
+ } + + { + isPopulated && !error && !!items.length && +
+ + + { + items.map((item) => { + return ( + + ); + }) + } + +
+ + + + +
+ Are you sure you want to search for all {totalRecords} missing episodes? +
+
+ This cannot be cancelled once started without restarting Sonarr. +
+
+ } + confirmLabel="Search" + onConfirm={this.onSearchAllMissingConfirmed} + onCancel={this.onConfirmSearchAllMissingModalClose} + /> + + +
+ } + + + ); + } +} + +Missing.propTypes = { + isFetching: PropTypes.bool.isRequired, + isPopulated: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + totalRecords: PropTypes.number, + isSearchingForEpisodes: PropTypes.bool.isRequired, + isSearchingForMissingEpisodes: PropTypes.bool.isRequired, + isSaving: PropTypes.bool.isRequired, + filterKey: PropTypes.string, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + onFilterSelect: PropTypes.func.isRequired, + onSearchSelectedPress: PropTypes.func.isRequired, + onToggleSelectedPress: PropTypes.func.isRequired, + onSearchAllMissingPress: PropTypes.func.isRequired +}; + +export default Missing; diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js new file mode 100644 index 000000000..97fb61897 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingConnector.js @@ -0,0 +1,168 @@ +import _ from 'lodash'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; +import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; +import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; +import * as wantedActions from 'Store/Actions/wantedActions'; +import { executeCommand } from 'Store/Actions/commandActions'; +import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; +import * as commandNames from 'Commands/commandNames'; +import Missing from './Missing'; + +function createMapStateToProps() { + return createSelector( + (state) => state.wanted.missing, + createCommandsSelector(), + (missing, commands) => { + const isSearchingForEpisodes = _.some(commands, { name: commandNames.EPISODE_SEARCH }); + const isSearchingForMissingEpisodes = _.some(commands, { name: commandNames.MISSING_EPISODE_SEARCH }); + + return { + isSearchingForEpisodes, + isSearchingForMissingEpisodes, + isSaving: _.some(missing.items, { isSaving: true }), + ...missing + }; + } + ); +} + +const mapDispatchToProps = { + ...wantedActions, + executeCommand, + fetchQueueDetails, + clearQueueDetails +}; + +class MissingConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.gotoMissingFirstPage(); + } + + componentDidUpdate(prevProps) { + if (hasDifferentItems(prevProps.items, this.props.items)) { + const episodeIds = selectUniqueIds(this.props.items, 'id'); + this.props.fetchQueueDetails({ episodeIds }); + } + } + + componentWillUnmount() { + this.props.clearMissing(); + this.props.clearQueueDetails(); + } + + // + // Listeners + + onFirstPagePress = () => { + this.props.gotoMissingFirstPage(); + } + + onPreviousPagePress = () => { + this.props.gotoMissingPreviousPage(); + } + + onNextPagePress = () => { + this.props.gotoMissingNextPage(); + } + + onLastPagePress = () => { + this.props.gotoMissingLastPage(); + } + + onPageSelect = (page) => { + this.props.gotoMissingPage({ page }); + } + + onSortPress = (sortKey) => { + this.props.setMissingSort({ sortKey }); + } + + onFilterSelect = (filterKey, filterValue) => { + this.props.setMissingFilter({ filterKey, filterValue }); + } + + onTableOptionChange = (payload) => { + this.props.setMissingTableOption(payload); + + if (payload.pageSize) { + this.props.gotoMissingFirstPage(); + } + } + + onSearchSelectedPress = (selected) => { + this.props.executeCommand({ + name: commandNames.EPISODE_SEARCH, + episodeIds: selected + }); + } + + onToggleSelectedPress = (selected) => { + const { + filterKey, + filterValue + } = this.props; + + this.props.batchToggleMissingEpisodes({ + episodeIds: selected, + monitored: filterKey !== 'monitored' || !filterValue + }); + } + + onSearchAllMissingPress = () => { + this.props.executeCommand({ + name: commandNames.MISSING_EPISODE_SEARCH + }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +MissingConnector.propTypes = { + items: PropTypes.arrayOf(PropTypes.object).isRequired, + filterKey: PropTypes.string.isRequired, + filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]), + fetchMissing: PropTypes.func.isRequired, + gotoMissingFirstPage: PropTypes.func.isRequired, + gotoMissingPreviousPage: PropTypes.func.isRequired, + gotoMissingNextPage: PropTypes.func.isRequired, + gotoMissingLastPage: PropTypes.func.isRequired, + gotoMissingPage: PropTypes.func.isRequired, + setMissingSort: PropTypes.func.isRequired, + setMissingFilter: PropTypes.func.isRequired, + setMissingTableOption: PropTypes.func.isRequired, + clearMissing: PropTypes.func.isRequired, + batchToggleMissingEpisodes: PropTypes.func.isRequired, + executeCommand: PropTypes.func.isRequired, + fetchQueueDetails: PropTypes.func.isRequired, + clearQueueDetails: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(MissingConnector); diff --git a/frontend/src/Wanted/Missing/MissingRow.css b/frontend/src/Wanted/Missing/MissingRow.css new file mode 100644 index 000000000..3ec895d66 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingRow.css @@ -0,0 +1,6 @@ +.episode, +.status { + composes: cell from 'Components/Table/Cells/TableRowCell.css'; + + width: 100px; +} diff --git a/frontend/src/Wanted/Missing/MissingRow.js b/frontend/src/Wanted/Missing/MissingRow.js new file mode 100644 index 000000000..5eae99673 --- /dev/null +++ b/frontend/src/Wanted/Missing/MissingRow.js @@ -0,0 +1,155 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import episodeEntities from 'Episode/episodeEntities'; +import EpisodeTitleLink from 'Episode/EpisodeTitleLink'; +import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector'; +import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber'; +import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector'; +import ArtistNameLink from 'Artist/ArtistNameLink'; +import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; +import TableRow from 'Components/Table/TableRow'; +import TableRowCell from 'Components/Table/Cells/TableRowCell'; +import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; +import styles from './MissingRow.css'; + +function MissingRow(props) { + const { + id, + episodeFileId, + series, + seasonNumber, + episodeNumber, + absoluteEpisodeNumber, + sceneSeasonNumber, + sceneEpisodeNumber, + sceneAbsoluteEpisodeNumber, + airDateUtc, + title, + isSelected, + columns, + onSelectedChange + } = props; + + return ( + + + + { + columns.map((column) => { + const { + name, + isVisible + } = column; + + if (!isVisible) { + return null; + } + + if (name === 'series.sortTitle') { + return ( + + + + ); + } + + if (name === 'episode') { + return ( + + + + ); + } + + if (name === 'episodeTitle') { + return ( + + + + ); + } + + if (name === 'airDateUtc') { + return ( + + ); + } + + if (name === 'status') { + return ( + + + + ); + } + + if (name === 'actions') { + return ( + + ); + } + }) + } + + ); +} + +MissingRow.propTypes = { + id: PropTypes.number.isRequired, + episodeFileId: PropTypes.number, + series: PropTypes.object.isRequired, + seasonNumber: PropTypes.number.isRequired, + episodeNumber: PropTypes.number.isRequired, + absoluteEpisodeNumber: PropTypes.number, + sceneSeasonNumber: PropTypes.number, + sceneEpisodeNumber: PropTypes.number, + sceneAbsoluteEpisodeNumber: PropTypes.number, + airDateUtc: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + isSelected: PropTypes.bool, + columns: PropTypes.arrayOf(PropTypes.object).isRequired, + onSelectedChange: PropTypes.func.isRequired +}; + +export default MissingRow; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 000000000..04e0e11ef --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,15 @@ +html, +body { + height: 100%; /* needed for proper layout */ +} + +body { + overflow: hidden; + background-color: #f5f7fa; +} + +@media only screen and (max-width: $breakpointSmall) { + body { + overflow-y: auto; + } +} diff --git a/frontend/src/index.html b/frontend/src/index.html new file mode 100644 index 000000000..b875687b2 --- /dev/null +++ b/frontend/src/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Sonarr (Preview) + + + + + + +
+
+ + + + + + + + + diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 000000000..396a7971c --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,18 @@ +import React from 'react'; +import { render } from 'react-dom'; +import createHistory from 'history/createBrowserHistory'; +import createAppStore from 'Store/createAppStore'; +import App from './App/App'; +import 'Styles/globals.css'; +import './index.css'; + +const history = createHistory(); +const store = createAppStore(history); + +render( + , + document.getElementById('root') +); diff --git a/frontend/src/jQuery/jquery.ajax.js b/frontend/src/jQuery/jquery.ajax.js new file mode 100644 index 000000000..8acc1897d --- /dev/null +++ b/frontend/src/jQuery/jquery.ajax.js @@ -0,0 +1,47 @@ +const $ = require('JsLibraries/jquery'); + +const absUrlRegex = /^(https?:)?\/\//i; +const apiRoot = window.Sonarr.apiRoot; +const urlBase = window.Sonarr.urlBase; + +function isRelative(xhr) { + return !absUrlRegex.test(xhr.url); +} + +function moveBodyToQuery(xhr) { + if (xhr.data && xhr.type === 'DELETE') { + if (xhr.url.contains('?')) { + xhr.url += '&'; + } else { + xhr.url += '?'; + } + xhr.url += $.param(xhr.data); + delete xhr.data; + } +} + +function addRootUrl(xhr) { + const url = xhr.url; + if (url.startsWith('/signalr')) { + xhr.url = urlBase + xhr.url; + } else { + xhr.url = apiRoot + xhr.url; + } +} + +function addApiKey(xhr) { + xhr.headers = xhr.headers || {}; + xhr.headers['X-Api-Key'] = window.Sonarr.apiKey; +} + +module.exports = function(jQuery) { + const originalAjax = jQuery.ajax; + jQuery.ajax = function(xhr) { + if (xhr && isRelative(xhr)) { + moveBodyToQuery(xhr); + addRootUrl(xhr); + addApiKey(xhr); + } + return originalAjax.apply(this, arguments); + }; +}; diff --git a/frontend/src/login.html b/frontend/src/login.html new file mode 100644 index 000000000..80ad76710 --- /dev/null +++ b/frontend/src/login.html @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Login - Sonarr + + + + + +
+
+
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+ +
+ + + + + + Forgot your password? +
+ + + +
+
+
+ +
+ © + 2010-2017 + - + Sonarr +
+
+
+ + + + diff --git a/src/UI/oauth.html b/frontend/src/oauth.html similarity index 84% rename from src/UI/oauth.html rename to frontend/src/oauth.html index fe6ddf864..16a34dbf3 100644 --- a/src/UI/oauth.html +++ b/frontend/src/oauth.html @@ -2,7 +2,7 @@ - oauth landing page + OAuth landing page @@ -10,4 +10,4 @@ Shouldn't see this - \ No newline at end of file + diff --git a/frontend/src/polyfills.js b/frontend/src/polyfills.js new file mode 100644 index 000000000..9ee333095 --- /dev/null +++ b/frontend/src/polyfills.js @@ -0,0 +1,39 @@ +window.console = window.console || {}; +window.console.log = window.console.log || function() {}; +window.console.group = window.console.group || function() {}; +window.console.groupEnd = window.console.groupEnd || function() {}; +window.console.debug = window.console.debug || function() {}; +window.console.warn = window.console.warn || function() {}; +window.console.assert = window.console.assert || function() {}; + +if (!String.prototype.startsWith) { + Object.defineProperty(String.prototype, 'startsWith', { + enumerable: false, + configurable: false, + writable: false, + value(searchString, position) { + position = position || 0; + return this.indexOf(searchString, position) === position; + } + }); +} + +if (!String.prototype.endsWith) { + Object.defineProperty(String.prototype, 'endsWith', { + enumerable: false, + configurable: false, + writable: false, + value(searchString, position) { + position = position || this.length; + position = position - searchString.length; + var lastIndex = this.lastIndexOf(searchString); + return lastIndex !== -1 && lastIndex === position; + } + }); +} + +if (!('contains' in String.prototype)) { + String.prototype.contains = function(str, startIndex) { + return -1 !== String.prototype.indexOf.call(this, str, startIndex); + }; +} diff --git a/frontend/src/preload.js b/frontend/src/preload.js new file mode 100644 index 000000000..f8ec3c40e --- /dev/null +++ b/frontend/src/preload.js @@ -0,0 +1 @@ +__webpack_public_path__ = `${window.Sonarr.urlBase}/`; diff --git a/frontend/src/vendor.js b/frontend/src/vendor.js new file mode 100644 index 000000000..2b08817be --- /dev/null +++ b/frontend/src/vendor.js @@ -0,0 +1,5 @@ +/* Base */ +// require('jquery'); +require('lodash'); +require('moment'); +// require('signalR'); diff --git a/gulpFile.js b/gulpFile.js index 28dc9b0f1..73636a918 100644 --- a/gulpFile.js +++ b/gulpFile.js @@ -1 +1 @@ -require('./gulp/gulpFile.js'); +require('./frontend/gulp/gulpFile.js'); diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json new file mode 100644 index 000000000..8e12daea1 --- /dev/null +++ b/npm-shrinkwrap.json @@ -0,0 +1,5242 @@ +{ + "name": "lidarr", + "version": "1.0.0", + "dependencies": { + "acorn": { + "version": "3.1.0", + "from": "acorn@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.1.0.tgz" + }, + "acorn-jsx": { + "version": "3.0.1", + "from": "acorn-jsx@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz" + }, + "acorn-to-esprima": { + "version": "2.0.8", + "from": "acorn-to-esprima@>=2.0.6 <3.0.0", + "resolved": "https://registry.npmjs.org/acorn-to-esprima/-/acorn-to-esprima-2.0.8.tgz" + }, + "add-px-to-style": { + "version": "1.0.0", + "from": "add-px-to-style@1.0.0", + "resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz" + }, + "ajv": { + "version": "4.11.8", + "from": "ajv@>=4.7.0 <5.0.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz" + }, + "ajv-keywords": { + "version": "1.5.1", + "from": "ajv-keywords@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz" + }, + "align-text": { + "version": "0.1.4", + "from": "align-text@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz" + }, + "alphanum-sort": { + "version": "1.0.2", + "from": "alphanum-sort@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz" + }, + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + }, + "ansi-escapes": { + "version": "1.4.0", + "from": "ansi-escapes@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz" + }, + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "anymatch": { + "version": "1.3.0", + "from": "anymatch@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz" + }, + "archy": { + "version": "1.0.0", + "from": "archy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz" + }, + "argparse": { + "version": "1.0.7", + "from": "argparse@>=1.0.7 <2.0.0", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz" + }, + "arr-diff": { + "version": "2.0.0", + "from": "arr-diff@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz" + }, + "arr-flatten": { + "version": "1.0.1", + "from": "arr-flatten@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.0.1.tgz" + }, + "array-differ": { + "version": "1.0.0", + "from": "array-differ@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz" + }, + "array-find-index": { + "version": "1.0.1", + "from": "array-find-index@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.1.tgz" + }, + "array-union": { + "version": "1.0.1", + "from": "array-union@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.1.tgz" + }, + "array-uniq": { + "version": "1.0.2", + "from": "array-uniq@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz" + }, + "array-unique": { + "version": "0.2.1", + "from": "array-unique@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz" + }, + "arrify": { + "version": "1.0.1", + "from": "arrify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + }, + "asap": { + "version": "2.0.4", + "from": "asap@>=2.0.3 <2.1.0", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.4.tgz" + }, + "assert": { + "version": "1.4.0", + "from": "assert@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.0.tgz" + }, + "assertion-error": { + "version": "1.0.1", + "from": "assertion-error@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.1.tgz" + }, + "async-each": { + "version": "1.0.0", + "from": "async-each@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.0.tgz" + }, + "autoprefixer": { + "version": "6.3.6", + "from": "autoprefixer@6.3.6", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-6.3.6.tgz" + }, + "babel-code-frame": { + "version": "6.8.0", + "from": "babel-code-frame@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.8.0.tgz" + }, + "babel-core": { + "version": "6.9.0", + "from": "babel-core@6.9.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.9.0.tgz" + }, + "babel-eslint": { + "version": "7.1.0", + "from": "babel-eslint@7.1.0", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-7.1.0.tgz", + "dependencies": { + "babel-code-frame": { + "version": "6.16.0", + "from": "babel-code-frame@>=6.16.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.16.0.tgz" + }, + "babel-traverse": { + "version": "6.18.0", + "from": "babel-traverse@>=6.15.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.18.0.tgz" + }, + "babel-types": { + "version": "6.18.0", + "from": "babel-types@>=6.15.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.18.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.18.0", + "from": "babel-runtime@>=6.9.1 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.18.0.tgz" + } + } + }, + "babylon": { + "version": "6.13.1", + "from": "babylon@>=6.11.2 <7.0.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.13.1.tgz" + }, + "globals": { + "version": "9.12.0", + "from": "globals@>=9.0.0 <10.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.12.0.tgz" + }, + "js-tokens": { + "version": "2.0.0", + "from": "js-tokens@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-2.0.0.tgz" + } + } + }, + "babel-generator": { + "version": "6.9.0", + "from": "babel-generator@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.9.0.tgz" + }, + "babel-helper-builder-binary-assignment-operator-visitor": { + "version": "6.8.0", + "from": "babel-helper-builder-binary-assignment-operator-visitor@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.8.0.tgz" + }, + "babel-helper-builder-react-jsx": { + "version": "6.22.0", + "from": "babel-helper-builder-react-jsx@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@^6.22.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "babel-types": { + "version": "6.22.0", + "from": "babel-types@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@^0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-helper-call-delegate": { + "version": "6.8.0", + "from": "babel-helper-call-delegate@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-call-delegate/-/babel-helper-call-delegate-6.8.0.tgz" + }, + "babel-helper-define-map": { + "version": "6.9.0", + "from": "babel-helper-define-map@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.9.0.tgz" + }, + "babel-helper-explode-assignable-expression": { + "version": "6.8.0", + "from": "babel-helper-explode-assignable-expression@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.8.0.tgz" + }, + "babel-helper-function-name": { + "version": "6.8.0", + "from": "babel-helper-function-name@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.8.0.tgz" + }, + "babel-helper-get-function-arity": { + "version": "6.8.0", + "from": "babel-helper-get-function-arity@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.8.0.tgz" + }, + "babel-helper-hoist-variables": { + "version": "6.8.0", + "from": "babel-helper-hoist-variables@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.8.0.tgz" + }, + "babel-helper-optimise-call-expression": { + "version": "6.8.0", + "from": "babel-helper-optimise-call-expression@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.8.0.tgz" + }, + "babel-helper-regex": { + "version": "6.9.0", + "from": "babel-helper-regex@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-regex/-/babel-helper-regex-6.9.0.tgz" + }, + "babel-helper-remap-async-to-generator": { + "version": "6.11.2", + "from": "babel-helper-remap-async-to-generator@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.11.2.tgz" + }, + "babel-helper-replace-supers": { + "version": "6.8.0", + "from": "babel-helper-replace-supers@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.8.0.tgz" + }, + "babel-helpers": { + "version": "6.8.0", + "from": "babel-helpers@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.8.0.tgz" + }, + "babel-loader": { + "version": "6.2.4", + "from": "babel-loader@6.2.4", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.2.4.tgz" + }, + "babel-messages": { + "version": "6.8.0", + "from": "babel-messages@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.8.0.tgz" + }, + "babel-plugin-check-es2015-constants": { + "version": "6.8.0", + "from": "babel-plugin-check-es2015-constants@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.8.0.tgz" + }, + "babel-plugin-syntax-async-functions": { + "version": "6.8.0", + "from": "babel-plugin-syntax-async-functions@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.8.0.tgz" + }, + "babel-plugin-syntax-class-properties": { + "version": "6.13.0", + "from": "babel-plugin-syntax-class-properties@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz" + }, + "babel-plugin-syntax-decorators": { + "version": "6.8.0", + "from": "babel-plugin-syntax-decorators@>=6.1.18 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.8.0.tgz" + }, + "babel-plugin-syntax-exponentiation-operator": { + "version": "6.8.0", + "from": "babel-plugin-syntax-exponentiation-operator@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.8.0.tgz" + }, + "babel-plugin-syntax-flow": { + "version": "6.18.0", + "from": "babel-plugin-syntax-flow@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz" + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "from": "babel-plugin-syntax-jsx@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz" + }, + "babel-plugin-syntax-object-rest-spread": { + "version": "6.8.0", + "from": "babel-plugin-syntax-object-rest-spread@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.8.0.tgz" + }, + "babel-plugin-syntax-trailing-function-commas": { + "version": "6.8.0", + "from": "babel-plugin-syntax-trailing-function-commas@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.8.0.tgz" + }, + "babel-plugin-transform-async-to-generator": { + "version": "6.8.0", + "from": "babel-plugin-transform-async-to-generator@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.8.0.tgz" + }, + "babel-plugin-transform-class-properties": { + "version": "6.16.0", + "from": "babel-plugin-transform-class-properties@6.16.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.16.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.11.6", + "from": "babel-runtime@>=6.9.1 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.11.6.tgz" + } + } + }, + "babel-plugin-transform-decorators-legacy": { + "version": "1.3.4", + "from": "babel-plugin-transform-decorators-legacy@>=1.3.4 <2.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz" + }, + "babel-plugin-transform-es2015-arrow-functions": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-arrow-functions@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-block-scoped-functions": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-block-scoped-functions@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-block-scoping": { + "version": "6.9.0", + "from": "babel-plugin-transform-es2015-block-scoping@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.9.0.tgz" + }, + "babel-plugin-transform-es2015-classes": { + "version": "6.9.0", + "from": "babel-plugin-transform-es2015-classes@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.9.0.tgz" + }, + "babel-plugin-transform-es2015-computed-properties": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-computed-properties@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-destructuring": { + "version": "6.9.0", + "from": "babel-plugin-transform-es2015-destructuring@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.9.0.tgz" + }, + "babel-plugin-transform-es2015-duplicate-keys": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-duplicate-keys@>=6.6.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-for-of": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-for-of@>=6.6.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-function-name": { + "version": "6.9.0", + "from": "babel-plugin-transform-es2015-function-name@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.9.0.tgz" + }, + "babel-plugin-transform-es2015-literals": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-literals@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-modules-commonjs": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-modules-commonjs@>=6.6.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-object-super": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-object-super@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-parameters": { + "version": "6.9.0", + "from": "babel-plugin-transform-es2015-parameters@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.9.0.tgz" + }, + "babel-plugin-transform-es2015-shorthand-properties": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-shorthand-properties@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-spread": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-spread@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-sticky-regex": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-sticky-regex@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-template-literals": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-template-literals@>=6.6.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-typeof-symbol": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-typeof-symbol@>=6.6.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.8.0.tgz" + }, + "babel-plugin-transform-es2015-unicode-regex": { + "version": "6.8.0", + "from": "babel-plugin-transform-es2015-unicode-regex@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.8.0.tgz" + }, + "babel-plugin-transform-exponentiation-operator": { + "version": "6.8.0", + "from": "babel-plugin-transform-exponentiation-operator@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.8.0.tgz" + }, + "babel-plugin-transform-flow-strip-types": { + "version": "6.22.0", + "from": "babel-plugin-transform-flow-strip-types@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-plugin-transform-object-rest-spread": { + "version": "6.8.0", + "from": "babel-plugin-transform-object-rest-spread@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.8.0.tgz" + }, + "babel-plugin-transform-react-display-name": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-display-name@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@^6.22.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@^0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-plugin-transform-react-jsx": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-jsx@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@^6.22.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@^0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-plugin-transform-react-jsx-self": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-jsx-self@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@^6.22.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@^0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-plugin-transform-react-jsx-source": { + "version": "6.22.0", + "from": "babel-plugin-transform-react-jsx-source@>=6.22.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.22.0", + "from": "babel-runtime@^6.22.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.22.0.tgz" + }, + "regenerator-runtime": { + "version": "0.10.1", + "from": "regenerator-runtime@^0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.1.tgz" + } + } + }, + "babel-plugin-transform-regenerator": { + "version": "6.9.0", + "from": "babel-plugin-transform-regenerator@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.9.0.tgz" + }, + "babel-plugin-transform-strict-mode": { + "version": "6.8.0", + "from": "babel-plugin-transform-strict-mode@>=6.8.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.8.0.tgz" + }, + "babel-preset-decorators-legacy": { + "version": "1.0.0", + "from": "babel-preset-decorators-legacy@1.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-decorators-legacy/-/babel-preset-decorators-legacy-1.0.0.tgz" + }, + "babel-preset-es2015": { + "version": "6.9.0", + "from": "babel-preset-es2015@6.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-es2015/-/babel-preset-es2015-6.9.0.tgz" + }, + "babel-preset-react": { + "version": "6.22.0", + "from": "babel-preset-react@6.22.0", + "resolved": "https://registry.npmjs.org/babel-preset-react/-/babel-preset-react-6.22.0.tgz" + }, + "babel-preset-stage-2": { + "version": "6.5.0", + "from": "babel-preset-stage-2@6.5.0", + "resolved": "https://registry.npmjs.org/babel-preset-stage-2/-/babel-preset-stage-2-6.5.0.tgz" + }, + "babel-preset-stage-3": { + "version": "6.11.0", + "from": "babel-preset-stage-3@>=6.3.13 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-stage-3/-/babel-preset-stage-3-6.11.0.tgz" + }, + "babel-register": { + "version": "6.9.0", + "from": "babel-register@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.9.0.tgz" + }, + "babel-runtime": { + "version": "6.9.0", + "from": "babel-runtime@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.9.0.tgz" + }, + "babel-template": { + "version": "6.9.0", + "from": "babel-template@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.9.0.tgz" + }, + "babel-traverse": { + "version": "6.9.0", + "from": "babel-traverse@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.9.0.tgz" + }, + "babel-types": { + "version": "6.9.0", + "from": "babel-types@>=6.9.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.9.0.tgz" + }, + "babylon": { + "version": "6.8.0", + "from": "babylon@>=6.7.0 <7.0.0", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.8.0.tgz" + }, + "balanced-match": { + "version": "0.4.1", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" + }, + "Base64": { + "version": "0.2.1", + "from": "Base64@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz" + }, + "base64-js": { + "version": "0.0.8", + "from": "base64-js@0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz" + }, + "beeper": { + "version": "1.1.0", + "from": "beeper@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.0.tgz" + }, + "big.js": { + "version": "3.1.3", + "from": "big.js@>=3.1.3 <4.0.0", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.1.3.tgz" + }, + "binary-extensions": { + "version": "1.4.0", + "from": "binary-extensions@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.4.0.tgz" + }, + "bl": { + "version": "0.7.0", + "from": "bl@>=0.7.0 <0.8.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + } + } + }, + "block-stream": { + "version": "0.0.9", + "from": "block-stream@*", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz" + }, + "bluebird": { + "version": "3.4.0", + "from": "bluebird@>=3.1.1 <4.0.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.0.tgz" + }, + "body-parser": { + "version": "1.14.2", + "from": "body-parser@>=1.14.0 <1.15.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.2.tgz", + "dependencies": { + "qs": { + "version": "5.2.0", + "from": "qs@5.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-5.2.0.tgz" + } + } + }, + "brace-expansion": { + "version": "1.1.4", + "from": "brace-expansion@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz" + }, + "braces": { + "version": "1.8.5", + "from": "braces@>=1.8.2 <2.0.0", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz" + }, + "browserify-zlib": { + "version": "0.1.4", + "from": "browserify-zlib@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz" + }, + "browserslist": { + "version": "1.3.4", + "from": "browserslist@>=1.3.1 <1.4.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-1.3.4.tgz" + }, + "buffer": { + "version": "3.6.0", + "from": "buffer@>=3.0.3 <4.0.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz" + }, + "buffer-shims": { + "version": "1.0.0", + "from": "buffer-shims@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz" + }, + "bufferstreams": { + "version": "1.0.1", + "from": "bufferstreams@1.0.1", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-1.0.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.0.33 <2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "builtin-modules": { + "version": "1.1.1", + "from": "builtin-modules@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz" + }, + "bytes": { + "version": "2.2.0", + "from": "bytes@2.2.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.2.0.tgz" + }, + "caller-path": { + "version": "0.1.0", + "from": "caller-path@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz" + }, + "callsites": { + "version": "0.2.0", + "from": "callsites@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz" + }, + "camelcase": { + "version": "2.1.1", + "from": "camelcase@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz" + }, + "camelcase-keys": { + "version": "2.1.0", + "from": "camelcase-keys@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz" + }, + "caniuse-db": { + "version": "1.0.30000492", + "from": "caniuse-db@>=1.0.30000444 <2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30000492.tgz" + }, + "caniuse-lite": { + "version": "1.0.30000708", + "from": "caniuse-lite@>=1.0.30000697 <2.0.0", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000708.tgz" + }, + "center-align": { + "version": "0.1.3", + "from": "center-align@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz" + }, + "chai": { + "version": "3.5.0", + "from": "chai@>=3.2.0 <4.0.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "dependencies": { + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } + } + }, + "chokidar": { + "version": "1.5.1", + "from": "chokidar@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.5.1.tgz" + }, + "circular-json": { + "version": "0.3.1", + "from": "circular-json@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.1.tgz" + }, + "clap": { + "version": "1.1.1", + "from": "clap@>=1.0.9 <2.0.0", + "resolved": "https://registry.npmjs.org/clap/-/clap-1.1.1.tgz" + }, + "classnames": { + "version": "2.2.5", + "from": "classnames@2.2.5", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz" + }, + "clean-css": { + "version": "4.1.2", + "from": "clean-css@4.1.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.1.2.tgz" + }, + "cli-cursor": { + "version": "1.0.2", + "from": "cli-cursor@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz" + }, + "cli-width": { + "version": "2.1.0", + "from": "cli-width@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz" + }, + "clipboard": { + "version": "1.7.1", + "from": "clipboard@latest", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-1.7.1.tgz" + }, + "cliui": { + "version": "2.1.0", + "from": "cliui@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "from": "wordwrap@0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz" + } + } + }, + "clone": { + "version": "1.0.2", + "from": "clone@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz" + }, + "clone-regexp": { + "version": "1.0.0", + "from": "clone-regexp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-1.0.0.tgz" + }, + "clone-stats": { + "version": "0.0.1", + "from": "clone-stats@>=0.0.1 <0.0.2", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz" + }, + "co": { + "version": "4.6.0", + "from": "co@>=4.6.0 <5.0.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + }, + "coa": { + "version": "1.0.1", + "from": "coa@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/coa/-/coa-1.0.1.tgz" + }, + "code-point-at": { + "version": "1.0.0", + "from": "code-point-at@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz" + }, + "color": { + "version": "0.11.3", + "from": "color@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/color/-/color-0.11.3.tgz" + }, + "color-convert": { + "version": "1.3.1", + "from": "color-convert@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.3.1.tgz" + }, + "color-diff": { + "version": "0.1.7", + "from": "color-diff@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/color-diff/-/color-diff-0.1.7.tgz" + }, + "color-name": { + "version": "1.1.1", + "from": "color-name@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz" + }, + "color-string": { + "version": "0.3.0", + "from": "color-string@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-0.3.0.tgz" + }, + "colorguard": { + "version": "1.2.0", + "from": "colorguard@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/colorguard/-/colorguard-1.2.0.tgz", + "dependencies": { + "yargs": { + "version": "1.3.3", + "from": "yargs@>=1.2.6 <2.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-1.3.3.tgz" + } + } + }, + "colormin": { + "version": "1.1.0", + "from": "colormin@>=1.0.5 <2.0.0", + "resolved": "https://registry.npmjs.org/colormin/-/colormin-1.1.0.tgz" + }, + "colors": { + "version": "1.1.2", + "from": "colors@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz" + }, + "commander": { + "version": "2.9.0", + "from": "commander@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" + }, + "concat-map": { + "version": "0.0.1", + "from": "concat-map@0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + }, + "concat-stream": { + "version": "1.5.1", + "from": "concat-stream@>=1.4.7 <2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz" + } + } + }, + "concat-with-sourcemaps": { + "version": "1.0.4", + "from": "concat-with-sourcemaps@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz" + }, + "console-browserify": { + "version": "1.1.0", + "from": "console-browserify@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz" + }, + "consolidate": { + "version": "0.14.1", + "from": "consolidate@>=0.14.1 <0.15.0", + "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.14.1.tgz" + }, + "constants-browserify": { + "version": "0.0.1", + "from": "constants-browserify@0.0.1", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-0.0.1.tgz" + }, + "content-type": { + "version": "1.0.2", + "from": "content-type@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.2.tgz" + }, + "convert-source-map": { + "version": "1.5.0", + "from": "convert-source-map@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz" + }, + "core-js": { + "version": "2.4.0", + "from": "core-js@>=2.4.0 <3.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.0.tgz" + }, + "core-util-is": { + "version": "1.0.2", + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "cosmiconfig": { + "version": "1.1.0", + "from": "cosmiconfig@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-1.1.0.tgz" + }, + "create-react-class": { + "version": "15.6.0", + "from": "create-react-class@>=15.6.0 <16.0.0", + "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.0.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + }, + "fbjs": { + "version": "0.8.12", + "from": "fbjs@^0.8.9", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz" + }, + "js-tokens": { + "version": "3.0.1", + "from": "js-tokens@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz" + }, + "loose-envify": { + "version": "1.3.1", + "from": "loose-envify@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" + } + } + }, + "crypto-browserify": { + "version": "3.2.8", + "from": "crypto-browserify@>=3.2.6 <3.3.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.2.8.tgz" + }, + "css-color-names": { + "version": "0.0.3", + "from": "css-color-names@0.0.3", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.3.tgz" + }, + "css-loader": { + "version": "0.23.1", + "from": "css-loader@0.23.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-0.23.1.tgz" + }, + "css-rule-stream": { + "version": "1.1.0", + "from": "css-rule-stream@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/css-rule-stream/-/css-rule-stream-1.1.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.3 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "css-selector-tokenizer": { + "version": "0.5.4", + "from": "css-selector-tokenizer@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.5.4.tgz" + }, + "css-tokenize": { + "version": "1.0.1", + "from": "css-tokenize@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/css-tokenize/-/css-tokenize-1.0.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.0.33 <2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "cssesc": { + "version": "0.1.0", + "from": "cssesc@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-0.1.0.tgz" + }, + "cssnano": { + "version": "3.7.1", + "from": "cssnano@>=2.6.1 <4.0.0", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-3.7.1.tgz" + }, + "csso": { + "version": "2.0.0", + "from": "csso@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-2.0.0.tgz" + }, + "d": { + "version": "0.1.1", + "from": "d@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz" + }, + "date-now": { + "version": "0.1.4", + "from": "date-now@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz" + }, + "dateformat": { + "version": "1.0.12", + "from": "dateformat@>=1.0.11 <2.0.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz" + }, + "debug": { + "version": "2.2.0", + "from": "debug@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz" + }, + "decamelize": { + "version": "1.2.0", + "from": "decamelize@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + }, + "deep-eql": { + "version": "0.1.3", + "from": "deep-eql@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz", + "dependencies": { + "type-detect": { + "version": "0.1.1", + "from": "type-detect@0.1.1", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz" + } + } + }, + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + }, + "defaults": { + "version": "1.0.3", + "from": "defaults@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz" + }, + "defined": { + "version": "1.0.0", + "from": "defined@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" + }, + "del": { + "version": "2.2.0", + "from": "del@2.2.0", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.0.tgz" + }, + "delegate": { + "version": "3.1.3", + "from": "delegate@>=3.1.2 <4.0.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.1.3.tgz" + }, + "depd": { + "version": "1.1.0", + "from": "depd@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz" + }, + "deprecated": { + "version": "0.0.1", + "from": "deprecated@>=0.0.1 <0.0.2", + "resolved": "https://registry.npmjs.org/deprecated/-/deprecated-0.0.1.tgz" + }, + "detect-indent": { + "version": "3.0.1", + "from": "detect-indent@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-3.0.1.tgz" + }, + "diff": { + "version": "1.4.0", + "from": "diff@>=1.3.2 <2.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz" + }, + "disparity": { + "version": "2.0.0", + "from": "disparity@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/disparity/-/disparity-2.0.0.tgz" + }, + "disposables": { + "version": "1.0.1", + "from": "disposables@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz" + }, + "dnd-core": { + "version": "2.4.0", + "from": "dnd-core@>=2.4.0 <3.0.0", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.4.0.tgz" + }, + "doctrine": { + "version": "1.2.2", + "from": "doctrine@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.2.2.tgz", + "dependencies": { + "esutils": { + "version": "1.1.6", + "from": "esutils@>=1.1.6 <2.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz" + } + } + }, + "doiuse": { + "version": "2.4.1", + "from": "doiuse@>=2.4.1 <3.0.0", + "resolved": "https://registry.npmjs.org/doiuse/-/doiuse-2.4.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "source-map": { + "version": "0.4.4", + "from": "source-map@>=0.4.2 <0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.3 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "dom-css": { + "version": "2.1.0", + "from": "dom-css@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz" + }, + "dom-helpers": { + "version": "3.2.1", + "from": "dom-helpers@>=2.4.0 <3.0.0||>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.2.1.tgz" + }, + "domain-browser": { + "version": "1.1.7", + "from": "domain-browser@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz" + }, + "duplexer": { + "version": "0.1.1", + "from": "duplexer@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz" + }, + "duplexer2": { + "version": "0.0.2", + "from": "duplexer2@0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.1.9 <1.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "ee-first": { + "version": "1.1.1", + "from": "ee-first@1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + }, + "electron-to-chromium": { + "version": "1.3.16", + "from": "electron-to-chromium@>=1.3.16 <2.0.0", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.16.tgz" + }, + "element-class": { + "version": "0.2.2", + "from": "element-class@latest", + "resolved": "https://registry.npmjs.org/element-class/-/element-class-0.2.2.tgz" + }, + "emojis-list": { + "version": "2.0.1", + "from": "emojis-list@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.0.1.tgz" + }, + "encoding": { + "version": "0.1.12", + "from": "encoding@>=0.1.11 <0.2.0", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz" + }, + "end-of-stream": { + "version": "0.1.5", + "from": "end-of-stream@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz" + }, + "enhanced-resolve": { + "version": "0.9.1", + "from": "enhanced-resolve@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-0.9.1.tgz", + "dependencies": { + "memory-fs": { + "version": "0.2.0", + "from": "memory-fs@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz" + } + } + }, + "errno": { + "version": "0.1.4", + "from": "errno@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz" + }, + "error-ex": { + "version": "1.3.0", + "from": "error-ex@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.0.tgz" + }, + "es5-ext": { + "version": "0.10.11", + "from": "es5-ext@>=0.10.8 <0.11.0", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.11.tgz" + }, + "es6-iterator": { + "version": "2.0.0", + "from": "es6-iterator@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz" + }, + "es6-map": { + "version": "0.1.4", + "from": "es6-map@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.4.tgz", + "dependencies": { + "es6-symbol": { + "version": "3.1.0", + "from": "es6-symbol@>=3.1.0 <3.2.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.0.tgz" + } + } + }, + "es6-promise": { + "version": "3.2.1", + "from": "es6-promise@>=3.1.2 <4.0.0", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz" + }, + "es6-set": { + "version": "0.1.4", + "from": "es6-set@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.4.tgz" + }, + "es6-symbol": { + "version": "3.0.2", + "from": "es6-symbol@>=3.0.1 <3.1.0", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.0.2.tgz" + }, + "es6-weak-map": { + "version": "2.0.1", + "from": "es6-weak-map@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz" + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + }, + "escope": { + "version": "3.6.0", + "from": "escope@>=3.6.0 <4.0.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz" + }, + "esformatter": { + "version": "0.9.3", + "from": "esformatter@0.9.3", + "resolved": "https://registry.npmjs.org/esformatter/-/esformatter-0.9.3.tgz", + "dependencies": { + "debug": { + "version": "0.7.4", + "from": "debug@>=0.7.4 <0.8.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-0.7.4.tgz" + }, + "glob": { + "version": "5.0.15", + "from": "glob@>=5.0.3 <6.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz" + }, + "supports-color": { + "version": "1.3.1", + "from": "supports-color@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.3.1.tgz" + }, + "user-home": { + "version": "2.0.0", + "from": "user-home@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz" + } + } + }, + "eslint": { + "version": "2.10.2", + "from": "eslint@2.10.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.10.2.tgz", + "dependencies": { + "glob": { + "version": "7.1.1", + "from": "glob@>=7.0.3 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz" + }, + "globals": { + "version": "9.14.0", + "from": "globals@>=9.2.0 <10.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-9.14.0.tgz" + }, + "minimatch": { + "version": "3.0.3", + "from": "minimatch@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz" + }, + "strip-json-comments": { + "version": "1.0.4", + "from": "strip-json-comments@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz" + }, + "user-home": { + "version": "2.0.0", + "from": "user-home@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz" + } + } + }, + "eslint-loader": { + "version": "1.3.0", + "from": "eslint-loader@1.3.0", + "resolved": "https://registry.npmjs.org/eslint-loader/-/eslint-loader-1.3.0.tgz" + }, + "eslint-plugin-filenames": { + "version": "1.0.0", + "from": "eslint-plugin-filenames@1.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.0.0.tgz" + }, + "eslint-plugin-react": { + "version": "5.2.2", + "from": "eslint-plugin-react@5.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-5.2.2.tgz" + }, + "espree": { + "version": "3.1.4", + "from": "espree@3.1.4", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.1.4.tgz" + }, + "esprima": { + "version": "2.7.2", + "from": "esprima@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" + }, + "esrecurse": { + "version": "4.1.0", + "from": "esrecurse@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz", + "dependencies": { + "estraverse": { + "version": "4.1.1", + "from": "estraverse@>=4.1.0 <4.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz" + } + } + }, + "estraverse": { + "version": "4.2.0", + "from": "estraverse@>=4.2.0 <5.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz" + }, + "esutils": { + "version": "2.0.2", + "from": "esutils@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz" + }, + "event-emitter": { + "version": "0.3.4", + "from": "event-emitter@>=0.3.4 <0.4.0", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz" + }, + "event-stream": { + "version": "3.3.2", + "from": "event-stream@>=3.1.7 <4.0.0", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.2.tgz" + }, + "events": { + "version": "1.1.0", + "from": "events@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.0.tgz" + }, + "execall": { + "version": "1.0.0", + "from": "execall@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/execall/-/execall-1.0.0.tgz" + }, + "exenv": { + "version": "1.2.2", + "from": "exenv@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz" + }, + "exit-hook": { + "version": "1.1.1", + "from": "exit-hook@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz" + }, + "expand-brackets": { + "version": "0.1.5", + "from": "expand-brackets@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz" + }, + "expand-range": { + "version": "1.8.2", + "from": "expand-range@>=1.8.1 <2.0.0", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz" + }, + "extend": { + "version": "2.0.1", + "from": "extend@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-2.0.1.tgz" + }, + "extglob": { + "version": "0.3.2", + "from": "extglob@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz" + }, + "extract-text-webpack-plugin": { + "version": "1.0.1", + "from": "extract-text-webpack-plugin@1.0.1", + "resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-1.0.1.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.5.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + } + } + }, + "fancy-log": { + "version": "1.2.0", + "from": "fancy-log@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.2.0.tgz" + }, + "fast-levenshtein": { + "version": "2.0.5", + "from": "fast-levenshtein@>=2.0.4 <2.1.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz" + }, + "fastparse": { + "version": "1.1.1", + "from": "fastparse@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.1.tgz" + }, + "faye-websocket": { + "version": "0.7.3", + "from": "faye-websocket@>=0.7.2 <0.8.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.7.3.tgz" + }, + "fbjs": { + "version": "0.8.12", + "from": "fbjs@>=0.8.4 <0.9.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + } + } + }, + "figures": { + "version": "1.7.0", + "from": "figures@>=1.3.5 <2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz" + }, + "file-entry-cache": { + "version": "1.3.1", + "from": "file-entry-cache@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.3.1.tgz" + }, + "file-loader": { + "version": "0.9.0", + "from": "file-loader@0.9.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-0.9.0.tgz" + }, + "filename-regex": { + "version": "2.0.0", + "from": "filename-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.0.tgz" + }, + "filesize": { + "version": "3.5.4", + "from": "filesize@latest", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.4.tgz" + }, + "fill-range": { + "version": "2.2.3", + "from": "fill-range@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz" + }, + "find-index": { + "version": "0.1.1", + "from": "find-index@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz" + }, + "find-up": { + "version": "1.1.2", + "from": "find-up@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "dependencies": { + "path-exists": { + "version": "2.1.0", + "from": "path-exists@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz" + } + } + }, + "findup-sync": { + "version": "0.3.0", + "from": "findup-sync@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "dependencies": { + "glob": { + "version": "5.0.15", + "from": "glob@>=5.0.0 <5.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz" + } + } + }, + "first-chunk-stream": { + "version": "1.0.0", + "from": "first-chunk-stream@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz" + }, + "flagged-respawn": { + "version": "0.3.2", + "from": "flagged-respawn@>=0.3.2 <0.4.0", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz" + }, + "flat-cache": { + "version": "1.2.1", + "from": "flat-cache@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.1.tgz" + }, + "flatten": { + "version": "1.0.2", + "from": "flatten@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz" + }, + "for-in": { + "version": "0.1.5", + "from": "for-in@>=0.1.5 <0.2.0", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.5.tgz" + }, + "for-own": { + "version": "0.1.4", + "from": "for-own@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.4.tgz" + }, + "from": { + "version": "0.1.3", + "from": "from@>=0.0.0 <1.0.0", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.3.tgz" + }, + "fs-readfile-promise": { + "version": "2.0.1", + "from": "fs-readfile-promise@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/fs-readfile-promise/-/fs-readfile-promise-2.0.1.tgz" + }, + "fs.realpath": { + "version": "1.0.0", + "from": "fs.realpath@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + }, + "fstream": { + "version": "1.0.9", + "from": "fstream@>=1.0.7 <2.0.0", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.9.tgz" + }, + "gather-stream": { + "version": "1.0.0", + "from": "gather-stream@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/gather-stream/-/gather-stream-1.0.0.tgz" + }, + "gaze": { + "version": "0.5.2", + "from": "gaze@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-0.5.2.tgz" + }, + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" + }, + "get-node-dimensions": { + "version": "1.2.0", + "from": "get-node-dimensions@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/get-node-dimensions/-/get-node-dimensions-1.2.0.tgz" + }, + "get-stdin": { + "version": "4.0.1", + "from": "get-stdin@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz" + }, + "glob": { + "version": "6.0.4", + "from": "glob@>=6.0.1 <7.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz" + }, + "glob-base": { + "version": "0.3.0", + "from": "glob-base@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz" + }, + "glob-parent": { + "version": "2.0.0", + "from": "glob-parent@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz" + }, + "glob-stream": { + "version": "3.1.18", + "from": "glob-stream@>=3.1.5 <4.0.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-3.1.18.tgz", + "dependencies": { + "glob": { + "version": "4.5.3", + "from": "glob@>=4.3.1 <5.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-4.5.3.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "glob-watcher": { + "version": "0.0.6", + "from": "glob-watcher@>=0.0.6 <0.0.7", + "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-0.0.6.tgz" + }, + "glob2base": { + "version": "0.0.12", + "from": "glob2base@>=0.0.12 <0.0.13", + "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz" + }, + "globals": { + "version": "8.18.0", + "from": "globals@>=8.3.0 <9.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-8.18.0.tgz" + }, + "globby": { + "version": "4.1.0", + "from": "globby@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-4.1.0.tgz" + }, + "globjoin": { + "version": "0.1.4", + "from": "globjoin@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz" + }, + "globule": { + "version": "0.1.0", + "from": "globule@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/globule/-/globule-0.1.0.tgz", + "dependencies": { + "glob": { + "version": "3.1.21", + "from": "glob@>=3.1.21 <3.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz" + }, + "graceful-fs": { + "version": "1.2.3", + "from": "graceful-fs@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz" + }, + "inherits": { + "version": "1.0.2", + "from": "inherits@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz" + }, + "lodash": { + "version": "1.0.2", + "from": "lodash@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.0.2.tgz" + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + } + } + }, + "glogg": { + "version": "1.0.0", + "from": "glogg@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz" + }, + "good-listener": { + "version": "1.2.2", + "from": "good-listener@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz" + }, + "graceful-fs": { + "version": "4.1.4", + "from": "graceful-fs@>=4.1.2 <5.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz" + }, + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + }, + "growl": { + "version": "1.8.1", + "from": "growl@1.8.1", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz" + }, + "gulp": { + "version": "3.9.1", + "from": "gulp@3.9.1", + "resolved": "https://registry.npmjs.org/gulp/-/gulp-3.9.1.tgz", + "dependencies": { + "semver": { + "version": "4.3.6", + "from": "semver@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + } + } + }, + "gulp-cached": { + "version": "1.1.0", + "from": "gulp-cached@1.1.0", + "resolved": "https://registry.npmjs.org/gulp-cached/-/gulp-cached-1.1.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.17 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.5.1", + "from": "through2@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz" + }, + "xtend": { + "version": "3.0.0", + "from": "xtend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz" + } + } + }, + "gulp-clean-css": { + "version": "3.3.1", + "from": "gulp-clean-css@3.3.1", + "resolved": "https://registry.npmjs.org/gulp-clean-css/-/gulp-clean-css-3.3.1.tgz", + "dependencies": { + "dateformat": { + "version": "2.0.0", + "from": "dateformat@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz" + }, + "gulp-util": { + "version": "3.0.8", + "from": "gulp-util@3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz" + }, + "object-assign": { + "version": "3.0.0", + "from": "object-assign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz" + } + } + }, + "gulp-concat": { + "version": "2.6.0", + "from": "gulp-concat@2.6.0", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.3 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "gulp-declare": { + "version": "0.3.0", + "from": "gulp-declare@0.3.0", + "resolved": "https://registry.npmjs.org/gulp-declare/-/gulp-declare-0.3.0.tgz" + }, + "gulp-livereload": { + "version": "3.8.1", + "from": "gulp-livereload@3.8.1", + "resolved": "https://registry.npmjs.org/gulp-livereload/-/gulp-livereload-3.8.1.tgz", + "dependencies": { + "ansi-regex": { + "version": "0.2.1", + "from": "ansi-regex@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-0.2.1.tgz" + }, + "ansi-styles": { + "version": "1.1.0", + "from": "ansi-styles@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.1.0.tgz" + }, + "chalk": { + "version": "0.5.1", + "from": "chalk@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.5.1.tgz" + }, + "has-ansi": { + "version": "0.1.0", + "from": "has-ansi@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-0.1.0.tgz" + }, + "lodash.assign": { + "version": "3.2.0", + "from": "lodash.assign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz" + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + }, + "strip-ansi": { + "version": "0.3.0", + "from": "strip-ansi@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.3.0.tgz" + }, + "supports-color": { + "version": "0.2.0", + "from": "supports-color@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-0.2.0.tgz" + } + } + }, + "gulp-postcss": { + "version": "6.1.1", + "from": "gulp-postcss@6.1.1", + "resolved": "https://registry.npmjs.org/gulp-postcss/-/gulp-postcss-6.1.1.tgz" + }, + "gulp-print": { + "version": "2.0.1", + "from": "gulp-print@2.0.1", + "resolved": "https://registry.npmjs.org/gulp-print/-/gulp-print-2.0.1.tgz", + "dependencies": { + "map-stream": { + "version": "0.0.6", + "from": "map-stream@>=0.0.6 <0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.6.tgz" + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "from": "gulp-sourcemaps@1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "dependencies": { + "vinyl": { + "version": "1.1.1", + "from": "vinyl@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.1.1.tgz" + } + } + }, + "gulp-stripbom": { + "version": "1.0.4", + "from": "gulp-stripbom@1.0.4", + "resolved": "https://registry.npmjs.org/gulp-stripbom/-/gulp-stripbom-1.0.4.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.17 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "strip-bom": { + "version": "1.0.0", + "from": "strip-bom@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz" + }, + "through2": { + "version": "0.5.1", + "from": "through2@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.5.1.tgz" + }, + "xtend": { + "version": "3.0.0", + "from": "xtend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz" + } + } + }, + "gulp-util": { + "version": "3.0.7", + "from": "gulp-util@3.0.7", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.7.tgz", + "dependencies": { + "object-assign": { + "version": "3.0.0", + "from": "object-assign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz" + } + } + }, + "gulp-watch": { + "version": "4.3.5", + "from": "gulp-watch@4.3.5", + "resolved": "https://registry.npmjs.org/gulp-watch/-/gulp-watch-4.3.5.tgz", + "dependencies": { + "glob": { + "version": "5.0.15", + "from": "glob@>=5.0.13 <6.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz" + } + } + }, + "gulp-wrap": { + "version": "0.13.0", + "from": "gulp-wrap@0.13.0", + "resolved": "https://registry.npmjs.org/gulp-wrap/-/gulp-wrap-0.13.0.tgz" + }, + "gulplog": { + "version": "1.0.0", + "from": "gulplog@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz" + }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + }, + "has-flag": { + "version": "1.0.0", + "from": "has-flag@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz" + }, + "has-gulplog": { + "version": "0.1.0", + "from": "has-gulplog@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz" + }, + "has-own": { + "version": "1.0.0", + "from": "has-own@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/has-own/-/has-own-1.0.0.tgz" + }, + "history": { + "version": "4.6.3", + "from": "history@latest", + "resolved": "https://registry.npmjs.org/history/-/history-4.6.3.tgz" + }, + "hoist-non-react-statics": { + "version": "1.2.0", + "from": "hoist-non-react-statics@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz" + }, + "home-or-tmp": { + "version": "1.0.0", + "from": "home-or-tmp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-1.0.0.tgz" + }, + "hosted-git-info": { + "version": "2.1.5", + "from": "hosted-git-info@>=2.1.4 <3.0.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.1.5.tgz" + }, + "html-comment-regex": { + "version": "1.1.0", + "from": "html-comment-regex@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.0.tgz" + }, + "html-tags": { + "version": "1.1.1", + "from": "html-tags@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-1.1.1.tgz" + }, + "http-browserify": { + "version": "1.7.0", + "from": "http-browserify@>=1.3.2 <2.0.0", + "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz" + }, + "http-errors": { + "version": "1.3.1", + "from": "http-errors@>=1.3.1 <1.4.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz" + }, + "https-browserify": { + "version": "0.0.0", + "from": "https-browserify@0.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-0.0.0.tgz" + }, + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@0.4.13", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + }, + "icss-replace-symbols": { + "version": "1.0.2", + "from": "icss-replace-symbols@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.0.2.tgz" + }, + "ieee754": { + "version": "1.1.6", + "from": "ieee754@>=1.1.4 <2.0.0", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.6.tgz" + }, + "ignore": { + "version": "3.2.0", + "from": "ignore@>=3.1.2 <4.0.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.2.0.tgz" + }, + "imurmurhash": { + "version": "0.1.4", + "from": "imurmurhash@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + }, + "indent-string": { + "version": "2.1.0", + "from": "indent-string@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "dependencies": { + "repeating": { + "version": "2.0.1", + "from": "repeating@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz" + } + } + }, + "indexes-of": { + "version": "1.0.1", + "from": "indexes-of@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz" + }, + "indexof": { + "version": "0.0.1", + "from": "indexof@0.0.1", + "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz" + }, + "inflight": { + "version": "1.0.5", + "from": "inflight@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "inquirer": { + "version": "0.12.0", + "from": "inquirer@>=0.12.0 <0.13.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz" + }, + "interpret": { + "version": "1.0.1", + "from": "interpret@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.1.tgz" + }, + "invariant": { + "version": "2.2.1", + "from": "invariant@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.1.tgz" + }, + "irregular-plurals": { + "version": "1.2.0", + "from": "irregular-plurals@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-1.2.0.tgz" + }, + "is": { + "version": "3.1.0", + "from": "is@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/is/-/is-3.1.0.tgz" + }, + "is-absolute-url": { + "version": "2.0.0", + "from": "is-absolute-url@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.0.0.tgz" + }, + "is-arrayish": { + "version": "0.2.1", + "from": "is-arrayish@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + }, + "is-binary-path": { + "version": "1.0.1", + "from": "is-binary-path@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz" + }, + "is-buffer": { + "version": "1.1.3", + "from": "is-buffer@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.3.tgz" + }, + "is-builtin-module": { + "version": "1.0.0", + "from": "is-builtin-module@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz" + }, + "is-directory": { + "version": "0.3.1", + "from": "is-directory@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz" + }, + "is-dotfile": { + "version": "1.0.2", + "from": "is-dotfile@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.2.tgz" + }, + "is-equal-shallow": { + "version": "0.1.3", + "from": "is-equal-shallow@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz" + }, + "is-extendable": { + "version": "0.1.1", + "from": "is-extendable@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz" + }, + "is-extglob": { + "version": "1.0.0", + "from": "is-extglob@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz" + }, + "is-finite": { + "version": "1.0.1", + "from": "is-finite@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.1.tgz" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + }, + "is-glob": { + "version": "2.0.1", + "from": "is-glob@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz" + }, + "is-my-json-valid": { + "version": "2.15.0", + "from": "is-my-json-valid@>=2.10.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz" + }, + "is-number": { + "version": "2.1.0", + "from": "is-number@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz" + }, + "is-path-cwd": { + "version": "1.0.0", + "from": "is-path-cwd@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz" + }, + "is-path-in-cwd": { + "version": "1.0.0", + "from": "is-path-in-cwd@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz" + }, + "is-path-inside": { + "version": "1.0.0", + "from": "is-path-inside@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz" + }, + "is-plain-obj": { + "version": "1.1.0", + "from": "is-plain-obj@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" + }, + "is-posix-bracket": { + "version": "0.1.1", + "from": "is-posix-bracket@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz" + }, + "is-primitive": { + "version": "2.0.0", + "from": "is-primitive@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz" + }, + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + }, + "is-regexp": { + "version": "1.0.0", + "from": "is-regexp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" + }, + "is-resolvable": { + "version": "1.0.0", + "from": "is-resolvable@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz" + }, + "is-stream": { + "version": "1.1.0", + "from": "is-stream@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz" + }, + "is-supported-regexp-flag": { + "version": "1.0.0", + "from": "is-supported-regexp-flag@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-supported-regexp-flag/-/is-supported-regexp-flag-1.0.0.tgz" + }, + "is-svg": { + "version": "2.0.1", + "from": "is-svg@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-2.0.1.tgz" + }, + "is-utf8": { + "version": "0.2.1", + "from": "is-utf8@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "isexe": { + "version": "1.1.2", + "from": "isexe@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-1.1.2.tgz" + }, + "isobject": { + "version": "2.1.0", + "from": "isobject@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "from": "isomorphic-fetch@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "jade": { + "version": "0.26.3", + "from": "jade@0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "dependencies": { + "commander": { + "version": "0.6.1", + "from": "commander@0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz" + }, + "mkdirp": { + "version": "0.3.0", + "from": "mkdirp@0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz" + } + } + }, + "jdu": { + "version": "1.0.0", + "from": "https://registry.npmjs.org/jdu/-/jdu-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/jdu/-/jdu-1.0.0.tgz" + }, + "js-base64": { + "version": "2.1.9", + "from": "js-base64@>=2.1.9 <3.0.0", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.1.9.tgz" + }, + "js-tokens": { + "version": "1.0.3", + "from": "js-tokens@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-1.0.3.tgz" + }, + "js-yaml": { + "version": "3.6.1", + "from": "js-yaml@>=3.6.1 <3.7.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz" + }, + "jsesc": { + "version": "0.5.0", + "from": "jsesc@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + }, + "json-stable-stringify": { + "version": "1.0.1", + "from": "json-stable-stringify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.1 <6.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "json5": { + "version": "0.4.0", + "from": "json5@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.4.0.tgz" + }, + "jsonfilter": { + "version": "1.1.2", + "from": "jsonfilter@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsonfilter/-/jsonfilter-1.1.2.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "stream-combiner": { + "version": "0.2.2", + "from": "stream-combiner@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.3 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "jsonify": { + "version": "0.0.0", + "from": "jsonify@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz" + }, + "jsonparse": { + "version": "0.0.5", + "from": "jsonparse@0.0.5", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz" + }, + "jsonpointer": { + "version": "4.0.0", + "from": "jsonpointer@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.0.tgz" + }, + "JSONStream": { + "version": "0.8.4", + "from": "JSONStream@>=0.8.4 <0.9.0", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.8.4.tgz" + }, + "jsx-ast-utils": { + "version": "1.3.1", + "from": "jsx-ast-utils@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.3.1.tgz" + }, + "kind-of": { + "version": "3.0.3", + "from": "kind-of@>=3.0.2 <4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.0.3.tgz" + }, + "known-css-properties": { + "version": "0.0.4", + "from": "known-css-properties@>=0.0.4 <0.0.5", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.0.4.tgz" + }, + "lazy-cache": { + "version": "1.0.4", + "from": "lazy-cache@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz" + }, + "ldjson-stream": { + "version": "1.2.1", + "from": "ldjson-stream@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/ldjson-stream/-/ldjson-stream-1.2.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "levn": { + "version": "0.3.0", + "from": "levn@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" + }, + "liftoff": { + "version": "2.2.1", + "from": "liftoff@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.2.1.tgz" + }, + "livereload-js": { + "version": "2.2.2", + "from": "livereload-js@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.2.2.tgz" + }, + "load-json-file": { + "version": "1.1.0", + "from": "load-json-file@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz" + }, + "loader-utils": { + "version": "0.2.15", + "from": "loader-utils@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.15.tgz", + "dependencies": { + "json5": { + "version": "0.5.0", + "from": "json5@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.0.tgz" + } + } + }, + "lodash": { + "version": "4.17.4", + "from": "lodash@4.17.4", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz" + }, + "lodash-es": { + "version": "4.13.1", + "from": "lodash-es@>=4.2.1 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.13.1.tgz" + }, + "lodash._arraycopy": { + "version": "3.0.0", + "from": "lodash._arraycopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz" + }, + "lodash._arrayeach": { + "version": "3.0.0", + "from": "lodash._arrayeach@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz" + }, + "lodash._baseassign": { + "version": "3.2.0", + "from": "lodash._baseassign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "dependencies": { + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + } + } + }, + "lodash._baseclone": { + "version": "3.3.0", + "from": "lodash._baseclone@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz", + "dependencies": { + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + } + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "from": "lodash._basecopy@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" + }, + "lodash._basefor": { + "version": "3.0.3", + "from": "lodash._basefor@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefor/-/lodash._basefor-3.0.3.tgz" + }, + "lodash._basevalues": { + "version": "3.0.0", + "from": "lodash._basevalues@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz" + }, + "lodash._bindcallback": { + "version": "3.0.1", + "from": "lodash._bindcallback@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz" + }, + "lodash._createassigner": { + "version": "3.1.1", + "from": "lodash._createassigner@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz" + }, + "lodash._createcompounder": { + "version": "3.0.0", + "from": "lodash._createcompounder@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._createcompounder/-/lodash._createcompounder-3.0.0.tgz" + }, + "lodash._getnative": { + "version": "3.9.1", + "from": "lodash._getnative@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "from": "lodash._isiterateecall@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" + }, + "lodash._isnative": { + "version": "2.4.1", + "from": "lodash._isnative@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash._isnative/-/lodash._isnative-2.4.1.tgz" + }, + "lodash._objecttypes": { + "version": "2.4.1", + "from": "lodash._objecttypes@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.4.1.tgz" + }, + "lodash._reescape": { + "version": "3.0.0", + "from": "lodash._reescape@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz" + }, + "lodash._reevaluate": { + "version": "3.0.0", + "from": "lodash._reevaluate@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "from": "lodash._reinterpolate@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" + }, + "lodash._root": { + "version": "3.0.1", + "from": "lodash._root@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz" + }, + "lodash._shimkeys": { + "version": "2.4.1", + "from": "lodash._shimkeys@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.4.1.tgz" + }, + "lodash.camelcase": { + "version": "3.0.1", + "from": "lodash.camelcase@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-3.0.1.tgz" + }, + "lodash.clone": { + "version": "3.0.3", + "from": "lodash.clone@>=3.0.0 <3.1.0-0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-3.0.3.tgz" + }, + "lodash.deburr": { + "version": "3.2.0", + "from": "lodash.deburr@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.deburr/-/lodash.deburr-3.2.0.tgz" + }, + "lodash.defaults": { + "version": "2.4.1", + "from": "lodash.defaults@>=2.4.1 <3.0.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-2.4.1.tgz", + "dependencies": { + "lodash.keys": { + "version": "2.4.1", + "from": "lodash.keys@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.4.1.tgz" + } + } + }, + "lodash.escape": { + "version": "3.2.0", + "from": "lodash.escape@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz" + }, + "lodash.isarguments": { + "version": "3.0.8", + "from": "lodash.isarguments@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.8.tgz" + }, + "lodash.isarray": { + "version": "3.0.4", + "from": "lodash.isarray@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + }, + "lodash.isobject": { + "version": "2.4.1", + "from": "lodash.isobject@>=2.4.1 <2.5.0", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.4.1.tgz" + }, + "lodash.pickby": { + "version": "4.6.0", + "from": "lodash.pickby@>=4.6.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.pickby/-/lodash.pickby-4.6.0.tgz" + }, + "lodash.restparam": { + "version": "3.6.1", + "from": "lodash.restparam@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" + }, + "lodash.template": { + "version": "3.6.2", + "from": "lodash.template@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "dependencies": { + "lodash._basetostring": { + "version": "3.0.1", + "from": "lodash._basetostring@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz" + }, + "lodash.keys": { + "version": "3.1.2", + "from": "lodash.keys@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz" + } + } + }, + "lodash.templatesettings": { + "version": "3.1.1", + "from": "lodash.templatesettings@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz" + }, + "lodash.words": { + "version": "3.2.0", + "from": "lodash.words@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/lodash.words/-/lodash.words-3.2.0.tgz" + }, + "log-symbols": { + "version": "1.0.2", + "from": "log-symbols@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz" + }, + "longest": { + "version": "1.0.1", + "from": "longest@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz" + }, + "loose-envify": { + "version": "1.2.0", + "from": "loose-envify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.2.0.tgz" + }, + "loud-rejection": { + "version": "1.3.0", + "from": "loud-rejection@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.3.0.tgz" + }, + "lru-cache": { + "version": "2.7.3", + "from": "lru-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz" + }, + "map-obj": { + "version": "1.0.1", + "from": "map-obj@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" + }, + "map-stream": { + "version": "0.1.0", + "from": "map-stream@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz" + }, + "mathml-tag-names": { + "version": "2.0.1", + "from": "mathml-tag-names@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.0.1.tgz" + }, + "media-typer": { + "version": "0.3.0", + "from": "media-typer@0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + }, + "memory-fs": { + "version": "0.3.0", + "from": "memory-fs@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.3.0.tgz" + }, + "meow": { + "version": "3.7.0", + "from": "meow@>=3.3.0 <4.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz" + }, + "micromatch": { + "version": "2.3.8", + "from": "micromatch@>=2.1.5 <3.0.0", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.8.tgz" + }, + "mime-db": { + "version": "1.23.0", + "from": "mime-db@>=1.23.0 <1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + }, + "mime-types": { + "version": "2.1.11", + "from": "mime-types@>=2.1.11 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz" + }, + "mini-lr": { + "version": "0.1.9", + "from": "mini-lr@>=0.1.8 <0.2.0", + "resolved": "https://registry.npmjs.org/mini-lr/-/mini-lr-0.1.9.tgz" + }, + "minimatch": { + "version": "2.0.10", + "from": "minimatch@>=2.0.3 <3.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz" + }, + "minimist": { + "version": "1.2.0", + "from": "minimist@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" + }, + "mkdirp": { + "version": "0.5.1", + "from": "mkdirp@>=0.5.1 <0.6.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } + }, + "mobile-detect": { + "version": "1.3.6", + "from": "mobile-detect@latest", + "resolved": "https://registry.npmjs.org/mobile-detect/-/mobile-detect-1.3.6.tgz" + }, + "mocha": { + "version": "2.4.5", + "from": "mocha@>=2.2.5 <3.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.4.5.tgz", + "dependencies": { + "commander": { + "version": "2.3.0", + "from": "commander@2.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz" + }, + "escape-string-regexp": { + "version": "1.0.2", + "from": "escape-string-regexp@1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz" + }, + "glob": { + "version": "3.2.3", + "from": "glob@3.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz" + }, + "graceful-fs": { + "version": "2.0.3", + "from": "graceful-fs@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz" + }, + "minimatch": { + "version": "0.2.14", + "from": "minimatch@>=0.2.11 <0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz" + }, + "supports-color": { + "version": "1.2.0", + "from": "supports-color@1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz" + } + } + }, + "moment": { + "version": "2.17.1", + "from": "moment@latest", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.1.tgz" + }, + "mousetrap": { + "version": "1.6.0", + "from": "mousetrap@latest", + "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.0.tgz" + }, + "mout": { + "version": "1.0.0", + "from": "mout@>=0.9.0 <2.0.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-1.0.0.tgz" + }, + "ms": { + "version": "0.7.1", + "from": "ms@0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" + }, + "multimatch": { + "version": "2.1.0", + "from": "multimatch@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "dependencies": { + "minimatch": { + "version": "3.0.3", + "from": "minimatch@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz" + } + } + }, + "multipipe": { + "version": "0.1.2", + "from": "multipipe@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz" + }, + "mute-stream": { + "version": "0.0.5", + "from": "mute-stream@0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz" + }, + "new-from": { + "version": "0.0.3", + "from": "new-from@0.0.3", + "resolved": "https://registry.npmjs.org/new-from/-/new-from-0.0.3.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.1.8 <1.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "node-fetch": { + "version": "1.6.3", + "from": "node-fetch@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.6.3.tgz" + }, + "node-libs-browser": { + "version": "0.5.3", + "from": "node-libs-browser@>=0.4.0 <=0.6.0", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-0.5.3.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.1.13 <2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "node.extend": { + "version": "1.1.5", + "from": "node.extend@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.5.tgz" + }, + "normalize-package-data": { + "version": "2.3.5", + "from": "normalize-package-data@>=2.3.4 <3.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.3.5.tgz" + }, + "normalize-path": { + "version": "2.0.1", + "from": "normalize-path@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.0.1.tgz" + }, + "normalize-range": { + "version": "0.1.2", + "from": "normalize-range@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + }, + "normalize-selector": { + "version": "0.2.0", + "from": "normalize-selector@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz" + }, + "normalize-url": { + "version": "1.5.3", + "from": "normalize-url@>=1.4.0 <2.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-1.5.3.tgz" + }, + "normalize.css": { + "version": "5.0.0", + "from": "normalize.css@5.0.0", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-5.0.0.tgz" + }, + "npm-path": { + "version": "1.1.0", + "from": "npm-path@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-1.1.0.tgz" + }, + "npm-run": { + "version": "2.0.0", + "from": "npm-run@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/npm-run/-/npm-run-2.0.0.tgz" + }, + "npm-which": { + "version": "2.0.0", + "from": "npm-which@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-2.0.0.tgz" + }, + "nsdeclare": { + "version": "0.1.0", + "from": "nsdeclare@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/nsdeclare/-/nsdeclare-0.1.0.tgz" + }, + "num2fraction": { + "version": "1.2.2", + "from": "num2fraction@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz" + }, + "number-is-nan": { + "version": "1.0.0", + "from": "number-is-nan@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz" + }, + "object-assign": { + "version": "4.1.1", + "from": "object-assign@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + }, + "object-keys": { + "version": "0.4.0", + "from": "object-keys@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz" + }, + "object.omit": { + "version": "2.0.0", + "from": "object.omit@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.0.tgz" + }, + "on-finished": { + "version": "2.3.0", + "from": "on-finished@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + }, + "once": { + "version": "1.3.3", + "from": "once@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz" + }, + "onecolor": { + "version": "3.0.4", + "from": "onecolor@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/onecolor/-/onecolor-3.0.4.tgz" + }, + "onetime": { + "version": "1.1.0", + "from": "onetime@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz" + }, + "optimist": { + "version": "0.6.1", + "from": "optimist@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.10", + "from": "minimist@>=0.0.1 <0.1.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" + } + } + }, + "optionator": { + "version": "0.8.2", + "from": "optionator@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz" + }, + "orchestrator": { + "version": "0.3.7", + "from": "orchestrator@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/orchestrator/-/orchestrator-0.3.7.tgz" + }, + "ordered-read-streams": { + "version": "0.1.0", + "from": "ordered-read-streams@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz" + }, + "os-browserify": { + "version": "0.1.2", + "from": "os-browserify@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz" + }, + "os-homedir": { + "version": "1.0.1", + "from": "os-homedir@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz" + }, + "os-shim": { + "version": "0.1.3", + "from": "os-shim@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz" + }, + "os-tmpdir": { + "version": "1.0.1", + "from": "os-tmpdir@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.1.tgz" + }, + "pako": { + "version": "0.2.8", + "from": "pako@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.8.tgz" + }, + "parse-glob": { + "version": "3.0.4", + "from": "parse-glob@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz" + }, + "parse-json": { + "version": "2.2.0", + "from": "parse-json@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz" + }, + "parseurl": { + "version": "1.3.1", + "from": "parseurl@>=1.3.0 <1.4.0", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" + }, + "path-browserify": { + "version": "0.0.0", + "from": "path-browserify@0.0.0", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz" + }, + "path-exists": { + "version": "1.0.0", + "from": "path-exists@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-1.0.0.tgz" + }, + "path-is-absolute": { + "version": "1.0.0", + "from": "path-is-absolute@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + }, + "path-is-inside": { + "version": "1.0.1", + "from": "path-is-inside@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.1.tgz" + }, + "path-to-regexp": { + "version": "1.7.0", + "from": "path-to-regexp@>=1.5.3 <2.0.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + } + } + }, + "path-type": { + "version": "1.1.0", + "from": "path-type@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz" + }, + "pause-stream": { + "version": "0.0.11", + "from": "pause-stream@0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz" + }, + "pbkdf2-compat": { + "version": "2.0.1", + "from": "pbkdf2-compat@2.0.1", + "resolved": "https://registry.npmjs.org/pbkdf2-compat/-/pbkdf2-compat-2.0.1.tgz" + }, + "performance-now": { + "version": "2.1.0", + "from": "performance-now@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + }, + "pify": { + "version": "2.3.0", + "from": "pify@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + }, + "pinkie": { + "version": "2.0.4", + "from": "pinkie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + }, + "pipetteur": { + "version": "2.0.3", + "from": "pipetteur@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pipetteur/-/pipetteur-2.0.3.tgz" + }, + "plur": { + "version": "2.1.2", + "from": "plur@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-2.1.2.tgz" + }, + "pluralize": { + "version": "1.2.1", + "from": "pluralize@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz" + }, + "postcss": { + "version": "5.0.21", + "from": "postcss@>=5.0.19 <6.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.0.21.tgz" + }, + "postcss-calc": { + "version": "5.2.1", + "from": "postcss-calc@>=5.2.0 <6.0.0", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-5.2.1.tgz" + }, + "postcss-colormin": { + "version": "2.2.0", + "from": "postcss-colormin@>=2.1.8 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.0.tgz" + }, + "postcss-convert-values": { + "version": "2.4.0", + "from": "postcss-convert-values@>=2.3.4 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.4.0.tgz" + }, + "postcss-discard-comments": { + "version": "2.0.4", + "from": "postcss-discard-comments@>=2.0.4 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-2.0.4.tgz" + }, + "postcss-discard-duplicates": { + "version": "2.0.1", + "from": "postcss-discard-duplicates@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-2.0.1.tgz" + }, + "postcss-discard-empty": { + "version": "2.1.0", + "from": "postcss-discard-empty@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-2.1.0.tgz" + }, + "postcss-discard-overridden": { + "version": "0.1.1", + "from": "postcss-discard-overridden@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-0.1.1.tgz" + }, + "postcss-discard-unused": { + "version": "2.2.1", + "from": "postcss-discard-unused@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-2.2.1.tgz" + }, + "postcss-filter-plugins": { + "version": "2.0.0", + "from": "postcss-filter-plugins@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-filter-plugins/-/postcss-filter-plugins-2.0.0.tgz" + }, + "postcss-less": { + "version": "0.14.0", + "from": "postcss-less@>=0.14.0 <0.15.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-0.14.0.tgz" + }, + "postcss-loader": { + "version": "0.9.1", + "from": "postcss-loader@0.9.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-0.9.1.tgz" + }, + "postcss-media-query-parser": { + "version": "0.2.1", + "from": "postcss-media-query-parser@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.1.tgz" + }, + "postcss-merge-idents": { + "version": "2.1.6", + "from": "postcss-merge-idents@>=2.1.5 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-2.1.6.tgz" + }, + "postcss-merge-longhand": { + "version": "2.0.1", + "from": "postcss-merge-longhand@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-2.0.1.tgz" + }, + "postcss-merge-rules": { + "version": "2.0.9", + "from": "postcss-merge-rules@>=2.0.3 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-2.0.9.tgz" + }, + "postcss-message-helpers": { + "version": "2.0.0", + "from": "postcss-message-helpers@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-message-helpers/-/postcss-message-helpers-2.0.0.tgz" + }, + "postcss-minify-font-values": { + "version": "1.0.5", + "from": "postcss-minify-font-values@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-1.0.5.tgz" + }, + "postcss-minify-gradients": { + "version": "1.0.3", + "from": "postcss-minify-gradients@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-1.0.3.tgz" + }, + "postcss-minify-params": { + "version": "1.0.4", + "from": "postcss-minify-params@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-1.0.4.tgz" + }, + "postcss-minify-selectors": { + "version": "2.0.5", + "from": "postcss-minify-selectors@>=2.0.4 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-2.0.5.tgz" + }, + "postcss-modules-extract-imports": { + "version": "1.0.1", + "from": "postcss-modules-extract-imports@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.0.1.tgz" + }, + "postcss-modules-local-by-default": { + "version": "1.1.0", + "from": "postcss-modules-local-by-default@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.1.0.tgz" + }, + "postcss-modules-scope": { + "version": "1.0.1", + "from": "postcss-modules-scope@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.0.1.tgz" + }, + "postcss-modules-values": { + "version": "1.1.3", + "from": "postcss-modules-values@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.1.3.tgz" + }, + "postcss-nested": { + "version": "1.0.0", + "from": "postcss-nested@1.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-1.0.0.tgz" + }, + "postcss-normalize-charset": { + "version": "1.1.0", + "from": "postcss-normalize-charset@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-1.1.0.tgz" + }, + "postcss-normalize-url": { + "version": "3.0.7", + "from": "postcss-normalize-url@>=3.0.7 <4.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-3.0.7.tgz" + }, + "postcss-ordered-values": { + "version": "2.2.1", + "from": "postcss-ordered-values@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-2.2.1.tgz" + }, + "postcss-reduce-idents": { + "version": "2.3.0", + "from": "postcss-reduce-idents@>=2.2.2 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-2.3.0.tgz" + }, + "postcss-reduce-initial": { + "version": "1.0.0", + "from": "postcss-reduce-initial@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-1.0.0.tgz" + }, + "postcss-reduce-transforms": { + "version": "1.0.3", + "from": "postcss-reduce-transforms@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-1.0.3.tgz" + }, + "postcss-reporter": { + "version": "1.4.1", + "from": "postcss-reporter@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-1.4.1.tgz" + }, + "postcss-resolve-nested-selector": { + "version": "0.1.1", + "from": "postcss-resolve-nested-selector@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz" + }, + "postcss-scss": { + "version": "0.3.0", + "from": "postcss-scss@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-0.3.0.tgz", + "dependencies": { + "postcss": { + "version": "5.2.0", + "from": "postcss@>=5.2.0 <6.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.0.tgz" + } + } + }, + "postcss-selector-parser": { + "version": "2.1.0", + "from": "postcss-selector-parser@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.1.0.tgz" + }, + "postcss-simple-vars": { + "version": "3.0.0", + "from": "postcss-simple-vars@3.0.0", + "resolved": "https://registry.npmjs.org/postcss-simple-vars/-/postcss-simple-vars-3.0.0.tgz" + }, + "postcss-sorting": { + "version": "3.0.1", + "from": "postcss-sorting@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/postcss-sorting/-/postcss-sorting-3.0.1.tgz", + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "from": "ansi-styles@^3.1.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz" + }, + "chalk": { + "version": "2.0.1", + "from": "chalk@^2.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz" + }, + "color-convert": { + "version": "1.9.0", + "from": "color-convert@^1.9.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz" + }, + "has-flag": { + "version": "2.0.0", + "from": "has-flag@^2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz" + }, + "postcss": { + "version": "6.0.8", + "from": "postcss@^6.0.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz" + }, + "supports-color": { + "version": "4.2.1", + "from": "supports-color@^4.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz" + } + } + }, + "postcss-svgo": { + "version": "2.1.3", + "from": "postcss-svgo@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-2.1.3.tgz" + }, + "postcss-unique-selectors": { + "version": "2.0.2", + "from": "postcss-unique-selectors@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-2.0.2.tgz" + }, + "postcss-value-parser": { + "version": "3.3.0", + "from": "postcss-value-parser@>=3.2.3 <4.0.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz" + }, + "postcss-zindex": { + "version": "2.1.1", + "from": "postcss-zindex@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-2.1.1.tgz" + }, + "prefix-style": { + "version": "2.0.1", + "from": "prefix-style@2.0.1", + "resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz" + }, + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + }, + "prepend-http": { + "version": "1.0.4", + "from": "prepend-http@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz" + }, + "preserve": { + "version": "0.2.0", + "from": "preserve@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" + }, + "pretty-hrtime": { + "version": "1.0.2", + "from": "pretty-hrtime@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.2.tgz" + }, + "private": { + "version": "0.1.6", + "from": "private@>=0.1.6 <0.2.0", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.6.tgz" + }, + "process": { + "version": "0.11.3", + "from": "process@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.3.tgz" + }, + "process-nextick-args": { + "version": "1.0.7", + "from": "process-nextick-args@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" + }, + "progress": { + "version": "1.1.8", + "from": "progress@>=1.1.8 <2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz" + }, + "promise": { + "version": "7.1.1", + "from": "promise@>=7.1.1 <8.0.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.1.1.tgz" + }, + "prop-types": { + "version": "15.5.10", + "from": "prop-types@latest", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.5.10.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + }, + "fbjs": { + "version": "0.8.12", + "from": "fbjs@>=0.8.9 <0.9.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz" + }, + "js-tokens": { + "version": "3.0.1", + "from": "js-tokens@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.1.tgz" + }, + "loose-envify": { + "version": "1.3.1", + "from": "loose-envify@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" + } + } + }, + "protochain": { + "version": "1.0.3", + "from": "protochain@>=1.0.3 <2.0.0", + "resolved": "https://registry.npmjs.org/protochain/-/protochain-1.0.3.tgz" + }, + "prr": { + "version": "0.0.0", + "from": "prr@>=0.0.0 <0.1.0", + "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz" + }, + "punycode": { + "version": "1.4.1", + "from": "punycode@>=1.4.1 <2.0.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + }, + "q": { + "version": "1.4.1", + "from": "q@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz" + }, + "qs": { + "version": "2.2.5", + "from": "qs@>=2.2.3 <2.3.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-2.2.5.tgz" + }, + "query-string": { + "version": "4.2.2", + "from": "https://registry.npmjs.org/query-string/-/query-string-4.2.2.tgz", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.2.2.tgz" + }, + "querystring": { + "version": "0.2.0", + "from": "querystring@0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz" + }, + "querystring-es3": { + "version": "0.2.1", + "from": "querystring-es3@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz" + }, + "raf": { + "version": "3.3.2", + "from": "raf@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.3.2.tgz" + }, + "randomatic": { + "version": "1.1.5", + "from": "randomatic@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.5.tgz" + }, + "raven-js": { + "version": "3.9.2", + "from": "raven-js@>=3.1.1 <4.0.0", + "resolved": "https://registry.npmjs.org/raven-js/-/raven-js-3.9.2.tgz" + }, + "raw-body": { + "version": "2.1.6", + "from": "raw-body@>=2.1.5 <2.2.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.6.tgz", + "dependencies": { + "bytes": { + "version": "2.3.0", + "from": "bytes@2.3.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-2.3.0.tgz" + } + } + }, + "react": { + "version": "15.6.1", + "from": "react@15.6.1", + "resolved": "https://registry.npmjs.org/react/-/react-15.6.1.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@^1.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + }, + "fbjs": { + "version": "0.8.12", + "from": "fbjs@>=0.8.9 <0.9.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz" + } + } + }, + "react-addons-shallow-compare": { + "version": "15.6.0", + "from": "react-addons-shallow-compare@15.6.0", + "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.0.tgz" + }, + "react-async-script": { + "version": "0.9.1", + "from": "react-async-script@0.9.1", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-0.9.1.tgz" + }, + "react-autosuggest": { + "version": "9.3.0", + "from": "react-autosuggest@9.3.0", + "resolved": "https://registry.npmjs.org/react-autosuggest/-/react-autosuggest-9.3.0.tgz" + }, + "react-autowhatever": { + "version": "10.1.0", + "from": "react-autowhatever@>=10.1.0 <11.0.0", + "resolved": "https://registry.npmjs.org/react-autowhatever/-/react-autowhatever-10.1.0.tgz" + }, + "react-custom-scrollbars": { + "version": "4.1.2", + "from": "react-custom-scrollbars@latest", + "resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.1.2.tgz" + }, + "react-dnd": { + "version": "2.4.0", + "from": "react-dnd@2.4.0", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.4.0.tgz" + }, + "react-dnd-html5-backend": { + "version": "2.4.1", + "from": "react-dnd-html5-backend@2.4.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.4.1.tgz" + }, + "react-document-title": { + "version": "2.0.3", + "from": "react-document-title@2.0.3", + "resolved": "https://registry.npmjs.org/react-document-title/-/react-document-title-2.0.3.tgz" + }, + "react-dom": { + "version": "15.6.1", + "from": "react-dom@15.6.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.1.tgz", + "dependencies": { + "core-js": { + "version": "1.2.7", + "from": "core-js@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz" + }, + "fbjs": { + "version": "0.8.12", + "from": "fbjs@>=0.8.9 <0.9.0", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.12.tgz" + } + } + }, + "react-google-recaptcha": { + "version": "0.9.6", + "from": "react-google-recaptcha@0.9.6", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-0.9.6.tgz" + }, + "react-lazyload": { + "version": "2.2.7", + "from": "react-lazyload@2.2.7", + "resolved": "https://registry.npmjs.org/react-lazyload/-/react-lazyload-2.2.7.tgz" + }, + "react-measure": { + "version": "1.4.7", + "from": "react-measure@1.4.7", + "resolved": "https://registry.npmjs.org/react-measure/-/react-measure-1.4.7.tgz" + }, + "react-portal": { + "version": "3.1.0", + "from": "react-portal@3.1.0", + "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-3.1.0.tgz" + }, + "react-redux": { + "version": "5.0.5", + "from": "react-redux@5.0.5", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.0.5.tgz" + }, + "react-router": { + "version": "4.1.1", + "from": "react-router@>=4.1.1 <5.0.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.1.1.tgz", + "dependencies": { + "invariant": { + "version": "2.2.2", + "from": "invariant@>=2.2.2 <3.0.0", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz" + }, + "js-tokens": { + "version": "3.0.2", + "from": "js-tokens@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz" + }, + "loose-envify": { + "version": "1.3.1", + "from": "loose-envify@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" + } + } + }, + "react-router-dom": { + "version": "4.1.1", + "from": "react-router-dom@latest", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.1.1.tgz", + "dependencies": { + "history": { + "version": "4.6.3", + "from": "history@>=4.5.1 <5.0.0", + "resolved": "https://registry.npmjs.org/history/-/history-4.6.3.tgz" + }, + "js-tokens": { + "version": "3.0.2", + "from": "js-tokens@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz" + }, + "loose-envify": { + "version": "1.3.1", + "from": "loose-envify@>=1.3.1 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" + }, + "react-router": { + "version": "4.1.1", + "from": "react-router@>=4.1.1 <5.0.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.1.1.tgz", + "dependencies": { + "invariant": { + "version": "2.2.2", + "from": "invariant@>=2.2.2 <3.0.0", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz" + } + } + } + } + }, + "react-router-redux": { + "version": "5.0.0-alpha.6", + "from": "react-router-redux@next", + "resolved": "https://registry.npmjs.org/react-router-redux/-/react-router-redux-5.0.0-alpha.6.tgz" + }, + "react-side-effect": { + "version": "1.1.3", + "from": "react-side-effect@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.1.3.tgz" + }, + "react-slider": { + "version": "0.8.0", + "from": "react-slider@0.8.0", + "resolved": "https://registry.npmjs.org/react-slider/-/react-slider-0.8.0.tgz" + }, + "react-tabs": { + "version": "1.1.0", + "from": "react-tabs@1.1.0", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-1.1.0.tgz" + }, + "react-tag-autocomplete": { + "version": "5.4.0", + "from": "react-tag-autocomplete@5.4.0", + "resolved": "https://registry.npmjs.org/react-tag-autocomplete/-/react-tag-autocomplete-5.4.0.tgz" + }, + "react-tether": { + "version": "0.5.7", + "from": "react-tether@0.5.7", + "resolved": "https://registry.npmjs.org/react-tether/-/react-tether-0.5.7.tgz" + }, + "react-themeable": { + "version": "1.1.0", + "from": "react-themeable@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/react-themeable/-/react-themeable-1.1.0.tgz", + "dependencies": { + "object-assign": { + "version": "3.0.0", + "from": "object-assign@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz" + } + } + }, + "react-virtualized": { + "version": "9.8.0", + "from": "react-virtualized@9.8.0", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.8.0.tgz", + "dependencies": { + "babel-runtime": { + "version": "6.23.0", + "from": "babel-runtime@>=6.11.6 <7.0.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz" + }, + "js-tokens": { + "version": "3.0.2", + "from": "js-tokens@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz" + }, + "loose-envify": { + "version": "1.3.1", + "from": "loose-envify@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz" + }, + "regenerator-runtime": { + "version": "0.10.5", + "from": "regenerator-runtime@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz" + } + } + }, + "read-file-stdin": { + "version": "0.2.1", + "from": "read-file-stdin@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/read-file-stdin/-/read-file-stdin-0.2.1.tgz" + }, + "read-pkg": { + "version": "1.1.0", + "from": "read-pkg@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz" + }, + "read-pkg-up": { + "version": "1.0.1", + "from": "read-pkg-up@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz" + }, + "readable-stream": { + "version": "2.2.9", + "from": "readable-stream@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.9.tgz", + "dependencies": { + "string_decoder": { + "version": "1.0.0", + "from": "string_decoder@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.0.tgz" + } + } + }, + "readdirp": { + "version": "2.0.0", + "from": "readdirp@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.0.0.tgz" + }, + "readline2": { + "version": "1.0.1", + "from": "readline2@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz" + }, + "rechoir": { + "version": "0.6.2", + "from": "rechoir@>=0.6.2 <0.7.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz" + }, + "redent": { + "version": "1.0.0", + "from": "redent@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz" + }, + "reduce-css-calc": { + "version": "1.2.4", + "from": "reduce-css-calc@>=1.2.0 <2.0.0", + "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-1.2.4.tgz", + "dependencies": { + "balanced-match": { + "version": "0.1.0", + "from": "balanced-match@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz" + } + } + }, + "reduce-function-call": { + "version": "1.0.1", + "from": "reduce-function-call@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.1.tgz", + "dependencies": { + "balanced-match": { + "version": "0.1.0", + "from": "balanced-match@~0.1.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.1.0.tgz" + } + } + }, + "reduce-reducers": { + "version": "0.1.2", + "from": "reduce-reducers@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.1.2.tgz" + }, + "redux": { + "version": "3.7.0", + "from": "redux@3.7.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.0.tgz" + }, + "redux-actions": { + "version": "2.0.3", + "from": "redux-actions@2.0.3", + "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.0.3.tgz", + "dependencies": { + "lodash-es": { + "version": "4.17.4", + "from": "lodash-es@>=4.17.4 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz" + } + } + }, + "redux-batched-actions": { + "version": "0.2.0", + "from": "redux-batched-actions@0.2.0", + "resolved": "https://registry.npmjs.org/redux-batched-actions/-/redux-batched-actions-0.2.0.tgz" + }, + "redux-localstorage": { + "version": "0.4.1", + "from": "redux-localstorage@0.4.1", + "resolved": "https://registry.npmjs.org/redux-localstorage/-/redux-localstorage-0.4.1.tgz" + }, + "redux-raven-middleware": { + "version": "1.2.0", + "from": "redux-raven-middleware@latest", + "resolved": "https://registry.npmjs.org/redux-raven-middleware/-/redux-raven-middleware-1.2.0.tgz" + }, + "redux-thunk": { + "version": "2.2.0", + "from": "redux-thunk@2.2.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.2.0.tgz" + }, + "regenerate": { + "version": "1.2.1", + "from": "regenerate@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.2.1.tgz" + }, + "regenerator-runtime": { + "version": "0.9.5", + "from": "regenerator-runtime@>=0.9.5 <0.10.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.9.5.tgz" + }, + "regex-cache": { + "version": "0.4.3", + "from": "regex-cache@>=0.4.2 <0.5.0", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz" + }, + "regexpu-core": { + "version": "1.0.0", + "from": "regexpu-core@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz" + }, + "regjsgen": { + "version": "0.2.0", + "from": "regjsgen@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz" + }, + "regjsparser": { + "version": "0.1.5", + "from": "regjsparser@>=0.1.4 <0.2.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz" + }, + "repeat-element": { + "version": "1.1.2", + "from": "repeat-element@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz" + }, + "repeat-string": { + "version": "1.5.4", + "from": "repeat-string@>=1.5.0 <2.0.0", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.5.4.tgz" + }, + "repeating": { + "version": "1.1.3", + "from": "repeating@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz" + }, + "replace-ext": { + "version": "0.0.1", + "from": "replace-ext@0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz" + }, + "require-from-string": { + "version": "1.2.0", + "from": "require-from-string@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-1.2.0.tgz" + }, + "require-nocache": { + "version": "1.0.0", + "from": "require-nocache@latest", + "resolved": "https://registry.npmjs.org/require-nocache/-/require-nocache-1.0.0.tgz" + }, + "require-uncached": { + "version": "1.0.3", + "from": "require-uncached@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz" + }, + "reselect": { + "version": "3.0.1", + "from": "reselect@3.0.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz" + }, + "resize-observer-polyfill": { + "version": "1.4.2", + "from": "resize-observer-polyfill@>=1.4.2 <2.0.0", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.4.2.tgz" + }, + "resolve": { + "version": "1.1.7", + "from": "resolve@>=1.1.5 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz" + }, + "resolve-from": { + "version": "1.0.1", + "from": "resolve-from@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz" + }, + "resolve-pathname": { + "version": "2.1.0", + "from": "resolve-pathname@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-2.1.0.tgz" + }, + "restore-cursor": { + "version": "1.0.1", + "from": "restore-cursor@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz" + }, + "right-align": { + "version": "0.1.3", + "from": "right-align@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz" + }, + "rimraf": { + "version": "2.5.2", + "from": "rimraf@>=2.2.8 <3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz", + "dependencies": { + "glob": { + "version": "7.0.3", + "from": "glob@>=7.0.0 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz" + } + } + }, + "ripemd160": { + "version": "0.2.0", + "from": "ripemd160@0.2.0", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-0.2.0.tgz" + }, + "rocambole": { + "version": "0.7.0", + "from": "rocambole@>=0.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/rocambole/-/rocambole-0.7.0.tgz" + }, + "rocambole-indent": { + "version": "2.0.4", + "from": "rocambole-indent@>=2.0.4 <3.0.0", + "resolved": "https://registry.npmjs.org/rocambole-indent/-/rocambole-indent-2.0.4.tgz", + "dependencies": { + "mout": { + "version": "0.11.1", + "from": "mout@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz" + } + } + }, + "rocambole-linebreak": { + "version": "1.0.1", + "from": "rocambole-linebreak@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/rocambole-linebreak/-/rocambole-linebreak-1.0.1.tgz", + "dependencies": { + "semver": { + "version": "4.3.6", + "from": "semver@>=4.3.1 <5.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + } + } + }, + "rocambole-node": { + "version": "1.0.0", + "from": "rocambole-node@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/rocambole-node/-/rocambole-node-1.0.0.tgz" + }, + "rocambole-token": { + "version": "1.2.1", + "from": "rocambole-token@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/rocambole-token/-/rocambole-token-1.2.1.tgz" + }, + "rocambole-whitespace": { + "version": "1.0.0", + "from": "rocambole-whitespace@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/rocambole-whitespace/-/rocambole-whitespace-1.0.0.tgz" + }, + "run-async": { + "version": "0.1.0", + "from": "run-async@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz" + }, + "run-sequence": { + "version": "1.2.0", + "from": "run-sequence@1.2.0", + "resolved": "https://registry.npmjs.org/run-sequence/-/run-sequence-1.2.0.tgz" + }, + "rx-lite": { + "version": "3.1.2", + "from": "rx-lite@>=3.1.2 <4.0.0", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz" + }, + "sax": { + "version": "1.2.1", + "from": "sax@>=1.2.1 <1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" + }, + "section-iterator": { + "version": "2.0.0", + "from": "section-iterator@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/section-iterator/-/section-iterator-2.0.0.tgz" + }, + "select": { + "version": "1.1.2", + "from": "select@>=1.1.2 <2.0.0", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz" + }, + "semver": { + "version": "5.3.0", + "from": "semver@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0||>=4.0.0 <5.0.0||>=5.0.0 <6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz" + }, + "sequencify": { + "version": "0.0.7", + "from": "sequencify@>=0.0.7 <0.1.0", + "resolved": "https://registry.npmjs.org/sequencify/-/sequencify-0.0.7.tgz" + }, + "serializerr": { + "version": "1.0.2", + "from": "serializerr@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/serializerr/-/serializerr-1.0.2.tgz" + }, + "setimmediate": { + "version": "1.0.5", + "from": "setimmediate@>=1.0.5 <2.0.0", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz" + }, + "sha.js": { + "version": "2.2.6", + "from": "sha.js@2.2.6", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.2.6.tgz" + }, + "shallow-equal": { + "version": "1.0.0", + "from": "shallow-equal@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.0.0.tgz" + }, + "shallowequal": { + "version": "1.0.1", + "from": "shallowequal@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.0.1.tgz" + }, + "shebang-regex": { + "version": "1.0.0", + "from": "shebang-regex@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz" + }, + "shelljs": { + "version": "0.6.1", + "from": "shelljs@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.1.tgz" + }, + "sigmund": { + "version": "1.0.1", + "from": "sigmund@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz" + }, + "signal-exit": { + "version": "2.1.2", + "from": "signal-exit@>=2.1.2 <3.0.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-2.1.2.tgz" + }, + "slash": { + "version": "1.0.0", + "from": "slash@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz" + }, + "slice-ansi": { + "version": "0.0.4", + "from": "slice-ansi@0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz" + }, + "sort-keys": { + "version": "1.1.2", + "from": "sort-keys@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz" + }, + "source-list-map": { + "version": "0.1.6", + "from": "source-list-map@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-0.1.6.tgz" + }, + "source-map": { + "version": "0.5.6", + "from": "source-map@>=0.5.5 <0.6.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" + }, + "source-map-support": { + "version": "0.2.10", + "from": "source-map-support@>=0.2.10 <0.3.0", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.2.10.tgz", + "dependencies": { + "source-map": { + "version": "0.1.32", + "from": "source-map@0.1.32", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz" + } + } + }, + "sparkles": { + "version": "1.0.0", + "from": "sparkles@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz" + }, + "spawn-sync": { + "version": "1.0.15", + "from": "spawn-sync@>=1.0.5 <2.0.0", + "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz" + }, + "spdx-correct": { + "version": "1.0.2", + "from": "spdx-correct@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz" + }, + "spdx-exceptions": { + "version": "1.0.4", + "from": "spdx-exceptions@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-1.0.4.tgz" + }, + "spdx-expression-parse": { + "version": "1.0.2", + "from": "spdx-expression-parse@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.2.tgz" + }, + "spdx-license-ids": { + "version": "1.2.1", + "from": "spdx-license-ids@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.1.tgz" + }, + "specificity": { + "version": "0.2.1", + "from": "specificity@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.2.1.tgz" + }, + "split": { + "version": "0.3.3", + "from": "split@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz" + }, + "split2": { + "version": "0.2.1", + "from": "split2@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-0.2.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + } + } + }, + "sprintf-js": { + "version": "1.0.3", + "from": "sprintf-js@>=1.0.2 <1.1.0", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + }, + "statuses": { + "version": "1.3.0", + "from": "statuses@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.0.tgz" + }, + "stdin": { + "version": "0.0.1", + "from": "stdin@*", + "resolved": "https://registry.npmjs.org/stdin/-/stdin-0.0.1.tgz" + }, + "stream-browserify": { + "version": "1.0.0", + "from": "stream-browserify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-1.0.0.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.1.14", + "from": "readable-stream@>=1.0.27-1 <2.0.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz" + } + } + }, + "stream-combiner": { + "version": "0.0.4", + "from": "stream-combiner@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz" + }, + "stream-consume": { + "version": "0.1.0", + "from": "stream-consume@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/stream-consume/-/stream-consume-0.1.0.tgz" + }, + "streamqueue": { + "version": "1.1.1", + "from": "streamqueue@1.1.1", + "resolved": "https://registry.npmjs.org/streamqueue/-/streamqueue-1.1.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + } + } + }, + "strict-uri-encode": { + "version": "1.1.0", + "from": "strict-uri-encode@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "string-width": { + "version": "1.0.1", + "from": "string-width@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "strip-bom": { + "version": "2.0.0", + "from": "strip-bom@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz" + }, + "strip-bom-stream": { + "version": "1.0.0", + "from": "strip-bom-stream@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz" + }, + "strip-indent": { + "version": "1.0.1", + "from": "strip-indent@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz" + }, + "strip-json-comments": { + "version": "0.1.3", + "from": "strip-json-comments@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-0.1.3.tgz" + }, + "style-loader": { + "version": "0.13.1", + "from": "style-loader@0.13.1", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.13.1.tgz" + }, + "style-search": { + "version": "0.1.0", + "from": "style-search@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz" + }, + "stylehacks": { + "version": "2.3.1", + "from": "stylehacks@>=2.3.0 <3.0.0", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-2.3.1.tgz" + }, + "stylelint": { + "version": "7.3.1", + "from": "stylelint@7.3.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-7.3.1.tgz", + "dependencies": { + "get-stdin": { + "version": "5.0.1", + "from": "get-stdin@>=5.0.0 <6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz" + }, + "glob": { + "version": "7.1.0", + "from": "glob@>=7.0.3 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.0.tgz" + }, + "globby": { + "version": "6.0.0", + "from": "globby@>=6.0.0 <7.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.0.0.tgz" + }, + "ignore": { + "version": "3.1.5", + "from": "ignore@>=3.1.3 <4.0.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.1.5.tgz" + }, + "minimatch": { + "version": "3.0.3", + "from": "minimatch@^3.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz" + }, + "postcss-selector-parser": { + "version": "2.2.1", + "from": "postcss-selector-parser@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.1.tgz" + }, + "resolve-from": { + "version": "2.0.0", + "from": "resolve-from@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz" + } + } + }, + "stylelint-order": { + "version": "0.6.0", + "from": "stylelint-order@latest", + "resolved": "https://registry.npmjs.org/stylelint-order/-/stylelint-order-0.6.0.tgz", + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "from": "ansi-regex@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz" + }, + "ansi-styles": { + "version": "3.2.0", + "from": "ansi-styles@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz" + }, + "autoprefixer": { + "version": "7.1.2", + "from": "autoprefixer@>=7.1.2 <8.0.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.2.tgz" + }, + "balanced-match": { + "version": "1.0.0", + "from": "balanced-match@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz" + }, + "brace-expansion": { + "version": "1.1.8", + "from": "brace-expansion@>=1.1.7 <2.0.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz" + }, + "browserslist": { + "version": "2.2.2", + "from": "browserslist@>=2.1.5 <3.0.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.2.2.tgz" + }, + "chalk": { + "version": "2.0.1", + "from": "chalk@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.0.1.tgz" + }, + "color-convert": { + "version": "1.9.0", + "from": "color-convert@>=1.9.0 <2.0.0", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz" + }, + "cosmiconfig": { + "version": "2.2.2", + "from": "cosmiconfig@>=2.1.3 <3.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-2.2.2.tgz" + }, + "debug": { + "version": "2.6.8", + "from": "debug@>=2.6.8 <3.0.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz" + }, + "file-entry-cache": { + "version": "2.0.0", + "from": "file-entry-cache@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz" + }, + "get-stdin": { + "version": "5.0.1", + "from": "get-stdin@>=5.0.1 <6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz" + }, + "glob": { + "version": "7.1.2", + "from": "glob@>=7.0.3 <8.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz" + }, + "globby": { + "version": "6.1.0", + "from": "globby@>=6.1.0 <7.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "dependencies": { + "pify": { + "version": "2.3.0", + "from": "pify@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + } + } + }, + "has-flag": { + "version": "2.0.0", + "from": "has-flag@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz" + }, + "html-tags": { + "version": "2.0.0", + "from": "html-tags@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz" + }, + "ignore": { + "version": "3.3.3", + "from": "ignore@>=3.3.3 <4.0.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "from": "is-fullwidth-code-point@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz" + }, + "known-css-properties": { + "version": "0.2.0", + "from": "known-css-properties@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.2.0.tgz" + }, + "micromatch": { + "version": "2.3.11", + "from": "micromatch@>=2.3.11 <3.0.0", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz" + }, + "minimatch": { + "version": "3.0.4", + "from": "minimatch@>=3.0.4 <4.0.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + }, + "ms": { + "version": "2.0.0", + "from": "ms@2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + }, + "pify": { + "version": "3.0.0", + "from": "pify@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + }, + "postcss": { + "version": "6.0.8", + "from": "postcss@>=6.0.7 <7.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.8.tgz" + }, + "postcss-less": { + "version": "1.1.0", + "from": "postcss-less@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-1.1.0.tgz", + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.3 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "dependencies": { + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } + } + }, + "has-flag": { + "version": "1.0.0", + "from": "has-flag@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz" + }, + "postcss": { + "version": "5.2.17", + "from": "postcss@>=5.2.16 <6.0.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.17.tgz" + }, + "supports-color": { + "version": "3.2.3", + "from": "supports-color@>=3.2.3 <4.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz" + } + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "from": "postcss-media-query-parser@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz" + }, + "postcss-reporter": { + "version": "4.0.0", + "from": "postcss-reporter@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/postcss-reporter/-/postcss-reporter-4.0.0.tgz", + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } + } + }, + "postcss-scss": { + "version": "1.0.2", + "from": "postcss-scss@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-1.0.2.tgz" + }, + "postcss-selector-parser": { + "version": "2.2.3", + "from": "postcss-selector-parser@>=2.2.3 <3.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-2.2.3.tgz" + }, + "resolve-from": { + "version": "3.0.0", + "from": "resolve-from@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz" + }, + "specificity": { + "version": "0.3.1", + "from": "specificity@>=0.3.1 <0.4.0", + "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.3.1.tgz" + }, + "string-width": { + "version": "2.1.1", + "from": "string-width@>=2.1.0 <3.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "dependencies": { + "strip-ansi": { + "version": "4.0.0", + "from": "strip-ansi@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" + } + } + }, + "stylelint": { + "version": "8.0.0", + "from": "stylelint@>=8.0.0 <9.0.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-8.0.0.tgz" + }, + "sugarss": { + "version": "1.0.0", + "from": "sugarss@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-1.0.0.tgz" + }, + "supports-color": { + "version": "4.2.1", + "from": "supports-color@>=4.2.0 <5.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.2.1.tgz" + }, + "table": { + "version": "4.0.1", + "from": "table@>=4.0.1 <5.0.0", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.1.tgz", + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + } + } + } + } + }, + "sugarss": { + "version": "0.1.6", + "from": "sugarss@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-0.1.6.tgz", + "dependencies": { + "postcss": { + "version": "5.2.0", + "from": "postcss@^5.2.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.0.tgz" + } + } + }, + "supports-color": { + "version": "3.1.2", + "from": "supports-color@>=3.1.2 <4.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz" + }, + "svg-tags": { + "version": "1.0.0", + "from": "svg-tags@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz" + }, + "svgo": { + "version": "0.6.6", + "from": "svgo@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-0.6.6.tgz" + }, + "symbol-observable": { + "version": "1.0.4", + "from": "symbol-observable@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz" + }, + "sync-exec": { + "version": "0.5.0", + "from": "sync-exec@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/sync-exec/-/sync-exec-0.5.0.tgz" + }, + "synesthesia": { + "version": "1.0.1", + "from": "synesthesia@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/synesthesia/-/synesthesia-1.0.1.tgz" + }, + "table": { + "version": "3.7.8", + "from": "table@>=3.7.8 <4.0.0", + "resolved": "https://registry.npmjs.org/table/-/table-3.7.8.tgz" + }, + "tapable": { + "version": "0.1.10", + "from": "tapable@>=0.1.8 <0.2.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.1.10.tgz" + }, + "tar": { + "version": "2.2.1", + "from": "tar@>=2.1.1 <3.0.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz" + }, + "tar.gz": { + "version": "1.0.3", + "from": "tar.gz@1.0.3", + "resolved": "https://registry.npmjs.org/tar.gz/-/tar.gz-1.0.3.tgz", + "dependencies": { + "bluebird": { + "version": "2.10.2", + "from": "bluebird@>=2.9.34 <3.0.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.10.2.tgz" + }, + "mout": { + "version": "0.11.1", + "from": "mout@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz" + } + } + }, + "tether": { + "version": "1.4.0", + "from": "tether@>=1.3.7 <2.0.0", + "resolved": "https://registry.npmjs.org/tether/-/tether-1.4.0.tgz" + }, + "text-table": { + "version": "0.2.0", + "from": "text-table@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + }, + "through": { + "version": "2.3.8", + "from": "through@>=2.3.6 <3.0.0", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + }, + "through2": { + "version": "2.0.3", + "from": "through2@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz" + }, + "tildify": { + "version": "1.2.0", + "from": "tildify@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz" + }, + "time-stamp": { + "version": "1.0.1", + "from": "time-stamp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.0.1.tgz" + }, + "timers-browserify": { + "version": "1.4.2", + "from": "timers-browserify@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz" + }, + "tiny-emitter": { + "version": "2.0.0", + "from": "tiny-emitter@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.0.tgz" + }, + "to-camel-case": { + "version": "1.0.0", + "from": "to-camel-case@1.0.0", + "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz" + }, + "to-fast-properties": { + "version": "1.0.2", + "from": "to-fast-properties@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.2.tgz" + }, + "to-no-case": { + "version": "1.0.2", + "from": "to-no-case@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz" + }, + "to-space-case": { + "version": "1.0.0", + "from": "to-space-case@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz" + }, + "trim-newlines": { + "version": "1.0.0", + "from": "trim-newlines@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz" + }, + "tryit": { + "version": "1.0.2", + "from": "tryit@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.2.tgz" + }, + "tty-browserify": { + "version": "0.0.0", + "from": "tty-browserify@0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz" + }, + "tv4": { + "version": "1.2.7", + "from": "tv4@>=1.2.7 <2.0.0", + "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz" + }, + "type-check": { + "version": "0.3.2", + "from": "type-check@>=0.3.2 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" + }, + "type-detect": { + "version": "1.0.0", + "from": "type-detect@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz" + }, + "type-is": { + "version": "1.6.13", + "from": "type-is@>=1.6.10 <1.7.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.13.tgz" + }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.6 <0.0.7", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + }, + "ua-parser-js": { + "version": "0.7.12", + "from": "ua-parser-js@>=0.7.9 <0.8.0", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.12.tgz" + }, + "uglify-to-browserify": { + "version": "1.0.2", + "from": "uglify-to-browserify@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz" + }, + "uniq": { + "version": "1.0.1", + "from": "uniq@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz" + }, + "uniqid": { + "version": "1.0.0", + "from": "uniqid@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/uniqid/-/uniqid-1.0.0.tgz" + }, + "uniqs": { + "version": "2.0.0", + "from": "uniqs@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz" + }, + "unique-stream": { + "version": "1.0.0", + "from": "unique-stream@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-1.0.0.tgz" + }, + "unpipe": { + "version": "1.0.0", + "from": "unpipe@1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + }, + "url": { + "version": "0.10.3", + "from": "url@>=0.10.1 <0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "dependencies": { + "punycode": { + "version": "1.3.2", + "from": "punycode@1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz" + } + } + }, + "url-loader": { + "version": "0.5.7", + "from": "url-loader@0.5.7", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-0.5.7.tgz", + "dependencies": { + "mime": { + "version": "1.2.11", + "from": "mime@>=1.2.0 <1.3.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz" + } + } + }, + "user-home": { + "version": "1.1.1", + "from": "user-home@>=1.1.1 <2.0.0", + "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz" + }, + "util": { + "version": "0.10.3", + "from": "util@>=0.10.3 <0.11.0", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + }, + "v8flags": { + "version": "2.0.11", + "from": "v8flags@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-2.0.11.tgz" + }, + "validate-npm-package-license": { + "version": "3.0.1", + "from": "validate-npm-package-license@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz" + }, + "value-equal": { + "version": "0.2.1", + "from": "value-equal@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-0.2.1.tgz" + }, + "vinyl": { + "version": "0.5.3", + "from": "vinyl@>=0.5.0 <0.6.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz" + }, + "vinyl-bufferstream": { + "version": "1.0.1", + "from": "vinyl-bufferstream@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-bufferstream/-/vinyl-bufferstream-1.0.1.tgz" + }, + "vinyl-file": { + "version": "1.3.0", + "from": "vinyl-file@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-file/-/vinyl-file-1.3.0.tgz", + "dependencies": { + "vinyl": { + "version": "1.1.1", + "from": "vinyl@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.1.1.tgz" + } + } + }, + "vinyl-fs": { + "version": "0.3.14", + "from": "vinyl-fs@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-0.3.14.tgz", + "dependencies": { + "clone": { + "version": "0.2.0", + "from": "clone@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz" + }, + "graceful-fs": { + "version": "3.0.8", + "from": "graceful-fs@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-3.0.8.tgz" + }, + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.33-1 <1.1.0-0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "strip-bom": { + "version": "1.0.0", + "from": "strip-bom@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-1.0.0.tgz" + }, + "through2": { + "version": "0.6.5", + "from": "through2@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz" + }, + "vinyl": { + "version": "0.4.6", + "from": "vinyl@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz" + } + } + }, + "vinyl-map": { + "version": "1.0.1", + "from": "vinyl-map@>=1.0.1 <2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-map/-/vinyl-map-1.0.1.tgz", + "dependencies": { + "isarray": { + "version": "0.0.1", + "from": "isarray@0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" + }, + "readable-stream": { + "version": "1.0.34", + "from": "readable-stream@>=1.0.17 <1.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz" + }, + "through2": { + "version": "0.4.2", + "from": "through2@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.4.2.tgz" + }, + "xtend": { + "version": "2.1.2", + "from": "xtend@>=2.1.1 <2.2.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz" + } + } + }, + "vinyl-sourcemaps-apply": { + "version": "0.2.1", + "from": "vinyl-sourcemaps-apply@0.2.1", + "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz" + }, + "vm-browserify": { + "version": "0.0.4", + "from": "vm-browserify@0.0.4", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz" + }, + "warning": { + "version": "3.0.0", + "from": "warning@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz" + }, + "watchpack": { + "version": "0.2.9", + "from": "watchpack@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-0.2.9.tgz", + "dependencies": { + "async": { + "version": "0.9.2", + "from": "async@>=0.9.0 <0.10.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz" + } + } + }, + "webpack": { + "version": "1.13.1", + "from": "webpack@1.13.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.13.1.tgz", + "dependencies": { + "async": { + "version": "1.5.2", + "from": "async@>=1.3.0 <2.0.0", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz" + }, + "interpret": { + "version": "0.6.6", + "from": "interpret@>=0.6.4 <0.7.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-0.6.6.tgz" + }, + "uglify-js": { + "version": "2.6.2", + "from": "uglify-js@>=2.6.0 <2.7.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.6.2.tgz", + "dependencies": { + "async": { + "version": "0.2.10", + "from": "async@>=0.2.6 <0.3.0", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz" + } + } + } + } + }, + "webpack-core": { + "version": "0.6.8", + "from": "webpack-core@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.8.tgz", + "dependencies": { + "source-map": { + "version": "0.4.4", + "from": "source-map@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz" + } + } + }, + "webpack-sources": { + "version": "0.1.3", + "from": "webpack-sources@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-0.1.3.tgz" + }, + "webpack-stream": { + "version": "2.1.1", + "from": "webpack-stream@2.1.1", + "resolved": "https://registry.npmjs.org/webpack-stream/-/webpack-stream-2.1.1.tgz", + "dependencies": { + "memory-fs": { + "version": "0.2.0", + "from": "memory-fs@>=0.2.0 <0.3.0-0", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz" + } + } + }, + "websocket-driver": { + "version": "0.6.5", + "from": "websocket-driver@>=0.3.6", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz" + }, + "websocket-extensions": { + "version": "0.1.1", + "from": "websocket-extensions@>=0.1.1", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz" + }, + "whatwg-fetch": { + "version": "2.0.2", + "from": "whatwg-fetch@>=0.10.0", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.2.tgz" + }, + "whet.extend": { + "version": "0.9.9", + "from": "whet.extend@>=0.9.9 <0.10.0", + "resolved": "https://registry.npmjs.org/whet.extend/-/whet.extend-0.9.9.tgz" + }, + "which": { + "version": "1.2.9", + "from": "which@>=1.2.4 <2.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.9.tgz" + }, + "window-size": { + "version": "0.1.0", + "from": "window-size@0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz" + }, + "wordwrap": { + "version": "1.0.0", + "from": "wordwrap@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + }, + "wrappy": { + "version": "1.0.2", + "from": "wrappy@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + }, + "write": { + "version": "0.2.1", + "from": "write@>=0.2.1 <0.3.0", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz" + }, + "write-file-stdout": { + "version": "0.0.2", + "from": "write-file-stdout@0.0.2", + "resolved": "https://registry.npmjs.org/write-file-stdout/-/write-file-stdout-0.0.2.tgz" + }, + "xregexp": { + "version": "3.1.1", + "from": "xregexp@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.1.1.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + }, + "yargs": { + "version": "3.10.0", + "from": "yargs@>=3.10.0 <3.11.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "dependencies": { + "camelcase": { + "version": "1.2.1", + "from": "camelcase@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz" + } + } + } + } +} diff --git a/package.json b/package.json index 12bc565c3..a81722ce8 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,103 @@ { - "name": "Lidarr", + "name": "lidarr", "version": "1.0.0", "description": "Lidarr", - "main": "main.js", "scripts": { "build": "gulp build", "start": "gulp watch" }, "repository": { "type": "git", - "url": "git://github.com/lidarr/Lidarr.git" + "url": "git://github.com/Lidarr/Lidarr.git" }, - "author": "", + "author": "Team Lidarr", "license": "GPL-3.0", - "gitHead": "9ff7aa1bf7fe38c4c5bdb92f56c8ad556916ed67", "readmeFilename": "readme.md", "dependencies": { - "autoprefixer-core": "5.2.1", - "del": "1.2.0", - "gulp": "3.9.0", + "autoprefixer": "6.3.6", + "babel-core": "6.9.0", + "babel-eslint": "7.1.0", + "babel-loader": "6.2.4", + "babel-plugin-transform-class-properties": "6.16.0", + "babel-preset-decorators-legacy": "1.0.0", + "babel-preset-es2015": "6.9.0", + "babel-preset-react": "6.22.0", + "babel-preset-stage-2": "6.5.0", + "classnames": "2.2.5", + "clipboard": "1.7.1", + "css-loader": "0.23.1", + "del": "2.2.0", + "element-class": "0.2.2", + "esformatter": "0.9.3", + "eslint": "2.10.2", + "eslint-loader": "1.3.0", + "eslint-plugin-filenames": "1.0.0", + "eslint-plugin-react": "5.2.2", + "extract-text-webpack-plugin": "1.0.1", + "file-loader": "0.9.0", + "filesize": "3.5.4", + "gulp": "3.9.1", "gulp-cached": "1.1.0", + "gulp-clean-css": "3.3.1", "gulp-concat": "2.6.0", "gulp-declare": "0.3.0", - "gulp-handlebars": "3.0.1", - "gulp-jshint": "1.11.2", - "gulp-less": "3.0.3", - "gulp-livereload": "3.8.0", - "gulp-postcss": "6.0.0", - "gulp-print": "1.1.0", - "gulp-replace": "0.5.3", - "gulp-run": "1.6.8", - "gulp-sourcemaps": "1.5.2", + "gulp-livereload": "3.8.1", + "gulp-postcss": "6.1.1", + "gulp-print": "2.0.1", + "gulp-sourcemaps": "1.6.0", "gulp-stripbom": "1.0.4", - "gulp-webpack": "1.5.0", - "gulp-wrap": "0.11.0", - "handlebars": "3.0.3", - "jshint-loader": "0.8.3", - "jshint-stylish": "2.0.1", - "run-sequence": "1.1.1", - "streamqueue": "1.1.0", - "tar.gz": "0.1.1", - "webpack": "1.12.0", - "webpack-stream": "2.1.0" + "gulp-util": "3.0.7", + "gulp-watch": "4.3.5", + "gulp-wrap": "0.13.0", + "history": "4.6.3", + "jdu": "1.0.0", + "lodash": "4.17.4", + "mobile-detect": "1.3.6", + "moment": "2.17.1", + "mousetrap": "1.6.0", + "normalize.css": "5.0.0", + "postcss-loader": "0.9.1", + "postcss-nested": "1.0.0", + "postcss-simple-vars": "3.0.0", + "prop-types": "15.5.10", + "query-string": "https://registry.npmjs.org/query-string/-/query-string-4.2.2.tgz", + "react": "15.6.1", + "react-addons-shallow-compare": "15.6.0", + "react-async-script": "0.9.1", + "react-autosuggest": "9.3.0", + "react-custom-scrollbars": "4.1.2", + "react-dnd": "2.4.0", + "react-dnd-html5-backend": "2.4.1", + "react-document-title": "2.0.3", + "react-dom": "15.6.1", + "react-google-recaptcha": "0.9.6", + "react-lazyload": "2.2.7", + "react-measure": "1.4.7", + "react-portal": "3.1.0", + "react-redux": "5.0.5", + "react-router-dom": "4.1.1", + "react-router-redux": "5.0.0-alpha.6", + "react-slider": "0.8.0", + "react-tabs": "1.1.0", + "react-tag-autocomplete": "5.4.0", + "react-tether": "0.5.7", + "react-virtualized": "9.8.0", + "redux": "3.7.0", + "redux-actions": "2.0.3", + "redux-batched-actions": "0.2.0", + "redux-localstorage": "0.4.1", + "redux-raven-middleware": "1.2.0", + "redux-thunk": "2.2.0", + "require-nocache": "1.0.0", + "reselect": "3.0.1", + "run-sequence": "1.2.0", + "streamqueue": "1.1.1", + "style-loader": "0.13.1", + "stylelint": "7.3.1", + "stylelint-order": "0.6.0", + "tar.gz": "1.0.3", + "url-loader": "0.5.7", + "webpack": "1.13.1", + "webpack-stream": "2.1.1" } } diff --git a/src/Lidarr.sln b/src/Lidarr.sln new file mode 100644 index 000000000..037bd252c --- /dev/null +++ b/src/Lidarr.sln @@ -0,0 +1,339 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.10 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{57A04B72-8088-4F75-A582-1158CF8291F7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common", "{47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Dummy", "NzbDrone.Test.Dummy\NzbDrone.Test.Dummy.csproj", "{FAFB5948-A222-4CF6-AD14-026BE7564802}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Test.Common", "NzbDrone.Test.Common\NzbDrone.Test.Common.csproj", "{CADDFCE0-7509-4430-8364-2074E1EEFCA2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core.Test", "NzbDrone.Core.Test\NzbDrone.Core.Test.csproj", "{193ADD3B-792B-4173-8E4C-5A3F8F0237F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host.Test", "NzbDrone.App.Test\NzbDrone.Host.Test.csproj", "{C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update.Test", "NzbDrone.Update.Test\NzbDrone.Update.Test.csproj", "{35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common.Test", "NzbDrone.Common.Test\NzbDrone.Common.Test.csproj", "{BEC74619-DDBB-4FBA-B517-D3E20AFC9997}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api.Test", "NzbDrone.Api.Test\NzbDrone.Api.Test.csproj", "{D18A5DEB-5102-4775-A1AF-B75DAAA8907B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Libraries.Test", "NzbDrone.Libraries.Test\NzbDrone.Libraries.Test.csproj", "{CBF6B8B0-A015-413A-8C86-01238BB45770}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Integration.Test", "NzbDrone.Integration.Test\NzbDrone.Integration.Test.csproj", "{8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Automation.Test", "NzbDrone.Automation.Test\NzbDrone.Automation.Test.csproj", "{CC26800D-F67E-464B-88DE-8EB1A0C227A3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsServiceHelpers", "WindowsServiceHelpers", "{F9E67978-5CD6-4A5F-827B-4249711C0B02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceInstall", "ServiceHelpers\ServiceInstall\ServiceInstall.csproj", "{6BCE712F-846D-4846-9D1B-A66B858DA755}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceUninstall", "ServiceHelpers\ServiceUninstall\ServiceUninstall.csproj", "{700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Core", "NzbDrone.Core\NzbDrone.Core.csproj", "{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Update", "NzbDrone.Update\NzbDrone.Update.csproj", "{4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Common", "NzbDrone.Common\NzbDrone.Common.csproj", "{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}" + ProjectSection(ProjectDependencies) = postProject + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{1E6B3CBE-1578-41C1-9BF9-78D818740BE9}" + ProjectSection(SolutionItems) = preProject + .nuget\NuGet.exe = .nuget\NuGet.exe + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Api", "NzbDrone.Api\NzbDrone.Api.csproj", "{FD286DF8-2D3A-4394-8AD5-443FADE55FB2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{486ADF86-DD89-4E19-B805-9D94F19800D9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Console", "NzbDrone.Console\NzbDrone.Console.csproj", "{3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Host", "NzbDrone.Host\NzbDrone.Host.csproj", "{95C11A9E-56ED-456A-8447-2C89C1139266}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone", "NzbDrone\NzbDrone.csproj", "{D12F7F2F-8A3C-415F-88FA-6DD061A84869}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.SignalR", "NzbDrone.SignalR\NzbDrone.SignalR.csproj", "{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Core", "Microsoft.AspNet.SignalR.Core\Microsoft.AspNet.SignalR.Core.csproj", "{1B9A82C4-BCA1-4834-A33E-226F17BE070B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.SignalR.Owin", "Microsoft.AspNet.SignalR.Owin\Microsoft.AspNet.SignalR.Owin.csproj", "{2B8C6DAD-4D85-41B1-83FD-248D9F347522}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Marr.Data", "Marr.Data\Marr.Data.csproj", "{F6FC6BE7-0847-4817-A1ED-223DC647C3D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono", "NzbDrone.Mono\NzbDrone.Mono.csproj", "{15AD7579-A314-4626-B556-663F51D97CD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows", "NzbDrone.Windows\NzbDrone.Windows.csproj", "{911284D3-F130-459E-836C-2430B6FBF21D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{0F0D4998-8F5D-4467-A909-BB192C4B3B4B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Platform", "Platform", "{4EACDBBC-BCD7-4765-A57B-3E08331E4749}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Windows.Test", "NzbDrone.Windows.Test\NzbDrone.Windows.Test.csproj", "{80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NzbDrone.Mono.Test", "NzbDrone.Mono.Test\NzbDrone.Mono.Test.csproj", "{40D72824-7D02-4A77-9106-8FE0EEA2B997}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoTorrent", "MonoTorrent\MonoTorrent.csproj", "{411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesCore", "LogentriesCore\LogentriesCore.csproj", "{90D6E9FC-7B88-4E1B-B018-8FA742274558}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LogentriesNLog", "LogentriesNLog\LogentriesNLog.csproj", "{9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}" + ProjectSection(ProjectDependencies) = postProject + {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {90D6E9FC-7B88-4E1B-B018-8FA742274558} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CurlSharp", "ExternalModules\CurlSharp\CurlSharp\CurlSharp.csproj", "{74420A79-CC16-442C-8B1E-7C1B913844F0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lidarr.Api.V3", "Sonarr.Api.V3\Lidarr.Api.V3.csproj", "{7140FF1F-79BE-492F-9188-B21A050BF708}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lidarr.Http", "Sonarr.Http\Lidarr.Http.csproj", "{5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Mono|x86 = Mono|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.ActiveCfg = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Debug|x86.Build.0 = Debug|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Mono|x86.Build.0 = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.ActiveCfg = Release|x86 + {FAFB5948-A222-4CF6-AD14-026BE7564802}.Release|x86.Build.0 = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Debug|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.ActiveCfg = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Mono|x86.Build.0 = Debug|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.ActiveCfg = Release|x86 + {CADDFCE0-7509-4430-8364-2074E1EEFCA2}.Release|x86.Build.0 = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Debug|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.ActiveCfg = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Mono|x86.Build.0 = Debug|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.ActiveCfg = Release|x86 + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0}.Release|x86.Build.0 = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Debug|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.ActiveCfg = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Mono|x86.Build.0 = Debug|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.ActiveCfg = Release|x86 + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5}.Release|x86.Build.0 = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Debug|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.ActiveCfg = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Mono|x86.Build.0 = Debug|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.ActiveCfg = Release|x86 + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97}.Release|x86.Build.0 = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Debug|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.ActiveCfg = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Mono|x86.Build.0 = Debug|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.ActiveCfg = Release|x86 + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997}.Release|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.ActiveCfg = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Debug|x86.Build.0 = Debug|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Mono|x86.Build.0 = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.ActiveCfg = Release|x86 + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B}.Release|x86.Build.0 = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Debug|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.ActiveCfg = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Mono|x86.Build.0 = Debug|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.ActiveCfg = Release|x86 + {CBF6B8B0-A015-413A-8C86-01238BB45770}.Release|x86.Build.0 = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Debug|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.ActiveCfg = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Mono|x86.Build.0 = Debug|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.ActiveCfg = Release|x86 + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB}.Release|x86.Build.0 = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Debug|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.ActiveCfg = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Mono|x86.Build.0 = Debug|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.ActiveCfg = Release|x86 + {CC26800D-F67E-464B-88DE-8EB1A0C227A3}.Release|x86.Build.0 = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Debug|x86.Build.0 = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Mono|x86.ActiveCfg = Debug|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.ActiveCfg = Release|x86 + {6BCE712F-846D-4846-9D1B-A66B858DA755}.Release|x86.Build.0 = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Debug|x86.Build.0 = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Mono|x86.ActiveCfg = Debug|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.ActiveCfg = Release|x86 + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4}.Release|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.ActiveCfg = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Debug|x86.Build.0 = Debug|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Mono|x86.Build.0 = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.ActiveCfg = Release|x86 + {FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}.Release|x86.Build.0 = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Debug|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.ActiveCfg = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Mono|x86.Build.0 = Debug|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.ActiveCfg = Release|x86 + {4CCC53CD-8D5E-4CC4-97D2-5C9312AC2BD7}.Release|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.ActiveCfg = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Debug|x86.Build.0 = Debug|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Mono|x86.Build.0 = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.ActiveCfg = Release|x86 + {F2BE0FDF-6E47-4827-A420-DD4EF82407F8}.Release|x86.Build.0 = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.ActiveCfg = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Debug|x86.Build.0 = Debug|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.ActiveCfg = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Mono|x86.Build.0 = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.ActiveCfg = Release|x86 + {FD286DF8-2D3A-4394-8AD5-443FADE55FB2}.Release|x86.Build.0 = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Debug|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.ActiveCfg = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Mono|x86.Build.0 = Debug|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.ActiveCfg = Release|x86 + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976}.Release|x86.Build.0 = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Debug|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.ActiveCfg = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Mono|x86.Build.0 = Debug|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.ActiveCfg = Release|x86 + {95C11A9E-56ED-456A-8447-2C89C1139266}.Release|x86.Build.0 = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.ActiveCfg = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Debug|x86.Build.0 = Debug|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Mono|x86.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.ActiveCfg = Release|x86 + {D12F7F2F-8A3C-415F-88FA-6DD061A84869}.Release|x86.Build.0 = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Debug|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.ActiveCfg = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Mono|x86.Build.0 = Debug|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.ActiveCfg = Release|x86 + {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}.Release|x86.Build.0 = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.ActiveCfg = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Debug|x86.Build.0 = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.ActiveCfg = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Mono|x86.Build.0 = Debug|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.ActiveCfg = Release|x86 + {1B9A82C4-BCA1-4834-A33E-226F17BE070B}.Release|x86.Build.0 = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.ActiveCfg = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Debug|x86.Build.0 = Debug|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.ActiveCfg = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Mono|x86.Build.0 = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.ActiveCfg = Release|x86 + {2B8C6DAD-4D85-41B1-83FD-248D9F347522}.Release|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.ActiveCfg = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Debug|x86.Build.0 = Debug|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Mono|x86.Build.0 = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.ActiveCfg = Release|x86 + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7}.Release|x86.Build.0 = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.ActiveCfg = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Debug|x86.Build.0 = Debug|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Mono|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.ActiveCfg = Release|x86 + {15AD7579-A314-4626-B556-663F51D97CD1}.Release|x86.Build.0 = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.ActiveCfg = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Debug|x86.Build.0 = Debug|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Mono|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.ActiveCfg = Release|x86 + {911284D3-F130-459E-836C-2430B6FBF21D}.Release|x86.Build.0 = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.ActiveCfg = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Debug|x86.Build.0 = Debug|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Mono|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.ActiveCfg = Release|x86 + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA}.Release|x86.Build.0 = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.ActiveCfg = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Debug|x86.Build.0 = Debug|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Mono|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.ActiveCfg = Release|x86 + {40D72824-7D02-4A77-9106-8FE0EEA2B997}.Release|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.ActiveCfg = Debug|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Debug|x86.Build.0 = Debug|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.ActiveCfg = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Mono|x86.Build.0 = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.ActiveCfg = Release|x86 + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8}.Release|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.ActiveCfg = Debug|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Debug|x86.Build.0 = Debug|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.ActiveCfg = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Mono|x86.Build.0 = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.ActiveCfg = Release|x86 + {90D6E9FC-7B88-4E1B-B018-8FA742274558}.Release|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.ActiveCfg = Debug|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Debug|x86.Build.0 = Debug|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.ActiveCfg = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Mono|x86.Build.0 = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.ActiveCfg = Release|x86 + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB}.Release|x86.Build.0 = Release|x86 + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.ActiveCfg = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Debug|x86.Build.0 = Debug|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Mono|x86.Build.0 = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.ActiveCfg = Release|Any CPU + {74420A79-CC16-442C-8B1E-7C1B913844F0}.Release|x86.Build.0 = Release|Any CPU + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.ActiveCfg = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Debug|x86.Build.0 = Debug|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Mono|x86.Build.0 = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.ActiveCfg = Release|x86 + {7140FF1F-79BE-492F-9188-B21A050BF708}.Release|x86.Build.0 = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.ActiveCfg = Debug|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Debug|x86.Build.0 = Debug|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.ActiveCfg = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Mono|x86.Build.0 = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.ActiveCfg = Release|x86 + {5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}.Release|x86.Build.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {FAFB5948-A222-4CF6-AD14-026BE7564802} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} + {CADDFCE0-7509-4430-8364-2074E1EEFCA2} = {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} + {193ADD3B-792B-4173-8E4C-5A3F8F0237F0} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {C0EA1A40-91AD-4EEB-BD16-2DDDEBD20AE5} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {35388E8E-0CDB-4A84-AD16-E4B6EFDA5D97} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {BEC74619-DDBB-4FBA-B517-D3E20AFC9997} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {D18A5DEB-5102-4775-A1AF-B75DAAA8907B} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {CBF6B8B0-A015-413A-8C86-01238BB45770} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {8CEFECD0-A6C2-498F-98B1-3FBE5820F9AB} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {CC26800D-F67E-464B-88DE-8EB1A0C227A3} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {6BCE712F-846D-4846-9D1B-A66B858DA755} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} + {700D0B95-95CD-43F3-B6C9-FAA0FC1358D4} = {F9E67978-5CD6-4A5F-827B-4249711C0B02} + {3DCA7B58-B8B3-49AC-9D9E-56F4A0460976} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {95C11A9E-56ED-456A-8447-2C89C1139266} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {D12F7F2F-8A3C-415F-88FA-6DD061A84869} = {486ADF86-DD89-4E19-B805-9D94F19800D9} + {1B9A82C4-BCA1-4834-A33E-226F17BE070B} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {2B8C6DAD-4D85-41B1-83FD-248D9F347522} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {F6FC6BE7-0847-4817-A1ED-223DC647C3D7} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {15AD7579-A314-4626-B556-663F51D97CD1} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {911284D3-F130-459E-836C-2430B6FBF21D} = {0F0D4998-8F5D-4467-A909-BB192C4B3B4B} + {4EACDBBC-BCD7-4765-A57B-3E08331E4749} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {80B51429-7A0E-46D6-BEE3-C80DCB1C4EAA} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} + {40D72824-7D02-4A77-9106-8FE0EEA2B997} = {4EACDBBC-BCD7-4765-A57B-3E08331E4749} + {411A9E0E-FDC6-4E25-828A-0C2CD1CD96F8} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {90D6E9FC-7B88-4E1B-B018-8FA742274558} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {9DC31DE3-79FF-47A8-96B4-6BA18F6BB1CB} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + {74420A79-CC16-442C-8B1E-7C1B913844F0} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + EnterpriseLibraryConfigurationToolBinariesPath = packages\Unity.2.1.505.0\lib\NET35;packages\Unity.2.1.505.2\lib\NET35 + SolutionGuid = {2C047BC5-490F-4DCE-962F-141370D23765} + EndGlobalSection + GlobalSection(MonoDevelopProperties) = preSolution + StartupItem = NzbDrone.Console\NzbDrone.Console.csproj + EndGlobalSection + GlobalSection(JSLint) = preSolution + SolutionConfigurationLocation = JSLintOptions.xml + EndGlobalSection +EndGlobal diff --git a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs index 385a9b989..e78709c2f 100644 --- a/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs +++ b/src/NzbDrone.Api.Test/ClientSchemaTests/SchemaBuilderFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.ClientSchema; +using Lidarr.Http.ClientSchema; using NzbDrone.Core.Annotations; using NzbDrone.Test.Common; @@ -45,4 +45,4 @@ namespace NzbDrone.Api.Test.ClientSchemaTests public string Other { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index 69402ccc5..3e32acc6d 100644 --- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -90,6 +90,10 @@ {CADDFCE0-7509-4430-8364-2074E1EEFCA2} NzbDrone.Test.Common + + {5370bff7-1bd7-46bc-af06-7d9ea5cda1d6} + Lidarr.Http + diff --git a/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs b/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs index 4fa964a2f..2409d932a 100644 --- a/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs +++ b/src/NzbDrone.Api/AlbumStudio/AlbumStudioModule.cs @@ -1,5 +1,5 @@ -using Nancy; -using NzbDrone.Api.Extensions; +using Nancy; +using Lidarr.Http.Extensions; using NzbDrone.Core.Music; namespace NzbDrone.Api.AlbumPass diff --git a/src/NzbDrone.Api/Albums/AlbumModule.cs b/src/NzbDrone.Api/Albums/AlbumModule.cs index e2d635fd0..bf36929a6 100644 --- a/src/NzbDrone.Api/Albums/AlbumModule.cs +++ b/src/NzbDrone.Api/Albums/AlbumModule.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.REST; using NzbDrone.Core.Music; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.DecisionEngine; @@ -12,7 +12,7 @@ namespace NzbDrone.Api.Albums public AlbumModule(IArtistService artistService, IArtistStatisticsService artistStatisticsService, IAlbumService albumService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster) { diff --git a/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs b/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs index 59fe8fc2d..f799c02ac 100644 --- a/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs +++ b/src/NzbDrone.Api/Albums/AlbumModuleWithSignalR.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation; @@ -13,20 +13,21 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music; using NzbDrone.Core.ArtistStats; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Albums { - public abstract class AlbumModuleWithSignalR : NzbDroneRestModuleWithSignalR + public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR { protected readonly IAlbumService _albumService; protected readonly IArtistStatisticsService _artistStatisticsService; protected readonly IArtistService _artistService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected AlbumModuleWithSignalR(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { @@ -41,7 +42,7 @@ namespace NzbDrone.Api.Albums protected AlbumModuleWithSignalR(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, string resource) : base(signalRBroadcaster, resource) diff --git a/src/NzbDrone.Api/Albums/AlbumResource.cs b/src/NzbDrone.Api/Albums/AlbumResource.cs index a283deebf..e66d67c64 100644 --- a/src/NzbDrone.Api/Albums/AlbumResource.cs +++ b/src/NzbDrone.Api/Albums/AlbumResource.cs @@ -1,9 +1,9 @@ -using NzbDrone.Core.Music; +using NzbDrone.Core.Music; using System; using System.Collections.Generic; using System.Linq; using System.Text; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Api.Music; using NzbDrone.Core.MediaCover; diff --git a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs b/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs deleted file mode 100644 index 580196363..000000000 --- a/src/NzbDrone.Api/Authentication/EnableAuthInNancy.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Text; -using Nancy; -using Nancy.Authentication.Basic; -using Nancy.Authentication.Forms; -using Nancy.Bootstrapper; -using Nancy.Cryptography; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Extensions.Pipelines; -using NzbDrone.Core.Authentication; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Authentication -{ - public class EnableAuthInNancy : IRegisterNancyPipeline - { - private readonly IAuthenticationService _authenticationService; - private readonly IConfigService _configService; - private readonly IConfigFileProvider _configFileProvider; - - public EnableAuthInNancy(IAuthenticationService authenticationService, - IConfigService configService, - IConfigFileProvider configFileProvider) - { - _authenticationService = authenticationService; - _configService = configService; - _configFileProvider = configFileProvider; - } - - public int Order => 10; - - public void Register(IPipelines pipelines) - { - if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) - { - RegisterFormsAuth(pipelines); - } - - else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) - { - pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Lidarr")); - } - - pipelines.BeforeRequest.AddItemToEndOfPipeline((Func) RequiresAuthentication); - pipelines.AfterRequest.AddItemToEndOfPipeline((Action) RemoveLoginHooksForApiCalls); - } - - private Response RequiresAuthentication(NancyContext context) - { - Response response = null; - - if (!_authenticationService.IsAuthenticated(context)) - { - response = new Response { StatusCode = HttpStatusCode.Unauthorized }; - } - - return response; - } - - private void RegisterFormsAuth(IPipelines pipelines) - { - var cryptographyConfiguration = new CryptographyConfiguration( - new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), - new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) - ); - - FormsAuthentication.Enable(pipelines, new FormsAuthenticationConfiguration - { - RedirectUrl = _configFileProvider.UrlBase + "/login", - UserMapper = _authenticationService, - CryptographyConfiguration = cryptographyConfiguration - }); - } - - private void RemoveLoginHooksForApiCalls(NancyContext context) - { - if (context.Request.IsApiRequest()) - { - if ((context.Response.StatusCode == HttpStatusCode.SeeOther && - context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || - context.Response.StatusCode == HttpStatusCode.Unauthorized) - { - context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized); - } - } - } - } -} diff --git a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs index 1687b31e3..6ee093e49 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistModule.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistModule.cs @@ -1,9 +1,10 @@ -using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Blacklisting; using NzbDrone.Core.Datastore; +using Lidarr.Http; namespace NzbDrone.Api.Blacklist { - public class BlacklistModule : NzbDroneRestModule + public class BlacklistModule : LidarrRestModule { private readonly IBlacklistService _blacklistService; diff --git a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs index d534e720f..563f9d95a 100644 --- a/src/NzbDrone.Api/Blacklist/BlacklistResource.cs +++ b/src/NzbDrone.Api/Blacklist/BlacklistResource.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Music; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; namespace NzbDrone.Api.Blacklist { @@ -17,6 +18,7 @@ namespace NzbDrone.Api.Blacklist public DownloadProtocol Protocol { get; set; } public string Indexer { get; set; } public string Message { get; set; } + public Language Language { get; set; } public ArtistResource Artist { get; set; } } diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index 352fad3a6..5a857c699 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Api.Episodes; @@ -16,9 +16,9 @@ namespace NzbDrone.Api.Calendar public CalendarModule(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster, "calendar") + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "calendar") { GetResourceAll = GetCalendar; } diff --git a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs b/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs deleted file mode 100644 index 4e796bd8c..000000000 --- a/src/NzbDrone.Api/ClientSchema/FieldDefinitionAttribute.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - -} \ No newline at end of file diff --git a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs b/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs deleted file mode 100644 index 6af07257f..000000000 --- a/src/NzbDrone.Api/ClientSchema/SchemaDeserializer.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace NzbDrone.Api.ClientSchema -{ - public static class SchemaDeserializer - { - - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Commands/CommandModule.cs b/src/NzbDrone.Api/Commands/CommandModule.cs index fcaeef9c4..ecf2a6f5c 100644 --- a/src/NzbDrone.Api/Commands/CommandModule.cs +++ b/src/NzbDrone.Api/Commands/CommandModule.cs @@ -1,19 +1,21 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.Validation; +using Lidarr.Http.Extensions; using NzbDrone.Common; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.ProgressMessaging; using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Mapping; +using Lidarr.Http.Validation; namespace NzbDrone.Api.Commands { - public class CommandModule : NzbDroneRestModuleWithSignalR, IHandle + public class CommandModule : LidarrRestModuleWithSignalR, IHandle { private readonly IManageCommandQueue _commandQueueManager; private readonly IServiceFactory _serviceFactory; @@ -63,4 +65,4 @@ namespace NzbDrone.Api.Commands } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Commands/CommandResource.cs b/src/NzbDrone.Api/Commands/CommandResource.cs index cf09f12ac..5fd2db0ed 100644 --- a/src/NzbDrone.Api/Commands/CommandResource.cs +++ b/src/NzbDrone.Api/Commands/CommandResource.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Api.Commands diff --git a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs index b9930ad0d..3ad22e818 100644 --- a/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs +++ b/src/NzbDrone.Api/Config/DownloadClientConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/HostConfigModule.cs b/src/NzbDrone.Api/Config/HostConfigModule.cs index 367bf770d..7bf5a7f5a 100644 --- a/src/NzbDrone.Api/Config/HostConfigModule.cs +++ b/src/NzbDrone.Api/Config/HostConfigModule.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using System.Reflection; using FluentValidation; using NzbDrone.Common.EnvironmentInfo; @@ -8,10 +8,11 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; namespace NzbDrone.Api.Config { - public class HostConfigModule : NzbDroneRestModule + public class HostConfigModule : LidarrRestModule { private readonly IConfigFileProvider _configFileProvider; private readonly IConfigService _configService; diff --git a/src/NzbDrone.Api/Config/HostConfigResource.cs b/src/NzbDrone.Api/Config/HostConfigResource.cs index 930e0301c..312571f0f 100644 --- a/src/NzbDrone.Api/Config/HostConfigResource.cs +++ b/src/NzbDrone.Api/Config/HostConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; using NzbDrone.Core.Update; diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/NzbDrone.Api/Config/IndexerConfigModule.cs index 73c2442b8..9a290e650 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigModule.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigModule.cs @@ -1,5 +1,5 @@ -using FluentValidation; -using NzbDrone.Api.Validation; +using FluentValidation; +using Lidarr.Http.Validation; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config @@ -25,4 +25,4 @@ namespace NzbDrone.Api.Config return IndexerConfigResourceMapper.ToResource(model); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Config/IndexerConfigResource.cs b/src/NzbDrone.Api/Config/IndexerConfigResource.cs index 179e28c3f..4a14e3bdd 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigResource.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs index b2a7c9b65..281533067 100644 --- a/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs +++ b/src/NzbDrone.Api/Config/MediaManagementConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Configuration; using NzbDrone.Core.MediaFiles; diff --git a/src/NzbDrone.Api/Config/NamingConfigModule.cs b/src/NzbDrone.Api/Config/NamingConfigModule.cs index abe7096c9..395e7f15e 100644 --- a/src/NzbDrone.Api/Config/NamingConfigModule.cs +++ b/src/NzbDrone.Api/Config/NamingConfigModule.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; @@ -6,11 +6,13 @@ using Nancy.Responses; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Config { - public class NamingConfigModule : NzbDroneRestModule + public class NamingConfigModule : LidarrRestModule { private readonly INamingConfigService _namingConfigService; private readonly IFilenameSampleService _filenameSampleService; diff --git a/src/NzbDrone.Api/Config/NamingConfigResource.cs b/src/NzbDrone.Api/Config/NamingConfigResource.cs index 47acd6a24..dd8572847 100644 --- a/src/NzbDrone.Api/Config/NamingConfigResource.cs +++ b/src/NzbDrone.Api/Config/NamingConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Organizer; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs index e5d324950..49a4af748 100644 --- a/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs +++ b/src/NzbDrone.Api/Config/NzbDroneConfigModule.cs @@ -1,11 +1,12 @@ -using System.Linq; +using System.Linq; using System.Reflection; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Configuration; +using Lidarr.Http; namespace NzbDrone.Api.Config { - public abstract class NzbDroneConfigModule : NzbDroneRestModule where TResource : RestResource, new() + public abstract class NzbDroneConfigModule : LidarrRestModule where TResource : RestResource, new() { private readonly IConfigService _configService; diff --git a/src/NzbDrone.Api/Config/UiConfigResource.cs b/src/NzbDrone.Api/Config/UiConfigResource.cs index 7c7d27b67..44e6ff0b1 100644 --- a/src/NzbDrone.Api/Config/UiConfigResource.cs +++ b/src/NzbDrone.Api/Config/UiConfigResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Configuration; namespace NzbDrone.Api.Config diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs b/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs index f6d8354b4..c59ef45e0 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs +++ b/src/NzbDrone.Api/DiskSpace/DiskSpaceModule.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.DiskSpace; +using Lidarr.Http; namespace NzbDrone.Api.DiskSpace { - public class DiskSpaceModule :NzbDroneRestModule + public class DiskSpaceModule : LidarrRestModule { private readonly IDiskSpaceService _diskSpaceService; diff --git a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs b/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs index fc36f9d5c..71069da5b 100644 --- a/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs +++ b/src/NzbDrone.Api/DiskSpace/DiskSpaceResource.cs @@ -1,4 +1,4 @@ -using NzbDrone.Api.REST; +using Lidarr.Http.REST; namespace NzbDrone.Api.DiskSpace { diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index d89a0068c..f4536c79a 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using NLog; -using NzbDrone.Api.REST; +using Lidarr.Http; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; @@ -15,14 +15,14 @@ using System; namespace NzbDrone.Api.EpisodeFiles { - public class EpisodeFileModule : NzbDroneRestModuleWithSignalR, + public class EpisodeFileModule : LidarrRestModuleWithSignalR, IHandle { private readonly IMediaFileService _mediaFileService; private readonly IDiskProvider _diskProvider; private readonly IRecycleBinProvider _recycleBinProvider; private readonly ISeriesService _seriesService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IUpgradableSpecification _upgradableSpecification; private readonly Logger _logger; public EpisodeFileModule(IBroadcastSignalRMessage signalRBroadcaster, @@ -30,7 +30,7 @@ namespace NzbDrone.Api.EpisodeFiles IDiskProvider diskProvider, IRecycleBinProvider recycleBinProvider, ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, Logger logger) : base(signalRBroadcaster) { @@ -38,7 +38,7 @@ namespace NzbDrone.Api.EpisodeFiles _diskProvider = diskProvider; _recycleBinProvider = recycleBinProvider; _seriesService = seriesService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; _logger = logger; GetResourceById = GetEpisodeFile; GetResourceAll = GetEpisodeFiles; @@ -95,4 +95,4 @@ namespace NzbDrone.Api.EpisodeFiles BroadcastResourceChange(ModelAction.Updated, message.EpisodeFile.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index bd856776d..a284a8d4f 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -1,7 +1,10 @@ -using System; +using System; using System.IO; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Api.EpisodeFiles { @@ -15,13 +18,14 @@ namespace NzbDrone.Api.EpisodeFiles public DateTime DateAdded { get; set; } public string SceneName { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public bool QualityCutoffNotMet { get; set; } } public static class EpisodeFileResourceMapper { - private static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model) + private static EpisodeFileResource ToResource(this EpisodeFile model) { if (model == null) return null; @@ -41,7 +45,7 @@ namespace NzbDrone.Api.EpisodeFiles }; } - public static EpisodeFileResource ToResource(this Core.MediaFiles.EpisodeFile model, Core.Tv.Series series, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) + public static EpisodeFileResource ToResource(this EpisodeFile model, Core.Tv.Series series, IUpgradableSpecification upgradableSpecification) { if (model == null) return null; @@ -57,7 +61,8 @@ namespace NzbDrone.Api.EpisodeFiles DateAdded = model.DateAdded, SceneName = model.SceneName, Quality = model.Quality, - QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(series.Profile.Value, model.Quality) + Language = model.Language, + QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(series.Profile.Value, series.LanguageProfile.Value, model.Quality, model.Language) }; } } diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs index 7f6f5692c..fb4b8933d 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.REST; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; using NzbDrone.SignalR; @@ -10,7 +10,7 @@ namespace NzbDrone.Api.Episodes { public EpisodeModule(ISeriesService seriesService, IEpisodeService episodeService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster) { diff --git a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs index d4c1deb27..a170f49f8 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Api.EpisodeFiles; using NzbDrone.Api.Series; @@ -9,20 +9,21 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Episodes { - public abstract class EpisodeModuleWithSignalR : NzbDroneRestModuleWithSignalR, + public abstract class EpisodeModuleWithSignalR : LidarrRestModuleWithSignalR, IHandle, IHandle { protected readonly IEpisodeService _episodeService; protected readonly ISeriesService _seriesService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + protected readonly IUpgradableSpecification _qualityUpgradableSpecification; protected EpisodeModuleWithSignalR(IEpisodeService episodeService, ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { @@ -35,7 +36,7 @@ namespace NzbDrone.Api.Episodes protected EpisodeModuleWithSignalR(IEpisodeService episodeService, ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, string resource) : base(signalRBroadcaster, resource) diff --git a/src/NzbDrone.Api/Episodes/EpisodeResource.cs b/src/NzbDrone.Api/Episodes/EpisodeResource.cs index 3ff489f38..8faadb272 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeResource.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeResource.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using NzbDrone.Api.EpisodeFiles; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Core.Tv; diff --git a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs b/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs deleted file mode 100644 index d98925f8e..000000000 --- a/src/NzbDrone.Api/ErrorManagement/NzbDroneErrorPipeline.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Data.SQLite; -using FluentValidation; -using NLog; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Core.Exceptions; -using HttpStatusCode = Nancy.HttpStatusCode; - -namespace NzbDrone.Api.ErrorManagement -{ - public class NzbDroneErrorPipeline - { - private readonly Logger _logger; - - public NzbDroneErrorPipeline(Logger logger) - { - _logger = logger; - } - - public Response HandleException(NancyContext context, Exception exception) - { - _logger.Trace("Handling Exception"); - - var apiException = exception as ApiException; - - if (apiException != null) - { - _logger.Warn(apiException, "API Error"); - return apiException.ToErrorResponse(); - } - - var validationException = exception as ValidationException; - - if (validationException != null) - { - _logger.Warn("Invalid request {0}", validationException.Message); - - return validationException.Errors.AsResponse(HttpStatusCode.BadRequest); - } - - var clientException = exception as NzbDroneClientException; - - if (clientException != null) - { - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse((HttpStatusCode)clientException.StatusCode); - } - - var sqLiteException = exception as SQLiteException; - - if (sqLiteException != null) - { - if (context.Request.Method == "PUT" || context.Request.Method == "POST") - { - if (sqLiteException.Message.Contains("constraint failed")) - return new ErrorModel - { - Message = exception.Message, - }.AsResponse(HttpStatusCode.Conflict); - } - - _logger.Error(sqLiteException, "[{0} {1}]", context.Request.Method, context.Request.Path); - } - - _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); - - return new ErrorModel - { - Message = exception.Message, - Description = exception.ToString() - }.AsResponse(HttpStatusCode.InternalServerError); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs b/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs deleted file mode 100644 index 00488657b..000000000 --- a/src/NzbDrone.Api/Extensions/Pipelines/NzbDroneVersionPipeline.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Nancy; -using Nancy.Bootstrapper; -using NzbDrone.Common.EnvironmentInfo; - -namespace NzbDrone.Api.Extensions.Pipelines -{ - public class NzbDroneVersionPipeline : IRegisterNancyPipeline - { - public int Order => 0; - - public void Register(IPipelines pipelines) - { - pipelines.AfterRequest.AddItemToStartOfPipeline((Action) Handle); - } - - private void Handle(NancyContext context) - { - if (!context.Response.Headers.ContainsKey("X-ApplicationVersion")) - { - context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString()); - } - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs index 392c3c0c5..4e3ac967e 100644 --- a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs +++ b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.IO; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; @@ -73,4 +73,4 @@ namespace NzbDrone.Api.FileSystem }).AsResponse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs b/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs deleted file mode 100644 index 974e117f9..000000000 --- a/src/NzbDrone.Api/Frontend/Mappers/LoginHtmlMapper.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Text.RegularExpressions; -using Nancy; -using NLog; -using NzbDrone.Common.Disk; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Api.Frontend.Mappers -{ - public class LoginHtmlMapper : StaticResourceMapperBase - { - private readonly IDiskProvider _diskProvider; - private readonly IConfigFileProvider _configFileProvider; - private readonly Func _cacheBreakProviderFactory; - private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex("(?<=(?:href|src|data-main)=\").*?(?=\")", RegexOptions.Compiled | RegexOptions.IgnoreCase); - - private static string URL_BASE; - private string _generatedContent; - - public LoginHtmlMapper(IAppFolderInfo appFolderInfo, - IDiskProvider diskProvider, - IConfigFileProvider configFileProvider, - Func cacheBreakProviderFactory, - Logger logger) - : base(diskProvider, logger) - { - _diskProvider = diskProvider; - _configFileProvider = configFileProvider; - _cacheBreakProviderFactory = cacheBreakProviderFactory; - _indexPath = Path.Combine(appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, "login.html"); - - URL_BASE = configFileProvider.UrlBase; - } - - public override string Map(string resourceUrl) - { - return _indexPath; - } - - public override bool CanHandle(string resourceUrl) - { - return resourceUrl.StartsWith("/login"); - } - - public override Response GetResponse(string resourceUrl) - { - var response = base.GetResponse(resourceUrl); - response.Headers["X-UA-Compatible"] = "IE=edge"; - - return response; - } - - protected override Stream GetContentStream(string filePath) - { - var text = GetLoginText(); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - writer.Write(text); - writer.Flush(); - stream.Position = 0; - return stream; - } - - private string GetLoginText() - { - if (RuntimeInfo.IsProduction && _generatedContent != null) - { - return _generatedContent; - } - - var text = _diskProvider.ReadAllText(_indexPath); - - var cacheBreakProvider = _cacheBreakProviderFactory(); - - text = ReplaceRegex.Replace(text, match => - { - var url = cacheBreakProvider.AddCacheBreakerToPath(match.Value); - return URL_BASE + url; - }); - - _generatedContent = text; - - return _generatedContent; - } - } -} diff --git a/src/NzbDrone.Api/Health/HealthModule.cs b/src/NzbDrone.Api/Health/HealthModule.cs index 2699fa7d6..d5fa9140d 100644 --- a/src/NzbDrone.Api/Health/HealthModule.cs +++ b/src/NzbDrone.Api/Health/HealthModule.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.HealthCheck; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Health { - public class HealthModule : NzbDroneRestModuleWithSignalR, + public class HealthModule : LidarrRestModuleWithSignalR, IHandle { private readonly IHealthCheckService _healthCheckService; @@ -28,4 +29,4 @@ namespace NzbDrone.Api.Health BroadcastResourceChange(ModelAction.Sync); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Health/HealthResource.cs b/src/NzbDrone.Api/Health/HealthResource.cs index e860cb778..32a78bd53 100644 --- a/src/NzbDrone.Api/Health/HealthResource.cs +++ b/src/NzbDrone.Api/Health/HealthResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Common.Http; using NzbDrone.Core.HealthCheck; diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index 8d2c8bb21..59e58df27 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -1,29 +1,30 @@ -using System; +using System; using Nancy; using NzbDrone.Api.Episodes; using NzbDrone.Api.Albums; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Api.Series; using NzbDrone.Api.Music; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.History; +using Lidarr.Http; namespace NzbDrone.Api.History { - public class HistoryModule : NzbDroneRestModule + public class HistoryModule : LidarrRestModule { private readonly IHistoryService _historyService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IUpgradableSpecification _upgradableSpecification; private readonly IFailedDownloadService _failedDownloadService; public HistoryModule(IHistoryService historyService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IFailedDownloadService failedDownloadService) { _historyService = historyService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _failedDownloadService = failedDownloadService; GetResourcePaged = GetHistory; @@ -39,7 +40,10 @@ namespace NzbDrone.Api.History if (model.Artist != null) { - resource.QualityCutoffNotMet = _qualityUpgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, model.Quality); + resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, + model.Artist.LanguageProfile, + model.Quality, + model.Language); } return resource; diff --git a/src/NzbDrone.Api/History/HistoryResource.cs b/src/NzbDrone.Api/History/HistoryResource.cs index 1279641be..327331be9 100644 --- a/src/NzbDrone.Api/History/HistoryResource.cs +++ b/src/NzbDrone.Api/History/HistoryResource.cs @@ -1,12 +1,13 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Api.Episodes; using NzbDrone.Api.Albums; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Api.Music; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Api.History @@ -20,6 +21,7 @@ namespace NzbDrone.Api.History public bool QualityCutoffNotMet { get; set; } public DateTime Date { get; set; } public string DownloadId { get; set; } + public Language Language { get; set; } public HistoryEventType EventType { get; set; } diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 858263b4e..6b90afbbb 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FluentValidation; using Nancy; @@ -10,7 +10,7 @@ using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; using Nancy.ModelBinding; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Common.Cache; using HttpStatusCode = System.Net.HttpStatusCode; @@ -117,4 +117,4 @@ namespace NzbDrone.Api.Indexers return base.MapDecision(decision, initialWeight); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs b/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs index 32344ef34..47fae1b06 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModuleBase.cs @@ -1,9 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; +using Lidarr.Http; namespace NzbDrone.Api.Indexers { - public abstract class ReleaseModuleBase : NzbDroneRestModule + public abstract class ReleaseModuleBase : LidarrRestModule { protected virtual List MapDecisions(IEnumerable decisions) { diff --git a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs b/src/NzbDrone.Api/Indexers/ReleasePushModule.cs index c25e45726..65c7d46d5 100644 --- a/src/NzbDrone.Api/Indexers/ReleasePushModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleasePushModule.cs @@ -1,4 +1,4 @@ -using Nancy; +using Nancy; using Nancy.ModelBinding; using FluentValidation; using NzbDrone.Core.DecisionEngine; @@ -6,7 +6,7 @@ using NzbDrone.Core.Download; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Parser.Model; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NLog; namespace NzbDrone.Api.Indexers diff --git a/src/NzbDrone.Api/Indexers/ReleaseResource.cs b/src/NzbDrone.Api/Indexers/ReleaseResource.cs index a8f8cdfab..544a86325 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseResource.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseResource.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; using System.Linq; @@ -154,4 +155,4 @@ namespace NzbDrone.Api.Indexers return model; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs b/src/NzbDrone.Api/Logs/LogFileModuleBase.cs index d8a12d1bf..4af69e543 100644 --- a/src/NzbDrone.Api/Logs/LogFileModuleBase.cs +++ b/src/NzbDrone.Api/Logs/LogFileModuleBase.cs @@ -1,14 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Common.Disk; using Nancy; using Nancy.Responses; using NzbDrone.Core.Configuration; +using Lidarr.Http; namespace NzbDrone.Api.Logs { - public abstract class LogFileModuleBase : NzbDroneRestModule + public abstract class LogFileModuleBase : LidarrRestModule { protected const string LOGFILE_ROUTE = @"/(?[-.a-zA-Z0-9]+?\.txt)"; @@ -68,4 +69,4 @@ namespace NzbDrone.Api.Logs protected abstract string DownloadUrlRoot { get; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Logs/LogFileResource.cs b/src/NzbDrone.Api/Logs/LogFileResource.cs index 9f67c8af7..e0ccc1924 100644 --- a/src/NzbDrone.Api/Logs/LogFileResource.cs +++ b/src/NzbDrone.Api/Logs/LogFileResource.cs @@ -1,5 +1,5 @@ -using System; -using NzbDrone.Api.REST; +using System; +using Lidarr.Http.REST; namespace NzbDrone.Api.Logs { diff --git a/src/NzbDrone.Api/Logs/LogModule.cs b/src/NzbDrone.Api/Logs/LogModule.cs index 88ead3ec0..323333132 100644 --- a/src/NzbDrone.Api/Logs/LogModule.cs +++ b/src/NzbDrone.Api/Logs/LogModule.cs @@ -1,8 +1,10 @@ -using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Instrumentation; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Logs { - public class LogModule : NzbDroneRestModule + public class LogModule : LidarrRestModule { private readonly ILogService _logService; @@ -49,4 +51,4 @@ namespace NzbDrone.Api.Logs return ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Logs/LogResource.cs b/src/NzbDrone.Api/Logs/LogResource.cs index 504a45839..4f98dfeb8 100644 --- a/src/NzbDrone.Api/Logs/LogResource.cs +++ b/src/NzbDrone.Api/Logs/LogResource.cs @@ -1,5 +1,5 @@ -using System; -using NzbDrone.Api.REST; +using System; +using Lidarr.Http.REST; namespace NzbDrone.Api.Logs { diff --git a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs index bcd99de1b..1b2201c4b 100644 --- a/src/NzbDrone.Api/ManualImport/ManualImportModule.cs +++ b/src/NzbDrone.Api/ManualImport/ManualImportModule.cs @@ -2,10 +2,11 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.MediaFiles.TrackImport.Manual; using NzbDrone.Core.Qualities; +using Lidarr.Http; namespace NzbDrone.Api.ManualImport { - public class ManualImportModule : NzbDroneRestModule + public class ManualImportModule : LidarrRestModule { private readonly IManualImportService _manualImportService; diff --git a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs index 99aeb0897..e1844d718 100644 --- a/src/NzbDrone.Api/ManualImport/ManualImportResource.cs +++ b/src/NzbDrone.Api/ManualImport/ManualImportResource.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Api.Episodes; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Api.Series; using NzbDrone.Common.Crypto; using NzbDrone.Core.DecisionEngine; diff --git a/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs b/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs index c2d92aae7..fe0a4542d 100644 --- a/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs +++ b/src/NzbDrone.Api/Music/ArtistBulkImportModule.cs @@ -1,8 +1,8 @@ using System.Collections; using System.Collections.Generic; using Nancy; -using NzbDrone.Api.REST; -using NzbDrone.Api.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.Extensions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Parser; @@ -16,6 +16,7 @@ using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.RootFolders; using NzbDrone.Common.Cache; using NzbDrone.Core.Music; +using Lidarr.Http; namespace NzbDrone.Api.Music { @@ -28,7 +29,7 @@ namespace NzbDrone.Api.Music } } - public class MusicBulkImportModule : NzbDroneRestModule + public class MusicBulkImportModule : LidarrRestModule { private readonly ISearchForNewArtist _searchProxy; private readonly IRootFolderService _rootFolderService; diff --git a/src/NzbDrone.Api/Music/ArtistEditorModule.cs b/src/NzbDrone.Api/Music/ArtistEditorModule.cs index ca8ec3598..6d250f25e 100644 --- a/src/NzbDrone.Api/Music/ArtistEditorModule.cs +++ b/src/NzbDrone.Api/Music/ArtistEditorModule.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Core.Music; namespace NzbDrone.Api.Music diff --git a/src/NzbDrone.Api/Music/ArtistLookupModule.cs b/src/NzbDrone.Api/Music/ArtistLookupModule.cs index faa0eca09..c7bec8f84 100644 --- a/src/NzbDrone.Api/Music/ArtistLookupModule.cs +++ b/src/NzbDrone.Api/Music/ArtistLookupModule.cs @@ -1,15 +1,16 @@ -using Nancy; -using NzbDrone.Api.Extensions; +using Nancy; +using Lidarr.Http.Extensions; using NzbDrone.Core.MediaCover; using NzbDrone.Core.MetadataSource; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using Lidarr.Http; namespace NzbDrone.Api.Music { - public class ArtistLookupModule : NzbDroneRestModule + public class ArtistLookupModule : LidarrRestModule { private readonly ISearchForNewArtist _searchProxy; diff --git a/src/NzbDrone.Api/Music/ArtistModule.cs b/src/NzbDrone.Api/Music/ArtistModule.cs index 91ffc0f76..3e1b97b65 100644 --- a/src/NzbDrone.Api/Music/ArtistModule.cs +++ b/src/NzbDrone.Api/Music/ArtistModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation; @@ -14,10 +14,12 @@ using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Validation; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Music { - public class ArtistModule : NzbDroneRestModuleWithSignalR, + public class ArtistModule : LidarrRestModuleWithSignalR, IHandle, IHandle, IHandle, @@ -39,9 +41,9 @@ namespace NzbDrone.Api.Music RootFolderValidator rootFolderValidator, ArtistPathValidator seriesPathValidator, ArtistExistsValidator artistExistsValidator, - DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, - ProfileExistsValidator profileExistsValidator + ProfileExistsValidator profileExistsValidator, + LanguageProfileExistsValidator languageProfileExistsValidator ) : base(signalRBroadcaster) { @@ -56,19 +58,20 @@ namespace NzbDrone.Api.Music CreateResource = AddArtist; UpdateResource = UpdatArtist; DeleteResource = DeleteArtist; - - Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + + SharedValidator.RuleFor(s => s.ProfileId).ValidId(); + SharedValidator.RuleFor(s => s.LanguageProfileId); SharedValidator.RuleFor(s => s.Path) .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator) .When(s => !s.Path.IsNullOrWhiteSpace()); SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator); PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); diff --git a/src/NzbDrone.Api/Music/ArtistResource.cs b/src/NzbDrone.Api/Music/ArtistResource.cs index 6d9c447f5..47537cf71 100644 --- a/src/NzbDrone.Api/Music/ArtistResource.cs +++ b/src/NzbDrone.Api/Music/ArtistResource.cs @@ -1,5 +1,4 @@ -using NzbDrone.Api.REST; -using NzbDrone.Api.Series; +using Lidarr.Http.REST; using NzbDrone.Api.Albums; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Music; @@ -44,6 +43,7 @@ namespace NzbDrone.Api.Music //View & Edit public string Path { get; set; } public int ProfileId { get; set; } + public int LanguageProfileId { get; set; } //Editing Only public bool AlbumFolder { get; set; } @@ -97,6 +97,7 @@ namespace NzbDrone.Api.Music Path = model.Path, ProfileId = model.ProfileId, + LanguageProfileId = model.LanguageProfileId, Monitored = model.Monitored, AlbumFolder = model.AlbumFolder, @@ -154,6 +155,7 @@ namespace NzbDrone.Api.Music Path = resource.Path, ProfileId = resource.ProfileId, + LanguageProfileId = resource.LanguageProfileId, AlbumFolder = resource.AlbumFolder, Monitored = resource.Monitored, diff --git a/src/NzbDrone.Api/Music/ListImport.cs b/src/NzbDrone.Api/Music/ListImport.cs index 456f95243..8488456fd 100644 --- a/src/NzbDrone.Api/Music/ListImport.cs +++ b/src/NzbDrone.Api/Music/ListImport.cs @@ -1,8 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Nancy; using Nancy.Extensions; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Core.Music; namespace NzbDrone.Api.Music @@ -27,4 +27,4 @@ namespace NzbDrone.Api.Music return _artistService.AddArtists(Artists).ToResource().AsResponse(HttpStatusCode.Accepted); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/NancyBootstrapper.cs b/src/NzbDrone.Api/NancyBootstrapper.cs deleted file mode 100644 index 1415dd4c2..000000000 --- a/src/NzbDrone.Api/NancyBootstrapper.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Linq; -using Nancy.Bootstrapper; -using Nancy.Diagnostics; -using NLog; -using NzbDrone.Api.Extensions.Pipelines; -using NzbDrone.Common.EnvironmentInfo; -using NzbDrone.Common.Instrumentation; -using NzbDrone.Core.Instrumentation; -using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Messaging.Events; -using TinyIoC; - -namespace NzbDrone.Api -{ - public class NancyBootstrapper : TinyIoCNancyBootstrapper - { - private readonly TinyIoCContainer _tinyIoCContainer; - private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(NancyBootstrapper)); - - public NancyBootstrapper(TinyIoCContainer tinyIoCContainer) - { - _tinyIoCContainer = tinyIoCContainer; - } - - protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) - { - Logger.Info("Starting Web Server"); - - if (RuntimeInfo.IsProduction) - { - DiagnosticsHook.Disable(pipelines); - } - - RegisterPipelines(pipelines); - - container.Resolve().Register(); - container.Resolve().PublishEvent(new ApplicationStartedEvent()); - } - - private void RegisterPipelines(IPipelines pipelines) - { - var pipelineRegistrars = _tinyIoCContainer.ResolveAll().OrderBy(v => v.Order).ToList(); - - foreach (var registerNancyPipeline in pipelineRegistrars) - { - registerNancyPipeline.Register(pipelines); - } - } - - protected override TinyIoCContainer GetApplicationContainer() - { - return _tinyIoCContainer; - } - - protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" }; - - protected override byte[] FavIcon => null; - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 47bed9e5e..90349629f 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -90,20 +90,10 @@ - - - - - - - - - - @@ -119,12 +109,6 @@ - - - - - - @@ -164,31 +148,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -206,14 +165,10 @@ - - - - @@ -228,15 +183,8 @@ - - - - - - - @@ -254,12 +202,8 @@ - - - - @@ -287,6 +231,10 @@ {7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36} NzbDrone.SignalR + + {5370bff7-1bd7-46bc-af06-7d9ea5cda1d6} + Lidarr.Http + diff --git a/src/NzbDrone.Api/NzbDroneRestModule.cs b/src/NzbDrone.Api/NzbDroneRestModule.cs deleted file mode 100644 index 4cc103d95..000000000 --- a/src/NzbDrone.Api/NzbDroneRestModule.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneRestModule : RestModule where TResource : RestResource, new() - { - protected string Resource { get; private set; } - - protected NzbDroneRestModule() - : this(new TResource().ResourceName) - { - } - - protected NzbDroneRestModule(string resource) - : base("/api/" + resource.Trim('/')) - { - Resource = resource; - PostValidator.RuleFor(r => r.Id).IsZero(); - PutValidator.RuleFor(r => r.Id).ValidId(); - } - - protected PagingResource ApplyToPage(Func, PagingSpec> function, PagingSpec pagingSpec, Converter mapper) - { - pagingSpec = function(pagingSpec); - - return new PagingResource - { - Page = pagingSpec.Page, - PageSize = pagingSpec.PageSize, - SortDirection = pagingSpec.SortDirection, - SortKey = pagingSpec.SortKey, - TotalRecords = pagingSpec.TotalRecords, - Records = pagingSpec.Records.ConvertAll(mapper) - }; - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs b/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs deleted file mode 100644 index a2061a770..000000000 --- a/src/NzbDrone.Api/NzbDroneRestModuleWithSignalR.cs +++ /dev/null @@ -1,66 +0,0 @@ -using NzbDrone.Api.REST; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Datastore.Events; -using NzbDrone.Core.Messaging.Events; -using NzbDrone.SignalR; - -namespace NzbDrone.Api -{ - public abstract class NzbDroneRestModuleWithSignalR : NzbDroneRestModule, IHandle> - where TResource : RestResource, new() - where TModel : ModelBase, new() - { - private readonly IBroadcastSignalRMessage _signalRBroadcaster; - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) - { - _signalRBroadcaster = signalRBroadcaster; - } - - protected NzbDroneRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) - : base(resource) - { - _signalRBroadcaster = signalRBroadcaster; - } - - public void Handle(ModelEvent message) - { - if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) - { - BroadcastResourceChange(message.Action); - } - - BroadcastResourceChange(message.Action, message.Model.Id); - } - - protected void BroadcastResourceChange(ModelAction action, int id) - { - var resource = GetResourceById(id); - BroadcastResourceChange(action, resource); - } - - - protected void BroadcastResourceChange(ModelAction action, TResource resource) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(resource, action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - - - protected void BroadcastResourceChange(ModelAction action) - { - var signalRMessage = new SignalRMessage - { - Name = Resource, - Body = new ResourceChangeMessage(action) - }; - - _signalRBroadcaster.BroadcastMessage(signalRMessage); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/Parse/ParseModule.cs b/src/NzbDrone.Api/Parse/ParseModule.cs index 486ed8ace..d3448d5ba 100644 --- a/src/NzbDrone.Api/Parse/ParseModule.cs +++ b/src/NzbDrone.Api/Parse/ParseModule.cs @@ -1,10 +1,11 @@ -using NzbDrone.Api.Albums; +using NzbDrone.Api.Albums; using NzbDrone.Api.Music; using NzbDrone.Core.Parser; +using Lidarr.Http; namespace NzbDrone.Api.Parse { - public class ParseModule : NzbDroneRestModule + public class ParseModule : LidarrRestModule { private readonly IParsingService _parsingService; @@ -47,4 +48,4 @@ namespace NzbDrone.Api.Parse } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Parse/ParseResource.cs b/src/NzbDrone.Api/Parse/ParseResource.cs index df19e42de..91ab9adb6 100644 --- a/src/NzbDrone.Api/Parse/ParseResource.cs +++ b/src/NzbDrone.Api/Parse/ParseResource.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.REST; using NzbDrone.Api.Music; using NzbDrone.Api.Albums; using NzbDrone.Core.Parser.Model; @@ -13,4 +13,4 @@ namespace NzbDrone.Api.Parse public ArtistResource Artist { get; set; } public List Albums { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs index e7975b661..61aa00ea3 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using FluentValidation.Results; -using NzbDrone.Api.REST; -using NzbDrone.Api.Validation; +using Lidarr.Http.REST; +using Lidarr.Http.Validation; using NzbDrone.Core.Profiles.Delay; +using Lidarr.Http; namespace NzbDrone.Api.Profiles.Delay { - public class DelayProfileModule : NzbDroneRestModule + public class DelayProfileModule : LidarrRestModule { private readonly IDelayProfileService _delayProfileService; @@ -72,4 +73,4 @@ namespace NzbDrone.Api.Profiles.Delay return _delayProfileService.All().ToResource(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs index e35df9043..cfc1f39b9 100644 --- a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Delay; diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs index 147bc69aa..63cc61362 100644 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageModule.cs @@ -1,11 +1,12 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Core.Parser; +using NzbDrone.Core.Languages; +using Lidarr.Http; namespace NzbDrone.Api.Profiles.Languages { - public class LanguageModule : NzbDroneRestModule + public class LanguageModule : LidarrRestModule { public LanguageModule() { @@ -36,4 +37,4 @@ namespace NzbDrone.Api.Profiles.Languages .ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs index 09e5ba28c..ca1b81aed 100644 --- a/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs +++ b/src/NzbDrone.Api/Profiles/Languages/LanguageResource.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Newtonsoft.Json; +using Lidarr.Http.REST; namespace NzbDrone.Api.Profiles.Languages { @@ -10,4 +10,4 @@ namespace NzbDrone.Api.Profiles.Languages public string Name { get; set; } public string NameLower => Name.ToLowerInvariant(); } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs index e5803db20..35a51c82b 100644 --- a/src/NzbDrone.Api/Profiles/ProfileModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Validation; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Profiles { - public class ProfileModule : NzbDroneRestModule + public class ProfileModule : LidarrRestModule { private readonly IProfileService _profileService; @@ -15,7 +17,6 @@ namespace NzbDrone.Api.Profiles SharedValidator.RuleFor(c => c.Name).NotEmpty(); SharedValidator.RuleFor(c => c.Cutoff).NotNull(); SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); - SharedValidator.RuleFor(c => c.Language).ValidLanguage(); GetResourceAll = GetAll; GetResourceById = GetById; @@ -53,4 +54,4 @@ namespace NzbDrone.Api.Profiles return _profileService.All().ToResource(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs index ee02bcb32..3660cbec0 100644 --- a/src/NzbDrone.Api/Profiles/ProfileResource.cs +++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs @@ -1,8 +1,9 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Parser; using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Profiles @@ -12,7 +13,6 @@ namespace NzbDrone.Api.Profiles public string Name { get; set; } public Quality Cutoff { get; set; } public List Items { get; set; } - public Language Language { get; set; } } public class ProfileQualityItemResource : RestResource @@ -33,8 +33,7 @@ namespace NzbDrone.Api.Profiles Name = model.Name, Cutoff = model.Cutoff, - Items = model.Items.ConvertAll(ToResource), - Language = model.Language + Items = model.Items.ConvertAll(ToResource) }; } @@ -59,8 +58,7 @@ namespace NzbDrone.Api.Profiles Name = resource.Name, Cutoff = (Quality)resource.Cutoff.Id, - Items = resource.Items.ConvertAll(ToModel), - Language = resource.Language + Items = resource.Items.ConvertAll(ToModel) }; } @@ -80,4 +78,4 @@ namespace NzbDrone.Api.Profiles return models.Select(ToResource).ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs index ec5f3ae01..da8cb5797 100644 --- a/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs +++ b/src/NzbDrone.Api/Profiles/ProfileSchemaModule.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Parser; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Profiles { - public class ProfileSchemaModule : NzbDroneRestModule + public class ProfileSchemaModule : LidarrRestModule { private readonly IQualityDefinitionService _qualityDefinitionService; @@ -28,9 +30,8 @@ namespace NzbDrone.Api.Profiles var profile = new Profile(); profile.Cutoff = Quality.Unknown; profile.Items = items; - profile.Language = Language.English; return new List { profile.ToResource() }; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/ProviderModuleBase.cs b/src/NzbDrone.Api/ProviderModuleBase.cs index fa5313b0a..0bb676896 100644 --- a/src/NzbDrone.Api/ProviderModuleBase.cs +++ b/src/NzbDrone.Api/ProviderModuleBase.cs @@ -1,18 +1,20 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; using Nancy; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.Extensions; +using Lidarr.Http.ClientSchema; +using Lidarr.Http.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; using Newtonsoft.Json; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api { - public abstract class ProviderModuleBase : NzbDroneRestModule + public abstract class ProviderModuleBase : LidarrRestModule where TProviderDefinition : ProviderDefinition, new() where TProvider : IProvider where TProviderResource : ProviderResource, new() diff --git a/src/NzbDrone.Api/ProviderResource.cs b/src/NzbDrone.Api/ProviderResource.cs index 9927a09cc..3ddbd5e37 100644 --- a/src/NzbDrone.Api/ProviderResource.cs +++ b/src/NzbDrone.Api/ProviderResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; -using NzbDrone.Api.ClientSchema; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.ClientSchema; +using Lidarr.Http.REST; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Api @@ -17,4 +17,4 @@ namespace NzbDrone.Api public List Presets { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs index 1b5351300..2ac7a7a3d 100644 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionModule.cs @@ -1,9 +1,11 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Qualities; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Qualities { - public class QualityDefinitionModule : NzbDroneRestModule + public class QualityDefinitionModule : LidarrRestModule { private readonly IQualityDefinitionService _qualityDefinitionService; @@ -34,4 +36,4 @@ namespace NzbDrone.Api.Qualities return _qualityDefinitionService.All().ToResource(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs index ea0edc0ab..684d5d29a 100644 --- a/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs +++ b/src/NzbDrone.Api/Qualities/QualityDefinitionResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Qualities; namespace NzbDrone.Api.Qualities @@ -62,4 +62,4 @@ namespace NzbDrone.Api.Qualities return models.Select(ToResource).ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Queue/QueueActionModule.cs b/src/NzbDrone.Api/Queue/QueueActionModule.cs index 89ca897b7..a7b0c7555 100644 --- a/src/NzbDrone.Api/Queue/QueueActionModule.cs +++ b/src/NzbDrone.Api/Queue/QueueActionModule.cs @@ -1,16 +1,17 @@ -using System; +using System; using Nancy; using Nancy.Responses; -using NzbDrone.Api.Extensions; -using NzbDrone.Api.REST; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Queue; +using Lidarr.Http; namespace NzbDrone.Api.Queue { - public class QueueActionModule : NzbDroneRestModule + public class QueueActionModule : LidarrRestModule { private readonly IQueueService _queueService; private readonly ITrackedDownloadService _trackedDownloadService; diff --git a/src/NzbDrone.Api/Queue/QueueModule.cs b/src/NzbDrone.Api/Queue/QueueModule.cs index 00e614132..aa0e062d2 100644 --- a/src/NzbDrone.Api/Queue/QueueModule.cs +++ b/src/NzbDrone.Api/Queue/QueueModule.cs @@ -1,14 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Queue; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Queue { - public class QueueModule : NzbDroneRestModuleWithSignalR, + public class QueueModule : LidarrRestModuleWithSignalR, IHandle, IHandle { private readonly IQueueService _queueService; @@ -45,4 +46,4 @@ namespace NzbDrone.Api.Queue BroadcastResourceChange(ModelAction.Sync); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Queue/QueueResource.cs b/src/NzbDrone.Api/Queue/QueueResource.cs index 858c10dcf..51ce30ed1 100644 --- a/src/NzbDrone.Api/Queue/QueueResource.cs +++ b/src/NzbDrone.Api/Queue/QueueResource.cs @@ -1,6 +1,6 @@ -using System; +using System; using System.Collections.Generic; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Qualities; using NzbDrone.Api.Series; using NzbDrone.Api.Episodes; diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs index a61b5f7b3..1b3506611 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingModule.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; namespace NzbDrone.Api.RemotePathMappings { - public class RemotePathMappingModule : NzbDroneRestModule + public class RemotePathMappingModule : LidarrRestModule { private readonly IRemotePathMappingService _remotePathMappingService; @@ -64,4 +65,4 @@ namespace NzbDrone.Api.RemotePathMappings _remotePathMappingService.Update(mapping); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs index 60c01b682..779f929b0 100644 --- a/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs +++ b/src/NzbDrone.Api/RemotePathMappings/RemotePathMappingResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.RemotePathMappings; namespace NzbDrone.Api.RemotePathMappings diff --git a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs index 918b3a50b..032d9540a 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Restrictions; +using Lidarr.Http; namespace NzbDrone.Api.Restrictions { - public class RestrictionModule : NzbDroneRestModule + public class RestrictionModule : LidarrRestModule { private readonly IRestrictionService _restrictionService; diff --git a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs index 14085e820..cd49cf623 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Restrictions; namespace NzbDrone.Api.Restrictions diff --git a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs index e87e581de..93b781126 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderModule.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderModule.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentValidation; using NzbDrone.Core.RootFolders; using NzbDrone.Core.Validation.Paths; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.RootFolders { - public class RootFolderModule : NzbDroneRestModuleWithSignalR + public class RootFolderModule : LidarrRestModuleWithSignalR { private readonly IRootFolderService _rootFolderService; @@ -14,7 +15,6 @@ namespace NzbDrone.Api.RootFolders IBroadcastSignalRMessage signalRBroadcaster, RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, - DroneFactoryValidator droneFactoryValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator, StartupFolderValidator startupFolderValidator, FolderWritableValidator folderWritableValidator) @@ -31,7 +31,6 @@ namespace NzbDrone.Api.RootFolders .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) - .SetValidator(droneFactoryValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(startupFolderValidator) .SetValidator(pathExistsValidator) @@ -60,4 +59,4 @@ namespace NzbDrone.Api.RootFolders _rootFolderService.Remove(id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs index 86efef529..2fefad440 100644 --- a/src/NzbDrone.Api/RootFolders/RootFolderResource.cs +++ b/src/NzbDrone.Api/RootFolders/RootFolderResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.RootFolders; namespace NzbDrone.Api.RootFolders @@ -48,4 +48,4 @@ namespace NzbDrone.Api.RootFolders return models.Select(ToResource).ToList(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs b/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs index 93cd25ce5..20afcf722 100644 --- a/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs +++ b/src/NzbDrone.Api/SeasonPass/SeasonPassModule.cs @@ -1,5 +1,5 @@ -using Nancy; -using NzbDrone.Api.Extensions; +using Nancy; +using Lidarr.Http.Extensions; using NzbDrone.Core.Tv; namespace NzbDrone.Api.SeasonPass diff --git a/src/NzbDrone.Api/Series/SeriesEditorModule.cs b/src/NzbDrone.Api/Series/SeriesEditorModule.cs index 87cd53113..a54ceff7e 100644 --- a/src/NzbDrone.Api/Series/SeriesEditorModule.cs +++ b/src/NzbDrone.Api/Series/SeriesEditorModule.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Nancy; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; +using Lidarr.Http.Mapping; using NzbDrone.Core.Tv; namespace NzbDrone.Api.Series diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs index 693f9a360..c7f6cbc7d 100644 --- a/src/NzbDrone.Api/Series/SeriesModule.cs +++ b/src/NzbDrone.Api/Series/SeriesModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FluentValidation; @@ -15,10 +15,12 @@ using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.DataAugmentation.Scene; using NzbDrone.Core.Validation; using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Series { - public class SeriesModule : NzbDroneRestModuleWithSignalR, + public class SeriesModule : LidarrRestModuleWithSignalR, IHandle, IHandle, IHandle, @@ -43,9 +45,9 @@ namespace NzbDrone.Api.Series RootFolderValidator rootFolderValidator, SeriesPathValidator seriesPathValidator, SeriesExistsValidator seriesExistsValidator, - DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, - ProfileExistsValidator profileExistsValidator + ProfileExistsValidator profileExistsValidator, + LanguageProfileExistsValidator languageProfileExistsValidator ) : base(signalRBroadcaster) { @@ -62,18 +64,19 @@ namespace NzbDrone.Api.Series UpdateResource = UpdateSeries; DeleteResource = DeleteSeries; - Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.ProfileId)); + SharedValidator.RuleFor(s => s.ProfileId).ValidId(); + SharedValidator.RuleFor(s => s.LanguageProfileId); SharedValidator.RuleFor(s => s.Path) .Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator) .When(s => !s.Path.IsNullOrWhiteSpace()); SharedValidator.RuleFor(s => s.ProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator); PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); diff --git a/src/NzbDrone.Api/Series/SeriesResource.cs b/src/NzbDrone.Api/Series/SeriesResource.cs index 86de03eb7..f686538c7 100644 --- a/src/NzbDrone.Api/Series/SeriesResource.cs +++ b/src/NzbDrone.Api/Series/SeriesResource.cs @@ -1,7 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.MediaCover; using NzbDrone.Core.Tv; @@ -52,6 +52,7 @@ namespace NzbDrone.Api.Series //View & Edit public string Path { get; set; } public int ProfileId { get; set; } + public int LanguageProfileId { get; set; } //Editing Only public bool SeasonFolder { get; set; } @@ -126,7 +127,8 @@ namespace NzbDrone.Api.Series Path = model.Path, ProfileId = model.ProfileId, - + LanguageProfileId = model.LanguageProfileId, + SeasonFolder = model.SeasonFolder, Monitored = model.Monitored, @@ -180,6 +182,7 @@ namespace NzbDrone.Api.Series Path = resource.Path, ProfileId = resource.ProfileId, + LanguageProfileId = resource.LanguageProfileId, SeasonFolder = resource.SeasonFolder, Monitored = resource.Monitored, diff --git a/src/NzbDrone.Api/System/Backup/BackupModule.cs b/src/NzbDrone.Api/System/Backup/BackupModule.cs index b5074793e..2260c191d 100644 --- a/src/NzbDrone.Api/System/Backup/BackupModule.cs +++ b/src/NzbDrone.Api/System/Backup/BackupModule.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NzbDrone.Core.Backup; +using Lidarr.Http; namespace NzbDrone.Api.System.Backup { - public class BackupModule : NzbDroneRestModule + public class BackupModule : LidarrRestModule { private readonly IBackupService _backupService; @@ -21,9 +22,9 @@ namespace NzbDrone.Api.System.Backup return backups.Select(b => new BackupResource { - Id = b.Path.GetHashCode(), - Name = Path.GetFileName(b.Path), - Path = b.Path, + Id = b.Name.GetHashCode(), + Name = Path.GetFileName(b.Name), + Path = b.Name, Type = b.Type, Time = b.Time }).ToList(); diff --git a/src/NzbDrone.Api/System/Backup/BackupResource.cs b/src/NzbDrone.Api/System/Backup/BackupResource.cs index 7eac82838..b8252e66d 100644 --- a/src/NzbDrone.Api/System/Backup/BackupResource.cs +++ b/src/NzbDrone.Api/System/Backup/BackupResource.cs @@ -1,5 +1,5 @@ -using System; -using NzbDrone.Api.REST; +using System; +using Lidarr.Http.REST; using NzbDrone.Core.Backup; namespace NzbDrone.Api.System.Backup diff --git a/src/NzbDrone.Api/System/SystemModule.cs b/src/NzbDrone.Api/System/SystemModule.cs index c62ed3b9e..1df7c495a 100644 --- a/src/NzbDrone.Api/System/SystemModule.cs +++ b/src/NzbDrone.Api/System/SystemModule.cs @@ -1,6 +1,6 @@ -using Nancy; +using Nancy; using Nancy.Routing; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; diff --git a/src/NzbDrone.Api/System/Tasks/TaskModule.cs b/src/NzbDrone.Api/System/Tasks/TaskModule.cs index db8c4f376..fc196fd01 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskModule.cs +++ b/src/NzbDrone.Api/System/Tasks/TaskModule.cs @@ -1,14 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Jobs; using NzbDrone.Core.Messaging.Events; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.System.Tasks { - public class TaskModule : NzbDroneRestModuleWithSignalR, IHandle + public class TaskModule : LidarrRestModuleWithSignalR, IHandle { private readonly ITaskManager _taskManager; diff --git a/src/NzbDrone.Api/System/Tasks/TaskResource.cs b/src/NzbDrone.Api/System/Tasks/TaskResource.cs index fda392cae..05992d626 100644 --- a/src/NzbDrone.Api/System/Tasks/TaskResource.cs +++ b/src/NzbDrone.Api/System/Tasks/TaskResource.cs @@ -1,5 +1,5 @@ -using System; -using NzbDrone.Api.REST; +using System; +using Lidarr.Http.REST; namespace NzbDrone.Api.System.Tasks { diff --git a/src/NzbDrone.Api/Tags/TagModule.cs b/src/NzbDrone.Api/Tags/TagModule.cs index d2a01667c..1fdd5ae01 100644 --- a/src/NzbDrone.Api/Tags/TagModule.cs +++ b/src/NzbDrone.Api/Tags/TagModule.cs @@ -1,12 +1,14 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tags; using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Tags { - public class TagModule : NzbDroneRestModuleWithSignalR, IHandle + public class TagModule : LidarrRestModuleWithSignalR, IHandle { private readonly ITagService _tagService; diff --git a/src/NzbDrone.Api/Tags/TagResource.cs b/src/NzbDrone.Api/Tags/TagResource.cs index 678107bf5..3d2599f8e 100644 --- a/src/NzbDrone.Api/Tags/TagResource.cs +++ b/src/NzbDrone.Api/Tags/TagResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Tags; namespace NzbDrone.Api.Tags diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs b/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs index 6cbf8e09a..577c15ea6 100644 --- a/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs +++ b/src/NzbDrone.Api/TrackFiles/TrackFileModule.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using NLog; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; @@ -13,10 +13,12 @@ using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.SignalR; using System; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.TrackFiles { - public class TrackFileModule : NzbDroneRestModuleWithSignalR, + public class TrackFileModule : LidarrRestModuleWithSignalR, IHandle { private readonly IMediaFileService _mediaFileService; @@ -24,7 +26,7 @@ namespace NzbDrone.Api.TrackFiles private readonly IRecycleBinProvider _recycleBinProvider; private readonly ISeriesService _seriesService; private readonly IArtistService _artistService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IUpgradableSpecification _upgradableSpecification; private readonly Logger _logger; public TrackFileModule(IBroadcastSignalRMessage signalRBroadcaster, @@ -33,7 +35,7 @@ namespace NzbDrone.Api.TrackFiles IRecycleBinProvider recycleBinProvider, ISeriesService seriesService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, Logger logger) : base(signalRBroadcaster) { @@ -42,7 +44,7 @@ namespace NzbDrone.Api.TrackFiles _recycleBinProvider = recycleBinProvider; _seriesService = seriesService; _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; _logger = logger; GetResourceById = GetTrackFile; GetResourceAll = GetTrackFiles; @@ -55,7 +57,7 @@ namespace NzbDrone.Api.TrackFiles var trackFile = _mediaFileService.Get(id); var artist = _artistService.GetArtist(trackFile.ArtistId); - return trackFile.ToResource(artist, _qualityUpgradableSpecification); + return trackFile.ToResource(artist, _upgradableSpecification); } private List GetTrackFiles() @@ -69,7 +71,7 @@ namespace NzbDrone.Api.TrackFiles var artist = _artistService.GetArtist(artistId); - return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _qualityUpgradableSpecification)); + return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); } private void SetQuality(TrackFileResource trackFileResource) @@ -97,4 +99,4 @@ namespace NzbDrone.Api.TrackFiles BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs b/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs index 4c75dc429..0a3aeb66b 100644 --- a/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs +++ b/src/NzbDrone.Api/TrackFiles/TrackFileResource.cs @@ -1,7 +1,8 @@ -using System; +using System; using System.IO; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Api.TrackFiles { @@ -12,6 +13,7 @@ namespace NzbDrone.Api.TrackFiles public string RelativePath { get; set; } public string Path { get; set; } public long Size { get; set; } + public Language Language { get; set; } public DateTime DateAdded { get; set; } //public string SceneName { get; set; } public QualityModel Quality { get; set; } @@ -41,7 +43,7 @@ namespace NzbDrone.Api.TrackFiles }; } - public static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model, Core.Music.Artist artist, Core.DecisionEngine.IQualityUpgradableSpecification qualityUpgradableSpecification) + public static TrackFileResource ToResource(this Core.MediaFiles.TrackFile model, Core.Music.Artist artist, Core.DecisionEngine.IUpgradableSpecification upgradableSpecification) { if (model == null) return null; @@ -57,7 +59,8 @@ namespace NzbDrone.Api.TrackFiles DateAdded = model.DateAdded, //SceneName = model.SceneName, Quality = model.Quality, - QualityCutoffNotMet = qualityUpgradableSpecification.CutoffNotMet(artist.Profile.Value, model.Quality) + Language = model.Language, + QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value, artist.LanguageProfile.Value, model.Quality, model.Language) }; } } diff --git a/src/NzbDrone.Api/Tracks/RenameTrackModule.cs b/src/NzbDrone.Api/Tracks/RenameTrackModule.cs index 9467ec43e..871b19ddb 100644 --- a/src/NzbDrone.Api/Tracks/RenameTrackModule.cs +++ b/src/NzbDrone.Api/Tracks/RenameTrackModule.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.REST; using NzbDrone.Core.MediaFiles; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Tracks { - public class RenameTrackModule : NzbDroneRestModule + public class RenameTrackModule : LidarrRestModule { private readonly IRenameTrackFileService _renameTrackFileService; diff --git a/src/NzbDrone.Api/Tracks/RenameTrackResource.cs b/src/NzbDrone.Api/Tracks/RenameTrackResource.cs index 12f67bb60..26d3466de 100644 --- a/src/NzbDrone.Api/Tracks/RenameTrackResource.cs +++ b/src/NzbDrone.Api/Tracks/RenameTrackResource.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; namespace NzbDrone.Api.Tracks { diff --git a/src/NzbDrone.Api/Tracks/TrackModule.cs b/src/NzbDrone.Api/Tracks/TrackModule.cs index fa3222c51..acb8b3bc3 100644 --- a/src/NzbDrone.Api/Tracks/TrackModule.cs +++ b/src/NzbDrone.Api/Tracks/TrackModule.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using NzbDrone.Api.REST; +using System.Collections.Generic; +using Lidarr.Http.REST; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.SignalR; @@ -10,9 +10,9 @@ namespace NzbDrone.Api.Tracks { public TrackModule(IArtistService artistService, ITrackService trackService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) - : base(trackService, artistService, qualityUpgradableSpecification, signalRBroadcaster) + : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) { GetResourceAll = GetTracks; UpdateResource = SetMonitored; diff --git a/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs b/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs index f5926768e..81f71056d 100644 --- a/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs +++ b/src/NzbDrone.Api/Tracks/TrackModuleWithSignalR.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Api.TrackFiles; using NzbDrone.Api.Music; @@ -10,39 +10,41 @@ using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.Music; using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Tracks { - public abstract class TrackModuleWithSignalR : NzbDroneRestModuleWithSignalR, + public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR, IHandle { protected readonly ITrackService _trackService; protected readonly IArtistService _artistService; - protected readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + protected readonly IUpgradableSpecification _upgradableSpecification; protected TrackModuleWithSignalR(ITrackService trackService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(signalRBroadcaster) { _trackService = trackService; _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; GetResourceById = GetTrack; } protected TrackModuleWithSignalR(ITrackService trackService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification upgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster, string resource) : base(signalRBroadcaster, resource) { _trackService = trackService; _artistService = artistService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; GetResourceById = GetTrack; } @@ -68,7 +70,7 @@ namespace NzbDrone.Api.Tracks } if (includeTrackFile && track.TrackFileId != 0) { - resource.TrackFile = track.TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); + resource.TrackFile = track.TrackFile.Value.ToResource(artist, _upgradableSpecification); } } @@ -96,7 +98,7 @@ namespace NzbDrone.Api.Tracks } if (includeTrackFile && tracks[i].TrackFileId != 0) { - resource.TrackFile = tracks[i].TrackFile.Value.ToResource(artist, _qualityUpgradableSpecification); + resource.TrackFile = tracks[i].TrackFile.Value.ToResource(artist, _upgradableSpecification); } } } diff --git a/src/NzbDrone.Api/Tracks/TrackResource.cs b/src/NzbDrone.Api/Tracks/TrackResource.cs index b8a007671..835a4074f 100644 --- a/src/NzbDrone.Api/Tracks/TrackResource.cs +++ b/src/NzbDrone.Api/Tracks/TrackResource.cs @@ -1,9 +1,9 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using NzbDrone.Api.TrackFiles; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Api.Music; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Api/Update/UpdateModule.cs b/src/NzbDrone.Api/Update/UpdateModule.cs index 2104f23ea..062396d0d 100644 --- a/src/NzbDrone.Api/Update/UpdateModule.cs +++ b/src/NzbDrone.Api/Update/UpdateModule.cs @@ -1,11 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Update; +using Lidarr.Http; +using Lidarr.Http.Mapping; namespace NzbDrone.Api.Update { - public class UpdateModule : NzbDroneRestModule + public class UpdateModule : LidarrRestModule { private readonly IRecentUpdateProvider _recentUpdateProvider; @@ -42,4 +44,4 @@ namespace NzbDrone.Api.Update return resources; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Api/Update/UpdateResource.cs b/src/NzbDrone.Api/Update/UpdateResource.cs index dca6f6725..a8380828e 100644 --- a/src/NzbDrone.Api/Update/UpdateResource.cs +++ b/src/NzbDrone.Api/Update/UpdateResource.cs @@ -1,8 +1,8 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Core.Update; namespace NzbDrone.Api.Update diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs index d2d08edab..4b7a22a53 100644 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -1,8 +1,9 @@ -using NzbDrone.Api.Episodes; +using NzbDrone.Api.Episodes; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Wanted { @@ -13,7 +14,7 @@ namespace NzbDrone.Api.Wanted public CutoffModule(IEpisodeCutoffService episodeCutoffService, IEpisodeService episodeService, ISeriesService seriesService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(episodeService, seriesService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/cutoff") { diff --git a/src/NzbDrone.Api/Wanted/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs index 018ecec62..7a70124ab 100644 --- a/src/NzbDrone.Api/Wanted/MissingModule.cs +++ b/src/NzbDrone.Api/Wanted/MissingModule.cs @@ -1,10 +1,12 @@ -using NzbDrone.Api.Episodes; +using NzbDrone.Api.Episodes; using NzbDrone.Api.Albums; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.ArtistStats; using NzbDrone.Core.Music; +using NzbDrone.Core.Tv; using NzbDrone.SignalR; +using Lidarr.Http; namespace NzbDrone.Api.Wanted { @@ -13,7 +15,7 @@ namespace NzbDrone.Api.Wanted public MissingModule(IAlbumService albumService, IArtistStatisticsService artistStatisticsService, IArtistService artistService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IBroadcastSignalRMessage signalRBroadcaster) : base(albumService, artistStatisticsService, artistService, qualityUpgradableSpecification, signalRBroadcaster, "wanted/missing") { diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs index b262c9918..f58e7942c 100644 --- a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -1,8 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using NLog; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; @@ -16,7 +15,6 @@ namespace NzbDrone.Common.Disk public class FileSystemLookupService : IFileSystemLookupService { private readonly IDiskProvider _diskProvider; - private readonly Logger _logger; private readonly HashSet _setToRemove = new HashSet { @@ -48,10 +46,9 @@ namespace NzbDrone.Common.Disk "@eadir" }; - public FileSystemLookupService(IDiskProvider diskProvider, Logger logger) + public FileSystemLookupService(IDiskProvider diskProvider) { _diskProvider = diskProvider; - _logger = logger; } public FileSystemResult LookupContents(string query, bool includeFiles) @@ -154,6 +151,16 @@ namespace NzbDrone.Common.Disk .ToList(); } + private static string GetVolumeName(IMount mountInfo) + { + if (mountInfo.VolumeLabel.IsNullOrWhiteSpace()) + { + return mountInfo.Name; + } + + return $"{mountInfo.Name} ({mountInfo.VolumeLabel})"; + } + private string GetDirectoryPath(string path) { if (path.Last() != Path.DirectorySeparatorChar) @@ -164,7 +171,7 @@ namespace NzbDrone.Common.Disk return path; } - private string GetParent(string path) + private static string GetParent(string path) { var di = new DirectoryInfo(path); diff --git a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs index cb432addc..e518455ef 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/IRuntimeInfo.cs @@ -1,14 +1,17 @@ -using System; +using System; namespace NzbDrone.Common.EnvironmentInfo { public interface IRuntimeInfo { + DateTime StartTime { get; } bool IsUserInteractive { get; } bool IsAdmin { get; } bool IsWindowsService { get; } bool IsExiting { get; set; } + bool IsTray { get; } + RuntimeMode Mode { get; } bool RestartPending { get; set; } string ExecutingApplication { get; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs index a53862311..9764ac270 100644 --- a/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeInfo.cs @@ -1,20 +1,23 @@ -using System; +using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Security.Principal; using System.ServiceProcess; using NLog; +using NzbDrone.Common.Processes; namespace NzbDrone.Common.EnvironmentInfo { public class RuntimeInfo : IRuntimeInfo { private readonly Logger _logger; + private readonly DateTime _startTime = DateTime.UtcNow; public RuntimeInfo(IServiceProvider serviceProvider, Logger logger) { _logger = logger; + IsWindowsService = !IsUserInteractive && OsInfo.IsWindows && @@ -35,6 +38,14 @@ namespace NzbDrone.Common.EnvironmentInfo IsProduction = InternalIsProduction(); } + public DateTime StartTime + { + get + { + return _startTime; + } + } + public static bool IsUserInteractive => Environment.UserInteractive; bool IRuntimeInfo.IsUserInteractive => IsUserInteractive; @@ -59,6 +70,39 @@ namespace NzbDrone.Common.EnvironmentInfo public bool IsWindowsService { get; private set; } public bool IsExiting { get; set; } + + public bool IsTray + { + get + { + if (OsInfo.IsWindows) + { + return IsUserInteractive && Process.GetCurrentProcess().ProcessName.Equals(ProcessProvider.NZB_DRONE_PROCESS_NAME, StringComparison.InvariantCultureIgnoreCase); + } + + return false; + } + } + + public RuntimeMode Mode + { + get + { + if (IsWindowsService) + { + return RuntimeMode.Service; + } + + if (IsTray) + { + return RuntimeMode.Tray; + } + + return RuntimeMode.Console; + } + } + + public bool RestartPending { get; set; } public string ExecutingApplication { get; } diff --git a/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs new file mode 100644 index 000000000..fc5a1867d --- /dev/null +++ b/src/NzbDrone.Common/EnvironmentInfo/RuntimeMode.cs @@ -0,0 +1,9 @@ +namespace NzbDrone.Common.EnvironmentInfo +{ + public enum RuntimeMode + { + Console, + Service, + Tray + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 611cdc4cc..0bce4df70 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -93,6 +93,7 @@ + diff --git a/src/NzbDrone.Common/Serializer/Json.cs b/src/NzbDrone.Common/Serializer/Json.cs index 31e0d3f0b..e40a8181d 100644 --- a/src/NzbDrone.Common/Serializer/Json.cs +++ b/src/NzbDrone.Common/Serializer/Json.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -10,6 +10,7 @@ namespace NzbDrone.Common.Serializer { private static readonly JsonSerializer Serializer; private static readonly JsonSerializerSettings SerializerSetting; + private static readonly JsonSerializerSettings DeserializerSetting; static Json() { @@ -24,22 +25,33 @@ namespace NzbDrone.Common.Serializer SerializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); - //SerializerSetting.Converters.Add(new IntConverter()); SerializerSetting.Converters.Add(new VersionConverter()); SerializerSetting.Converters.Add(new HttpUriConverter()); Serializer = JsonSerializer.Create(SerializerSetting); + DeserializerSetting = new JsonSerializerSettings + { + DateTimeZoneHandling = DateTimeZoneHandling.Utc, + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Include, + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + DeserializerSetting.Converters.Add(new StringEnumConverter { CamelCaseText = true }); + DeserializerSetting.Converters.Add(new VersionConverter()); + } public static T Deserialize(string json) where T : new() { - return JsonConvert.DeserializeObject(json, SerializerSetting); + return JsonConvert.DeserializeObject(json, DeserializerSetting); } public static object Deserialize(string json, Type type) { - return JsonConvert.DeserializeObject(json, type, SerializerSetting); + return JsonConvert.DeserializeObject(json, type, DeserializerSetting); } public static bool TryDeserialize(string json, out T result) where T : new() @@ -78,4 +90,4 @@ namespace NzbDrone.Common.Serializer Serialize(model, new StreamWriter(outputStream)); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs index 3878c41c9..fbd8f295d 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseRelationshipFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; @@ -6,6 +6,7 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Test.Datastore { @@ -17,6 +18,7 @@ namespace NzbDrone.Core.Test.Datastore { var episodeFile = Builder.CreateNew() .With(c => c.Quality = new QualityModel()) + .With(c => c.Language = Language.English) .BuildNew(); Db.Insert(episodeFile); diff --git a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs index 1633baae4..48aaebcbe 100644 --- a/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/MarrDataLazyLoadingFixture.cs @@ -1,11 +1,14 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.Datastore { @@ -17,18 +20,28 @@ namespace NzbDrone.Core.Test.Datastore public void Setup() { var profile = new Profile - { - Name = "Test", - Cutoff = Quality.MP3_320, - Items = Qualities.QualityFixture.GetDefaultQualities() - }; + { + Name = "Test", + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }; + + var languageProfile = new LanguageProfile + { + Name = "Test", + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }; + + - profile = Db.Insert(profile); + languageProfile = Db.Insert(languageProfile); var series = Builder.CreateListOfSize(1) .All() .With(v => v.ProfileId = profile.Id) + .With(v => v.LanguageProfileId = languageProfile.Id) .BuildListOfNew(); Db.InsertMany(series); @@ -65,6 +78,7 @@ namespace NzbDrone.Core.Test.Datastore { Assert.IsNotNull(episode.Series); Assert.IsFalse(episode.Series.Profile.IsLoaded); + Assert.IsFalse(episode.Series.LanguageProfile.IsLoaded); } } @@ -100,8 +114,28 @@ namespace NzbDrone.Core.Test.Datastore { Assert.IsNotNull(episode.Series); Assert.IsTrue(episode.Series.Profile.IsLoaded); + Assert.IsFalse(episode.Series.LanguageProfile.IsLoaded); + } + } + + [Test] + public void should_explicit_load_languageprofile_if_joined() + { + var db = Mocker.Resolve(); + var DataMapper = db.GetDataMapper(); + + var episodes = DataMapper.Query() + .Join(Marr.Data.QGen.JoinType.Inner, v => v.Series, (l, r) => l.SeriesId == r.Id) + .Join(Marr.Data.QGen.JoinType.Inner, v => v.LanguageProfile, (l, r) => l.ProfileId == r.Id) + .ToList(); + + foreach (var episode in episodes) + { + Assert.IsNotNull(episode.Series); + Assert.IsFalse(episode.Series.Profile.IsLoaded); + Assert.IsTrue(episode.Series.LanguageProfile.IsLoaded); } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs index 1d6c113b8..729c692b8 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/075_force_lib_updateFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Test.Framework; @@ -55,21 +55,30 @@ namespace NzbDrone.Core.Test.Datastore.Migration { var db = WithMigrationTestDb(c => { + c.Insert.IntoTable("Profiles").Row(new + + { + Name = "Profile1", + CutOff = 0, + Items = "[]", + Language = 1 + }); c.Insert.IntoTable("Series").Row(new { Tvdbid = 1, - TvRageId =1, - Title ="Title1", - CleanTitle ="CleanTitle1", - Status =1, - Images ="", - Path ="c:\\test", - Monitored =1, - SeasonFolder =1, - Runtime= 0, - SeriesType=0, - UseSceneNumbering =0, - LastInfoSync = "2000-01-01 00:00:00" + TvRageId = 1, + Title = "Title1", + CleanTitle = "CleanTitle1", + Status = 1, + Images = "", + Path = "c:\\test", + Monitored = 1, + SeasonFolder = 1, + Runtime = 0, + SeriesType = 0, + UseSceneNumbering = 0, + LastInfoSync = "2000-01-01 00:00:00", + ProfileId = 1 }); c.Insert.IntoTable("Series").Row(new @@ -86,7 +95,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration Runtime = 0, SeriesType = 0, UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00" + LastInfoSync = "2000-01-01 00:00:00", + ProfileId = 1 }); }); @@ -95,4 +105,4 @@ namespace NzbDrone.Core.Test.Datastore.Migration series.Should().OnlyContain(c => c.LastInfoSync.Value.Year == 2014); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs index e333fb9a1..ea8dcc013 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/079_dedupe_tagsFixture.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore.Migration; @@ -14,6 +14,15 @@ namespace NzbDrone.Core.Test.Datastore.Migration { var db = WithMigrationTestDb(c => { + c.Insert.IntoTable("Profiles").Row(new + + { + Name = "Profile1", + CutOff = 0, + Items = "[]", + Language = 1 + }); + c.Insert.IntoTable("Series").Row(new { Tvdbid = 1, @@ -28,12 +37,13 @@ namespace NzbDrone.Core.Test.Datastore.Migration Runtime = 0, SeriesType = 0, UseSceneNumbering = 0, - LastInfoSync = "2000-01-01 00:00:00" + LastInfoSync = "2000-01-01 00:00:00", + ProfileId = 1 }); c.Insert.IntoTable("Tags").Row(new { - Label = "test" + Label = "test" }); }); @@ -46,6 +56,14 @@ namespace NzbDrone.Core.Test.Datastore.Migration { var db = WithMigrationTestDb(c => { + c.Insert.IntoTable("Profiles").Row(new + + { + Name = "Profile1", + CutOff = 0, + Items = "[]", + Language = 1 + }); c.Insert.IntoTable("Series").Row(new { Tvdbid = 1, @@ -61,7 +79,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration SeriesType = 0, UseSceneNumbering = 0, LastInfoSync = "2000-01-01 00:00:00", - Tags = "[]" + Tags = "[]", + ProfileId = 1 }); c.Insert.IntoTable("Tags").Row(new @@ -113,6 +132,14 @@ namespace NzbDrone.Core.Test.Datastore.Migration { var db = WithMigrationTestDb(c => { + c.Insert.IntoTable("Profiles").Row(new + + { + Name = "Profile1", + CutOff = 0, + Items = "[]", + Language = 1 + }); c.Insert.IntoTable("Series").Row(new { Tvdbid = 1, @@ -128,7 +155,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration SeriesType = 0, UseSceneNumbering = 0, LastInfoSync = "2000-01-01 00:00:00", - Tags = "[2]" + Tags = "[2]", + ProfileId = 1 }); c.Insert.IntoTable("Tags").Row(new @@ -151,6 +179,15 @@ namespace NzbDrone.Core.Test.Datastore.Migration { var db = WithMigrationTestDb(c => { + c.Insert.IntoTable("Profiles").Row(new + + { + Name = "Profile1", + CutOff = 0, + Items = "[]", + Language = 1 + }); + c.Insert.IntoTable("Series").Row(new { Tvdbid = 1, @@ -166,7 +203,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration SeriesType = 0, UseSceneNumbering = 0, LastInfoSync = "2000-01-01 00:00:00", - Tags = "[2]" + Tags = "[2]", + ProfileId = 1 }); c.Insert.IntoTable("Series").Row(new @@ -184,7 +222,8 @@ namespace NzbDrone.Core.Test.Datastore.Migration SeriesType = 0, UseSceneNumbering = 0, LastInfoSync = "2000-01-01 00:00:00", - Tags = "[]" + Tags = "[]", + ProfileId = 1 }); c.Insert.IntoTable("Tags").Row(new diff --git a/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs index 5a8a1fe02..200a43056 100644 --- a/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/Migration/108_fix_metadata_file_extensionsFixture.cs @@ -1,8 +1,9 @@ -using System.Linq; +using System.Linq; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore.Migration; using NzbDrone.Core.Parser; +using NzbDrone.Core.Languages; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.Datastore.Migration @@ -45,7 +46,7 @@ namespace NzbDrone.Core.Test.Datastore.Migration RelativePath = "Series.Title.S01E01.en.srt", Added = "2016-05-30 20:23:02.3725923", LastUpdated = "2016-05-30 20:23:02.3725923", - Language = Language.English, + Language = Core.Languages.Language.English, Extension = "en.srt" }); }); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index ab91dba4a..81adab1aa 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -1,50 +1,226 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - public class CutoffSpecificationFixture : CoreTest + public class CutoffSpecificationFixture : CoreTest { [Test] public void should_return_true_if_current_album_is_less_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_192, new Revision(version: 2))).Should().BeTrue(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language.English).Should().BeTrue(); } [Test] public void should_return_false_if_current_album_is_equal_to_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English).Should().BeFalse(); } [Test] public void should_return_false_if_current_album_is_greater_than_cutoff() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_512, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_256, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), Language.English).Should().BeFalse(); } [Test] public void should_return_true_when_new_album_is_proper_but_existing_is_not() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 1)), - new QualityModel(Quality.MP3_256, new Revision(version: 2))).Should().BeTrue(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 1)),Language.English, + new QualityModel(Quality.MP3_320, new Revision(version: 2))).Should().BeTrue(); + } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher() { - Subject.CutoffNotMet(new Profile { Cutoff = Quality.MP3_256, Items = Qualities.QualityFixture.GetDefaultQualities() }, - new QualityModel(Quality.MP3_256, new Revision(version: 2)), - new QualityModel(Quality.MP3_512, new Revision(version: 2))).Should().BeFalse(); + Subject.CutoffNotMet( + new Profile + + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities() + }, + new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }, + new QualityModel(Quality.MP3_320, new Revision(version: 2)),Language.English, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_quality_cutoff_is_met_and_quality_is_higher_but_language_is_not_met() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet(_profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.English, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_met() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.Spanish, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_320, new Revision(version: 2)), + Language.French, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeFalse(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_new_quality_is_higher_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), + Language.French, + new QualityModel(Quality.FLAC, new Revision(version: 2))).Should().BeTrue(); + } + + [Test] + public void should_return_true_if_cutoff_is_not_met_and_language_is_higher() + { + + Profile _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.MP3_256, new Revision(version: 2)), + Language.French).Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs index df79537ae..18f92f69d 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/HistorySpecificationFixture.cs @@ -9,11 +9,14 @@ using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -24,8 +27,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteAlbum _parseResultMulti; private RemoteAlbum _parseResultSingle; - private QualityModel _upgradableQuality; - private QualityModel _notupgradableQuality; + private Tuple _upgradableQuality; + private Tuple _notupgradableQuality; private Artist _fakeArtist; private const int FIRST_ALBUM_ID = 1; private const int SECOND_ALBUM_ID = 2; @@ -33,7 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _upgradeHistory = Mocker.Resolve(); var singleAlbumList = new List { new Album { Id = FIRST_ALBUM_ID} }; @@ -45,34 +48,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _fakeArtist = Builder.CreateNew() .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = LanguageFixture.GetDefaultLanguages() }) .Build(); _parseResultMulti = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language = Language.English }, Albums = doubleAlbumList }; _parseResultSingle = new RemoteAlbum { Artist = _fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192, new Revision(version: 2)), Language = Language.English }, Albums = singleAlbumList }; - _upgradableQuality = new QualityModel(Quality.MP3_192, new Revision(version: 1)); - _notupgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 2)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_192, new Revision(version: 1)), Language.English); + _notupgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 2)), Language.English); Mocker.GetMock() .SetupGet(s => s.EnableCompletedDownloadHandling) .Returns(true); } - private void GivenMostRecentForAlbum(int albumId, string downloadId, QualityModel quality, DateTime date, HistoryEventType eventType) + private void GivenMostRecentForAlbum(int albumId, string downloadId, Tuple quality, DateTime date, HistoryEventType eventType) { Mocker.GetMock().Setup(s => s.MostRecentForAlbum(albumId)) - .Returns(new History.History { DownloadId = downloadId, Quality = quality, Date = date, EventType = eventType }); + .Returns(new History.History { DownloadId = downloadId, Quality = quality.Item1, Date = date, EventType = eventType, Language = quality.Item2 }); } private void GivenCdhDisabled() @@ -160,7 +164,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.English); GivenMostRecentForAlbum(FIRST_ALBUM_ID, string.Empty, _upgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed); @@ -172,7 +176,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish); GivenMostRecentForAlbum(FIRST_ALBUM_ID, string.Empty, _upgradableQuality, DateTime.UtcNow, HistoryEventType.Grabbed); @@ -200,7 +204,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests GivenCdhDisabled(); _fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }; _parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); - _upgradableQuality = new QualityModel(Quality.MP3_512, new Revision(version: 1)); + _upgradableQuality = new Tuple(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish); GivenMostRecentForAlbum(FIRST_ALBUM_ID, "test", _upgradableQuality, DateTime.UtcNow.AddDays(-100), HistoryEventType.Grabbed); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs index 5bb65c62e..d9b21e3f6 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/LanguageSpecificationFixture.cs @@ -1,10 +1,11 @@ -using FluentAssertions; +using FluentAssertions; using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; -using NzbDrone.Core.Parser; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; @@ -19,19 +20,25 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { + + LanguageProfile _profile = new LazyLoaded(new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish), + Cutoff = Language.Spanish + }); + + _remoteAlbum = new RemoteAlbum { ParsedAlbumInfo = new ParsedAlbumInfo { Language = Language.English }, + Artist = new Artist - { - Profile = new LazyLoaded(new Profile - { - Language = Language.English - }) - } + { + LanguageProfile = _profile + } }; } @@ -42,7 +49,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithGermanRelease() { - _remoteAlbum.ParsedAlbumInfo.Language = Language.German; + _remoteAlbum.ParsedAlbumInfo.Language = Language.German; } [Test] @@ -61,4 +68,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Mocker.Resolve().IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs index 1f3d4d03d..722cc71e8 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs @@ -1,11 +1,11 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Moq; using NzbDrone.Core.Indexers; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Music; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.DecisionEngine; @@ -14,6 +14,9 @@ using FluentAssertions; using FizzWare.NBuilder; using NzbDrone.Common.Extensions; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -33,11 +36,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Build(); } - private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) + private RemoteAlbum GivenRemoteAlbum(List albums, QualityModel quality, Language language, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet) { var remoteAlbum = new RemoteAlbum(); remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); remoteAlbum.ParsedAlbumInfo.Quality = quality; + remoteAlbum.ParsedAlbumInfo.Language = language; remoteAlbum.Albums = new List(); remoteAlbum.Albums.AddRange(albums); @@ -48,8 +52,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests remoteAlbum.Release.DownloadProtocol = downloadProtocol; remoteAlbum.Artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish + }).Build(); return remoteAlbum; } @@ -67,8 +78,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_propers_before_non_propers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 1))); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 2))); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 1)), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -81,8 +92,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_put_higher_quality_before_lower() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -95,10 +106,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_age_then_largest_rounded_to_200mb() { - var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), size: 100.Megabytes(), age: 1); - var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1200.Megabytes(), age: 1000); - var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 1250.Megabytes(), age: 10); - var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 3000.Megabytes(), age: 1); + var remoteAlbumSd = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English, size: 100.Megabytes(), age: 1); + var remoteAlbumHdSmallOld = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 1200.Megabytes(), age: 1000); + var remoteAlbumSmallYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 1250.Megabytes(), age: 10); + var remoteAlbumHdLargeYoung = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 3000.Megabytes(), age: 1); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbumSd)); @@ -113,8 +124,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_order_by_youngest() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 10); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), age: 5); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, age: 10); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, age: 5); var decisions = new List(); @@ -128,8 +139,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_not_throw_if_no_episodes_are_found() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), size: 500.Megabytes()); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 500.Megabytes()); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, size: 500.Megabytes()); remoteAlbum1.Albums = new List(); @@ -145,8 +156,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Usenet); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -161,8 +172,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenPreferredDownloadProtocol(DownloadProtocol.Torrent); - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Torrent); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), downloadProtocol: DownloadProtocol.Usenet); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Torrent); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English, downloadProtocol: DownloadProtocol.Usenet); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -175,8 +186,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_single_album_over_multi_album() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1), GivenAlbum(2) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var decisions = new List(); decisions.Add(new DownloadDecision(remoteAlbum1)); @@ -189,8 +200,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_seeders() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -209,14 +220,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests decisions.Add(new DownloadDecision(remoteAlbum2)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - ((TorrentInfo) qualifiedReports.First().RemoteAlbum.Release).Seeders.Should().Be(torrentInfo2.Seeders); + ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Seeders.Should().Be(torrentInfo2.Seeders); } [Test] public void should_prefer_releases_with_more_peers_given_equal_number_of_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -243,8 +254,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_releases_with_more_peers_no_seeds() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -272,8 +283,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_first_release_if_peers_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -295,14 +306,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests decisions.Add(new DownloadDecision(remoteAlbum2)); var qualifiedReports = Subject.PrioritizeDecisions(decisions); - ((TorrentInfo) qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); + ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); } [Test] public void should_prefer_first_release_if_age_and_size_are_too_similar() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.English); remoteAlbum1.Release.PublishDate = DateTime.UtcNow.AddDays(-100); remoteAlbum1.Release.Size = 200.Megabytes(); @@ -321,8 +332,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_prefer_quality_over_the_number_of_peers() { - var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_512)); - var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192)); + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_512), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_192), Language.English); var torrentInfo1 = new TorrentInfo(); torrentInfo1.PublishDate = DateTime.Now; @@ -346,5 +357,37 @@ namespace NzbDrone.Core.Test.DecisionEngineTests var qualifiedReports = Subject.PrioritizeDecisions(decisions); ((TorrentInfo)qualifiedReports.First().RemoteAlbum.Release).Should().Be(torrentInfo1); } + + [Test] + public void should_order_by_language() + { + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.English); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.French); + var remoteAlbum3 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.German); + + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + decisions.Add(new DownloadDecision(remoteAlbum3)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Language.Should().Be(Language.French); + qualifiedReports.Last().RemoteAlbum.ParsedAlbumInfo.Language.Should().Be(Language.German); + } + + [Test] + public void should_put_higher_quality_before_lower_allways() + { + var remoteAlbum1 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_256), Language.French); + var remoteAlbum2 = GivenRemoteAlbum(new List { GivenAlbum(1) }, new QualityModel(Quality.MP3_320), Language.German); + + var decisions = new List(); + decisions.Add(new DownloadDecision(remoteAlbum1)); + decisions.Add(new DownloadDecision(remoteAlbum2)); + + var qualifiedReports = Subject.PrioritizeDecisions(decisions); + qualifiedReports.First().RemoteAlbum.ParsedAlbumInfo.Quality.Quality.Should().Be(Quality.MP3_320); + } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs index d362c6d0e..2fc4cf993 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityAllowedByProfileSpecificationFixture.cs @@ -1,10 +1,10 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using Marr.Data; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; @@ -63,4 +63,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs index a3791ba77..7461f4ca2 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs @@ -1,16 +1,19 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - - public class QualityUpgradeSpecificationFixture : CoreTest + + public class QualityUpgradeSpecificationFixture : CoreTest { public static object[] IsUpgradeTestCases = { @@ -22,7 +25,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new object[] { Quality.MP3_320, 1, Quality.MP3_320, 1, Quality.MP3_320, false }, new object[] { Quality.MP3_512, 1, Quality.MP3_512, 1, Quality.MP3_512, false } }; - + + public static object[] IsUpgradeTestCasesLanguages = + { + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 2, Language.English, Quality.MP3_192, Language.Spanish, true }, + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 1, Language.Spanish, Quality.MP3_192, Language.Spanish, true }, + new object[] { Quality.MP3_320, 1, Language.French, Quality.MP3_320, 2, Language.English, Quality.MP3_320, Language.Spanish, true }, + new object[] { Quality.MP3_192, 1, Language.English, Quality.MP3_192, 1, Language.English, Quality.MP3_192, Language.English, false }, + new object[] { Quality.MP3_320, 1, Language.English, Quality.MP3_320, 2, Language.Spanish, Quality.FLAC, Language.Spanish, false }, + new object[] { Quality.MP3_320, 1, Language.Spanish, Quality.MP3_320, 2, Language.French, Quality.MP3_320, Language.Spanish, false } + }; + [SetUp] public void Setup() { @@ -41,10 +54,41 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(true); - var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile - Subject.IsUpgradable(profile, new QualityModel(current, new Revision(version: currentVersion)), new QualityModel(newQuality, new Revision(version: newVersion))) - .Should().Be(expected); + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }; + + var langProfile = new LanguageProfile + + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.English + }; + + Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), Language.English, new QualityModel(newQuality, new Revision(version: newVersion)), Language.English) + .Should().Be(expected); + } + + [Test, TestCaseSource("IsUpgradeTestCasesLanguages")] + public void IsUpgradeTestLanguage(Quality current, int currentVersion, Language currentLanguage, Quality newQuality, int newVersion, Language newLanguage, Quality cutoff, Language languageCutoff, bool expected) + { + GivenAutoDownloadPropers(true); + + var profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + Cutoff = cutoff, + }; + + var langProfile = new LanguageProfile + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = languageCutoff + }; + + Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), currentLanguage, new QualityModel(newQuality, new Revision(version: newVersion)), newLanguage).Should().Be(expected); } [Test] @@ -52,10 +96,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { GivenAutoDownloadPropers(false); - var profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }; + var profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + var langProfile = new LanguageProfile + + { + Languages = LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.English + }; - Subject.IsUpgradable(profile, new QualityModel(Quality.MP3_192, new Revision(version: 2)), new QualityModel(Quality.MP3_192, new Revision(version: 1))) - .Should().BeFalse(); + Subject.IsUpgradable(profile, langProfile, new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language.English, new QualityModel(Quality.MP3_256, new Revision(version: 1)), Language.English) + .Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 38dc8d420..2e51b0989 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; @@ -6,11 +6,13 @@ using NUnit.Framework; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Queue; using NzbDrone.Core.Music; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -27,11 +29,18 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = Languages.LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish + }).Build(); _album = Builder.CreateNew() .With(e => e.ArtistId = _artist.Id) @@ -49,7 +58,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) - .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_192) }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256), Language = Language.Spanish }) .Build(); } @@ -95,14 +104,36 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_return_true_when_quality_in_queue_is_lower() { _artist.Profile.Value.Cutoff = Quality.MP3_512; + _artist.LanguageProfile.Value.Cutoff = Language.Spanish; var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.Spanish + }) + .Build(); + + GivenQueue(new List { remoteAlbum }); + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_quality_in_queue_is_lower_but_language_is_higher() + { + _artist.Profile.Value.Cutoff = Quality.FLAC; + _artist.LanguageProfile.Value.Cutoff = Language.Spanish; + + var remoteAlbum = Builder.CreateNew() + .With(r => r.Artist = _artist) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.English + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -116,9 +147,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192) + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -126,21 +157,39 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_return_false_when_qualities_are_the_same() + public void should_return_false_when_qualities_are_the_same_and_languages_are_the_same() { var remoteAlbum = Builder.CreateNew() .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_192) - }) + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.Spanish + }) .Build(); GivenQueue(new List { remoteAlbum }); Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } + [Test] + public void should_return_true_when_qualities_are_the_same_but_language_is_better() + { + var remoteAlbum = Builder.CreateNew() + .With(r => r.Artist = _artist) + .With(r => r.Albums = new List { _album }) + .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo + { + Quality = new QualityModel(Quality.MP3_192), + Language = Language.English, + }) + .Build(); + + GivenQueue(new List { remoteAlbum }); + Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); + } + [Test] public void should_return_false_when_quality_in_queue_is_better() { @@ -150,9 +199,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Artist = _artist) .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = new QualityModel(Quality.MP3_256) - }) + { + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English + }) .Build(); GivenQueue(new List { remoteAlbum }); @@ -167,7 +217,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -183,7 +234,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -201,7 +253,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album, _otherAlbum }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English }) .Build(); @@ -218,11 +271,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .All() .With(r => r.Artist = _artist) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo - { - Quality = - new QualityModel( - Quality.MP3_256) - }) + { + Quality = new QualityModel(Quality.MP3_256), + Language = Language.English + }) .TheFirst(1) .With(r => r.Albums = new List { _album }) .TheNext(1) @@ -235,7 +287,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests } [Test] - public void should_return_false_if_quality_in_queue_meets_cutoff() + public void should_return_false_if_quality_and_language_in_queue_meets_cutoff() { _artist.Profile.Value.Cutoff = _remoteAlbum.ParsedAlbumInfo.Quality.Quality; @@ -244,7 +296,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(r => r.Albums = new List { _album }) .With(r => r.ParsedAlbumInfo = new ParsedAlbumInfo { - Quality = new QualityModel(Quality.MP3_256) + Quality = new QualityModel(Quality.MP3_256), + Language = Language.Spanish }) .Build(); @@ -253,4 +306,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 9416dc0c7..35c174ccf 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -13,7 +13,9 @@ using NzbDrone.Core.Indexers; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync public class DelaySpecificationFixture : CoreTest { private Profile _profile; + private LanguageProfile _langProfile; private DelayProfile _delayProfile; private RemoteAlbum _remoteAlbum; @@ -34,12 +37,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile = Builder.CreateNew() .Build(); + _langProfile = Builder.CreateNew() + .Build(); + + _delayProfile = Builder.CreateNew() - .With(d => d.PreferredProtocol = DownloadProtocol.Usenet) - .Build(); + .With(d => d.PreferredProtocol = DownloadProtocol.Usenet) + .Build(); var artist = Builder.CreateNew() .With(s => s.Profile = _profile) + .With(s => s.LanguageProfile = _langProfile) .Build(); _remoteAlbum = Builder.CreateNew() @@ -53,6 +61,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _profile.Cutoff = Quality.MP3_320; + _langProfile.Cutoff = Language.Spanish; + _langProfile.Languages = Languages.LanguageFixture.GetDefaultLanguages(); + _remoteAlbum.ParsedAlbumInfo = new ParsedAlbumInfo(); _remoteAlbum.Release = new ReleaseInfo(); _remoteAlbum.Release.DownloadProtocol = DownloadProtocol.Usenet; @@ -61,7 +72,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny(), It.IsAny())) - .Returns(new List {}); + .Returns(new List { }); Mocker.GetMock() .Setup(s => s.BestForTags(It.IsAny>())) @@ -72,17 +83,20 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync .Returns(new List()); } - private void GivenExistingFile(QualityModel quality) + private void GivenExistingFile(QualityModel quality, Language language) { Mocker.GetMock() .Setup(s => s.GetFilesByAlbum(It.IsAny(), It.IsAny())) - .Returns(new List { new TrackFile { Quality = quality } }); + .Returns(new List { new TrackFile { + Quality = quality, + Language = language + } }); } private void GivenUpgradeForExistingFile() { - Mocker.GetMock() - .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny())) + Mocker.GetMock() + .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(true); } @@ -112,9 +126,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync } [Test] - public void should_be_true_when_quality_is_last_allowed_in_profile() + public void should_be_true_when_quality_and_language_is_last_allowed_in_profile() { _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_320); + _remoteAlbum.ParsedAlbumInfo.Language = Language.French; Subject.IsSatisfiedBy(_remoteAlbum, null).Accepted.Should().BeTrue(); } @@ -147,10 +162,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_256), Language.English); GivenUpgradeForExistingFile(); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny())) .Returns(true); @@ -165,10 +180,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(real: 1)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_256)); + GivenExistingFile(new QualityModel(Quality.MP3_256), Language.English); GivenUpgradeForExistingFile(); - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny())) .Returns(true); @@ -183,7 +198,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteAlbum.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)); _remoteAlbum.Release.PublishDate = DateTime.UtcNow; - GivenExistingFile(new QualityModel(Quality.MP3_192)); + GivenExistingFile(new QualityModel(Quality.MP3_192), Language.English); _delayProfile.UsenetDelay = 720; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 3a02acf7f..ab557ef4b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -9,7 +9,7 @@ using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; @@ -30,7 +30,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), DateAdded = DateTime.Now }; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs index 214cb2196..d66b23b0c 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeDiskSpecificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; @@ -7,11 +7,13 @@ using Moq; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.DecisionEngineTests { @@ -27,16 +29,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [SetUp] public void Setup() { - Mocker.Resolve(); + Mocker.Resolve(); - _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; - _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now }; + _firstFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now, Language = Language.English }; + _secondFile = new TrackFile { Quality = new QualityModel(Quality.FLAC, new Revision(version: 2)), DateAdded = DateTime.Now, Language = Language.English }; var singleEpisodeList = new List { new Album {}}; var doubleEpisodeList = new List { new Album {}, new Album {}, new Album {} }; + var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish); + var fakeArtist = Builder.CreateNew() - .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities()}) + .With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages }) .Build(); Mocker.GetMock() @@ -46,14 +51,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultMulti = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language = Language.English }, Albums = doubleEpisodeList }; _parseResultSingle = new RemoteAlbum { Artist = fakeArtist, - ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)) }, + ParsedAlbumInfo = new ParsedAlbumInfo { Quality = new QualityModel(Quality.MP3_256, new Revision(version: 2)), Language = Language.English }, Albums = singleEpisodeList }; @@ -108,4 +113,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs index 07ffe6f95..da5230278 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadApprovedReportsTests/DownloadApprovedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using FluentAssertions; @@ -8,7 +8,7 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs index 9e9652ab7..f6d421cb0 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; @@ -9,7 +9,7 @@ using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs index 4fb99c8f8..ec98ba123 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; using Moq; @@ -9,7 +9,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs index 46a16830a..da3d791fa 100644 --- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs +++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using FizzWare.NBuilder; using Marr.Data; @@ -11,7 +11,7 @@ using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; diff --git a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs index 34e5f2962..5c9b3cb1a 100644 --- a/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs +++ b/src/NzbDrone.Core.Test/HistoryTests/HistoryServiceFixture.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Linq; using FizzWare.NBuilder; using Moq; @@ -6,14 +6,14 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.History; using NzbDrone.Core.Qualities; -using System.Collections.Generic; using NzbDrone.Core.Test.Qualities; -using FluentAssertions; using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.HistoryTests { @@ -21,48 +21,34 @@ namespace NzbDrone.Core.Test.HistoryTests { private Profile _profile; private Profile _profileCustom; + private LanguageProfile _languageProfile; [SetUp] public void Setup() { - _profile = new Profile { Cutoff = Quality.MP3_256, Items = QualityFixture.GetDefaultQualities() }; - _profileCustom = new Profile { Cutoff = Quality.MP3_256, Items = QualityFixture.GetDefaultQualities(Quality.MP3_192) }; - } + _profile = new Profile + { + Cutoff = Quality.MP3_320, + Items = QualityFixture.GetDefaultQualities(), + }; - [Test] - public void should_return_null_if_no_history() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List()); + _profileCustom = new Profile - var quality = Subject.GetBestQualityInHistory(_profile, 2); + { + Cutoff = Quality.MP3_320, + Items = QualityFixture.GetDefaultQualities(Quality.MP3_256), - quality.Should().BeNull(); - } + }; - [Test] - public void should_return_best_quality() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List { new QualityModel(Quality.MP3_192), new QualityModel(Quality.MP3_256) }); - - var quality = Subject.GetBestQualityInHistory(_profile, 2); - quality.Should().Be(new QualityModel(Quality.MP3_256)); - } + _languageProfile = new LanguageProfile - [Test] - public void should_return_best_quality_with_custom_order() - { - Mocker.GetMock() - .Setup(v => v.GetBestQualityInHistory(2)) - .Returns(new List { new QualityModel(Quality.MP3_192), new QualityModel(Quality.MP3_256) }); + { + Cutoff = Language.Spanish, + Languages = Languages.LanguageFixture.GetDefaultLanguages() + }; - var quality = Subject.GetBestQualityInHistory(_profileCustom, 2); - quality.Should().Be(new QualityModel(Quality.MP3_192)); } [Test] @@ -75,11 +61,11 @@ namespace NzbDrone.Core.Test.HistoryTests .Build(); var localEpisode = new LocalEpisode - { - Series = series, - Episodes = episodes, - Path = @"C:\Test\Unsorted\Series.s01e01.mkv" - }; + { + Series = series, + Episodes = episodes, + Path = @"C:\Test\Unsorted\Series.s01e01.mkv" + }; Subject.Handle(new EpisodeImportedEvent(localEpisode, episodeFile, true, "sab", "abcd", true)); @@ -87,4 +73,4 @@ namespace NzbDrone.Core.Test.HistoryTests .Verify(v => v.Insert(It.Is(h => h.SourceTitle == Path.GetFileNameWithoutExtension(localEpisode.Path)))); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs new file mode 100644 index 000000000..4db1719df --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageFixture.cs @@ -0,0 +1,100 @@ +using System.Linq; +using System.Collections.Generic; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + public class LanguageFixture : CoreTest + { + public static object[] FromIntCases = + { + new object[] {1, Language.English}, + new object[] {2, Language.French}, + new object[] {3, Language.Spanish}, + new object[] {4, Language.German}, + new object[] {5, Language.Italian}, + new object[] {6, Language.Danish}, + new object[] {7, Language.Dutch}, + new object[] {8, Language.Japanese}, + new object[] {9, Language.Cantonese}, + new object[] {10, Language.Mandarin}, + new object[] {11, Language.Russian}, + new object[] {12, Language.Polish}, + new object[] {13, Language.Vietnamese}, + new object[] {14, Language.Swedish}, + new object[] {15, Language.Norwegian}, + new object[] {16, Language.Finnish}, + new object[] {17, Language.Turkish}, + new object[] {18, Language.Portuguese}, + new object[] {19, Language.Flemish}, + new object[] {20, Language.Greek}, + new object[] {21, Language.Korean}, + new object[] {22, Language.Hungarian} + }; + + public static object[] ToIntCases = + { + new object[] {Language.English, 1}, + new object[] {Language.French, 2}, + new object[] {Language.Spanish, 3}, + new object[] {Language.German, 4}, + new object[] {Language.Italian, 5}, + new object[] {Language.Danish, 6}, + new object[] {Language.Dutch, 7}, + new object[] {Language.Japanese, 8}, + new object[] {Language.Cantonese, 9}, + new object[] {Language.Mandarin, 10}, + new object[] {Language.Russian, 11}, + new object[] {Language.Polish, 12}, + new object[] {Language.Vietnamese, 13}, + new object[] {Language.Swedish, 14}, + new object[] {Language.Norwegian, 15}, + new object[] {Language.Finnish, 16}, + new object[] {Language.Turkish, 17}, + new object[] {Language.Portuguese, 18}, + new object[] {Language.Flemish, 19}, + new object[] {Language.Greek, 20}, + new object[] {Language.Korean, 21}, + new object[] {Language.Hungarian, 22} + }; + + [Test, TestCaseSource("FromIntCases")] + public void should_be_able_to_convert_int_to_languageTypes(int source, Language expected) + { + var language = (Language)source; + language.Should().Be(expected); + } + + [Test, TestCaseSource("ToIntCases")] + public void should_be_able_to_convert_languageTypes_to_int(Language source, int expected) + { + var i = (int)source; + i.Should().Be(expected); + } + + public static List GetDefaultLanguages(params Language[] allowed) + { + var languages = new List + { + Language.English, + Language.Spanish, + Language.French + }; + + if (allowed.Length == 0) + allowed = languages.ToArray(); + + var items = languages + .Except(allowed) + .Concat(allowed) + .Select(v => new ProfileLanguageItem { Language = v, Allowed = allowed.Contains(v) }).ToList(); + + return items; + } + } +} diff --git a/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs new file mode 100644 index 000000000..c262d1eeb --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageProfileRepositoryFixture.cs @@ -0,0 +1,32 @@ +using FluentAssertions; +using System.Linq; +using NUnit.Framework; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + public class LanguageProfileRepositoryFixture : DbTest + { + [Test] + public void should_be_able_to_read_and_write() + { + var profile = new LanguageProfile + { + Languages = Language.All.OrderByDescending(l => l.Name).Select(l => new ProfileLanguageItem {Language = l, Allowed = l == Language.English}).ToList(), + Name = "TestProfile", + Cutoff = Language.English + }; + + Subject.Insert(profile); + + + StoredModel.Name.Should().Be(profile.Name); + StoredModel.Cutoff.Should().Be(profile.Cutoff); + + StoredModel.Languages.Should().Equal(profile.Languages, (a, b) => a.Language == b.Language && a.Allowed == b.Allowed); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs new file mode 100644 index 000000000..873787d13 --- /dev/null +++ b/src/NzbDrone.Core.Test/Languages/LanguageProfileServiceFixture.cs @@ -0,0 +1,75 @@ +using System.Linq; +using FizzWare.NBuilder; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Test.Languages +{ + [TestFixture] + + public class LanguageProfileServiceFixture : CoreTest + { + [Test] + public void init_should_add_default_profiles() + { + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Once()); + } + + [Test] + //This confirms that new profiles are added only if no other profiles exists. + //We don't want to keep adding them back if a user deleted them on purpose. + public void Init_should_skip_if_any_profiles_already_exist() + { + Mocker.GetMock() + .Setup(s => s.All()) + .Returns(Builder.CreateListOfSize(2).Build().ToList()); + + Subject.Handle(new ApplicationStartedEvent()); + + Mocker.GetMock() + .Verify(v => v.Insert(It.IsAny()), Times.Never()); + } + + + [Test] + public void should_not_be_able_to_delete_profile_if_assigned_to_series() + { + var seriesList = Builder.CreateListOfSize(3) + .Random(1) + .With(c => c.LanguageProfileId = 2) + .Build().ToList(); + + + Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); + + Assert.Throws(() => Subject.Delete(2)); + + Mocker.GetMock().Verify(c => c.Delete(It.IsAny()), Times.Never()); + + } + + + [Test] + public void should_delete_profile_if_not_assigned_to_series() + { + var seriesList = Builder.CreateListOfSize(3) + .All() + .With(c => c.LanguageProfileId = 2) + .Build().ToList(); + + + Mocker.GetMock().Setup(c => c.GetAllSeries()).Returns(seriesList); + + Subject.Delete(1); + + Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs index 1245e0d12..e8775aaa6 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using FizzWare.NBuilder; @@ -12,11 +12,13 @@ using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles { @@ -36,6 +38,11 @@ namespace NzbDrone.Core.Test.MediaFiles var artist = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = Languages.LanguageFixture.GetDefaultLanguages() + }) .With(s => s.Path = @"C:\Test\Music\Alien Ant Farm".AsOsAgnostic()) .Build(); @@ -53,16 +60,16 @@ namespace NzbDrone.Core.Test.MediaFiles _approvedDecisions.Add(new ImportDecision ( new LocalTrack + { + Artist = artist, + Tracks = new List { track }, + Path = Path.Combine(artist.Path, "30 Rock - S01E01 - Pilot.avi"), + Quality = new QualityModel(Quality.MP3_256), + ParsedTrackInfo = new ParsedTrackInfo { - Artist = artist, - Tracks = new List { track }, - Path = Path.Combine(artist.Path, "30 Rock - S01E01 - Pilot.avi"), - Quality = new QualityModel(Quality.MP3_256), - ParsedTrackInfo = new ParsedTrackInfo - { - ReleaseGroup = "DRONE" - } - })); + ReleaseGroup = "DRONE" + } + })); } Mocker.GetMock() @@ -203,13 +210,13 @@ namespace NzbDrone.Core.Test.MediaFiles var sampleDecision = new ImportDecision (new LocalTrack - { - Artist = fileDecision.LocalTrack.Artist, - Tracks = new List { fileDecision.LocalTrack.Tracks.First() }, - Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(), - Quality = new QualityModel(Quality.MP3_256), - Size = 80.Megabytes() - }); + { + Artist = fileDecision.LocalTrack.Artist, + Tracks = new List { fileDecision.LocalTrack.Tracks.First() }, + Path = @"C:\Test\TV\30 Rock\30 Rock - S01E01 - Pilot.avi".AsOsAgnostic(), + Quality = new QualityModel(Quality.MP3_256), + Size = 80.Megabytes() + }); var all = new List(); diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs index 451c2cf45..c0fca112a 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/ImportDecisionMakerFixture.cs @@ -8,12 +8,14 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; using NzbDrone.Test.Common; using FizzWare.NBuilder; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles.TrackImport { @@ -54,6 +56,7 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport _artist = Builder.CreateNew() .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) + .With(e => e.LanguageProfile = new LanguageProfile { Languages = Languages.LanguageFixture.GetDefaultLanguages() }) .Build(); _quality = new QualityModel(Quality.MP3_256); @@ -62,15 +65,16 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport { Artist = _artist, Quality = _quality, + Language = Language.Spanish, Tracks = new List { new Track() }, - Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" + Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi" }; Mocker.GetMock() .Setup(c => c.GetLocalTrack(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_localTrack); - GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi".AsOsAgnostic() }); + GivenVideoFiles(new List { @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.Spanish.XviD-OSiTV.avi".AsOsAgnostic() }); } private void GivenSpecifications(params Mock[] mocks) @@ -179,6 +183,17 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport } [Test] + public void should_use_file_language_if_folder_language_is_null() + { + GivenSpecifications(_pass1, _pass2, _pass3); + var expectedLanguage = Parser.Parser.ParseLanguage(_audioFiles.Single()); + + var result = Subject.GetImportDecisions(_audioFiles, _artist); + + result.Single().LocalTrack.Language.Should().Be(expectedLanguage); + } + + [Test] public void should_use_file_quality_if_file_quality_was_determined_by_name() { GivenSpecifications(_pass1, _pass2, _pass3); @@ -223,6 +238,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport } [Test] + public void should_use_folder_language_when_greater_than_file_language() + { + GivenSpecifications(_pass1, _pass2, _pass3); + GivenVideoFiles(new string[] { @"C:\Test\Unsorted\The.Office.S03E115.Spanish.mkv".AsOsAgnostic() }); + + _localTrack.Path = _audioFiles.Single(); + _localTrack.Quality.Quality = Quality.MP3_320; + _localTrack.Language = Language.Spanish; + + var expectedLanguage = Language.French; + + var result = Subject.GetImportDecisions(_audioFiles, _artist, new ParsedTrackInfo { Language = expectedLanguage, Quality = new QualityModel(Quality.MP3_192) }); + + result.Single().LocalTrack.Language.Should().Be(expectedLanguage); + } + +[Test] public void should_not_throw_if_episodes_are_not_found() { GivenSpecifications(_pass1); diff --git a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs index fc21f0b62..22158edf6 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/TrackImport/Specifications/UpgradeSpecificationFixture.cs @@ -6,10 +6,12 @@ using NUnit.Framework; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.TrackImport.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications { @@ -23,15 +25,23 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications public void Setup() { _artist = Builder.CreateNew() - .With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() }) - .Build(); + .With(e => e.Profile = new Profile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }) + .With(l => l.LanguageProfile = new LanguageProfile + { + Languages = Languages.LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish, + }).Build(); _localTrack = new LocalTrack - { - Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", - Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), - Artist = _artist - }; + { + Path = @"C:\Test\Imagine Dragons\Imagine.Dragons.Song.1.mp3", + Quality = new QualityModel(Quality.MP3_256, new Revision(version: 1)), + Language = Language.Spanish, + Artist = _artist + }; } [Test] @@ -77,6 +87,24 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); } + [Test] + public void should_return_false_if_language_upgrade_for_existing_trackFile_and_quality_is_worse() + { + _localTrack.Tracks = Builder.CreateListOfSize(1) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + [Test] public void should_return_true_if_upgrade_for_existing_trackFile_for_multi_tracks() { @@ -94,6 +122,42 @@ namespace NzbDrone.Core.Test.MediaFiles.TrackImport.Specifications Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); } + [Test] + public void should_return_true_if_language_upgrade_for_existing_trackFile_for_multi_tracks_and_quality_is_same() + { + _localTrack.Tracks = Builder.CreateListOfSize(2) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.MP3_320, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_if_language_upgrade_for_existing_trackFile_for_multi_tracks_and_quality_is_worse() + { + _localTrack.Tracks = Builder.CreateListOfSize(2) + .All() + .With(e => e.TrackFileId = 1) + .With(e => e.TrackFile = new LazyLoaded( + new TrackFile + { + Quality = new QualityModel(Quality.FLAC, new Revision(version: 1)), + Language = Language.English + })) + .Build() + .ToList(); + + Subject.IsSatisfiedBy(_localTrack).Accepted.Should().BeFalse(); + } + [Test] public void should_return_false_if_not_an_upgrade_for_existing_trackFile() { diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index d296761bb..10ee75fc3 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -275,6 +275,9 @@ + + + diff --git a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs index c86948b17..03a98c2d2 100644 --- a/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs +++ b/src/NzbDrone.Core.Test/ParserTests/LanguageParserFixture.cs @@ -1,5 +1,6 @@ using FluentAssertions; using NUnit.Framework; +using NzbDrone.Core.Languages; using NzbDrone.Core.Parser; using NzbDrone.Core.Test.Framework; @@ -9,58 +10,207 @@ namespace NzbDrone.Core.Test.ParserTests [TestFixture] public class LanguageParserFixture : CoreTest { - [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL", Language.French)] - [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL", Language.Spanish)] - [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL", Language.German)] - [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL", Language.English)] - [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL", Language.Italian)] - [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL", Language.Danish)] - [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL", Language.Dutch)] - [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL", Language.Japanese)] - [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL", Language.Cantonese)] - [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL", Language.Mandarin)] - [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL", Language.Korean)] - [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL", Language.Russian)] - [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL", Language.Polish)] - [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL", Language.Vietnamese)] - [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL", Language.Swedish)] - [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL", Language.Norwegian)] - [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL", Language.Finnish)] - [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL", Language.Turkish)] - [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL", Language.Portuguese)] - [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL", Language.English)] - [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip", Language.Italian)] - [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND", Language.Flemish)] - [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo", Language.Greek)] - [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP", Language.German)] - [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)", Language.Dutch)] - [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD", Language.French)] - [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike", Language.Russian)] - [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA", Language.English)] - [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv", Language.Norwegian)] - [TestCase("Extant.S01E01.VOSTFR.HDTV.x264-RiDERS", Language.French)] - [TestCase("Constantine.2014.S01E01.WEBRiP.H264.AAC.5.1-NL.SUBS", Language.Dutch)] - [TestCase("Elementary - S02E16 - Kampfhaehne - mkv - by Videomann", Language.German)] - [TestCase("Two.Greedy.Italians.S01E01.The.Family.720p.HDTV.x264-FTP", Language.English)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.HUNDUB-LOL", Language.Hungarian)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.ENG.HUN-LOL", Language.Hungarian)] - [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL", Language.Hungarian)] - public void should_parse_language(string postTitle, Language language) + [TestCase("Castle.2009.S01E14.English.HDTV.XviD-LOL")] + [TestCase("Castle.2009.S01E14.Germany.HDTV.XviD-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD-LOL")] + [TestCase("Two.Greedy.Italians.S01E01.The.Family.720p.HDTV.x264-FTP")] + [TestCase("The.Trip.To.Italy.S02E01.720p.HDTV.x264-TLA")] + [TestCase("2 Broke Girls - S01E01 - Pilot.en.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.English.sub")] + [TestCase("2 Broke Girls - S01E01 - Pilot.english.sub")] + public void should_parse_language_english(string postTitle) { var result = LanguageParser.ParseLanguage(postTitle); - result.Should().Be(language); + result.Should().Be(Language.English); } - [TestCase("2 Broke Girls - S01E01 - Pilot.en.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.eng.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.English.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.english.sub", Language.English)] - [TestCase("2 Broke Girls - S01E01 - Pilot.sub", Language.Unknown)] - public void should_parse_subtitle_language(string fileName, Language language) + [TestCase("2 Broke Girls - S01E01 - Pilot.sub")] + public void should_parse_subtitle_language_unknown(string fileName) { var result = LanguageParser.ParseSubtitleLanguage(fileName); - result.Should().Be(language); + result.Should().Be(Language.Unknown); + } + + [TestCase("Castle.2009.S01E14.French.HDTV.XviD-LOL")] + [TestCase("Extant.S01E01.VOSTFR.HDTV.x264-RiDERS")] + [TestCase("Shield,.The.1x13.Tueurs.De.Flics.FR.DVDRip.XviD")] + public void should_parse_language_french(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.French.Id); + } + + [TestCase("Castle.2009.S01E14.Spanish.HDTV.XviD-LOL")] + public void should_parse_language_spanish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Spanish.Id); + } + + [TestCase("Castle.2009.S01E14.German.HDTV.XviD-LOL")] + [TestCase("Burn.Notice.S04E15.Brotherly.Love.GERMAN.DUBBED.WS.WEBRiP.XviD.REPACK-TVP")] + [TestCase("Elementary - S02E16 - Kampfhaehne - mkv - by Videomann")] + public void should_parse_language_german(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.German.Id); + } + + [TestCase("Castle.2009.S01E14.Italian.HDTV.XviD-LOL")] + [TestCase("person.of.interest.1x19.ita.720p.bdmux.x264-novarip")] + public void should_parse_language_italian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Italian.Id); + } + + [TestCase("Castle.2009.S01E14.Danish.HDTV.XviD-LOL")] + public void should_parse_language_danish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Danish.Id); + } + + [TestCase("Castle.2009.S01E14.Dutch.HDTV.XviD-LOL")] + [TestCase("Constantine.2014.S01E01.WEBRiP.H264.AAC.5.1-NL.SUBS")] + [TestCase("Ray Donovan - S01E01.720p.HDtv.x264-Evolve (NLsub)")] + public void should_parse_language_dutch(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Dutch.Id); + } + + [TestCase("Castle.2009.S01E14.Japanese.HDTV.XviD-LOL")] + public void should_parse_language_japanese(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Japanese.Id); + } + + [TestCase("Castle.2009.S01E14.Cantonese.HDTV.XviD-LOL")] + public void should_parse_language_cantonese(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Cantonese.Id); + } + + [TestCase("Castle.2009.S01E14.Mandarin.HDTV.XviD-LOL")] + public void should_parse_language_mandarin(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Mandarin.Id); + } + + [TestCase("Castle.2009.S01E14.Korean.HDTV.XviD-LOL")] + public void should_parse_language_korean(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Korean.Id); + } + + [TestCase("Castle.2009.S01E14.Russian.HDTV.XviD-LOL")] + [TestCase("True.Detective.S01E01.1080p.WEB-DL.Rus.Eng.TVKlondike")] + public void should_parse_language_russian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Russian.Id); + } + + [TestCase("Castle.2009.S01E14.Polish.HDTV.XviD-LOL")] + public void should_parse_language_polish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Polish.Id); + } + + [TestCase("Castle.2009.S01E14.Vietnamese.HDTV.XviD-LOL")] + public void should_parse_language_vietnamese(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Vietnamese.Id); + } + + [TestCase("Castle.2009.S01E14.Swedish.HDTV.XviD-LOL")] + public void should_parse_language_swedish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Swedish.Id); + } + + [TestCase("Castle.2009.S01E14.Norwegian.HDTV.XviD-LOL")] + [TestCase("Revolution S01E03 No Quarter 2012 WEB-DL 720p Nordic-philipo mkv")] + public void should_parse_language_norwegian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Norwegian.Id); + } + + [TestCase("Castle.2009.S01E14.Finnish.HDTV.XviD-LOL")] + public void should_parse_language_finnish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Finnish.Id); + } + + [TestCase("Castle.2009.S01E14.Turkish.HDTV.XviD-LOL")] + public void should_parse_language_turkish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Turkish.Id); + } + + [TestCase("Castle.2009.S01E14.Portuguese.HDTV.XviD-LOL")] + public void should_parse_language_portuguese(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Portuguese.Id); + } + + [TestCase("Salamander.S01E01.FLEMISH.HDTV.x264-BRiGAND")] + public void should_parse_language_flemish(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Flemish.Id); + } + + [TestCase("H.Polukatoikia.S03E13.Greek.PDTV.XviD-Ouzo")] + public void should_parse_language_greek(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Greek.Id); + } + + [TestCase("Castle.2009.S01E14.HDTV.XviD.HUNDUB-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD.ENG.HUN-LOL")] + [TestCase("Castle.2009.S01E14.HDTV.XviD.HUN-LOL")] + public void should_parse_language_hungarian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Hungarian.Id); + } + + [TestCase("Avatar.The.Last.Airbender.S01-03.DVDRip.HebDub")] + public void should_parse_language_hebrew(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Hebrew.Id); + } + + + [TestCase("Prison.Break.S05E01.WEBRip.x264.AC3.LT.EN-CNN")] + public void should_parse_language_lithuanian(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Lithuanian.Id); + } + + + [TestCase("The.​Walking.​Dead.​S07E11.​WEB Rip.​XviD.​Louige-​CZ.​EN.​5.​1")] + public void should_parse_language_czech(string postTitle) + { + var result = Parser.Parser.ParseTitle(postTitle); + result.Language.Id.Should().Be(Language.Czech.Id); } } } diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs index 97f5c7259..1c754c0c7 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileRepositoryFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -29,4 +29,4 @@ namespace NzbDrone.Core.Test.Profiles } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs index 4f799fa7d..4b7060730 100644 --- a/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Profiles/ProfileServiceFixture.cs @@ -3,7 +3,7 @@ using FizzWare.NBuilder; using Moq; using NUnit.Framework; using NzbDrone.Core.Lifecycle; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -72,4 +72,4 @@ namespace NzbDrone.Core.Test.Profiles Mocker.GetMock().Verify(c => c.Delete(1), Times.Once()); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs index 9b0e3e1c0..ce04b4c20 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityFixture.cs @@ -1,8 +1,8 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs index c42d93b7d..49a728375 100644 --- a/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs +++ b/src/NzbDrone.Core.Test/Qualities/QualityModelComparerFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; diff --git a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs index 1759b5322..a597b0de0 100644 --- a/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/EpisodeRepositoryTests/EpisodesWhereCutoffUnmetFixture.cs @@ -1,15 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { @@ -20,29 +22,38 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests private Series _unmonitoredSeries; private PagingSpec _pagingSpec; private List _qualitiesBelowCutoff; + private List _languagesBelowCutoff; private List _unairedEpisodes; - + [SetUp] public void Setup() { - var profile = new Profile - { + var profile = new Profile + { Id = 1, Cutoff = Quality.MP3_256, - Items = new List - { + Items = new List + { new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_192 }, new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 }, new ProfileQualityItem { Allowed = true, Quality = Quality.FLAC } } }; + var langProfile = new LanguageProfile + { + Id = 1, + Languages = Languages.LanguageFixture.GetDefaultLanguages(), + Cutoff = Language.Spanish + }; + _monitoredSeries = Builder.CreateNew() .With(s => s.TvRageId = RandomNumber) .With(s => s.Runtime = 30) .With(s => s.Monitored = true) .With(s => s.TitleSlug = "Title3") - .With(s => s.Id = profile.Id) + .With(s => s.ProfileId = profile.Id) + .With(s => s.LanguageProfileId = langProfile.Id) .BuildNew(); _unmonitoredSeries = Builder.CreateNew() @@ -50,34 +61,52 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .With(s => s.Runtime = 30) .With(s => s.Monitored = false) .With(s => s.TitleSlug = "Title2") - .With(s => s.Id = profile.Id) + .With(s => s.ProfileId = profile.Id) + .With(s => s.LanguageProfileId = langProfile.Id) .BuildNew(); _monitoredSeries.Id = Db.Insert(_monitoredSeries).Id; _unmonitoredSeries.Id = Db.Insert(_unmonitoredSeries).Id; _pagingSpec = new PagingSpec - { - Page = 1, - PageSize = 10, - SortKey = "AirDate", - SortDirection = SortDirection.Ascending - }; + { + Page = 1, + PageSize = 10, + SortKey = "AirDate", + SortDirection = SortDirection.Ascending + }; _qualitiesBelowCutoff = new List { new QualitiesBelowCutoff(profile.Id, new[] {Quality.MP3_192.Id}) }; - var qualityMet = new TrackFile { RelativePath = "a", Quality = new QualityModel { Quality = Quality.MP3_256 } }; - var qualityUnmet = new TrackFile { RelativePath = "b", Quality = new QualityModel { Quality = Quality.MP3_192 } }; - var qualityRawHD = new TrackFile { RelativePath = "c", Quality = new QualityModel { Quality = Quality.FLAC } }; - + _languagesBelowCutoff = new List + + { + new LanguagesBelowCutoff(profile.Id, new[] { Language.English.Id }) + }; + + var qualityMetLanguageUnmet = new TrackFile { RelativePath = "a", Quality = new QualityModel { Quality = Quality.MP3_256 }, Language = Language.English }; + var qualityMetLanguageMet = new TrackFile { RelativePath = "b", Quality = new QualityModel { Quality = Quality.MP3_256 }, Language = Language.Spanish }; + var qualityMetLanguageExceed = new TrackFile { RelativePath = "c", Quality = new QualityModel { Quality = Quality.MP3_256 }, Language = Language.French }; + var qualityUnmetLanguageUnmet = new TrackFile { RelativePath = "d", Quality = new QualityModel { Quality = Quality.MP3_192 }, Language = Language.English }; + var qualityUnmetLanguageMet = new TrackFile { RelativePath = "e", Quality = new QualityModel { Quality = Quality.MP3_192 }, Language = Language.Spanish }; + var qualityUnmetLanguageExceed = new TrackFile { RelativePath = "f", Quality = new QualityModel { Quality = Quality.MP3_192 }, Language = Language.French }; + var qualityRawHDLanguageUnmet = new TrackFile { RelativePath = "g", Quality = new QualityModel { Quality = Quality.FLAC }, Language = Language.English }; + var qualityRawHDLanguageMet = new TrackFile { RelativePath = "h", Quality = new QualityModel { Quality = Quality.FLAC }, Language = Language.Spanish }; + var qualityRawHDLanguageExceed = new TrackFile { RelativePath = "i", Quality = new QualityModel { Quality = Quality.FLAC }, Language = Language.French }; MediaFileRepository fileRepository = Mocker.Resolve(); - qualityMet = fileRepository.Insert(qualityMet); - qualityUnmet = fileRepository.Insert(qualityUnmet); - qualityRawHD = fileRepository.Insert(qualityRawHD); + qualityMetLanguageUnmet = fileRepository.Insert(qualityMetLanguageUnmet); + qualityMetLanguageMet = fileRepository.Insert(qualityMetLanguageMet); + qualityMetLanguageExceed = fileRepository.Insert(qualityMetLanguageExceed); + qualityUnmetLanguageUnmet = fileRepository.Insert(qualityUnmetLanguageUnmet); + qualityUnmetLanguageMet = fileRepository.Insert(qualityUnmetLanguageMet); + qualityUnmetLanguageExceed = fileRepository.Insert(qualityUnmetLanguageExceed); + qualityRawHDLanguageUnmet = fileRepository.Insert(qualityRawHDLanguageUnmet); + qualityRawHDLanguageMet = fileRepository.Insert(qualityRawHDLanguageMet); + qualityRawHDLanguageExceed = fileRepository.Insert(qualityRawHDLanguageExceed); var monitoredSeriesEpisodes = Builder.CreateListOfSize(4) .All() @@ -85,12 +114,12 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .With(e => e.SeriesId = _monitoredSeries.Id) .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) + .With(e => e.EpisodeFileId = qualityUnmetLanguageUnmet.Id) .TheFirst(1) .With(e => e.Monitored = false) - .With(e => e.EpisodeFileId = qualityMet.Id) + .With(e => e.EpisodeFileId = qualityMetLanguageMet.Id) .TheNext(1) - .With(e => e.EpisodeFileId = qualityRawHD.Id) + .With(e => e.EpisodeFileId = qualityRawHDLanguageExceed.Id) .TheLast(1) .With(e => e.SeasonNumber = 0) .Build(); @@ -101,25 +130,25 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests .With(e => e.SeriesId = _unmonitoredSeries.Id) .With(e => e.AirDateUtc = DateTime.Now.AddDays(-5)) .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) + .With(e => e.EpisodeFileId = qualityRawHDLanguageUnmet.Id) .TheFirst(1) .With(e => e.Monitored = false) - .With(e => e.EpisodeFileId = qualityMet.Id) + .With(e => e.EpisodeFileId = qualityMetLanguageMet.Id) .TheLast(1) .With(e => e.SeasonNumber = 0) .Build(); - _unairedEpisodes = Builder.CreateListOfSize(1) + _unairedEpisodes = Builder.CreateListOfSize(1) .All() .With(e => e.Id = 0) .With(e => e.SeriesId = _monitoredSeries.Id) .With(e => e.AirDateUtc = DateTime.Now.AddDays(5)) .With(e => e.Monitored = true) - .With(e => e.EpisodeFileId = qualityUnmet.Id) + .With(e => e.EpisodeFileId = qualityUnmetLanguageUnmet.Id) .Build() .ToList(); - + Db.InsertMany(monitoredSeriesEpisodes); Db.InsertMany(unmonitoredSeriesEpisodes); } @@ -139,7 +168,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { GivenMonitoredFilterExpression(); - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, _languagesBelowCutoff, false); spec.Records.Should().HaveCount(1); spec.Records.Should().OnlyContain(e => e.EpisodeFile.Value.Quality.Quality == Quality.MP3_192); @@ -150,7 +179,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { GivenMonitoredFilterExpression(); - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, _languagesBelowCutoff, false); spec.Records.Should().HaveCount(1); spec.Records.Should().OnlyContain(e => e.Monitored); @@ -161,7 +190,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests { GivenMonitoredFilterExpression(); - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, _languagesBelowCutoff, false); spec.Records.Should().HaveCount(1); spec.Records.Should().OnlyContain(e => e.Series.Monitored); @@ -174,7 +203,7 @@ namespace NzbDrone.Core.Test.TvTests.EpisodeRepositoryTests GivenMonitoredFilterExpression(); - var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, false); + var spec = Subject.EpisodesWhereCutoffUnmet(_pagingSpec, _qualitiesBelowCutoff, _languagesBelowCutoff, false); spec.Records.Should().HaveCount(2); spec.Records.Should().OnlyContain(e => e.Series.Monitored); diff --git a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs index e0febe4bc..331cbfa99 100644 --- a/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs +++ b/src/NzbDrone.Core.Test/TvTests/SeriesRepositoryTests/SeriesRepositoryFixture.cs @@ -1,10 +1,13 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; +using System.Linq; namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests { @@ -16,25 +19,36 @@ namespace NzbDrone.Core.Test.TvTests.SeriesRepositoryTests public void should_lazyload_quality_profile() { var profile = new Profile - { - Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_256, Quality.MP3_192), + { + Items = Qualities.QualityFixture.GetDefaultQualities(Quality.MP3_320, Quality.MP3_256, Quality.MP3_192), + + Cutoff = Quality.MP3_320, + Name = "TestProfile" + }; + + var langProfile = new LanguageProfile + { + Name = "TestProfile", + Languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English), + Cutoff = Language.English + }; - Cutoff = Quality.MP3_320, - Name = "TestProfile" - }; Mocker.Resolve().Insert(profile); + Mocker.Resolve().Insert(langProfile); var series = Builder.CreateNew().BuildNew(); series.ProfileId = profile.Id; + series.LanguageProfileId = langProfile.Id; Subject.Insert(series); StoredModel.Profile.Should().NotBeNull(); + StoredModel.LanguageProfile.Should().NotBeNull(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs index 85b9b044c..32e77e266 100644 --- a/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs +++ b/src/NzbDrone.Core/Annotations/FieldDefinitionAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace NzbDrone.Core.Annotations { @@ -31,6 +31,7 @@ namespace NzbDrone.Core.Annotations Tag, Action, Url, - Captcha + Captcha, + OAuth } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs index ec291a43f..dfcb9f4d9 100644 --- a/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs +++ b/src/NzbDrone.Core/ArtistStats/AlbumStatistics.cs @@ -1,4 +1,4 @@ -using System; +using System; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ArtistStats @@ -9,6 +9,7 @@ namespace NzbDrone.Core.ArtistStats public int AlbumId { get; set; } public int TrackFileCount { get; set; } public int TrackCount { get; set; } + public int AvailableTrackCount { get; set; } public int TotalTrackCount { get; set; } public long SizeOnDisk { get; set; } } diff --git a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs index c67af6921..298ee3bdc 100644 --- a/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs +++ b/src/NzbDrone.Core/ArtistStats/ArtistStatisticsRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using NzbDrone.Core.Datastore; @@ -58,7 +58,8 @@ namespace NzbDrone.Core.ArtistStats (SELECT Tracks.ArtistId, Tracks.AlbumId, - SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TotalTrackCount, + COUNT(*) AS TotalTrackCount, + SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS AvailableTrackCount, SUM(CASE WHEN Monitored = 1 OR TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackCount, SUM(CASE WHEN TrackFileId > 0 THEN 1 ELSE 0 END) AS TrackFileCount FROM Tracks diff --git a/src/NzbDrone.Core/Backup/Backup.cs b/src/NzbDrone.Core/Backup/Backup.cs index a4505d991..4dafd4394 100644 --- a/src/NzbDrone.Core/Backup/Backup.cs +++ b/src/NzbDrone.Core/Backup/Backup.cs @@ -1,10 +1,10 @@ -using System; +using System; namespace NzbDrone.Core.Backup { public class Backup { - public string Path { get; set; } + public string Name { get; set; } public BackupType Type { get; set; } public DateTime Time { get; set; } } diff --git a/src/NzbDrone.Core/Backup/BackupService.cs b/src/NzbDrone.Core/Backup/BackupService.cs index 8cc89d87b..1ff5c29a7 100644 --- a/src/NzbDrone.Core/Backup/BackupService.cs +++ b/src/NzbDrone.Core/Backup/BackupService.cs @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Backup { backups.AddRange(GetBackupFiles(folder).Select(b => new Backup { - Path = Path.GetFileName(b), + Name = Path.GetFileName(b), Type = backupType, Time = _diskProvider.FileGetLastWrite(b) })); diff --git a/src/NzbDrone.Core/Blacklisting/Blacklist.cs b/src/NzbDrone.Core/Blacklisting/Blacklist.cs index 44fa2e74c..b635a2f67 100644 --- a/src/NzbDrone.Core/Blacklisting/Blacklist.cs +++ b/src/NzbDrone.Core/Blacklisting/Blacklist.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Indexers; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Blacklisting { @@ -21,5 +22,6 @@ namespace NzbDrone.Core.Blacklisting public string Indexer { get; set; } public string Message { get; set; } public string TorrentInfoHash { get; set; } + public Language Language { get; set; } } } diff --git a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs index 74efb3b1f..30e86d510 100644 --- a/src/NzbDrone.Core/Blacklisting/BlacklistService.cs +++ b/src/NzbDrone.Core/Blacklisting/BlacklistService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; @@ -138,8 +138,9 @@ namespace NzbDrone.Core.Blacklisting Indexer = message.Data.GetValueOrDefault("indexer"), Protocol = (DownloadProtocol)Convert.ToInt32(message.Data.GetValueOrDefault("protocol")), Message = message.Message, - TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash") - }; + TorrentInfoHash = message.Data.GetValueOrDefault("torrentInfoHash"), + Language = message.Language + }; _blacklistRepository.Insert(blacklist); } diff --git a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs index b2bf33526..ecf29d351 100644 --- a/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs +++ b/src/NzbDrone.Core/Datastore/Converters/GuidConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using Marr.Data.Converters; using Marr.Data.Mapping; diff --git a/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs new file mode 100644 index 000000000..0d71b1b72 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/LanguageIntConverter.cs @@ -0,0 +1,65 @@ +using System; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using Newtonsoft.Json; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class LanguageIntConverter : JsonConverter, IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return Language.Unknown; + } + + var val = Convert.ToInt32(context.DbValue); + + return (Language)val; + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == DBNull.Value) return 0; + + if (clrValue as Language == null) + { + throw new InvalidOperationException("Attempted to save a language that isn't really a language"); + } + + var language = clrValue as Language; + return (int)language; + } + + public Type DbType + { + get + { + return typeof(int); + } + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Language); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var item = reader.Value; + return (Language)Convert.ToInt32(item); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(ToDB(value)); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs index 37d94e33d..a84e661a8 100644 --- a/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs +++ b/src/NzbDrone.Core/Datastore/Migration/036_update_with_quality_converters.cs @@ -1,9 +1,9 @@ -using FluentMigrator; +using FluentMigrator; using NzbDrone.Core.Datastore.Migration.Framework; using System.Data; using System.Linq; using NzbDrone.Common.Serializer; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using System.Collections.Generic; using NzbDrone.Core.Datastore.Converters; diff --git a/src/NzbDrone.Core/Datastore/Migration/102_add_language_to_episodeFiles_history_and_blacklist.cs b/src/NzbDrone.Core/Datastore/Migration/102_add_language_to_episodeFiles_history_and_blacklist.cs new file mode 100644 index 000000000..39f46e333 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/102_add_language_to_episodeFiles_history_and_blacklist.cs @@ -0,0 +1,97 @@ +using System.Data; +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Languages; +using System; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(102)] + public class add_language_to_episodeFiles_history_and_blacklist : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("EpisodeFiles") + .AddColumn("Language").AsInt32().NotNullable().WithDefaultValue(0); + + Alter.Table("History") + .AddColumn("Language").AsInt32().NotNullable().WithDefaultValue(0); + + Alter.Table("Blacklist") + .AddColumn("Language").AsInt32().NotNullable().WithDefaultValue(0); + + Execute.WithConnection(UpdateLanguage); + } + + private void UpdateLanguage(IDbConnection conn, IDbTransaction tran) + { + var LanguageConverter = new EmbeddedDocumentConverter(new LanguageIntConverter()); + + using (IDbCommand getSeriesCmd = conn.CreateCommand()) + { + getSeriesCmd.Transaction = tran; + getSeriesCmd.CommandText = @"SELECT Id, ProfileId FROM Series"; + using (IDataReader seriesReader = getSeriesCmd.ExecuteReader()) + { + while (seriesReader.Read()) + { + var seriesId = seriesReader.GetInt32(0); + var seriesProfileId = seriesReader.GetInt32(1); + + using (IDbCommand getProfileCmd = conn.CreateCommand()) + { + getProfileCmd.Transaction = tran; + getProfileCmd.CommandText = "SELECT Language FROM Profiles WHERE Id = ?"; + getProfileCmd.AddParameter(seriesProfileId); + IDataReader profilesReader = getProfileCmd.ExecuteReader(); + while (profilesReader.Read()) + { + var episodeLanguage = Language.English.Id; + try + { + episodeLanguage = profilesReader.GetInt32(0); + } catch (InvalidCastException e) + { + _logger.Debug("Language field not found in Profiles, using English as default." + e.Message); + } + + var validJson = LanguageConverter.ToDB(Language.FindById(episodeLanguage)); + + using (IDbCommand updateEpisodeFilesCmd = conn.CreateCommand()) + { + updateEpisodeFilesCmd.Transaction = tran; + updateEpisodeFilesCmd.CommandText = "UPDATE EpisodeFiles SET Language = ? WHERE SeriesId = ?"; + updateEpisodeFilesCmd.AddParameter(validJson); + updateEpisodeFilesCmd.AddParameter(seriesId); + + updateEpisodeFilesCmd.ExecuteNonQuery(); + } + + using (IDbCommand updateHistoryCmd = conn.CreateCommand()) + { + updateHistoryCmd.Transaction = tran; + updateHistoryCmd.CommandText = "UPDATE History SET Language = ? WHERE SeriesId = ?"; + updateHistoryCmd.AddParameter(validJson); + updateHistoryCmd.AddParameter(seriesId); + + updateHistoryCmd.ExecuteNonQuery(); + } + + using (IDbCommand updateBlacklistCmd = conn.CreateCommand()) + { + updateBlacklistCmd.Transaction = tran; + updateBlacklistCmd.CommandText = "UPDATE Blacklist SET Language = ? WHERE SeriesId = ?"; + updateBlacklistCmd.AddParameter(validJson); + updateBlacklistCmd.AddParameter(seriesId); + + updateBlacklistCmd.ExecuteNonQuery(); + } + } + } + } + } + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/111_create_language_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/111_create_language_profiles.cs new file mode 100644 index 000000000..57ac6ac59 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/111_create_language_profiles.cs @@ -0,0 +1,175 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Converters; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Core.Languages; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(111)] + public class create_language_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("LanguageProfiles").WithColumn("Name").AsString().Unique() + .WithColumn("Languages").AsString() + .WithColumn("Cutoff").AsInt32(); + + Alter.Table("Series").AddColumn("LanguageProfileId").AsInt32().WithDefaultValue(1); + + Execute.WithConnection(InsertDefaultLanguages); + + Delete.Column("Language").FromTable("Profiles"); + } + + private void InsertDefaultLanguages(IDbConnection conn, IDbTransaction tran) + { + var profiles = GetLanguageProfiles(conn, tran); + var languageConverter = new EmbeddedDocumentConverter(new LanguageIntConverter()); + + foreach (var profile in profiles.OrderBy(p => p.Id)) + { + using (IDbCommand insertNewLanguageProfileCmd = conn.CreateCommand()) + { + var itemsJson = languageConverter.ToDB(profile.Languages); + insertNewLanguageProfileCmd.Transaction = tran; + insertNewLanguageProfileCmd.CommandText = "INSERT INTO LanguageProfiles (Id, Name, Cutoff, Languages) VALUES (?, ?, ?, ?)"; + insertNewLanguageProfileCmd.AddParameter(profile.Id); + insertNewLanguageProfileCmd.AddParameter(profile.Name); + insertNewLanguageProfileCmd.AddParameter(profile.Cutoff.Id); + insertNewLanguageProfileCmd.AddParameter(itemsJson); + + insertNewLanguageProfileCmd.ExecuteNonQuery(); + } + + using (IDbCommand updateSeriesCmd = conn.CreateCommand()) + { + foreach (var profileId in profile.ProfileIds) + { + updateSeriesCmd.Transaction = tran; + updateSeriesCmd.CommandText = "UPDATE Series SET LanguageProfileId = ? WHERE ProfileId = ?"; + updateSeriesCmd.AddParameter(profile.Id); + updateSeriesCmd.AddParameter(profileId); + updateSeriesCmd.ExecuteNonQuery(); + } + } + } + } + + private List GetDefaultLanguageProfiles() + { + var profiles = new List(); + + var languages = GetOrderedLanguages().Select(v => new ProfileLanguageItem { Language = v, Allowed = v == Language.English }) + .ToList(); + + profiles.Add(new LanguageProfile111 + { + Id = 1, + Name = "English", + Cutoff = Language.English, + Languages = languages + }); + + return profiles; + + } + + private List GetLanguageProfiles(IDbConnection conn, IDbTransaction tran) + { + var profiles = GetDefaultLanguageProfiles(); + var thereAreProfiles = false; + + using (IDbCommand getProfilesCmd = conn.CreateCommand()) + { + getProfilesCmd.Transaction = tran; + getProfilesCmd.CommandText = @"SELECT Id, Language FROM Profiles"; + + using (IDataReader profileReader = getProfilesCmd.ExecuteReader()) + { + while (profileReader.Read()) + { + thereAreProfiles = true; + var profileId = profileReader.GetInt32(0); + var lang = Language.English.Id; + + try + { + lang = profileReader.GetInt32(1); + } + catch (InvalidCastException e) + { + _logger.Debug("Language field not found in Profiles, using English as default." + e.Message); + } + + if (profiles.None(p => p.Cutoff.Id == lang)) + { + var language = Language.FindById(lang); + var languages = GetOrderedLanguages().Select(l => new ProfileLanguageItem { Language = l, Allowed = l.Id == lang }) + .ToList(); + + profiles.Add(new LanguageProfile111 + { + Id = profiles.Count + 1, + Name = language.Name, + Cutoff = language, + Languages = languages, + ProfileIds = new List { profileId } + }); + } + else + { + profiles = profiles.Select(p => + { + if (p.Cutoff.Id == lang) + { + p.ProfileIds.Add(profileId); + } + + return p; + }).ToList(); + } + } + } + } + + if (!thereAreProfiles) + { + return new List(); + } + + return profiles; + } + + private List GetOrderedLanguages() + { + var orderedLanguages = Language.All + .Where(l => l != Language.Unknown) + .OrderByDescending(l => l.Name) + .ToList(); + + orderedLanguages.Insert(0, Language.Unknown); + + return orderedLanguages; + } + + private class LanguageProfile111 + { + public int Id { get; set; } + public List ProfileIds { get; set; } + public string Name { get; set; } + public Language Cutoff { get; set; } + public List Languages { get; set; } + + public LanguageProfile111 () + { + ProfileIds = new List(); + } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs deleted file mode 100644 index 117412062..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/111_setup_music.cs +++ /dev/null @@ -1,106 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(111)] - public class setup_music : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Create.TableForModel("Artists") - .WithColumn("ForeignArtistId").AsString().Unique() - .WithColumn("MBId").AsString().Nullable() - .WithColumn("AMId").AsString().Nullable() - .WithColumn("TADBId").AsInt32().Nullable() - .WithColumn("DiscogsId").AsInt32().Nullable() - .WithColumn("Name").AsString() - .WithColumn("NameSlug").AsString().Nullable().Unique() - .WithColumn("CleanName").AsString().Indexed() - .WithColumn("Status").AsInt32() - .WithColumn("Overview").AsString().Nullable() - .WithColumn("Images").AsString() - .WithColumn("Path").AsString().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("AlbumFolder").AsBoolean() - .WithColumn("LastInfoSync").AsDateTime().Nullable() - .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("DateFormed").AsDateTime().Nullable() - .WithColumn("Members").AsString().Nullable() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Genres").AsString().Nullable() - .WithColumn("SortName").AsString().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() - .WithColumn("Tags").AsString().Nullable() - .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("AddOptions").AsString().Nullable(); - - Create.TableForModel("Albums") - .WithColumn("ForeignAlbumId").AsString().Unique() - .WithColumn("ArtistId").AsInt32() - .WithColumn("MBId").AsString().Nullable().Indexed() - .WithColumn("AMId").AsString().Nullable() - .WithColumn("TADBId").AsInt32().Nullable().Indexed() - .WithColumn("DiscogsId").AsInt32().Nullable() - .WithColumn("Title").AsString() - .WithColumn("TitleSlug").AsString().Nullable().Unique() - .WithColumn("CleanTitle").AsString().Indexed() - .WithColumn("Overview").AsString().Nullable() - .WithColumn("Images").AsString() - .WithColumn("Path").AsString().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("LastInfoSync").AsDateTime().Nullable() - .WithColumn("LastDiskSync").AsDateTime().Nullable() - .WithColumn("ReleaseDate").AsDateTime().Nullable() - .WithColumn("Ratings").AsString().Nullable() - .WithColumn("Genres").AsString().Nullable() - .WithColumn("Label").AsString().Nullable() - .WithColumn("SortTitle").AsString().Nullable() - .WithColumn("ProfileId").AsInt32().Nullable() - .WithColumn("Tags").AsString().Nullable() - .WithColumn("Added").AsDateTime().Nullable() - .WithColumn("AlbumType").AsString() - .WithColumn("AddOptions").AsString().Nullable(); - - Create.TableForModel("Tracks") - .WithColumn("ForeignTrackId").AsString().Unique() - .WithColumn("ArtistId").AsInt32().Indexed() - .WithColumn("AlbumId").AsInt32() - .WithColumn("MBId").AsString().Nullable().Indexed() - .WithColumn("TrackNumber").AsInt32() - .WithColumn("Title").AsString().Nullable() - .WithColumn("Explicit").AsBoolean() - .WithColumn("Compilation").AsBoolean() - .WithColumn("DiscNumber").AsInt32().Nullable() - .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() - .WithColumn("Monitored").AsBoolean() - .WithColumn("Ratings").AsString().Nullable(); - - Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() - .OnColumn("AlbumId").Ascending() - .OnColumn("TrackNumber").Ascending(); - - Create.TableForModel("TrackFiles") - .WithColumn("ArtistId").AsInt32().Indexed() - .WithColumn("AlbumId").AsInt32().Indexed() - .WithColumn("Quality").AsString() - .WithColumn("Size").AsInt64() - .WithColumn("DateAdded").AsDateTime() - .WithColumn("SceneName").AsString().Nullable() - .WithColumn("ReleaseGroup").AsString().Nullable() - .WithColumn("MediaInfo").AsString().Nullable() - .WithColumn("RelativePath").AsString().Nullable(); - - Alter.Table("NamingConfig") - .AddColumn("ArtistFolderFormat").AsString().Nullable() - .AddColumn("RenameTracks").AsBoolean().Nullable() - .AddColumn("StandardTrackFormat").AsString().Nullable() - .AddColumn("AlbumFolderFormat").AsString().Nullable(); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs b/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs deleted file mode 100644 index 48d287843..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/112_music_history.cs +++ /dev/null @@ -1,36 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(112)] - public class music_history : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("History") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("AlbumId").AsInt32().WithDefaultValue(0); - - Alter.Table("PendingReleases") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("ParsedAlbumInfo").AsString().WithDefaultValue(""); - - Alter.Table("Tracks") - .AddColumn("Duration").AsInt32().WithDefaultValue(0); - - Alter.Table("Albums") - .AddColumn("Duration").AsInt32().WithDefaultValue(0); - - Delete.Column("SeriesId").FromTable("History"); - Delete.Column("EpisodeId").FromTable("History"); - Delete.Column("SeriesId").FromTable("PendingReleases"); - Delete.Column("ParsedEpisodeInfo").FromTable("PendingReleases"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/112_setup_music.cs b/src/NzbDrone.Core/Datastore/Migration/112_setup_music.cs new file mode 100644 index 000000000..2e68c591e --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/112_setup_music.cs @@ -0,0 +1,108 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(112)] + public class setup_music : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("Artists") + .WithColumn("ForeignArtistId").AsString().Unique() + .WithColumn("MBId").AsString().Nullable() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable() + .WithColumn("DiscogsId").AsInt32().Nullable() + .WithColumn("Name").AsString() + .WithColumn("NameSlug").AsString().Nullable().Unique() + .WithColumn("CleanName").AsString().Indexed() + .WithColumn("Status").AsInt32() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("AlbumFolder").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("DateFormed").AsDateTime().Nullable() + .WithColumn("Members").AsString().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("SortName").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AddOptions").AsString().Nullable() + .WithColumn("LanguageProfileId").AsInt32().WithDefaultValue(1); + + Create.TableForModel("Albums") + .WithColumn("ForeignAlbumId").AsString().Unique() + .WithColumn("ArtistId").AsInt32() + .WithColumn("MBId").AsString().Nullable().Indexed() + .WithColumn("AMId").AsString().Nullable() + .WithColumn("TADBId").AsInt32().Nullable().Indexed() + .WithColumn("DiscogsId").AsInt32().Nullable() + .WithColumn("Title").AsString() + .WithColumn("TitleSlug").AsString().Nullable().Unique() + .WithColumn("CleanTitle").AsString().Indexed() + .WithColumn("Overview").AsString().Nullable() + .WithColumn("Images").AsString() + .WithColumn("Path").AsString().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("LastInfoSync").AsDateTime().Nullable() + .WithColumn("LastDiskSync").AsDateTime().Nullable() + .WithColumn("ReleaseDate").AsDateTime().Nullable() + .WithColumn("Ratings").AsString().Nullable() + .WithColumn("Genres").AsString().Nullable() + .WithColumn("Label").AsString().Nullable() + .WithColumn("SortTitle").AsString().Nullable() + .WithColumn("ProfileId").AsInt32().Nullable() + .WithColumn("Tags").AsString().Nullable() + .WithColumn("Added").AsDateTime().Nullable() + .WithColumn("AlbumType").AsString() + .WithColumn("AddOptions").AsString().Nullable(); + + Create.TableForModel("Tracks") + .WithColumn("ForeignTrackId").AsString().Unique() + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32() + .WithColumn("MBId").AsString().Nullable().Indexed() + .WithColumn("TrackNumber").AsInt32() + .WithColumn("Title").AsString().Nullable() + .WithColumn("Explicit").AsBoolean() + .WithColumn("Compilation").AsBoolean() + .WithColumn("DiscNumber").AsInt32().Nullable() + .WithColumn("TrackFileId").AsInt32().Nullable().Indexed() + .WithColumn("Monitored").AsBoolean() + .WithColumn("Ratings").AsString().Nullable(); + + Create.Index().OnTable("Tracks").OnColumn("ArtistId").Ascending() + .OnColumn("AlbumId").Ascending() + .OnColumn("TrackNumber").Ascending(); + + Create.TableForModel("TrackFiles") + .WithColumn("ArtistId").AsInt32().Indexed() + .WithColumn("AlbumId").AsInt32().Indexed() + .WithColumn("Quality").AsString() + .WithColumn("Size").AsInt64() + .WithColumn("DateAdded").AsDateTime() + .WithColumn("SceneName").AsString().Nullable() + .WithColumn("ReleaseGroup").AsString().Nullable() + .WithColumn("MediaInfo").AsString().Nullable() + .WithColumn("RelativePath").AsString().Nullable() + .WithColumn("Language").AsInt32().WithDefaultValue(0); + + Alter.Table("NamingConfig") + .AddColumn("ArtistFolderFormat").AsString().Nullable() + .AddColumn("RenameTracks").AsBoolean().Nullable() + .AddColumn("StandardTrackFormat").AsString().Nullable() + .AddColumn("AlbumFolderFormat").AsString().Nullable(); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs b/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs deleted file mode 100644 index 49c151f51..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/113_music_blacklist.cs +++ /dev/null @@ -1,24 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(113)] - public class music_blacklist : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Alter.Table("Blacklist") - .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) - .AddColumn("AlbumIds").AsString().WithDefaultValue(""); - - Delete.Column("SeriesId").FromTable("Blacklist"); - Delete.Column("EpisodeIds").FromTable("Blacklist"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/113_music_history.cs b/src/NzbDrone.Core/Datastore/Migration/113_music_history.cs new file mode 100644 index 000000000..81417304f --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/113_music_history.cs @@ -0,0 +1,36 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(113)] + public class music_history : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("History") + .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) + .AddColumn("AlbumId").AsInt32().WithDefaultValue(0); + + Alter.Table("PendingReleases") + .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) + .AddColumn("ParsedAlbumInfo").AsString().WithDefaultValue(""); + + Alter.Table("Tracks") + .AddColumn("Duration").AsInt32().WithDefaultValue(0); + + Alter.Table("Albums") + .AddColumn("Duration").AsInt32().WithDefaultValue(0); + + Delete.Column("SeriesId").FromTable("History"); + Delete.Column("EpisodeId").FromTable("History"); + Delete.Column("SeriesId").FromTable("PendingReleases"); + Delete.Column("ParsedEpisodeInfo").FromTable("PendingReleases"); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/114_music_blacklist.cs b/src/NzbDrone.Core/Datastore/Migration/114_music_blacklist.cs new file mode 100644 index 000000000..36a5deecf --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/114_music_blacklist.cs @@ -0,0 +1,24 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(114)] + public class music_blacklist : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("Blacklist") + .AddColumn("ArtistId").AsInt32().WithDefaultValue(0) + .AddColumn("AlbumIds").AsString().WithDefaultValue(""); + + Delete.Column("SeriesId").FromTable("Blacklist"); + Delete.Column("EpisodeIds").FromTable("Blacklist"); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs b/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs deleted file mode 100644 index 7d24e8626..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/114_remove_tv_naming.cs +++ /dev/null @@ -1,27 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(114)] - public class remove_tv_naming : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Delete.Column("RenameEpisodes").FromTable("NamingConfig"); - Delete.Column("StandardEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("DailyEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("AnimeEpisodeFormat").FromTable("NamingConfig"); - Delete.Column("SeasonFolderFormat").FromTable("NamingConfig"); - Delete.Column("SeriesFolderFormat").FromTable("NamingConfig"); - Delete.Column("MultiEpisodeStyle").FromTable("NamingConfig"); - - Execute.Sql("DELETE FROM Config WHERE [Key] = 'filedate'"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs b/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs deleted file mode 100644 index cb625ccdc..000000000 --- a/src/NzbDrone.Core/Datastore/Migration/115_change_drone_factory_variable_name.cs +++ /dev/null @@ -1,20 +0,0 @@ -using FluentMigrator; -using NzbDrone.Core.Datastore.Migration.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace NzbDrone.Core.Datastore.Migration -{ - [Migration(115)] - public class change_drone_factory_variable_name : NzbDroneMigrationBase - { - protected override void MainDbUpgrade() - { - Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesfolder'"); - Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesscaninterval'"); - } - - } -} diff --git a/src/NzbDrone.Core/Datastore/Migration/115_remove_tv_naming.cs b/src/NzbDrone.Core/Datastore/Migration/115_remove_tv_naming.cs new file mode 100644 index 000000000..84272f5b7 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/115_remove_tv_naming.cs @@ -0,0 +1,27 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(115)] + public class remove_tv_naming : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Delete.Column("RenameEpisodes").FromTable("NamingConfig"); + Delete.Column("StandardEpisodeFormat").FromTable("NamingConfig"); + Delete.Column("DailyEpisodeFormat").FromTable("NamingConfig"); + Delete.Column("AnimeEpisodeFormat").FromTable("NamingConfig"); + Delete.Column("SeasonFolderFormat").FromTable("NamingConfig"); + Delete.Column("SeriesFolderFormat").FromTable("NamingConfig"); + Delete.Column("MultiEpisodeStyle").FromTable("NamingConfig"); + + Execute.Sql("DELETE FROM Config WHERE [Key] = 'filedate'"); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/116_change_drone_factory_variable_name.cs b/src/NzbDrone.Core/Datastore/Migration/116_change_drone_factory_variable_name.cs new file mode 100644 index 000000000..a7cc52981 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/116_change_drone_factory_variable_name.cs @@ -0,0 +1,20 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(116)] + public class change_drone_factory_variable_name : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesfolder'"); + Execute.Sql("DELETE FROM Config WHERE [Key] = 'downloadedepisodesscaninterval'"); + } + + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs index 7ebac899c..a59bceb66 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Windows.Forms; using FluentMigrator; using NLog; using NzbDrone.Common.Instrumentation; diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs index 79a9eca45..a94ca0bc9 100644 --- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs +++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneSqliteProcessor.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Data; using System.Linq; using FluentMigrator; using FluentMigrator.Expressions; using FluentMigrator.Model; using FluentMigrator.Runner; +using FluentMigrator.Runner.Announcers; using FluentMigrator.Runner.Generators.SQLite; using FluentMigrator.Runner.Processors.SQLite; diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 37a3a7658..bc93a60dc 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using Marr.Data.Mapping; @@ -19,7 +19,7 @@ using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.Notifications; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Restrictions; using NzbDrone.Core.RootFolders; @@ -36,6 +36,8 @@ using NzbDrone.Core.Extras.Others; using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Datastore { @@ -55,7 +57,8 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.Enable) .Ignore(i => i.Protocol) .Ignore(i => i.SupportsRss) - .Ignore(i => i.SupportsSearch); + .Ignore(i => i.SupportsSearch) + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("Notifications") .Ignore(i => i.SupportsOnGrab) @@ -63,10 +66,12 @@ namespace NzbDrone.Core.Datastore .Ignore(i => i.SupportsOnUpgrade) .Ignore(i => i.SupportsOnRename); - Mapper.Entity().RegisterDefinition("Metadata"); + Mapper.Entity().RegisterDefinition("Metadata") + .Ignore(d => d.Tags); Mapper.Entity().RegisterDefinition("DownloadClients") - .Ignore(d => d.Protocol); + .Ignore(d => d.Protocol) + .Ignore(d => d.Tags); Mapper.Entity().RegisterModel("SceneMappings"); @@ -76,7 +81,9 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Series") .Ignore(s => s.RootFolderPath) .Relationship() - .HasOne(s => s.Profile, s => s.ProfileId); + .HasOne(s => s.Profile, s => s.ProfileId) + .HasOne(s => s.LanguageProfile, s => s.LanguageProfileId); + Mapper.Entity().RegisterModel("EpisodeFiles") .Ignore(f => f.Path) @@ -96,7 +103,8 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("Artists") .Ignore(s => s.RootFolderPath) .Relationship() - .HasOne(a => a.Profile, a => a.ProfileId); + .HasOne(a => a.Profile, a => a.ProfileId) + .HasOne(s => s.LanguageProfile, s => s.LanguageProfileId); Mapper.Entity().RegisterModel("Albums"); @@ -120,6 +128,7 @@ namespace NzbDrone.Core.Datastore .Ignore(d => d.Weight); Mapper.Entity().RegisterModel("Profiles"); + Mapper.Entity().RegisterModel("LanguageProfiles"); Mapper.Entity().RegisterModel("Logs"); Mapper.Entity().RegisterModel("NamingConfig"); Mapper.Entity().MapResultSet(); @@ -158,7 +167,9 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(Language), new LanguageIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new LanguageIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(HashSet), new EmbeddedDocumentConverter()); @@ -196,4 +207,4 @@ namespace NzbDrone.Core.Datastore } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index ab7020625..1410dfcc7 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Indexers; @@ -23,6 +23,7 @@ namespace NzbDrone.Core.DecisionEngine var comparers = new List { CompareQuality, + CompareLanguage, CompareProtocol, ComparePeersIfTorrent, CompareAlbumCount, @@ -45,7 +46,7 @@ namespace NzbDrone.Core.DecisionEngine private int CompareByReverse(TSubject left, TSubject right, Func funcValue) where TValue : IComparable { - return CompareBy(left, right, funcValue)*-1; + return CompareBy(left, right, funcValue) * -1; } private int CompareAll(params int[] comparers) @@ -60,6 +61,11 @@ namespace NzbDrone.Core.DecisionEngine CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.ParsedAlbumInfo.Quality.Revision.Version)); } + private int CompareLanguage(DownloadDecision x, DownloadDecision y) + { + return CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => remoteAlbum.Artist.LanguageProfile.Value.Languages.FindIndex(l => l.Language == remoteAlbum.ParsedAlbumInfo.Language)); + } + private int CompareProtocol(DownloadDecision x, DownloadDecision y) { var result = CompareBy(x.RemoteAlbum, y.RemoteAlbum, remoteAlbum => diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs index 469141328..710cdf2db 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs @@ -1,6 +1,7 @@ -using System.Linq; +using System.Linq; using System.Collections.Generic; using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.DecisionEngine { diff --git a/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs new file mode 100644 index 000000000..4f150279b --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/LanguageUpgradableSpecification.cs @@ -0,0 +1,75 @@ +using NLog; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.DecisionEngine +{ + public interface ILanguageUpgradableSpecification + { + bool IsUpgradable(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null); + bool CutoffNotMet(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null); + bool IsRevisionUpgrade(LanguageModel currentLanguage, LanguageModel newLanguage); + } + + public class LanguageUpgradableSpecification : ILanguageUpgradableSpecification + { + private readonly Logger _logger; + + public LanguageUpgradableSpecification(Logger logger) + { + _logger = logger; + } + + public bool IsUpgradable(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null) + { + if (newLanguage != null) + { + int compare = new LanguageModelComparer(profile).Compare(newLanguage, currentLanguage); + if (compare <= 0) + { + _logger.Debug("existing item has better or equal language. skipping"); + return false; + } + + if (IsRevisionUpgrade(currentLanguage, newLanguage)) + { + return true; + } + } + + return true; + } + + public bool CutoffNotMet(Profile profile, LanguageModel currentLanguage, LanguageModel newLanguage = null) + { + int compare = new LanguageModelComparer(profile).Compare(currentLanguage.Language, profile.Languages.Find(v => v.Allowed == true).Language); + + if (compare >= 0) + { + if (newLanguage != null && IsRevisionUpgrade(currentLanguage, newLanguage)) + { + return true; + } + + _logger.Debug("Existing item meets cut-off. skipping."); + return false; + } + + return true; + } + + public bool IsRevisionUpgrade(LanguageModel currentLanguage, LanguageModel newLanguage) + { + int compare = newLanguage.Revision.CompareTo(currentLanguage.Revision); + + if (currentLanguage.Language == newLanguage.Language && compare > 0) + { + _logger.Debug("New language is a better revision for existing quality"); + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs deleted file mode 100644 index 22c4824af..000000000 --- a/src/NzbDrone.Core/DecisionEngine/QualityUpgradableSpecification.cs +++ /dev/null @@ -1,72 +0,0 @@ -using NLog; -using NzbDrone.Core.Profiles; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.DecisionEngine -{ - public interface IQualityUpgradableSpecification - { - bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); - bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); - bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); - } - - public class QualityUpgradableSpecification : IQualityUpgradableSpecification - { - private readonly Logger _logger; - - public QualityUpgradableSpecification(Logger logger) - { - _logger = logger; - } - - public bool IsUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) - { - if (newQuality != null) - { - int compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); - if (compare <= 0) - { - return false; - } - - if (IsRevisionUpgrade(currentQuality, newQuality)) - { - return true; - } - } - - return true; - } - - public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) - { - var compare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); - - if (compare < 0) - { - return true; - } - - if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) - { - return true; - } - - return false; - - } - - public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) - { - var compare = newQuality.Revision.CompareTo(currentQuality.Revision); - - if (currentQuality.Quality == newQuality.Quality && compare > 0) - { - return true; - } - - return false; - } - } -} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index ff02eacdf..761b26482 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -9,13 +9,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { public class CutoffSpecification : IDecisionEngineSpecification { - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public CutoffSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, Logger logger, IMediaFileService mediaFileService) + public CutoffSpecification(UpgradableSpecification upgradableSpecification, Logger logger, IMediaFileService mediaFileService) { - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = upgradableSpecification; _logger = logger; _mediaFileService = mediaFileService; } @@ -33,9 +33,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - _logger.Debug("Comparing file quality with report. Existing file is {0}", lowestQuality); + _logger.Debug("Comparing file quality and language with report. Existing file is {0}", lowestQuality.Quality); - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, lowestQuality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.CutoffNotMet(subject.Artist.Profile, + subject.Artist.LanguageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality)) { _logger.Debug("Cutoff already met, rejecting."); return Decision.Reject("Existing file meets cutoff: {0}", subject.Artist.Profile.Value.Cutoff); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs index 3ee173b22..affc82b04 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/LanguageSpecification.cs @@ -1,4 +1,5 @@ -using NLog; +using NLog; +using System.Linq; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; @@ -17,11 +18,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public virtual Decision IsSatisfiedBy(RemoteAlbum subject, SearchCriteriaBase searchCriteria) { - var wantedLanguage = subject.Artist.Profile.Value.Language; + var wantedLanguage = subject.Artist.LanguageProfile.Value.Languages; + var _language = subject.ParsedAlbumInfo.Language; _logger.Debug("Checking if report meets language requirements. {0}", subject.ParsedAlbumInfo.Language); - if (subject.ParsedAlbumInfo.Language != wantedLanguage) + if (!wantedLanguage.Exists(v => v.Allowed && v.Language == _language)) { _logger.Debug("Report Language: {0} rejected because it is not wanted, wanted {1}", subject.ParsedAlbumInfo.Language, wantedLanguage); return Decision.Reject("{0} is wanted, but found {1}", wantedLanguage, subject.ParsedAlbumInfo.Language); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index 38464e36c..d139a9cf4 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -10,15 +10,15 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class QueueSpecification : IDecisionEngineSpecification { private readonly IQueueService _queueService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly Logger _logger; public QueueSpecification(IQueueService queueService, - QualityUpgradableSpecification qualityUpgradableSpecification, + UpgradableSpecification qualityUpgradableSpecification, Logger logger) { _queueService = queueService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _logger = logger; } @@ -36,14 +36,23 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); - if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, remoteAlbum.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.CutoffNotMet(subject.Artist.Profile, + subject.Artist.LanguageProfile, + remoteAlbum.ParsedAlbumInfo.Quality, + remoteAlbum.ParsedAlbumInfo.Language, + subject.ParsedAlbumInfo.Quality)) { return Decision.Reject("Quality for release in queue already meets cutoff: {0}", remoteAlbum.ParsedAlbumInfo.Quality); } _logger.Debug("Checking if release is higher quality than queued release. Queued quality is: {0}", remoteAlbum.ParsedAlbumInfo.Quality); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, remoteAlbum.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.IsUpgradable(subject.Artist.Profile, + subject.Artist.LanguageProfile, + remoteAlbum.ParsedAlbumInfo.Quality, + remoteAlbum.ParsedAlbumInfo.Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language)) { return Decision.Reject("Quality for release in queue is of equal or higher preference: {0}", remoteAlbum.ParsedAlbumInfo.Quality); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index ae9c64ec8..17484a814 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.IndexerSearch.Definitions; @@ -6,25 +6,26 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { public class DelaySpecification : IDecisionEngineSpecification { private readonly IPendingReleaseService _pendingReleaseService; - private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly IUpgradableSpecification _upgradableSpecification; private readonly IDelayProfileService _delayProfileService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; public DelaySpecification(IPendingReleaseService pendingReleaseService, - IQualityUpgradableSpecification qualityUpgradableSpecification, + IUpgradableSpecification qualityUpgradableSpecification, IDelayProfileService delayProfileService, IMediaFileService mediaFileService, Logger logger) { _pendingReleaseService = pendingReleaseService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _delayProfileService = delayProfileService; _mediaFileService = mediaFileService; _logger = logger; @@ -41,6 +42,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } var profile = subject.Artist.Profile.Value; + var languageProfile = subject.Artist.LanguageProfile.Value; var delayProfile = _delayProfileService.BestForTags(subject.Artist.Tags); var delay = delayProfile.GetProtocolDelay(subject.Release.DownloadProtocol); var isPreferredProtocol = subject.Release.DownloadProtocol == delayProfile.PreferredProtocol; @@ -52,6 +54,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync } var comparer = new QualityModelComparer(profile); + var comparerLanguage = new LanguageComparer(languageProfile); if (isPreferredProtocol) { @@ -62,17 +65,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (trackFiles.Any()) { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, lowestQuality, subject.ParsedAlbumInfo.Quality); - + var upgradable = _upgradableSpecification.IsUpgradable(profile, + languageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language); if (upgradable) { - var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(lowestQuality, subject.ParsedAlbumInfo.Quality); - - if (revisionUpgrade) - { - _logger.Debug("New quality is a better revision for existing quality, skipping delay"); - return Decision.Accept(); - } + _logger.Debug("New quality is a better revision for existing quality, skipping delay"); + return Decision.Accept(); } } } @@ -81,10 +83,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync // If quality meets or exceeds the best allowed quality in the profile accept it immediately var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality()); var isBestInProfile = comparer.Compare(subject.ParsedAlbumInfo.Quality, bestQualityInProfile) >= 0; + var isBestInProfileLanguage = comparerLanguage.Compare(subject.ParsedAlbumInfo.Language, languageProfile.LastAllowedLanguage()) >= 0; - if (isBestInProfile && isPreferredProtocol) + if (isBestInProfile && isBestInProfileLanguage && isPreferredProtocol) { - _logger.Debug("Quality is highest in profile for preferred protocol, will not delay"); + _logger.Debug("Quality and language is highest in profile for preferred protocol, will not delay"); return Decision.Accept(); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index d654cfd84..699214b11 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -11,17 +11,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync public class HistorySpecification : IDecisionEngineSpecification { private readonly IHistoryService _historyService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; private readonly Logger _logger; public HistorySpecification(IHistoryService historyService, - QualityUpgradableSpecification qualityUpgradableSpecification, + UpgradableSpecification qualityUpgradableSpecification, IConfigService configService, Logger logger) { _historyService = historyService; - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _configService = configService; _logger = logger; } @@ -47,8 +47,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) { var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12)); - var cutoffUnmet = _qualityUpgradableSpecification.CutoffNotMet(subject.Artist.Profile, mostRecent.Quality, subject.ParsedAlbumInfo.Quality); - var upgradeable = _qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, mostRecent.Quality, subject.ParsedAlbumInfo.Quality); + var cutoffUnmet = _upgradableSpecification.CutoffNotMet(subject.Artist.Profile, subject.Artist.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedAlbumInfo.Quality); + var upgradeable = _upgradableSpecification.IsUpgradable(subject.Artist.Profile, subject.Artist.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedAlbumInfo.Quality, subject.ParsedAlbumInfo.Language); if (!recent && cdhEnabled) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs index 09d9f4828..daf654b58 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/ProperSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.Configuration; @@ -10,12 +10,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { public class ProperSpecification : IDecisionEngineSpecification { - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _qualityUpgradableSpecification; private readonly IConfigService _configService; private readonly IMediaFileService _mediaFileService; private readonly Logger _logger; - public ProperSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IConfigService configService, IMediaFileService mediaFileService, Logger logger) + public ProperSpecification(UpgradableSpecification qualityUpgradableSpecification, IConfigService configService, IMediaFileService mediaFileService, Logger logger) { _qualityUpgradableSpecification = qualityUpgradableSpecification; _configService = configService; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index f5e75c7db..52ffda1e2 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; @@ -10,12 +10,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public class UpgradeDiskSpecification : IDecisionEngineSpecification { private readonly IMediaFileService _mediaFileService; - private readonly QualityUpgradableSpecification _qualityUpgradableSpecification; + private readonly UpgradableSpecification _upgradableSpecification; private readonly Logger _logger; - public UpgradeDiskSpecification(QualityUpgradableSpecification qualityUpgradableSpecification, IMediaFileService mediaFileService, Logger logger) + public UpgradeDiskSpecification(UpgradableSpecification qualityUpgradableSpecification, IMediaFileService mediaFileService, Logger logger) { - _qualityUpgradableSpecification = qualityUpgradableSpecification; + _upgradableSpecification = qualityUpgradableSpecification; _mediaFileService = mediaFileService; _logger = logger; } @@ -33,7 +33,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { var lowestQuality = trackFiles.Select(c => c.Quality).OrderBy(c => c.Quality.Id).First(); - if (!_qualityUpgradableSpecification.IsUpgradable(subject.Artist.Profile, lowestQuality, subject.ParsedAlbumInfo.Quality)) + if (!_upgradableSpecification.IsUpgradable(subject.Artist.Profile, + subject.Artist.LanguageProfile, + lowestQuality, + trackFiles[0].Language, + subject.ParsedAlbumInfo.Quality, + subject.ParsedAlbumInfo.Language)) { return Decision.Reject("Quality for existing file on disk is of equal or higher preference: {0}", lowestQuality); } diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs new file mode 100644 index 000000000..3c0cebf0d --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs @@ -0,0 +1,109 @@ +using NLog; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.DecisionEngine +{ + public interface IUpgradableSpecification + { + bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage); + bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null); + bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); + } + + public class UpgradableSpecification : IUpgradableSpecification + { + private readonly Logger _logger; + + public UpgradableSpecification(Logger logger) + { + _logger = logger; + } + + private bool IsLanguageUpgradable(LanguageProfile profile, Language currentLanguage, Language newLanguage = null) + { + if (newLanguage != null) + { + var compare = new LanguageComparer(profile).Compare(newLanguage, currentLanguage); + if (compare <= 0) + { + return false; + } + } + return true; + } + + private bool IsQualityUpgradable(Profile profile, QualityModel currentQuality, QualityModel newQuality = null) + { + if (newQuality != null) + { + var compare = new QualityModelComparer(profile).Compare(newQuality, currentQuality); + if (compare <= 0) + { + _logger.Debug("existing item has better quality. skipping"); + return false; + } + } + return true; + } + + + public bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage) + { + // If qualities are the same then check language + if (newQuality != null && currentQuality == newQuality) + { + return IsLanguageUpgradable(languageProfile, currentLanguage, newLanguage); + } + + // If quality is worse then always return false + if (!IsQualityUpgradable(profile, currentQuality, newQuality)) + { + _logger.Debug("existing item has better quality. skipping"); + return false; + } + + return true; + } + + public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null) + { + var languageCompare = new LanguageComparer(languageProfile).Compare(currentLanguage, languageProfile.Cutoff); + var qualityCompare = new QualityModelComparer(profile).Compare(currentQuality.Quality, profile.Cutoff); + + // If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language + if (languageCompare < 0) + { + return true; + } + + if (qualityCompare >= 0) + { + if (newQuality != null && IsRevisionUpgrade(currentQuality, newQuality)) + { + return true; + } + + _logger.Debug("Existing item meets cut-off. skipping."); + return false; + } + + return true; + } + + public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality) + { + var compare = newQuality.Revision.CompareTo(currentQuality.Revision); + + if (currentQuality.Quality == newQuality.Quality && compare > 0) + { + _logger.Debug("New quality is a better revision for existing quality"); + return true; + } + + return false; + } + } +} diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index 408839f88..5e2352a07 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -1,11 +1,10 @@ using System; -using System.IO; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Disk; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; using NzbDrone.Core.Tv; namespace NzbDrone.Core.DiskSpace @@ -18,51 +17,43 @@ namespace NzbDrone.Core.DiskSpace public class DiskSpaceService : IDiskSpaceService { private readonly ISeriesService _seriesService; - private readonly IConfigService _configService; private readonly IDiskProvider _diskProvider; private readonly Logger _logger; - public DiskSpaceService(ISeriesService seriesService, IConfigService configService, IDiskProvider diskProvider, Logger logger) + private static readonly Regex _regexSpecialDrive = new Regex("^/var/lib/(docker|rancher|kubelet)(/|$)|^/boot(/|$)|/docker(/var)?/aufs(/|$)", RegexOptions.Compiled); + + public DiskSpaceService(ISeriesService seriesService, IDiskProvider diskProvider, Logger logger) { _seriesService = seriesService; - _configService = configService; _diskProvider = diskProvider; _logger = logger; } public List GetFreeSpace() { - var diskSpace = new List(); - diskSpace.AddRange(GetSeriesFreeSpace()); - diskSpace.AddRange(GetDroneFactoryFreeSpace()); - diskSpace.AddRange(GetFixedDisksFreeSpace()); + var importantRootFolders = GetSeriesRootPaths().Distinct().ToList(); - return diskSpace.DistinctBy(d => d.Path).ToList(); + var optionalRootFolders = GetFixedDisksRootPaths().Except(importantRootFolders).Distinct().ToList(); + + var diskSpace = GetDiskSpace(importantRootFolders).Concat(GetDiskSpace(optionalRootFolders, true)).ToList(); + + return diskSpace; } - private IEnumerable GetSeriesFreeSpace() + private IEnumerable GetSeriesRootPaths() { - var seriesRootPaths = _seriesService.GetAllSeries() + return _seriesService.GetAllSeries() .Where(s => _diskProvider.FolderExists(s.Path)) .Select(s => _diskProvider.GetPathRoot(s.Path)) .Distinct(); - - return GetDiskSpace(seriesRootPaths); - } - - private IEnumerable GetDroneFactoryFreeSpace() - { - if (_configService.DownloadedAlbumsFolder.IsNotNullOrWhiteSpace() && _diskProvider.FolderExists(_configService.DownloadedAlbumsFolder)) - { - return GetDiskSpace(new[] { _diskProvider.GetPathRoot(_configService.DownloadedAlbumsFolder) }); - } - - return new List(); } - private IEnumerable GetFixedDisksFreeSpace() + private IEnumerable GetFixedDisksRootPaths() { - return GetDiskSpace(_diskProvider.GetMounts().Where(d => d.DriveType == DriveType.Fixed).Select(d => d.RootDirectory), true); + return _diskProvider.GetMounts() + .Where(d => d.DriveType == DriveType.Fixed) + .Where(d => !_regexSpecialDrive.IsMatch(d.RootDirectory)) + .Select(d => d.RootDirectory); } private IEnumerable GetDiskSpace(IEnumerable paths, bool suppressWarnings = false) diff --git a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs index 3cb2d6a8b..45a3c39c5 100644 --- a/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs +++ b/src/NzbDrone.Core/Download/Clients/rTorrent/RTorrentDirectoryValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using FluentValidation.Results; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.Clients.RTorrent; @@ -15,13 +15,11 @@ namespace NzbDrone.Core.Download.Clients.rTorrent { public RTorrentDirectoryValidator(RootFolderValidator rootFolderValidator, PathExistsValidator pathExistsValidator, - DroneFactoryValidator droneFactoryValidator, MappedNetworkDriveValidator mappedNetworkDriveValidator) { RuleFor(c => c.TvDirectory).Cascade(CascadeMode.StopOnFirstFailure) .IsValidPath() .SetValidator(rootFolderValidator) - .SetValidator(droneFactoryValidator) .SetValidator(mappedNetworkDriveValidator) .SetValidator(pathExistsValidator) .When(c => c.TvDirectory.IsNotNullOrWhiteSpace()) diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index 2e0533e50..e1a7f1467 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using NzbDrone.Common.Disk; @@ -21,6 +21,10 @@ namespace NzbDrone.Core.Download public DownloadItemStatus Status { get; set; } public bool IsEncrypted { get; set; } + + public bool CanMoveFiles { get; set; } + public bool CanBeRemoved { get; set; } + public bool IsReadOnly { get; set; } public bool Removed { get; set; } diff --git a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs index 680000d8f..c22dd9548 100644 --- a/src/NzbDrone.Core/Download/DownloadFailedEvent.cs +++ b/src/NzbDrone.Core/Download/DownloadFailedEvent.cs @@ -1,7 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.Messaging; using NzbDrone.Core.Download.TrackedDownloads; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Download { @@ -21,5 +22,6 @@ namespace NzbDrone.Core.Download public string Message { get; set; } public Dictionary Data { get; set; } public TrackedDownload TrackedDownload { get; set; } + public Language Language { get; set; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Download/FailedDownloadService.cs b/src/NzbDrone.Core/Download/FailedDownloadService.cs index 935401864..bc2fec122 100644 --- a/src/NzbDrone.Core/Download/FailedDownloadService.cs +++ b/src/NzbDrone.Core/Download/FailedDownloadService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Download.TrackedDownloads; @@ -95,7 +95,8 @@ namespace NzbDrone.Core.Download DownloadId = historyItem.DownloadId, Message = message, Data = historyItem.Data, - TrackedDownload = trackedDownload + TrackedDownload = trackedDownload, + Language = historyItem.Language }; _eventAggregator.PublishEvent(downloadFailedEvent); diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index d978b759d..4e05f5fea 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; @@ -145,8 +145,9 @@ namespace NzbDrone.Core.Download.Pending Timeleft = ect.Subtract(DateTime.UtcNow), EstimatedCompletionTime = ect, Status = "Pending", - Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol - }; + Protocol = pendingRelease.RemoteAlbum.Release.DownloadProtocol, + Indexer = pendingRelease.RemoteAlbum.Release.Indexer + }; queued.Add(queue); } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs index a760b71fe..674066245 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/DownloadMonitoringService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -13,7 +13,9 @@ namespace NzbDrone.Core.Download.TrackedDownloads { public class DownloadMonitoringService : IExecute, IHandle, - IHandle + IHandle, + IHandle + { private readonly IProvideDownloadClient _downloadClientProvider; private readonly IEventAggregator _eventAggregator; @@ -64,10 +66,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads { var clientTrackedDownloads = ProcessClientDownloads(downloadClient); - // Only track completed downloads if trackedDownloads.AddRange(clientTrackedDownloads.Where(DownloadIsTrackable)); } + _trackedDownloadService.UpdateTrackable(trackedDownloads); _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); } finally @@ -172,5 +174,12 @@ namespace NzbDrone.Core.Download.TrackedDownloads { _refreshDebounce.Execute(); } + + public void Handle(TrackedDownloadsRemovedEvent message) + { + var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList(); + + _eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads)); + } } } diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs index ac9ec462f..4436f1f04 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownload.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download.TrackedDownloads @@ -13,6 +13,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads public RemoteAlbum RemoteAlbum { get; set; } public TrackedDownloadStatusMessage[] StatusMessages { get; private set; } public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public bool IsTrackable { get; set; } public TrackedDownload() { diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs index b1e8189d4..434b67166 100644 --- a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadService.cs @@ -1,9 +1,11 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Cache; using NzbDrone.Common.Extensions; using NzbDrone.Core.History; +using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser; namespace NzbDrone.Core.Download.TrackedDownloads @@ -11,23 +13,30 @@ namespace NzbDrone.Core.Download.TrackedDownloads public interface ITrackedDownloadService { TrackedDownload Find(string downloadId); + void StopTracking(string downloadId); + void StopTracking(List downloadIds); TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem); + List GetTrackedDownloads(); + void UpdateTrackable(List trackedDownloads); } public class TrackedDownloadService : ITrackedDownloadService { private readonly IParsingService _parsingService; private readonly IHistoryService _historyService; + private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; private readonly ICached _cache; public TrackedDownloadService(IParsingService parsingService, - ICacheManager cacheManager, - IHistoryService historyService, - Logger logger) + ICacheManager cacheManager, + IHistoryService historyService, + IEventAggregator eventAggregator, + Logger logger) { _parsingService = parsingService; _historyService = historyService; + _eventAggregator = eventAggregator; _cache = cacheManager.GetCache(GetType()); _logger = logger; } @@ -37,13 +46,39 @@ namespace NzbDrone.Core.Download.TrackedDownloads return _cache.Find(downloadId); } + public void StopTracking(string downloadId) + { + var trackedDownload = _cache.Find(downloadId); + + _cache.Remove(downloadId); + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(new List { trackedDownload })); + } + + public void StopTracking(List downloadIds) + { + var trackedDownloads = new List(); + + foreach (var downloadId in downloadIds) + { + var trackedDownload = _cache.Find(downloadId); + _cache.Remove(downloadId); + trackedDownloads.Add(trackedDownload); + } + + _eventAggregator.PublishEvent(new TrackedDownloadsRemovedEvent(trackedDownloads)); + } + public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, DownloadClientItem downloadItem) { var existingItem = Find(downloadItem.DownloadId); if (existingItem != null && existingItem.State != TrackedDownloadStage.Downloading) { + LogItemChange(existingItem, existingItem.DownloadItem, downloadItem); + existingItem.DownloadItem = downloadItem; + existingItem.IsTrackable = true; + return existingItem; } @@ -51,7 +86,8 @@ namespace NzbDrone.Core.Download.TrackedDownloads { DownloadClient = downloadClient.Id, DownloadItem = downloadItem, - Protocol = downloadClient.Protocol + Protocol = downloadClient.Protocol, + IsTrackable = true }; try @@ -69,6 +105,10 @@ namespace NzbDrone.Core.Download.TrackedDownloads var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).First(); trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType); + var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed); + trackedDownload.Indexer = grabbedEvent?.Data["indexer"]; + + if (parsedAlbumInfo == null || trackedDownload.RemoteAlbum == null || trackedDownload.RemoteAlbum.Artist == null || @@ -87,6 +127,7 @@ namespace NzbDrone.Core.Download.TrackedDownloads if (trackedDownload.RemoteAlbum == null) { + _logger.Trace("No Album found for download '{0}', not tracking.", trackedDownload.DownloadItem.Title); return null; } } @@ -96,10 +137,45 @@ namespace NzbDrone.Core.Download.TrackedDownloads return null; } + LogItemChange(trackedDownload, existingItem?.DownloadItem, trackedDownload.DownloadItem); + _cache.Set(trackedDownload.DownloadItem.DownloadId, trackedDownload); return trackedDownload; } + public List GetTrackedDownloads() + { + return _cache.Values.ToList(); + } + + public void UpdateTrackable(List trackedDownloads) + { + var untrackable = GetTrackedDownloads().ExceptBy(t => t.DownloadItem.DownloadId, trackedDownloads, t => t.DownloadItem.DownloadId, StringComparer.CurrentCulture).ToList(); + + foreach (var trackedDownload in untrackable) + { + trackedDownload.IsTrackable = false; + } + } + + private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem existingItem, DownloadClientItem downloadItem) + { + if (existingItem == null || + existingItem.Status != downloadItem.Status || + existingItem.CanBeRemoved != downloadItem.CanBeRemoved || + existingItem.CanMoveFiles != downloadItem.CanMoveFiles) + { + _logger.Debug("Tracking '{0}:{1}': ClientState={2}{3} SonarrStage={4} Episode='{5}' OutputPath={6}.", + downloadItem.DownloadClient, downloadItem.Title, + downloadItem.Status, downloadItem.CanBeRemoved ? "" : + downloadItem.CanMoveFiles ? " (busy)" : " (readonly)", + trackedDownload.State, + trackedDownload.RemoteAlbum?.ParsedAlbumInfo, + downloadItem.OutputPath); + } + } + + private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType) { switch (eventType) diff --git a/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs new file mode 100644 index 000000000..76c926d5a --- /dev/null +++ b/src/NzbDrone.Core/Download/TrackedDownloads/TrackedDownloadsRemovedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Download.TrackedDownloads +{ + public class TrackedDownloadsRemovedEvent : IEvent + { + public List TrackedDownloads { get; private set; } + + public TrackedDownloadsRemovedEvent(List trackedDownloads) + { + TrackedDownloads = trackedDownloads; + } + } +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs index 11899124f..91f680f9d 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/MediaBrowser/MediaBrowserMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -21,7 +21,7 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.MediaBrowser SeriesMetadata = true; } - [FieldDefinition(0, Label = "Series Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Artist Metadata", Type = FieldType.Checkbox, HelpText = "series.xml")] public bool SeriesMetadata { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs index f0da481bf..569a7922c 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Roksbox/RoksboxMetadataSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -24,16 +24,16 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Roksbox EpisodeImages = true; } - [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox)] + [FieldDefinition(0, Label = "Episode Metadata", Type = FieldType.Checkbox, HelpText = "Season##\\filename.xml")] public bool EpisodeMetadata { get; set; } - [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox)] + [FieldDefinition(1, Label = "Series Images", Type = FieldType.Checkbox, HelpText = "Series Title.jpg")] public bool SeriesImages { get; set; } - [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox)] + [FieldDefinition(2, Label = "Season Images", Type = FieldType.Checkbox, HelpText = "Season ##.jpg")] public bool SeasonImages { get; set; } - [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox)] + [FieldDefinition(3, Label = "Episode Images", Type = FieldType.Checkbox, HelpText = "Season##\\filename.jpg")] public bool EpisodeImages { get; set; } public bool IsValid => true; diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs index 0ccd3ede6..7263b4c64 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleFile.cs @@ -1,5 +1,5 @@ -using NzbDrone.Core.Extras.Files; -using NzbDrone.Core.Parser; +using NzbDrone.Core.Extras.Files; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Extras.Subtitles { diff --git a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs index 52910a285..4bdc25638 100644 --- a/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs +++ b/src/NzbDrone.Core/Extras/Subtitles/SubtitleService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -10,6 +10,7 @@ using NzbDrone.Core.Extras.Files; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Parser; using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Extras.Subtitles { diff --git a/src/NzbDrone.Core/History/History.cs b/src/NzbDrone.Core/History/History.cs index 5c017a71c..8cbced081 100644 --- a/src/NzbDrone.Core/History/History.cs +++ b/src/NzbDrone.Core/History/History.cs @@ -1,8 +1,9 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.History { @@ -24,6 +25,7 @@ namespace NzbDrone.Core.History public Artist Artist { get; set; } public HistoryEventType EventType { get; set; } public Dictionary Data { get; set; } + public Language Language { get; set; } public string DownloadId { get; set; } @@ -38,4 +40,4 @@ namespace NzbDrone.Core.History DownloadFailed = 4, EpisodeFileDeleted = 5 } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/History/HistoryRepository.cs b/src/NzbDrone.Core/History/HistoryRepository.cs index 68cd40ae7..47346f003 100644 --- a/src/NzbDrone.Core/History/HistoryRepository.cs +++ b/src/NzbDrone.Core/History/HistoryRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; @@ -10,7 +10,6 @@ namespace NzbDrone.Core.History { public interface IHistoryRepository : IBasicRepository { - List GetBestQualityInHistory(int albumId); History MostRecentForAlbum(int albumId); History MostRecentForDownloadId(string downloadId); List FindByDownloadId(string downloadId); @@ -27,13 +26,6 @@ namespace NzbDrone.Core.History } - public List GetBestQualityInHistory(int albumId) - { - var history = Query.Where(c => c.AlbumId == albumId); - - return history.Select(h => h.Quality).ToList(); - } - public History MostRecentForAlbum(int albumId) { return Query.Where(h => h.AlbumId == albumId) @@ -77,4 +69,4 @@ namespace NzbDrone.Core.History return base.GetPagedQuery(baseQuery, pagingSpec); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/History/HistoryService.cs b/src/NzbDrone.Core/History/HistoryService.cs index 2f5707346..9a52b859f 100644 --- a/src/NzbDrone.Core/History/HistoryService.cs +++ b/src/NzbDrone.Core/History/HistoryService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -10,7 +10,9 @@ using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; using NzbDrone.Core.Music.Events; @@ -18,7 +20,6 @@ namespace NzbDrone.Core.History { public interface IHistoryService { - QualityModel GetBestQualityInHistory(Profile profile, int episodeId); PagingSpec Paged(PagingSpec pagingSpec); History MostRecentForAlbum(int episodeId); History MostRecentForDownloadId(string downloadId); @@ -73,14 +74,6 @@ namespace NzbDrone.Core.History return _historyRepository.FindByDownloadId(downloadId); } - public QualityModel GetBestQualityInHistory(Profile profile, int albumId) - { - var comparer = new QualityModelComparer(profile); - return _historyRepository.GetBestQualityInHistory(albumId) - .OrderByDescending(q => q, comparer) - .FirstOrDefault(); - } - [Obsolete("Used for Sonarr, not Lidarr")] private string FindDownloadId(EpisodeImportedEvent trackedDownload) { @@ -139,7 +132,8 @@ namespace NzbDrone.Core.History SourceTitle = message.Album.Release.Title, ArtistId = album.ArtistId, AlbumId = album.Id, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + Language = message.Album.ParsedAlbumInfo.Language }; history.Data.Add("Indexer", message.Album.Release.Indexer); @@ -196,8 +190,9 @@ namespace NzbDrone.Core.History SourceTitle = message.ImportedEpisode.SceneName ?? Path.GetFileNameWithoutExtension(message.EpisodeInfo.Path), ArtistId = message.ImportedEpisode.SeriesId, AlbumId = episode.Id, - DownloadId = downloadId - }; + DownloadId = downloadId, + Language = message.EpisodeInfo.Language + }; //Won't have a value since we publish this event before saving to DB. //history.Data.Add("FileId", message.ImportedEpisode.Id.ToString()); @@ -221,7 +216,8 @@ namespace NzbDrone.Core.History SourceTitle = message.SourceTitle, ArtistId = message.ArtistId, AlbumId = albumId, - DownloadId = message.DownloadId + DownloadId = message.DownloadId, + Language = message.Language }; history.Data.Add("DownloadClient", message.DownloadClient); @@ -263,4 +259,4 @@ namespace NzbDrone.Core.History _historyRepository.DeleteForArtist(message.Artist.Id); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs index 70a5be751..3adee9768 100644 --- a/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs +++ b/src/NzbDrone.Core/IndexerSearch/AlbumSearchService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Download; using NzbDrone.Core.Messaging.Commands; @@ -6,6 +6,8 @@ using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.IndexerSearch { class AlbumSearchService : IExecute + //IExecute, + //IExecute { private readonly ISearchForNzb _nzbSearchService; private readonly IProcessDownloadDecisions _processDownloadDecisions; diff --git a/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs new file mode 100644 index 000000000..65173f037 --- /dev/null +++ b/src/NzbDrone.Core/IndexerSearch/CutoffUnmetAlbumSearchCommand.cs @@ -0,0 +1,26 @@ +using NzbDrone.Core.Messaging.Commands; + +namespace NzbDrone.Core.IndexerSearch +{ + public class CutoffUnmetAlbumSearchCommand : Command + { + public int? SeriesId { get; set; } + + public override bool SendUpdatesToClient + { + get + { + return true; + } + } + + public CutoffUnmetAlbumSearchCommand() + { + } + + public CutoffUnmetAlbumSearchCommand(int seriesId) + { + SeriesId = seriesId; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs index e53bea79a..1fd197da6 100644 --- a/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs +++ b/src/NzbDrone.Core/Instrumentation/ReconfigureLogging.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NLog.Config; @@ -24,7 +24,7 @@ namespace NzbDrone.Core.Instrumentation var rules = LogManager.Configuration.LoggingRules; //Console - SetMinimumLogLevel(rules, "consoleLogger", minimumLogLevel); + SetMinimumLogLevel(rules, "consoleLogger", LogLevel.Trace); //Log Files SetMinimumLogLevel(rules, "appFileInfo", minimumLogLevel <= LogLevel.Info ? LogLevel.Info : LogLevel.Off); diff --git a/src/NzbDrone.Core/Languages/Language.cs b/src/NzbDrone.Core/Languages/Language.cs new file mode 100644 index 000000000..bb252ce07 --- /dev/null +++ b/src/NzbDrone.Core/Languages/Language.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Languages +{ + public class Language : IEmbeddedDocument, IEquatable + { + public int Id { get; set; } + public string Name { get; set; } + + public Language() + { + } + + private Language(int id, string name) + { + Id = id; + Name = name; + } + + public override string ToString() + { + return Name; + } + + public override int GetHashCode() + { + return Id.GetHashCode(); + } + + public bool Equals(Language other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + + return Equals(obj as Language); + } + + public static bool operator ==(Language left, Language right) + { + return Equals(left, right); + } + + public static bool operator !=(Language left, Language right) + { + return !Equals(left, right); + } + + public static Language Unknown { get { return new Language(0, "Unknown"); } } + public static Language English { get { return new Language(1, "English"); } } + public static Language French { get { return new Language(2, "French"); } } + public static Language Spanish { get { return new Language(3, "Spanish"); } } + public static Language German { get { return new Language(4, "German"); } } + public static Language Italian { get { return new Language(5, "Italian"); } } + public static Language Danish { get { return new Language(6, "Danish"); } } + public static Language Dutch { get { return new Language(7, "Dutch"); } } + public static Language Japanese { get { return new Language(8, "Japanese"); } } + public static Language Cantonese { get { return new Language(9, "Cantonese"); } } + public static Language Mandarin { get { return new Language(10, "Mandarin"); } } + public static Language Russian { get { return new Language(11, "Russian"); } } + public static Language Polish { get { return new Language(12, "Polish"); } } + public static Language Vietnamese { get { return new Language(13, "Vietnamese"); } } + public static Language Swedish { get { return new Language(14, "Swedish"); } } + public static Language Norwegian { get { return new Language(15, "Norwegian"); } } + public static Language Finnish { get { return new Language(16, "Finnish"); } } + public static Language Turkish { get { return new Language(17, "Turkish"); } } + public static Language Portuguese { get { return new Language(18, "Portuguese"); } } + public static Language Flemish { get { return new Language(19, "Flemish"); } } + public static Language Greek { get { return new Language(20, "Greek"); } } + public static Language Korean { get { return new Language(21, "Korean"); } } + public static Language Hungarian { get { return new Language(22, "Hungarian"); } } + public static Language Hebrew { get { return new Language(23, "Hebrew"); } } + public static Language Lithuanian { get { return new Language(24, "Lithuanian"); } } + public static Language Czech { get { return new Language(25, "Czech"); } } + + + public static List All + { + get + { + return new List + { + Unknown, + English, + French, + Spanish, + German, + Italian, + Danish, + Dutch, + Japanese, + Cantonese, + Mandarin, + Russian, + Polish, + Vietnamese, + Swedish, + Norwegian, + Finnish, + Turkish, + Portuguese, + Flemish, + Greek, + Korean, + Hungarian, + Hebrew, + Lithuanian, + Czech + }; + } + } + + public static Language FindById(int id) + { + if (id == 0) return Unknown; + + Language language = All.FirstOrDefault(v => v.Id == id); + + if (language == null) + { + throw new ArgumentException("ID does not match a known language", nameof(id)); + } + + return language; + } + + public static explicit operator Language(int id) + { + return FindById(id); + } + + public static explicit operator int(Language language) + { + return language.Id; + } + + public static explicit operator Language(string lang) + { + var language = All.FirstOrDefault(v => v.Name.Equals(lang, StringComparison.InvariantCultureIgnoreCase)); + + if (language == null) + { + throw new ArgumentException("Language does not match a known language", nameof(lang)); + } + + return language; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Languages/LanguageComparer.cs b/src/NzbDrone.Core/Languages/LanguageComparer.cs new file mode 100644 index 000000000..e286aa681 --- /dev/null +++ b/src/NzbDrone.Core/Languages/LanguageComparer.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Languages +{ + public class LanguageComparer : IComparer + { + private readonly LanguageProfile _profile; + + public LanguageComparer(LanguageProfile profile) + { + Ensure.That(profile, () => profile).IsNotNull(); + Ensure.That(profile.Languages, () => profile.Languages).HasItems(); + + _profile = profile; + } + + public int Compare(Language left, Language right) + { + int leftIndex = _profile.Languages.FindIndex(v => v.Language == left); + int rightIndex = _profile.Languages.FindIndex(v => v.Language == right); + + return leftIndex.CompareTo(rightIndex); + } + } +} diff --git a/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs b/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs new file mode 100644 index 000000000..3b04f024a --- /dev/null +++ b/src/NzbDrone.Core/Languages/LanguagesBelowCutoff.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Languages +{ + public class LanguagesBelowCutoff + { + public int ProfileId { get; set; } + public IEnumerable LanguageIds { get; set; } + + public LanguagesBelowCutoff(int profileId, IEnumerable languageIds) + { + ProfileId = profileId; + LanguageIds = languageIds; + } + } +} diff --git a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs index a06e39379..52742bd81 100644 --- a/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs +++ b/src/NzbDrone.Core/MediaFiles/Commands/DownloadedAlbumsScanCommand.cs @@ -5,10 +5,6 @@ namespace NzbDrone.Core.MediaFiles.Commands { public class DownloadedAlbumsScanCommand : Command { - public override bool SendUpdatesToClient => SendUpdates; - - public bool SendUpdates { get; set; } - // Properties used by third-party apps, do not modify. public string Path { get; set; } public string DownloadClientId { get; set; } diff --git a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs index 32b52f9e7..eab090ef3 100644 --- a/src/NzbDrone.Core/MediaFiles/DiskScanService.cs +++ b/src/NzbDrone.Core/MediaFiles/DiskScanService.cs @@ -82,7 +82,7 @@ namespace NzbDrone.Core.MediaFiles return; } - _logger.ProgressInfo("Scanning disk for {0}", artist.Name); + _logger.ProgressInfo("Scanning {0}", artist.Name); if (!_diskProvider.FolderExists(artist.Path)) { diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index ecce449b4..95e342404 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -1,10 +1,11 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using NzbDrone.Core.Datastore; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles { @@ -22,10 +23,11 @@ namespace NzbDrone.Core.MediaFiles public MediaInfoModel MediaInfo { get; set; } public LazyLoaded> Episodes { get; set; } public LazyLoaded Series { get; set; } + public Language Language { get; set; } public override string ToString() { return string.Format("[{0}] {1}", Id, RelativePath); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs index e485344ca..d7b52422e 100644 --- a/src/NzbDrone.Core/MediaFiles/MediaFileService.cs +++ b/src/NzbDrone.Core/MediaFiles/MediaFileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -15,9 +15,11 @@ namespace NzbDrone.Core.MediaFiles { TrackFile Add(TrackFile trackFile); void Update(TrackFile trackFile); + void Update(List trackFile); void Delete(TrackFile trackFile, DeleteMediaFileReason reason); List GetFilesByArtist(int artistId); List GetFilesByAlbum(int artistId, int albumId); + List GetFiles(IEnumerable ids); List GetFilesWithoutMediaInfo(); List FilterExistingFiles(List files, Artist artist); TrackFile Get(int id); @@ -50,6 +52,12 @@ namespace NzbDrone.Core.MediaFiles _mediaFileRepository.Update(trackFile); } + public void Update(List trackFiles) + { + _mediaFileRepository.UpdateMany(trackFiles); + } + + public void Delete(TrackFile trackFile, DeleteMediaFileReason reason) { //Little hack so we have the tracks and artist attached for the event consumers @@ -60,6 +68,11 @@ namespace NzbDrone.Core.MediaFiles _eventAggregator.PublishEvent(new TrackFileDeletedEvent(trackFile, reason)); } + public List GetFiles(IEnumerable ids) + { + return _mediaFileRepository.Get(ids).ToList(); + } + public List GetFilesWithoutMediaInfo() { @@ -101,4 +114,4 @@ namespace NzbDrone.Core.MediaFiles return _mediaFileRepository.GetFilesByAlbum(albumId); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/MediaFiles/TrackFile.cs b/src/NzbDrone.Core/MediaFiles/TrackFile.cs index 982089ef0..7a861a932 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackFile.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackFile.cs @@ -1,4 +1,4 @@ -using Marr.Data; +using Marr.Data; using NzbDrone.Core.Datastore; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles { @@ -27,6 +28,7 @@ namespace NzbDrone.Core.MediaFiles //public LazyLoaded> Episodes { get; set; } public LazyLoaded Artist { get; set; } public LazyLoaded> Tracks { get; set; } + public Language Language { get; set; } public override string ToString() { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs index 685df77d5..68e729ae6 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedEpisodes.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.Download; using NzbDrone.Core.Extras; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport { diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs index e652e09e7..71024a420 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportApprovedTracks.cs @@ -11,6 +11,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport { @@ -51,6 +52,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport var qualifiedImports = decisions.Where(c => c.Approved) .GroupBy(c => c.LocalTrack.Artist.Id, (i, s) => s .OrderByDescending(c => c.LocalTrack.Quality, new QualityModelComparer(s.First().LocalTrack.Artist.Profile)) + .ThenByDescending(c => c.LocalTrack.Language, new LanguageComparer(s.First().LocalTrack.Artist.LanguageProfile)) .ThenByDescending(c => c.LocalTrack.Size)) .SelectMany(c => c) .ToList(); @@ -85,6 +87,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport trackFile.AlbumId = _albumRepository.FindByArtistAndName(localTrack.Artist.Name, Parser.Parser.CleanArtistTitle(localTrack.ParsedTrackInfo.AlbumTitle)).Id; trackFile.ReleaseGroup = localTrack.ParsedTrackInfo.ReleaseGroup; trackFile.Tracks = localTrack.Tracks; + trackFile.Language = localTrack.Language; bool copyOnly; switch (importMode) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs index 8fa2b2f3f..9ee047b38 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/ImportDecisionMaker.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.MediaFiles.TrackImport { @@ -80,6 +81,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport if (localTrack != null) { localTrack.Quality = GetQuality(folderInfo, localTrack.Quality, artist); + localTrack.Language = GetLanguage(folderInfo, localTrack.Language, artist); localTrack.Size = _diskProvider.GetFileSize(file); _logger.Debug("Size: {0}", localTrack.Size); @@ -188,6 +190,37 @@ namespace NzbDrone.Core.MediaFiles.TrackImport return fileQuality; } + private Language GetLanguage(ParsedTrackInfo folderInfo, Language fileLanguage, Artist artist) + { + if (UseFolderLanguage (folderInfo, fileLanguage, artist)) + { + _logger.Debug("Using language from folder: {0}", folderInfo.Language); + return folderInfo.Language; + } + + return fileLanguage; + } + + private bool UseFolderLanguage(ParsedTrackInfo folderInfo, Language fileLanguage, Artist artist) + { + if (folderInfo == null) + { + return false; + } + + if (folderInfo.Language == Language.Unknown) + { + return false; + } + + if (new LanguageComparer(artist.LanguageProfile).Compare(folderInfo.Language, fileLanguage) > 0) + { + return true; + } + + return false; + } + private bool UseFolderQuality(ParsedTrackInfo folderInfo, QualityModel fileQuality, Artist artist) { if (folderInfo == null) diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs index 5e1ec2f74..581e0c72b 100644 --- a/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Specifications/UpgradeSpecification.cs @@ -4,6 +4,9 @@ using NLog; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Qualities; +using System.Collections.Generic; namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications { @@ -19,7 +22,12 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Specifications public Decision IsSatisfiedBy(LocalTrack localTrack) { var qualityComparer = new QualityModelComparer(localTrack.Artist.Profile); - if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) > 0)) + var languageComparer = new LanguageComparer(localTrack.Artist.LanguageProfile); + var profile = localTrack.Artist.Profile.Value; + + if (localTrack.Tracks.Any(e => e.TrackFileId != 0 && + languageComparer.Compare(e.TrackFile.Value.Language, localTrack.Language) > 0 && + qualityComparer.Compare(e.TrackFile.Value.Quality, localTrack.Quality) == 0)) { _logger.Debug("This file isn't an upgrade for all tracks. Skipping {0}", localTrack.Path); return Decision.Reject("Not an upgrade for existing track file(s)"); diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs index 20becd1f0..2eb164c03 100644 --- a/src/NzbDrone.Core/Messaging/Commands/Command.cs +++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs @@ -4,7 +4,20 @@ namespace NzbDrone.Core.Messaging.Commands { public abstract class Command { - public virtual bool SendUpdatesToClient => false; + private bool _sendUpdatesToClient; + + public virtual bool SendUpdatesToClient + { + get + { + return _sendUpdatesToClient; + } + + set + { + _sendUpdatesToClient = value; + } + } public virtual bool UpdateScheduledTask => true; @@ -13,6 +26,7 @@ namespace NzbDrone.Core.Messaging.Commands public string Name { get; private set; } public DateTime? LastExecutionTime { get; set; } public CommandTrigger Trigger { get; set; } + public bool SuppressMessages { get; set; } public Command() { diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs index d45547b8f..5fb2eb02a 100644 --- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs +++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -15,11 +15,12 @@ namespace NzbDrone.Core.Messaging.Commands { public interface IManageCommandQueue { + List PushMany(List commands) where TCommand : Command; CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command; CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified); IEnumerable Queue(CancellationToken cancellationToken); CommandModel Get(int id); - List GetStarted(); + List GetStarted(); void SetMessage(CommandModel command, string message); void Start(CommandModel command); void Complete(CommandModel command, string message); @@ -35,9 +36,9 @@ namespace NzbDrone.Core.Messaging.Commands private readonly Logger _logger; private readonly ICached _commandCache; - private readonly BlockingCollection _commandQueue; + private readonly BlockingCollection _commandQueue; - public CommandQueueManager(ICommandRepository repo, + public CommandQueueManager(ICommandRepository repo, IServiceFactory serviceFactory, ICacheManager cacheManager, Logger logger) @@ -50,6 +51,49 @@ namespace NzbDrone.Core.Messaging.Commands _commandQueue = new BlockingCollection(new CommandQueue()); } + public List PushMany(List commands) where TCommand : Command + { + _logger.Trace("Publishing {0} commands", commands.Count); + + var commandModels = new List(); + var existingCommands = _commandCache.Values.Where(q => q.Status == CommandStatus.Queued || + q.Status == CommandStatus.Started).ToList(); + + foreach (var command in commands) + { + var existing = existingCommands.SingleOrDefault(c => c.Name == command.Name && CommandEqualityComparer.Instance.Equals(c.Body, command)); + + if (existing != null) + { + continue; + } + + var commandModel = new CommandModel + + { + Name = command.Name, + Body = command, + QueuedAt = DateTime.UtcNow, + Trigger = CommandTrigger.Unspecified, + Priority = CommandPriority.Normal, + Status = CommandStatus.Queued + }; + + commandModels.Add(commandModel); + } + + _repo.InsertMany(commandModels); + + foreach (var commandModel in commandModels) + { + _commandCache.Set(commandModel.Id.ToString(), commandModel); + _commandQueue.Add(commandModel); + } + + return commandModels; + } + + public CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command { Ensure.That(command, () => command).IsNotNull(); @@ -123,7 +167,7 @@ namespace NzbDrone.Core.Messaging.Commands command.Status = CommandStatus.Started; _logger.Trace("Marking command as started: {0}", command.Name); - _commandCache.Set(command.Id.ToString(), command); + _commandCache.Set(command.Id.ToString(), command); _repo.Start(command); } @@ -135,7 +179,7 @@ namespace NzbDrone.Core.Messaging.Commands public void Fail(CommandModel command, string message, Exception e) { command.Exception = e.ToString(); - + Update(command, CommandStatus.Failed, message); } @@ -150,7 +194,7 @@ namespace NzbDrone.Core.Messaging.Commands public void CleanCommands() { _logger.Trace("Cleaning up old commands"); - + var old = _commandCache.Values.Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(-5)); foreach (var command in old) diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 8652e54d2..acdf913c1 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -21,12 +21,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private readonly IHttpClient _httpClient; private readonly Logger _logger; + private readonly IArtistService _artistService; private readonly IHttpRequestBuilderFactory _requestBuilder; - public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, Logger logger) + public SkyHookProxy(IHttpClient httpClient, ILidarrCloudRequestBuilder requestBuilder, IArtistService artistService, Logger logger) { _httpClient = httpClient; - _requestBuilder = requestBuilder.Search; + _requestBuilder = requestBuilder.Search; + _artistService = artistService; _logger = logger; } @@ -45,13 +47,13 @@ namespace NzbDrone.Core.MetadataSource.SkyHook .SetSegment("route", "artists/" + foreignArtistId) .Build(); - + httpRequest.AllowAutoRedirect = true; httpRequest.SuppressHttpError = true; var httpResponse = _httpClient.Get(httpRequest); - + if (httpResponse.HasHttpError) { @@ -106,8 +108,8 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var httpResponse = _httpClient.Get>(httpRequest); - - return httpResponse.Resource.SelectList(MapArtist); + + return httpResponse.Resource.SelectList(MapSearhResult); } catch (HttpException) { @@ -120,6 +122,18 @@ namespace NzbDrone.Core.MetadataSource.SkyHook } } + private Artist MapSearhResult(ArtistResource resource) + { + var artist = _artistService.FindById(resource.Id); + + if (artist == null) + { + artist = MapArtist(resource); + } + + return artist; + } + private static Album MapAlbum(AlbumResource resource) { Album album = new Album(); @@ -132,7 +146,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook var tracks = resource.Tracks.Select(MapTrack); album.Tracks = tracks.ToList(); - + return album; } @@ -149,7 +163,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook private static Artist MapArtist(ArtistResource resource) { - + Artist artist = new Artist(); artist.Name = resource.ArtistName; @@ -158,8 +172,10 @@ namespace NzbDrone.Core.MetadataSource.SkyHook artist.Overview = resource.Overview; artist.NameSlug = Parser.Parser.CleanArtistTitle(artist.Name); artist.CleanName = Parser.Parser.CleanArtistTitle(artist.Name); - artist.SortName = SeriesTitleNormalizer.Normalize(artist.Name,0); + artist.SortName = SeriesTitleNormalizer.Normalize(artist.Name, 0); artist.Images = resource.Images.Select(MapImage).ToList(); + artist.Status = ArtistStatusType.Continuing; // TODO: Remove HACK when we get from Metadata + artist.Ratings = MapRatings(null); // TODO: Remove HACK when we get from Metadata return artist; } @@ -183,14 +199,14 @@ namespace NzbDrone.Core.MetadataSource.SkyHook return newActor; } - private static SeriesStatusType MapArtistStatus(string status) + private static ArtistStatusType MapArtistStatus(string status) { if (status.Equals("ended", StringComparison.InvariantCultureIgnoreCase)) { - return SeriesStatusType.Ended; + return ArtistStatusType.Ended; } - return SeriesStatusType.Continuing; + return ArtistStatusType.Continuing; } private static Core.Music.Ratings MapRatings(RatingResource rating) diff --git a/src/NzbDrone.Core/Music/AddArtistValidator.cs b/src/NzbDrone.Core/Music/AddArtistValidator.cs index bc860f09e..2a03ed990 100644 --- a/src/NzbDrone.Core/Music/AddArtistValidator.cs +++ b/src/NzbDrone.Core/Music/AddArtistValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using FluentValidation.Results; using NzbDrone.Core.Validation.Paths; using System; @@ -17,7 +17,6 @@ namespace NzbDrone.Core.Music { public AddArtistValidator(RootFolderValidator rootFolderValidator, SeriesPathValidator seriesPathValidator, - DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, ArtistSlugValidator artistTitleSlugValidator) { @@ -25,7 +24,6 @@ namespace NzbDrone.Core.Music .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator); RuleFor(c => c.NameSlug).SetValidator(artistTitleSlugValidator);// TODO: Check if we are going to use a slug or artistName diff --git a/src/NzbDrone.Core/Music/AlbumRepository.cs b/src/NzbDrone.Core/Music/AlbumRepository.cs index e7c113322..51f5b869a 100644 --- a/src/NzbDrone.Core/Music/AlbumRepository.cs +++ b/src/NzbDrone.Core/Music/AlbumRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Marr.Data.QGen; using NzbDrone.Core.Datastore; @@ -20,13 +20,17 @@ namespace NzbDrone.Core.Music PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); List AlbumsBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Album album, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); } public class AlbumRepository : BasicRepository, IAlbumRepository { + private readonly IMainDatabase _database; + public AlbumRepository(IMainDatabase database, IEventAggregator eventAggregator) : base(database, eventAggregator) { + _database = database; } @@ -143,6 +147,19 @@ namespace NzbDrone.Core.Music SetFields(album, p => p.Monitored); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("monitored", monitored); + + var sql = "UPDATE Albums " + + "SET Monitored = @monitored " + + $"WHERE Id IN ({string.Join(", ", ids)})"; + + mapper.ExecuteNonQuery(sql); + } + public Album FindByName(string cleanTitle) { cleanTitle = cleanTitle.ToLowerInvariant(); diff --git a/src/NzbDrone.Core/Music/AlbumService.cs b/src/NzbDrone.Core/Music/AlbumService.cs index c6017ea06..d2e9b9a83 100644 --- a/src/NzbDrone.Core/Music/AlbumService.cs +++ b/src/NzbDrone.Core/Music/AlbumService.cs @@ -1,4 +1,4 @@ -using NLog; +using NLog; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Music.Events; using NzbDrone.Core.Organizer; @@ -27,6 +27,7 @@ namespace NzbDrone.Core.Music Album UpdateAlbum(Album album); List UpdateAlbums(List album); void SetAlbumMonitored(int albumId, bool monitored); + void SetMonitored(IEnumerable ids, bool monitored); PagingSpec AlbumsWithoutFiles(PagingSpec pagingSpec); List AlbumsBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List albums); @@ -166,6 +167,11 @@ namespace NzbDrone.Core.Music _logger.Debug("Monitored flag for Album:{0} was set to {1}", albumId, monitored); } + public void SetMonitored(IEnumerable ids, bool monitored) + { + _albumRepository.SetMonitored(ids, monitored); + } + public List UpdateAlbums(List album) { _logger.Debug("Updating {0} album", album.Count); diff --git a/src/NzbDrone.Core/Music/Artist.cs b/src/NzbDrone.Core/Music/Artist.cs index 7e803bd70..c33bbc489 100644 --- a/src/NzbDrone.Core/Music/Artist.cs +++ b/src/NzbDrone.Core/Music/Artist.cs @@ -1,7 +1,8 @@ -using Marr.Data; +using Marr.Data; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Music; using System; using System.Collections.Generic; @@ -36,14 +37,16 @@ namespace NzbDrone.Core.Music public bool AlbumFolder { get; set; } public DateTime? LastInfoSync { get; set; } public DateTime? LastDiskSync { get; set; } - public int Status { get; set; } // TODO: Figure out what this is, do we need it? + public ArtistStatusType Status { get; set; } public string Path { get; set; } public List Images { get; set; } public List Genres { get; set; } public string RootFolderPath { get; set; } public DateTime Added { get; set; } public LazyLoaded Profile { get; set; } + public LazyLoaded LanguageProfile { get; set; } public int ProfileId { get; set; } + public int LanguageProfileId { get; set; } public List Albums { get; set; } public HashSet Tags { get; set; } public AddArtistOptions AddOptions { get; set; } @@ -63,6 +66,7 @@ namespace NzbDrone.Core.Music Path = otherArtist.Path; Profile = otherArtist.Profile; + LanguageProfileId = otherArtist.LanguageProfileId; Albums = otherArtist.Albums; diff --git a/src/NzbDrone.Core/Music/ArtistStatusType.cs b/src/NzbDrone.Core/Music/ArtistStatusType.cs new file mode 100644 index 000000000..478959016 --- /dev/null +++ b/src/NzbDrone.Core/Music/ArtistStatusType.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Music +{ + public enum ArtistStatusType + { + Continuing = 0, + Ended = 1 + } +} diff --git a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs index 5c2d10045..4673a63a9 100644 --- a/src/NzbDrone.Core/Notifications/NotificationDefinition.cs +++ b/src/NzbDrone.Core/Notifications/NotificationDefinition.cs @@ -1,14 +1,9 @@ -using System.Collections.Generic; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Notifications { public class NotificationDefinition : ProviderDefinition { - public NotificationDefinition() - { - Tags = new HashSet(); - } public bool OnGrab { get; set; } public bool OnDownload { get; set; } @@ -18,7 +13,6 @@ namespace NzbDrone.Core.Notifications public bool SupportsOnDownload { get; set; } public bool SupportsOnUpgrade { get; set; } public bool SupportsOnRename { get; set; } - public HashSet Tags { get; set; } public override bool Enable => OnGrab || OnDownload || (OnDownload && OnUpgrade); } diff --git a/src/NzbDrone.Core/Notifications/NotificationService.cs b/src/NzbDrone.Core/Notifications/NotificationService.cs index 985126f19..7edb106fe 100644 --- a/src/NzbDrone.Core/Notifications/NotificationService.cs +++ b/src/NzbDrone.Core/Notifications/NotificationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NLog; @@ -69,22 +69,20 @@ namespace NzbDrone.Core.Notifications private bool ShouldHandleSeries(ProviderDefinition definition, Series series) { - var notificationDefinition = (NotificationDefinition)definition; - - if (notificationDefinition.Tags.Empty()) + if (definition.Tags.Empty()) { _logger.Debug("No tags set for this notification."); return true; } - if (notificationDefinition.Tags.Intersect(series.Tags).Any()) + if (definition.Tags.Intersect(series.Tags).Any()) { - _logger.Debug("Notification and series have one or more matching tags."); + _logger.Debug("Notification and series have one or more intersecting tags."); return true; } //TODO: this message could be more clear - _logger.Debug("{0} does not have any tags that match {1}'s tags", notificationDefinition.Name, series.Title); + _logger.Debug("{0} does not have any intersecting tags with {1}. Notification will not be sent.", definition.Name, series.Title); return false; } diff --git a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs index 470ed317a..44d176675 100644 --- a/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs +++ b/src/NzbDrone.Core/Notifications/Twitter/TwitterSettings.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using NzbDrone.Common.Extensions; using NzbDrone.Core.Annotations; using NzbDrone.Core.ThingiProvider; @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Notifications.Twitter public TwitterSettings() { DirectMessage = true; - AuthorizeNotification = "step1"; + AuthorizeNotification = "startOAuth"; } [FieldDefinition(0, Label = "Consumer Key", HelpText = "Consumer key from a Twitter application", HelpLink = "https://github.com/Lidarr/Lidarr/wiki/Twitter-Notifications")] @@ -55,7 +55,7 @@ namespace NzbDrone.Core.Notifications.Twitter [FieldDefinition(5, Label = "Direct Message", Type = FieldType.Checkbox, HelpText = "Send a direct message instead of a public message")] public bool DirectMessage { get; set; } - [FieldDefinition(6, Label = "Connect to twitter", Type = FieldType.Action)] + [FieldDefinition(6, Label = "Connect to Twitter", Type = FieldType.OAuth)] public string AuthorizeNotification { get; set; } public NzbDroneValidationResult Validate() diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 534145063..5184b5ded 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -167,6 +167,7 @@ + @@ -256,6 +257,7 @@ + @@ -295,11 +297,12 @@ Code - - - - - + + + + + + @@ -324,7 +327,6 @@ - @@ -354,6 +356,7 @@ + @@ -506,6 +509,7 @@ + @@ -614,6 +618,7 @@ + @@ -708,6 +713,9 @@ + + + @@ -851,6 +859,7 @@ + @@ -954,10 +963,17 @@ - + + + + + + + + @@ -1061,7 +1077,6 @@ - @@ -1072,11 +1087,11 @@ - - - + + + - + @@ -1115,6 +1130,7 @@ + @@ -1148,6 +1164,7 @@ + @@ -1183,15 +1200,15 @@ - + + - diff --git a/src/NzbDrone.Core/Parser/IsoLanguage.cs b/src/NzbDrone.Core/Parser/IsoLanguage.cs index 1bd198e50..c21ba27a7 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguage.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguage.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Core.Parser +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Parser { public class IsoLanguage { diff --git a/src/NzbDrone.Core/Parser/IsoLanguages.cs b/src/NzbDrone.Core/Parser/IsoLanguages.cs index ddbbe74c2..d82fd3c80 100644 --- a/src/NzbDrone.Core/Parser/IsoLanguages.cs +++ b/src/NzbDrone.Core/Parser/IsoLanguages.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { diff --git a/src/NzbDrone.Core/Parser/Language.cs b/src/NzbDrone.Core/Parser/Language.cs deleted file mode 100644 index f85281dd1..000000000 --- a/src/NzbDrone.Core/Parser/Language.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace NzbDrone.Core.Parser -{ - public enum Language - { - Unknown = 0, - English = 1, - French = 2, - Spanish = 3, - German = 4, - Italian = 5, - Danish = 6, - Dutch = 7, - Japanese = 8, - Cantonese = 9, - Mandarin = 10, - Russian = 11, - Polish = 12, - Vietnamese = 13, - Swedish = 14, - Norwegian = 15, - Finnish = 16, - Turkish = 17, - Portuguese = 18, - Flemish = 19, - Greek = 20, - Korean = 21, - Hungarian = 22 - } -} diff --git a/src/NzbDrone.Core/Parser/LanguageParser.cs b/src/NzbDrone.Core/Parser/LanguageParser.cs index a2de40b84..2fd2f2935 100644 --- a/src/NzbDrone.Core/Parser/LanguageParser.cs +++ b/src/NzbDrone.Core/Parser/LanguageParser.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using NLog; using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { diff --git a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs index 67ec2d873..170221707 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalEpisode.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -19,6 +20,7 @@ namespace NzbDrone.Core.Parser.Model public Series Series { get; set; } public List Episodes { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } @@ -37,4 +39,4 @@ namespace NzbDrone.Core.Parser.Model return Path; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs index 359ce6285..eb45514c2 100644 --- a/src/NzbDrone.Core/Parser/Model/LocalTrack.cs +++ b/src/NzbDrone.Core/Parser/Model/LocalTrack.cs @@ -1,10 +1,11 @@ -using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Music; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -22,6 +23,7 @@ namespace NzbDrone.Core.Parser.Model public Album Album { get; set; } public List Tracks { get; set; } public QualityModel Quality { get; set; } + public Language Language { get; set; } public MediaInfoModel MediaInfo { get; set; } public bool ExistingFile { get; set; } diff --git a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs index 7e8ff08b4..c116bec37 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedAlbumInfo.cs @@ -1,9 +1,10 @@ -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { diff --git a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs index 7adf609e8..821e75e94 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedEpisodeInfo.cs @@ -1,6 +1,7 @@ -using System.Linq; +using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -90,4 +91,4 @@ namespace NzbDrone.Core.Parser.Model return string.Format("{0} - {1} {2}", SeriesTitle, episodeString, Quality); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs index 6466e6634..4c1989ccd 100644 --- a/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs +++ b/src/NzbDrone.Core/Parser/Model/ParsedTrackInfo.cs @@ -1,9 +1,10 @@ -using NzbDrone.Common.Extensions; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Qualities; using System; using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser.Model { @@ -16,7 +17,7 @@ namespace NzbDrone.Core.Parser.Model public ArtistTitleInfo ArtistTitleInfo { get; set; } public QualityModel Quality { get; set; } public int[] TrackNumbers { get; set; } - //public Language Language { get; set; } + public Language Language { get; set; } public string ReleaseGroup { get; set; } public string ReleaseHash { get; set; } diff --git a/src/NzbDrone.Core/Parser/Parser.cs b/src/NzbDrone.Core/Parser/Parser.cs index 25e274916..bfdcd9146 100644 --- a/src/NzbDrone.Core/Parser/Parser.cs +++ b/src/NzbDrone.Core/Parser/Parser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -9,6 +9,7 @@ using NzbDrone.Common.Instrumentation; using NzbDrone.Core.MediaFiles.MediaInfo; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Tv; +using NzbDrone.Core.Languages; namespace NzbDrone.Core.Parser { @@ -312,6 +313,9 @@ namespace NzbDrone.Core.Parser private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?(?!\s).+?(?\b(?:ita|italian)\b)|(?german\b|videomann)|(?flemish)|(?greek)|(?(?:\W|_)(?:FR|VOSTFR)(?:\W|_))|(?\brus\b)|(?nl\W?subs?)|(?\b(?:HUNDUB|HUN)\b)|(?\b(?:español|castellano)\b)", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + private static readonly Regex YearInTitleRegex = new Regex(@"^(?.+?)(?:\W|_)?(?<year>\d{4})", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -335,15 +339,16 @@ namespace NzbDrone.Core.Parser var artistTitleInfo = new ArtistTitleInfo { Title = file.Tag.Title, - Year = (int) file.Tag.Year + Year = (int)file.Tag.Year }; var temp = new int[1]; - temp[0] = (int) trackNumber; + temp[0] = (int)trackNumber; var result = new ParsedTrackInfo { + Language = Language.English, //TODO Parse from Tag/Mediainfo AlbumTitle = file.Tag.Album, - ArtistTitle = file.Tag.FirstAlbumArtist, + ArtistTitle = file.Tag.FirstAlbumArtist, Quality = QualityParser.ParseQuality(trackName), TrackNumbers = temp, ArtistTitleInfo = artistTitleInfo, @@ -840,6 +845,99 @@ namespace NzbDrone.Core.Parser return title; } + public static Language ParseLanguage(string title) + { + var lowerTitle = title.ToLower(); + + if (lowerTitle.Contains("english")) + return Language.English; + + if (lowerTitle.Contains("french")) + return Language.French; + + if (lowerTitle.Contains("spanish")) + return Language.Spanish; + + if (lowerTitle.Contains("danish")) + return Language.Danish; + + if (lowerTitle.Contains("dutch")) + return Language.Dutch; + + if (lowerTitle.Contains("japanese")) + return Language.Japanese; + + if (lowerTitle.Contains("cantonese")) + return Language.Cantonese; + + if (lowerTitle.Contains("mandarin")) + return Language.Mandarin; + + if (lowerTitle.Contains("korean")) + return Language.Korean; + + if (lowerTitle.Contains("russian")) + return Language.Russian; + + if (lowerTitle.Contains("polish")) + return Language.Polish; + + if (lowerTitle.Contains("vietnamese")) + return Language.Vietnamese; + + if (lowerTitle.Contains("swedish")) + return Language.Swedish; + + if (lowerTitle.Contains("norwegian")) + return Language.Norwegian; + + if (lowerTitle.Contains("nordic")) + return Language.Norwegian; + + if (lowerTitle.Contains("finnish")) + return Language.Finnish; + + if (lowerTitle.Contains("turkish")) + return Language.Turkish; + + if (lowerTitle.Contains("portuguese")) + return Language.Portuguese; + + if (lowerTitle.Contains("hungarian")) + return Language.Hungarian; + + var match = LanguageRegex.Match(title); + + if (match.Groups["italian"].Captures.Cast<Capture>().Any()) + return Language.Italian; + + if (match.Groups["german"].Captures.Cast<Capture>().Any()) + return Language.German; + + if (match.Groups["flemish"].Captures.Cast<Capture>().Any()) + return Language.Flemish; + + if (match.Groups["greek"].Captures.Cast<Capture>().Any()) + return Language.Greek; + + if (match.Groups["spanish"].Captures.Cast<Capture>().Any()) + return Language.Spanish; + + if (match.Groups["french"].Success) + return Language.French; + + if (match.Groups["russian"].Success) + return Language.Russian; + + if (match.Groups["dutch"].Success) + return Language.Dutch; + + if (match.Groups["hungarian"].Success) + return Language.Hungarian; + + return Language.English; + } + private static SeriesTitleInfo GetSeriesTitleInfo(string title) { var seriesTitleInfo = new SeriesTitleInfo(); @@ -868,7 +966,7 @@ namespace NzbDrone.Core.Parser // Coppied from Radarr (https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Parser/Parser.cs) // TODO: Split into separate method and write unit tests for. - var parts = artistName.Split('.'); + var parts = artistName.Split('.'); artistName = ""; int n = 0; bool previousAcronym = false; diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs index 1e048aeff..5926b6d5e 100644 --- a/src/NzbDrone.Core/Parser/ParsingService.cs +++ b/src/NzbDrone.Core/Parser/ParsingService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using NLog; @@ -109,6 +109,7 @@ namespace NzbDrone.Core.Parser { Series = series, Quality = parsedEpisodeInfo.Quality, + Language = parsedEpisodeInfo.Language, Episodes = episodes, Path = filename, ParsedEpisodeInfo = parsedEpisodeInfo, @@ -674,6 +675,7 @@ namespace NzbDrone.Core.Parser { Artist = artist, Quality = parsedTrackInfo.Quality, + Language = parsedTrackInfo.Language, Tracks = tracks, Path = filename, ParsedTrackInfo = parsedTrackInfo, diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs index a367ce4eb..6c3db01a7 100644 --- a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Extensions; @@ -11,8 +11,10 @@ namespace NzbDrone.Core.Profiles.Delay void Delete(int id); List<DelayProfile> All(); DelayProfile Get(int id); + List<DelayProfile> AllForTag(int tagId); List<DelayProfile> AllForTags(HashSet<int> tagIds); DelayProfile BestForTags(HashSet<int> tagIds); + List<DelayProfile> Reorder(int id, int? afterId); } public class DelayProfileService : IDelayProfileService @@ -26,6 +28,8 @@ namespace NzbDrone.Core.Profiles.Delay public DelayProfile Add(DelayProfile profile) { + profile.Order = _repo.Count(); + return _repo.Insert(profile); } @@ -60,9 +64,15 @@ namespace NzbDrone.Core.Profiles.Delay return _repo.Get(id); } + public List<DelayProfile> AllForTag(int tagId) + { + return All().Where(r => r.Tags.Contains(tagId)) + .ToList(); + } + public List<DelayProfile> AllForTags(HashSet<int> tagIds) { - return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); + return All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); } public DelayProfile BestForTags(HashSet<int> tagIds) @@ -70,5 +80,72 @@ namespace NzbDrone.Core.Profiles.Delay return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()) .OrderBy(d => d.Order).First(); } + + public List<DelayProfile> Reorder(int id, int? afterId) + { + var all = All().OrderBy(d => d.Order) + .ToList(); + + var moving = all.SingleOrDefault(d => d.Id == id); + var after = afterId.HasValue ? all.SingleOrDefault(d => d.Id == afterId) : null; + + if (moving == null) + { + // TODO: This should throw + return all; + } + + var afterOrder = GetAfterOrder(moving, after); + var afterCount = afterOrder + 2; + var movingOrder = moving.Order; + + foreach (var delayProfile in all) + { + if (delayProfile.Id == 1) + { + continue; + } + + if (delayProfile.Id == id) + { + delayProfile.Order = afterOrder + 1; + } + + else if (delayProfile.Id == after?.Id) + { + delayProfile.Order = afterOrder; + } + + else if (delayProfile.Order > afterOrder) + { + delayProfile.Order = afterCount; + afterCount++; + } + + else if (delayProfile.Order > movingOrder) + { + delayProfile.Order--; + } + } + + _repo.UpdateMany(all); + + return All(); + } + + private int GetAfterOrder(DelayProfile moving, DelayProfile after) + { + if (after == null) + { + return 0; + } + + if (moving.Order < after.Order) + { + return after.Order - 1; + } + + return after.Order; + } } } diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs new file mode 100644 index 000000000..ff07eed25 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfile.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class LanguageProfile : ModelBase + { + public string Name { get; set; } + public List<ProfileLanguageItem> Languages { get; set; } + public Language Cutoff { get; set; } + + public Language LastAllowedLanguage() + { + return Languages.Last(q => q.Allowed).Language; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs new file mode 100644 index 000000000..bfba8045d --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileInUseException.cs @@ -0,0 +1,13 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class LanguageProfileInUseException : NzbDroneException + { + public LanguageProfileInUseException(int profileId) + : base("Language profile [{0}] is in use.", profileId) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs new file mode 100644 index 000000000..797c14efe --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileRepository.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Profiles.Languages +{ + public interface ILanguageProfileRepository : IBasicRepository<LanguageProfile> + { + bool Exists(int id); + } + + public class LanguageProfileRepository : BasicRepository<LanguageProfile>, ILanguageProfileRepository + { + public LanguageProfileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public bool Exists(int id) + { + return DataMapper.Query<LanguageProfile>().Where(p => p.Id == id).GetRowCount() == 1; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs new file mode 100644 index 000000000..a26e75270 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/LanguageProfileService.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Profiles.Languages +{ + public interface ILanguageProfileService + { + LanguageProfile Add(LanguageProfile profile); + void Update(LanguageProfile profile); + void Delete(int id); + List<LanguageProfile> All(); + LanguageProfile Get(int id); + bool Exists(int id); + } + + public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent> + { + private readonly ILanguageProfileRepository _profileRepository; + private readonly ISeriesService _seriesService; + private readonly Logger _logger; + + public LanguageProfileService(ILanguageProfileRepository profileRepository, ISeriesService seriesService, Logger logger) + { + _profileRepository = profileRepository; + _seriesService = seriesService; + _logger = logger; + } + + public LanguageProfile Add(LanguageProfile profile) + { + return _profileRepository.Insert(profile); + } + + public void Update(LanguageProfile profile) + { + _profileRepository.Update(profile); + } + + public void Delete(int id) + { + if (_seriesService.GetAllSeries().Any(c => c.LanguageProfileId == id)) + { + throw new LanguageProfileInUseException(id); + } + + _profileRepository.Delete(id); + } + + public List<LanguageProfile> All() + { + return _profileRepository.All().ToList(); + } + + public LanguageProfile Get(int id) + { + return _profileRepository.Get(id); + } + + public bool Exists(int id) + { + return _profileRepository.Exists(id); + } + + private LanguageProfile AddDefaultProfile(string name, Language cutoff, params Language[] allowed) + { + var languages = Language.All + .OrderByDescending(l => l.Name) + .Select(v => new ProfileLanguageItem { Language = v, Allowed = allowed.Contains(v) }) + .ToList(); + + var profile = new LanguageProfile + { + Name = name, + Cutoff = cutoff, + Languages = languages, + }; + + return Add(profile); + } + + public void Handle(ApplicationStartedEvent message) + { + if (All().Any()) return; + + _logger.Info("Setting up default language profiles"); + + AddDefaultProfile("English", Language.English, Language.English); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs b/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs new file mode 100644 index 000000000..a25ea2257 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Languages/ProfileLanguageItem.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Languages; + +namespace NzbDrone.Core.Profiles.Languages +{ + public class ProfileLanguageItem : IEmbeddedDocument + { + public Language Language { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs deleted file mode 100644 index 6215e9474..000000000 --- a/src/NzbDrone.Core/Profiles/Profile.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Parser; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Profiles -{ - public class Profile : ModelBase - { - public string Name { get; set; } - public Quality Cutoff { get; set; } - public List<ProfileQualityItem> Items { get; set; } - public Language Language { get; set; } - - public Quality LastAllowedQuality() - { - return Items.Last(q => q.Allowed).Quality; - } - } -} diff --git a/src/NzbDrone.Core/Profiles/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/ProfileInUseException.cs deleted file mode 100644 index d55523d9a..000000000 --- a/src/NzbDrone.Core/Profiles/ProfileInUseException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NzbDrone.Common.Exceptions; - -namespace NzbDrone.Core.Profiles -{ - public class ProfileInUseException : NzbDroneException - { - public ProfileInUseException(int profileId) - : base("Profile [{0}] is in use.", profileId) - { - - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs deleted file mode 100644 index 35c9ce360..000000000 --- a/src/NzbDrone.Core/Profiles/ProfileQualityItem.cs +++ /dev/null @@ -1,11 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Qualities; - -namespace NzbDrone.Core.Profiles -{ - public class ProfileQualityItem : IEmbeddedDocument - { - public Quality Quality { get; set; } - public bool Allowed { get; set; } - } -} diff --git a/src/NzbDrone.Core/Profiles/Quality/Profile.cs b/src/NzbDrone.Core/Profiles/Quality/Profile.cs new file mode 100644 index 000000000..d453d83d9 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/Profile.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class Profile : ModelBase + { + public string Name { get; set; } + public Quality Cutoff { get; set; } + public List<ProfileQualityItem> Items { get; set; } + + public Quality LastAllowedQuality() + { + return Items.Last(q => q.Allowed).Quality; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs new file mode 100644 index 000000000..b56c9f1db --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileInUseException.cs @@ -0,0 +1,13 @@ +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class ProfileInUseException : NzbDroneException + { + public ProfileInUseException(int profileId) + : base("Profile [{0}] is in use.", profileId) + { + + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs new file mode 100644 index 000000000..338654854 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileQualityItem.cs @@ -0,0 +1,11 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Qualities; + +namespace NzbDrone.Core.Profiles.Qualities +{ + public class ProfileQualityItem : IEmbeddedDocument + { + public Quality Quality { get; set; } + public bool Allowed { get; set; } + } +} diff --git a/src/NzbDrone.Core/Profiles/ProfileRepository.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs similarity index 88% rename from src/NzbDrone.Core/Profiles/ProfileRepository.cs rename to src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs index 4e071a0cf..1724d7a4f 100644 --- a/src/NzbDrone.Core/Profiles/ProfileRepository.cs +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileRepository.cs @@ -1,7 +1,7 @@ -using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Messaging.Events; -namespace NzbDrone.Core.Profiles +namespace NzbDrone.Core.Profiles.Qualities { public interface IProfileRepository : IBasicRepository<Profile> { diff --git a/src/NzbDrone.Core/Profiles/ProfileService.cs b/src/NzbDrone.Core/Profiles/Quality/ProfileService.cs similarity index 95% rename from src/NzbDrone.Core/Profiles/ProfileService.cs rename to src/NzbDrone.Core/Profiles/Quality/ProfileService.cs index 671d4127f..8461d25c6 100644 --- a/src/NzbDrone.Core/Profiles/ProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Quality/ProfileService.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; -using NzbDrone.Core.Parser; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Music; -namespace NzbDrone.Core.Profiles +namespace NzbDrone.Core.Profiles.Qualities { public interface IProfileService { @@ -75,7 +74,7 @@ namespace NzbDrone.Core.Profiles .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) }) .ToList(); - var profile = new Profile { Name = name, Cutoff = cutoff, Items = items, Language = Language.English }; + var profile = new Profile { Name = name, Cutoff = cutoff, Items = items}; return Add(profile); } @@ -103,4 +102,4 @@ namespace NzbDrone.Core.Profiles Quality.MP3_320); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index d2fc46e3c..edff516eb 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Lifecycle; @@ -11,6 +11,7 @@ namespace NzbDrone.Core.Qualities public interface IQualityDefinitionService { void Update(QualityDefinition qualityDefinition); + void UpdateMany(List<QualityDefinition> qualityDefinitions); List<QualityDefinition> All(); QualityDefinition GetById(int id); QualityDefinition Get(Quality quality); @@ -41,6 +42,11 @@ namespace NzbDrone.Core.Qualities _cache.Clear(); } + public void UpdateMany(List<QualityDefinition> qualityDefinitions) + { + _repo.UpdateMany(qualityDefinitions); + } + public List<QualityDefinition> All() { return GetAll().Values.OrderBy(d => d.Weight).ToList(); @@ -50,17 +56,17 @@ namespace NzbDrone.Core.Qualities { return GetAll().Values.Single(v => v.Id == id); } - + public QualityDefinition Get(Quality quality) { return GetAll()[quality]; } - + private void InsertMissingDefinitions() { List<QualityDefinition> insertList = new List<QualityDefinition>(); List<QualityDefinition> updateList = new List<QualityDefinition>(); - + var allDefinitions = Quality.DefaultQualityDefinitions.OrderBy(d => d.Weight).ToList(); var existingDefinitions = _repo.All().ToList(); @@ -83,7 +89,7 @@ namespace NzbDrone.Core.Qualities _repo.InsertMany(insertList); _repo.UpdateMany(updateList); _repo.DeleteMany(existingDefinitions); - + _cache.Clear(); } diff --git a/src/NzbDrone.Core/Qualities/QualityModel.cs b/src/NzbDrone.Core/Qualities/QualityModel.cs index a483d22c2..b5658415b 100644 --- a/src/NzbDrone.Core/Qualities/QualityModel.cs +++ b/src/NzbDrone.Core/Qualities/QualityModel.cs @@ -1,17 +1,18 @@ -using System; +using System; +using System.Linq; using Newtonsoft.Json; using NzbDrone.Core.Datastore; namespace NzbDrone.Core.Qualities { - public class QualityModel : IEmbeddedDocument, IEquatable<QualityModel> + public class QualityModel : IEmbeddedDocument, IEquatable<QualityModel>, IComparable { public Quality Quality { get; set; } public Revision Revision { get; set; } [JsonIgnore] public QualitySource QualitySource { get; set; } - + public QualityModel() : this(Quality.Unknown, new Revision()) { @@ -40,6 +41,45 @@ namespace NzbDrone.Core.Qualities } } + public int CompareTo(object obj) + { + var other = (QualityModel)obj; + var definition = Quality.DefaultQualityDefinitions.First(q => q.Quality == Quality); + var otherDefinition = Quality.DefaultQualityDefinitions.First(q => q.Quality == other.Quality); + + if (definition.Weight > otherDefinition.Weight) + { + return 1; + } + + if (definition.Weight < otherDefinition.Weight) + { + return -1; + } + + if (Revision.Real > other.Revision.Real) + { + return 1; + } + + if (Revision.Real < other.Revision.Real) + { + return -1; + } + + if (Revision.Version > other.Revision.Version) + { + return 1; + } + + if (Revision.Version < other.Revision.Version) + { + return -1; + } + + return 0; + } + public bool Equals(QualityModel other) { if (ReferenceEquals(null, other)) return false; diff --git a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs index 64f1939b8..029089030 100644 --- a/src/NzbDrone.Core/Qualities/QualityModelComparer.cs +++ b/src/NzbDrone.Core/Qualities/QualityModelComparer.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Common.EnsureThat; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Qualities { diff --git a/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs new file mode 100644 index 000000000..e8c52e1ab --- /dev/null +++ b/src/NzbDrone.Core/Queue/EstimatedCompletionTimeComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class EstimatedCompletionTimeComparer : IComparer<DateTime?> + { + public int Compare(DateTime? x, DateTime? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Queue/Queue.cs b/src/NzbDrone.Core/Queue/Queue.cs index 54c949baa..f2ed3d679 100644 --- a/src/NzbDrone.Core/Queue/Queue.cs +++ b/src/NzbDrone.Core/Queue/Queue.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; using NzbDrone.Core.Download.TrackedDownloads; @@ -27,5 +27,8 @@ namespace NzbDrone.Core.Queue public string DownloadId { get; set; } public RemoteAlbum RemoteAlbum { get; set; } public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + public string ErrorMessage { get; set; } } } diff --git a/src/NzbDrone.Core/Queue/QueueService.cs b/src/NzbDrone.Core/Queue/QueueService.cs index e86ebee29..2b3a1ce7a 100644 --- a/src/NzbDrone.Core/Queue/QueueService.cs +++ b/src/NzbDrone.Core/Queue/QueueService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Common.Crypto; @@ -12,6 +12,7 @@ namespace NzbDrone.Core.Queue { List<Queue> GetQueue(); Queue Find(int id); + void Remove(int id); } public class QueueService : IQueueService, IHandle<TrackedDownloadRefreshedEvent> @@ -34,6 +35,11 @@ namespace NzbDrone.Core.Queue return _queue.SingleOrDefault(q => q.Id == id); } + public void Remove(int id) + { + _queue.Remove(Find(id)); + } + public void Handle(TrackedDownloadRefreshedEvent message) { _queue = message.TrackedDownloads.OrderBy(c => c.DownloadItem.RemainingTime).SelectMany(MapQueue) @@ -72,9 +78,12 @@ namespace NzbDrone.Core.Queue Status = trackedDownload.DownloadItem.Status.ToString(), TrackedDownloadStatus = trackedDownload.Status.ToString(), StatusMessages = trackedDownload.StatusMessages.ToList(), + ErrorMessage = trackedDownload.DownloadItem.Message, RemoteAlbum = trackedDownload.RemoteAlbum, DownloadId = trackedDownload.DownloadItem.DownloadId, - Protocol = trackedDownload.Protocol + Protocol = trackedDownload.Protocol, + DownloadClient = trackedDownload.DownloadItem.DownloadClient, + Indexer = trackedDownload.Indexer }; if (queue.Timeleft.HasValue) diff --git a/src/NzbDrone.Core/Queue/TimeleftComparer.cs b/src/NzbDrone.Core/Queue/TimeleftComparer.cs new file mode 100644 index 000000000..2c051deeb --- /dev/null +++ b/src/NzbDrone.Core/Queue/TimeleftComparer.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Core.Queue +{ + public class TimeleftComparer : IComparer<TimeSpan?> + { + public int Compare(TimeSpan? x, TimeSpan? y) + { + if (!x.HasValue && !y.HasValue) + { + return 0; + } + + if (!x.HasValue && y.HasValue) + { + return 1; + } + + if (x.HasValue && !y.HasValue) + { + return -1; + } + + if (x.Value > y.Value) + { + return 1; + } + + if (x.Value < y.Value) + { + return -1; + } + + return 0; + } + } +} diff --git a/src/NzbDrone.Core/Restrictions/RestrictionService.cs b/src/NzbDrone.Core/Restrictions/RestrictionService.cs index 5d7cfba8d..90b9d4a6f 100644 --- a/src/NzbDrone.Core/Restrictions/RestrictionService.cs +++ b/src/NzbDrone.Core/Restrictions/RestrictionService.cs @@ -34,7 +34,7 @@ namespace NzbDrone.Core.Restrictions public List<Restriction> AllForTag(int tagId) { - return _repo.All().Where(r => r.Tags.Contains(tagId) || r.Tags.Empty()).ToList(); + return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList(); } public List<Restriction> AllForTags(HashSet<int> tagIds) diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs index 25a82d68f..0542ca94b 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatistics.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using NzbDrone.Core.Datastore; @@ -11,6 +11,7 @@ namespace NzbDrone.Core.SeriesStats public string PreviousAiringString { get; set; } public int EpisodeFileCount { get; set; } public int EpisodeCount { get; set; } + public int AvailableEpisodeCount { get; set; } public int TotalEpisodeCount { get; set; } public long SizeOnDisk { get; set; } public List<SeasonStatistics> SeasonStatistics { get; set; } diff --git a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs index 73e4e8b4b..a26e087d0 100644 --- a/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs +++ b/src/NzbDrone.Core/SeriesStats/SeriesStatisticsRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using NzbDrone.Core.Datastore; @@ -58,7 +58,8 @@ namespace NzbDrone.Core.SeriesStats (SELECT Episodes.SeriesId, Episodes.SeasonNumber, - SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS TotalEpisodeCount, + COUNT(*) AS TotalEpisodeCount, + SUM(CASE WHEN AirdateUtc <= @currentDate OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS AvailableEpisodeCount, SUM(CASE WHEN (Monitored = 1 AND AirdateUtc <= @currentDate) OR EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeCount, SUM(CASE WHEN EpisodeFileId > 0 THEN 1 ELSE 0 END) AS EpisodeFileCount, MIN(CASE WHEN AirDateUtc < @currentDate OR EpisodeFileId > 0 OR Monitored = 0 THEN NULL ELSE AirDateUtc END) AS NextAiringString, diff --git a/src/NzbDrone.Core/Tags/TagDetails.cs b/src/NzbDrone.Core/Tags/TagDetails.cs new file mode 100644 index 000000000..963fd6003 --- /dev/null +++ b/src/NzbDrone.Core/Tags/TagDetails.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Tags +{ + public class TagDetails : ModelBase + { + public string Label { get; set; } + public List<Series> Series { get; set; } + public List<NotificationDefinition> Notifications { get; set; } + public List<Restriction> Restrictions { get; set; } + public List<DelayProfile> DelayProfiles { get; set; } + } +} diff --git a/src/NzbDrone.Core/Tags/TagRepository.cs b/src/NzbDrone.Core/Tags/TagRepository.cs index 500502843..2921ca7c8 100644 --- a/src/NzbDrone.Core/Tags/TagRepository.cs +++ b/src/NzbDrone.Core/Tags/TagRepository.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Core.Tags public interface ITagRepository : IBasicRepository<Tag> { Tag GetByLabel(string label); + Tag FindByLabel(string label); } public class TagRepository : BasicRepository<Tag>, ITagRepository @@ -28,5 +29,10 @@ namespace NzbDrone.Core.Tags return model; } + + public Tag FindByLabel(string label) + { + return Query.Where(c => c.Label == label).SingleOrDefault(); + } } } diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index b7637a6f8..05a6f156c 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -1,6 +1,10 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Notifications; +using NzbDrone.Core.Profiles.Delay; +using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Tv; namespace NzbDrone.Core.Tags { @@ -8,6 +12,7 @@ namespace NzbDrone.Core.Tags { Tag GetTag(int tagId); Tag GetTag(string tag); + TagDetails Details(int tagId); List<Tag> All(); Tag Add(Tag tag); Tag Update(Tag tag); @@ -18,11 +23,24 @@ namespace NzbDrone.Core.Tags { private readonly ITagRepository _repo; private readonly IEventAggregator _eventAggregator; + private readonly IDelayProfileService _delayProfileService; + private readonly INotificationFactory _notificationFactory; + private readonly IRestrictionService _restrictionService; + private readonly ISeriesService _seriesService; - public TagService(ITagRepository repo, IEventAggregator eventAggregator) + public TagService(ITagRepository repo, + IEventAggregator eventAggregator, + IDelayProfileService delayProfileService, + INotificationFactory notificationFactory, + IRestrictionService restrictionService, + ISeriesService seriesService) { _repo = repo; _eventAggregator = eventAggregator; + _delayProfileService = delayProfileService; + _notificationFactory = notificationFactory; + _restrictionService = restrictionService; + _seriesService = seriesService; } public Tag GetTag(int tagId) @@ -42,6 +60,25 @@ namespace NzbDrone.Core.Tags } } + public TagDetails Details(int tagId) + { + var tag = GetTag(tagId); + var delayProfiles = _delayProfileService.AllForTag(tagId); + var notifications = _notificationFactory.AllForTag(tagId); + var restrictions = _restrictionService.AllForTag(tagId); + var series = _seriesService.AllForTag(tagId); + + return new TagDetails + { + Id = tagId, + Label = tag.Label, + DelayProfiles = delayProfiles, + Notifications = notifications, + Restrictions = restrictions, + Series = series + }; + } + public List<Tag> All() { return _repo.All().OrderBy(t => t.Label).ToList(); @@ -49,7 +86,12 @@ namespace NzbDrone.Core.Tags public Tag Add(Tag tag) { - //TODO: check for duplicate tag by label and return that tag instead? + var existingTag = _repo.FindByLabel(tag.Label); + + if (existingTag != null) + { + return existingTag; + } tag.Label = tag.Label.ToLowerInvariant(); diff --git a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs index ce6519e1b..2627cec14 100644 --- a/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/IProviderFactory.cs @@ -20,5 +20,6 @@ namespace NzbDrone.Core.ThingiProvider TProvider GetInstance(TProviderDefinition definition); ValidationResult Test(TProviderDefinition definition); object RequestAction(TProviderDefinition definition, string action, IDictionary<string, string> query); + List<TProviderDefinition> AllForTag(int tagId); } } \ No newline at end of file diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs index 45bd5a25a..d83c7dfda 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderDefinition.cs @@ -1,9 +1,15 @@ -using NzbDrone.Core.Datastore; +using System.Collections.Generic; +using NzbDrone.Core.Datastore; namespace NzbDrone.Core.ThingiProvider { public abstract class ProviderDefinition : ModelBase { + protected ProviderDefinition() + { + Tags = new HashSet<int>(); + } + private IProviderConfig _settings; public string Name { get; set; } @@ -12,6 +18,7 @@ namespace NzbDrone.Core.ThingiProvider public string ConfigContract { get; set; } public virtual bool Enable { get; set; } public ProviderMessage Message { get; set; } + public HashSet<int> Tags { get; set; } public IProviderConfig Settings { diff --git a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs index 0c64aa994..db98416d3 100644 --- a/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs +++ b/src/NzbDrone.Core/ThingiProvider/ProviderFactory.cs @@ -76,7 +76,7 @@ namespace NzbDrone.Core.ThingiProvider return definitions; } - public ValidationResult Test(TProviderDefinition definition) + public virtual ValidationResult Test(TProviderDefinition definition) { return GetInstance(definition).Test(); } @@ -167,5 +167,11 @@ namespace NzbDrone.Core.ThingiProvider _providerRepository.Delete(invalidDefinition); } } + + public List<TProviderDefinition> AllForTag(int tagId) + { + return All().Where(p => p.Tags.Contains(tagId)) + .ToList(); + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs b/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs new file mode 100644 index 000000000..304613d58 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/EscalationBackOff.cs @@ -0,0 +1,18 @@ +namespace NzbDrone.Core.ThingiProvider.Status +{ + public static class EscalationBackOff + { + public static readonly int[] Periods = + { + 0, + 5 * 60, + 15 * 60, + 30 * 60, + 60 * 60, + 3 * 60 * 60, + 6 * 60 * 60, + 12 * 60 * 60, + 24 * 60 * 60 + }; + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs new file mode 100644 index 000000000..cbf101f39 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusBase.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public abstract class ProviderStatusBase : ModelBase + { + public int ProviderId { get; set; } + + public DateTime? InitialFailure { get; set; } + public DateTime? MostRecentFailure { get; set; } + public int EscalationLevel { get; set; } + public DateTime? DisabledTill { get; set; } + + public virtual bool IsDisabled() + { + return DisabledTill.HasValue && DisabledTill.Value > DateTime.UtcNow; + } + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs new file mode 100644 index 000000000..2c1b184f1 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusRepository.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public interface IProviderStatusRepository<TModel> : IBasicRepository<TModel> + where TModel : ProviderStatusBase, new() + { + TModel FindByProviderId(int providerId); + } + + public class ProviderStatusRepository<TModel> : BasicRepository<TModel>, IProviderStatusRepository<TModel> + where TModel : ProviderStatusBase, new() + + { + public ProviderStatusRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public TModel FindByProviderId(int providerId) + { + return Query.Where(c => c.ProviderId == providerId).SingleOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs new file mode 100644 index 000000000..b74153947 --- /dev/null +++ b/src/NzbDrone.Core/ThingiProvider/Status/ProviderStatusServiceBase.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.ThingiProvider.Status +{ + public interface IProviderStatusServiceBase<TModel> + where TModel : ProviderStatusBase, new() + { + List<TModel> GetBlockedProviders(); + void RecordSuccess(int providerId); + void RecordFailure(int providerId, TimeSpan minimumBackOff = default(TimeSpan)); + void RecordConnectionFailure(int providerId); + } + + public abstract class ProviderStatusServiceBase<TProvider, TModel> : IProviderStatusServiceBase<TModel>, IHandleAsync<ProviderDeletedEvent<TProvider>> + where TProvider : IProvider + where TModel : ProviderStatusBase, new() + { + protected readonly object _syncRoot = new object(); + + protected readonly IProviderStatusRepository<TModel> _providerStatusRepository; + protected readonly IEventAggregator _eventAggregator; + protected readonly Logger _logger; + + protected int MaximumEscalationLevel { get; set; } = EscalationBackOff.Periods.Length - 1; + protected TimeSpan MinimumTimeSinceInitialFailure { get; set; } = TimeSpan.Zero; + + public ProviderStatusServiceBase(IProviderStatusRepository<TModel> providerStatusRepository, IEventAggregator eventAggregator, Logger logger) + { + _providerStatusRepository = providerStatusRepository; + _eventAggregator = eventAggregator; + _logger = logger; + } + + public virtual List<TModel> GetBlockedProviders() + { + return _providerStatusRepository.All().Where(v => v.IsDisabled()).ToList(); + } + + protected virtual TModel GetProviderStatus(int providerId) + { + return _providerStatusRepository.FindByProviderId(providerId) ?? new TModel { ProviderId = providerId }; + } + + protected virtual TimeSpan CalculateBackOffPeriod(TModel status) + { + var level = Math.Min(MaximumEscalationLevel, status.EscalationLevel); + + return TimeSpan.FromSeconds(EscalationBackOff.Periods[level]); + } + + public virtual void RecordSuccess(int providerId) + { + lock (_syncRoot) + { + var status = GetProviderStatus(providerId); + + if (status.EscalationLevel == 0) + { + return; + } + + status.EscalationLevel--; + status.DisabledTill = null; + + _providerStatusRepository.Upsert(status); + + _eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status)); + } + } + + protected virtual void RecordFailure(int providerId, TimeSpan minimumBackOff, bool escalate) + { + lock (_syncRoot) + { + var status = GetProviderStatus(providerId); + + var now = DateTime.UtcNow; + status.MostRecentFailure = now; + + if (status.EscalationLevel == 0) + { + status.InitialFailure = now; + status.EscalationLevel = 1; + escalate = false; + } + + var inGracePeriod = (status.InitialFailure.Value + MinimumTimeSinceInitialFailure) > now; + + if (escalate && !inGracePeriod) + { + status.EscalationLevel = Math.Min(MaximumEscalationLevel, status.EscalationLevel + 1); + } + + if (minimumBackOff != TimeSpan.Zero) + { + while (status.EscalationLevel < MaximumEscalationLevel && CalculateBackOffPeriod(status) < minimumBackOff) + { + status.EscalationLevel++; + } + } + + if (!inGracePeriod || minimumBackOff != TimeSpan.Zero) + { + status.DisabledTill = now + CalculateBackOffPeriod(status); + } + + _providerStatusRepository.Upsert(status); + + _eventAggregator.PublishEvent(new ProviderStatusChangedEvent<TProvider>(providerId, status)); + } + } + + public virtual void RecordFailure(int providerId, TimeSpan minimumBackOff = default(TimeSpan)) + { + RecordFailure(providerId, minimumBackOff, true); + } + + public virtual void RecordConnectionFailure(int providerId) + { + RecordFailure(providerId, default(TimeSpan), false); + } + + public virtual void HandleAsync(ProviderDeletedEvent<TProvider> message) + { + var providerStatus = _providerStatusRepository.FindByProviderId(message.ProviderId); + + if (providerStatus != null) + { + _providerStatusRepository.Delete(providerStatus); + } + } + } +} diff --git a/src/NzbDrone.Core/Tv/AddSeriesService.cs b/src/NzbDrone.Core/Tv/AddSeriesService.cs index 45d70e734..0a4a593d2 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesService.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -16,6 +16,7 @@ namespace NzbDrone.Core.Tv public interface IAddSeriesService { Series AddSeries(Series newSeries); + List<Series> AddSeries(List<Series> newSeries); } public class AddSeriesService : IAddSeriesService @@ -44,28 +45,28 @@ namespace NzbDrone.Core.Tv Ensure.That(newSeries, () => newSeries).IsNotNull(); newSeries = AddSkyhookData(newSeries); + newSeries = SetPropertiesAndValidate(newSeries); - if (string.IsNullOrWhiteSpace(newSeries.Path)) - { - //var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); - //newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); - } - - newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle(); - newSeries.SortTitle = SeriesTitleNormalizer.Normalize(newSeries.Title, newSeries.TvdbId); - newSeries.Added = DateTime.UtcNow; + _logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path); + _seriesService.AddSeries(newSeries); - var validationResult = _addSeriesValidator.Validate(newSeries); + return newSeries; + } - if (!validationResult.IsValid) + public List<Series> AddSeries(List<Series> newSeries) + { + var added = DateTime.UtcNow; + var seriesToAdd = new List<Series>(); + foreach (var s in newSeries) { - throw new ValidationException(validationResult.Errors); + // TODO: Verify if adding skyhook data will be slow + var series = AddSkyhookData(s); + series = SetPropertiesAndValidate(series); + series.Added = added; + seriesToAdd.Add(series); } + return _seriesService.AddSeries(seriesToAdd); - _logger.Info("Adding Series {0} Path: [{1}]", newSeries, newSeries.Path); - _seriesService.AddSeries(newSeries); - - return newSeries; } private Series AddSkyhookData(Series newSeries) @@ -79,7 +80,7 @@ namespace NzbDrone.Core.Tv catch (SeriesNotFoundException) { _logger.Error("tvdbid {1} was not found, it may have been removed from TheTVDB.", newSeries.TvdbId); - + throw new ValidationException(new List<ValidationFailure> { new ValidationFailure("TvdbId", "A series with this ID was not found", newSeries.TvdbId) @@ -95,5 +96,27 @@ namespace NzbDrone.Core.Tv return series; } + + private Series SetPropertiesAndValidate(Series newSeries) + { + if (string.IsNullOrWhiteSpace(newSeries.Path)) + { + //var folderName = _fileNameBuilder.GetSeriesFolder(newSeries); + //newSeries.Path = Path.Combine(newSeries.RootFolderPath, folderName); + } + + newSeries.CleanTitle = newSeries.Title.CleanSeriesTitle(); + newSeries.SortTitle = SeriesTitleNormalizer.Normalize(newSeries.Title, newSeries.TvdbId); + newSeries.Added = DateTime.UtcNow; + + var validationResult = _addSeriesValidator.Validate(newSeries); + + if (!validationResult.IsValid) + { + throw new ValidationException(validationResult.Errors); + } + + return newSeries; + } } } diff --git a/src/NzbDrone.Core/Tv/AddSeriesValidator.cs b/src/NzbDrone.Core/Tv/AddSeriesValidator.cs index 418815be6..a4fda6b4a 100644 --- a/src/NzbDrone.Core/Tv/AddSeriesValidator.cs +++ b/src/NzbDrone.Core/Tv/AddSeriesValidator.cs @@ -1,4 +1,4 @@ -using FluentValidation; +using FluentValidation; using FluentValidation.Results; using NzbDrone.Core.Validation.Paths; @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Tv { public AddSeriesValidator(RootFolderValidator rootFolderValidator, SeriesPathValidator seriesPathValidator, - DroneFactoryValidator droneFactoryValidator, SeriesAncestorValidator seriesAncestorValidator, SeriesTitleSlugValidator seriesTitleSlugValidator) { @@ -21,7 +20,6 @@ namespace NzbDrone.Core.Tv .IsValidPath() .SetValidator(rootFolderValidator) .SetValidator(seriesPathValidator) - .SetValidator(droneFactoryValidator) .SetValidator(seriesAncestorValidator); RuleFor(c => c.TitleSlug).SetValidator(seriesTitleSlugValidator); diff --git a/src/NzbDrone.Core/Tv/Episode.cs b/src/NzbDrone.Core/Tv/Episode.cs index dcb95069e..60f6a16e5 100644 --- a/src/NzbDrone.Core/Tv/Episode.cs +++ b/src/NzbDrone.Core/Tv/Episode.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using NzbDrone.Common.Extensions; @@ -7,7 +7,7 @@ using NzbDrone.Core.MediaFiles; namespace NzbDrone.Core.Tv { - public class Episode : ModelBase + public class Episode : ModelBase, IComparable { public Episode() { @@ -45,5 +45,32 @@ namespace NzbDrone.Core.Tv { return string.Format("[{0}]{1}", Id, Title.NullSafe()); } + + public int CompareTo(object obj) + { + var other = (Episode)obj; + + if (SeasonNumber > other.SeasonNumber) + { + return 1; + } + + if (SeasonNumber < other.SeasonNumber) + { + return -1; + } + + if (EpisodeNumber > other.EpisodeNumber) + { + return 1; + } + + if (EpisodeNumber < other.EpisodeNumber) + { + return -1; + } + + return 0; + } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs index 6747aa87e..b683a9913 100644 --- a/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeCutoffService.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Tv @@ -16,20 +18,24 @@ namespace NzbDrone.Core.Tv { private readonly IEpisodeRepository _episodeRepository; private readonly IProfileService _profileService; + private readonly ILanguageProfileService _languageProfileService; private readonly Logger _logger; - public EpisodeCutoffService(IEpisodeRepository episodeRepository, IProfileService profileService, Logger logger) + public EpisodeCutoffService(IEpisodeRepository episodeRepository, IProfileService profileService, ILanguageProfileService languageProfileService, Logger logger) { _episodeRepository = episodeRepository; _profileService = profileService; + _languageProfileService = languageProfileService; _logger = logger; } public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec) { var qualitiesBelowCutoff = new List<QualitiesBelowCutoff>(); + var languagesBelowCutoff = new List<LanguagesBelowCutoff>(); var profiles = _profileService.All(); - + var languageProfiles = _languageProfileService.All(); + //Get all items less than the cutoff foreach (var profile in profiles) { @@ -42,7 +48,18 @@ namespace NzbDrone.Core.Tv } } - return _episodeRepository.EpisodesWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, false); + foreach (var profile in languageProfiles) + { + var languageCutoffIndex = profile.Languages.FindIndex(v => v.Language == profile.Cutoff); + var belowLanguageCutoff = profile.Languages.Take(languageCutoffIndex).ToList(); + + if (belowLanguageCutoff.Any()) + { + languagesBelowCutoff.Add(new LanguagesBelowCutoff(profile.Id, belowLanguageCutoff.Select(l => l.Language.Id))); + } + } + + return _episodeRepository.EpisodesWhereCutoffUnmet(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff, false); } } } diff --git a/src/NzbDrone.Core/Tv/EpisodeRepository.cs b/src/NzbDrone.Core/Tv/EpisodeRepository.cs index 5a1f413ad..eb9c0ba38 100644 --- a/src/NzbDrone.Core/Tv/EpisodeRepository.cs +++ b/src/NzbDrone.Core/Tv/EpisodeRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Marr.Data.QGen; @@ -8,6 +8,7 @@ using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Languages; using NzbDrone.Core.Qualities; namespace NzbDrone.Core.Tv @@ -23,12 +24,13 @@ namespace NzbDrone.Core.Tv List<Episode> GetEpisodeByFileId(int fileId); List<Episode> EpisodesWithFiles(int seriesId); PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec, bool includeSpecials); - PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials); + PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, List<LanguagesBelowCutoff> languagesBelowCutoff, bool includeSpecials); List<Episode> FindEpisodesBySceneNumbering(int seriesId, int seasonNumber, int episodeNumber); Episode FindEpisodeBySceneNumbering(int seriesId, int sceneAbsoluteEpisodeNumber); List<Episode> EpisodesBetweenDates(DateTime startDate, DateTime endDate, bool includeUnmonitored); void SetMonitoredFlat(Episode episode, bool monitored); void SetMonitoredBySeason(int seriesId, int seasonNumber, bool monitored); + void SetMonitored(IEnumerable<int> ids, bool monitored); void SetFileId(int episodeId, int fileId); } @@ -115,7 +117,7 @@ namespace NzbDrone.Core.Tv return pagingSpec; } - public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, bool includeSpecials) + public PagingSpec<Episode> EpisodesWhereCutoffUnmet(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, List<LanguagesBelowCutoff> languagesBelowCutoff, bool includeSpecials) { var startingSeasonNumber = 1; @@ -124,9 +126,8 @@ namespace NzbDrone.Core.Tv startingSeasonNumber = 0; } - pagingSpec.TotalRecords = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, startingSeasonNumber).GetRowCount(); - pagingSpec.Records = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, startingSeasonNumber).ToList(); - + pagingSpec.TotalRecords = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff, startingSeasonNumber).GetRowCount(); + pagingSpec.Records = EpisodesWhereCutoffUnmetQuery(pagingSpec, qualitiesBelowCutoff, languagesBelowCutoff, startingSeasonNumber).ToList(); return pagingSpec; } @@ -189,6 +190,19 @@ namespace NzbDrone.Core.Tv mapper.ExecuteNonQuery(sql); } + public void SetMonitored(IEnumerable<int> ids, bool monitored) + { + var mapper = _database.GetDataMapper(); + + mapper.AddParameter("monitored", monitored); + + var sql = "UPDATE Episodes " + + "SET Monitored = @monitored " + + $"WHERE Id IN ({string.Join(", ", ids)})"; + + mapper.ExecuteNonQuery(sql); + } + public void SetFileId(int episodeId, int fileId) { SetFields(new Episode { Id = episodeId, EpisodeFileId = fileId }, episode => episode.EpisodeFileId); @@ -206,14 +220,17 @@ namespace NzbDrone.Core.Tv .Take(pagingSpec.PageSize); } - private SortBuilder<Episode> EpisodesWhereCutoffUnmetQuery(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, int startingSeasonNumber) + private SortBuilder<Episode> EpisodesWhereCutoffUnmetQuery(PagingSpec<Episode> pagingSpec, List<QualitiesBelowCutoff> qualitiesBelowCutoff, List<LanguagesBelowCutoff> languagesBelowCutoff, int startingSeasonNumber) { return Query.Join<Episode, Series>(JoinType.Inner, e => e.Series, (e, s) => e.SeriesId == s.Id) .Join<Episode, EpisodeFile>(JoinType.Left, e => e.EpisodeFile, (e, s) => e.EpisodeFileId == s.Id) .Where(pagingSpec.FilterExpression) .AndWhere(e => e.EpisodeFileId != 0) .AndWhere(e => e.SeasonNumber >= startingSeasonNumber) - .AndWhere(BuildQualityCutoffWhereClause(qualitiesBelowCutoff)) + .AndWhere( + String.Format("({0} OR {1})", + BuildLanguageCutoffWhereClause(languagesBelowCutoff), + BuildQualityCutoffWhereClause(qualitiesBelowCutoff))) .OrderBy(pagingSpec.OrderByClause(), pagingSpec.ToSortDirection()) .Skip(pagingSpec.PagingOffset()) .Take(pagingSpec.PageSize); @@ -225,6 +242,21 @@ namespace NzbDrone.Core.Tv currentTime.ToString("yyyy-MM-dd HH:mm:ss")); } + private string BuildLanguageCutoffWhereClause(List<LanguagesBelowCutoff> languagesBelowCutoff) + { + var clauses = new List<String>(); + + foreach (var language in languagesBelowCutoff) + { + foreach (var belowCutoff in language.LanguageIds) + { + clauses.Add(String.Format("([t1].[LanguageProfileId] = {0} AND [t2].[Language] = {1})", language.ProfileId, belowCutoff)); + } + } + + return String.Format("({0})", String.Join(" OR ", clauses)); + } + private string BuildQualityCutoffWhereClause(List<QualitiesBelowCutoff> qualitiesBelowCutoff) { var clauses = new List<string>(); diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs index 5a117ab11..800cf3e0f 100644 --- a/src/NzbDrone.Core/Tv/EpisodeService.cs +++ b/src/NzbDrone.Core/Tv/EpisodeService.cs @@ -29,6 +29,7 @@ namespace NzbDrone.Core.Tv List<Episode> GetEpisodesByFileId(int episodeFileId); void UpdateEpisode(Episode episode); void SetEpisodeMonitored(int episodeId, bool monitored); + void SetMonitored(IEnumerable<int> ids, bool monitored); void UpdateEpisodes(List<Episode> episodes); List<Episode> EpisodesBetweenDates(DateTime start, DateTime end, bool includeUnmonitored); void InsertMany(List<Episode> episodes); @@ -159,6 +160,11 @@ namespace NzbDrone.Core.Tv _logger.Debug("Monitored flag for Episode:{0} was set to {1}", episodeId, monitored); } + public void SetMonitored(IEnumerable<int> ids, bool monitored) + { + _episodeRepository.SetMonitored(ids, monitored); + } + public void SetEpisodeMonitoredBySeason(int seriesId, int seasonNumber, bool monitored) { _episodeRepository.SetMonitoredBySeason(seriesId, seasonNumber, monitored); @@ -222,4 +228,4 @@ namespace NzbDrone.Core.Tv } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs b/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs new file mode 100644 index 000000000..2810c05ed --- /dev/null +++ b/src/NzbDrone.Core/Tv/Events/SeriesImportedEvent.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.Tv.Events +{ + public class SeriesImportedEvent : IEvent + { + public List<int> SeriesIds { get; private set; } + + public SeriesImportedEvent(List<int> seriesIds) + { + SeriesIds = seriesIds; + } + } +} diff --git a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs index 39e32dbe7..1706cb2ca 100644 --- a/src/NzbDrone.Core/Tv/RefreshSeriesService.cs +++ b/src/NzbDrone.Core/Tv/RefreshSeriesService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -48,7 +48,7 @@ namespace NzbDrone.Core.Tv private void RefreshSeriesInfo(Series series) { - _logger.ProgressInfo("Updating Info for {0}", series.Title); + _logger.ProgressInfo("Updating {0}", series.Title); Tuple<Series, List<Episode>> tuple; diff --git a/src/NzbDrone.Core/Tv/Series.cs b/src/NzbDrone.Core/Tv/Series.cs index 8542d183b..66abd7fa5 100644 --- a/src/NzbDrone.Core/Tv/Series.cs +++ b/src/NzbDrone.Core/Tv/Series.cs @@ -1,9 +1,10 @@ -using System; +using System; using System.Collections.Generic; using Marr.Data; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Profiles.Languages; namespace NzbDrone.Core.Tv { @@ -30,6 +31,7 @@ namespace NzbDrone.Core.Tv public string AirTime { get; set; } public bool Monitored { get; set; } public int ProfileId { get; set; } + public int LanguageProfileId { get; set; } public bool SeasonFolder { get; set; } public DateTime? LastInfoSync { get; set; } public int Runtime { get; set; } @@ -48,6 +50,7 @@ namespace NzbDrone.Core.Tv public DateTime Added { get; set; } public DateTime? FirstAired { get; set; } public LazyLoaded<Profile> Profile { get; set; } + public LazyLoaded<LanguageProfile> LanguageProfile { get; set; } public List<Season> Seasons { get; set; } public HashSet<int> Tags { get; set; } @@ -65,6 +68,7 @@ namespace NzbDrone.Core.Tv Seasons = otherSeries.Seasons; Path = otherSeries.Path; ProfileId = otherSeries.ProfileId; + LanguageProfileId = otherSeries.LanguageProfileId; SeasonFolder = otherSeries.SeasonFolder; Monitored = otherSeries.Monitored; @@ -75,4 +79,4 @@ namespace NzbDrone.Core.Tv AddOptions = otherSeries.AddOptions; } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs b/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs index 2e7ee8005..7d884a343 100644 --- a/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs +++ b/src/NzbDrone.Core/Tv/SeriesAddedHandler.cs @@ -1,11 +1,13 @@ -using NzbDrone.Core.Messaging.Commands; +using System.Linq; +using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv.Commands; using NzbDrone.Core.Tv.Events; namespace NzbDrone.Core.Tv { - public class SeriesAddedHandler : IHandle<SeriesAddedEvent> + public class SeriesAddedHandler : IHandle<SeriesAddedEvent>, + IHandle<SeriesImportedEvent> { private readonly IManageCommandQueue _commandQueueManager; @@ -18,5 +20,10 @@ namespace NzbDrone.Core.Tv { _commandQueueManager.Push(new RefreshSeriesCommand(message.Series.Id)); } + + public void Handle(SeriesImportedEvent message) + { + _commandQueueManager.PushMany(message.SeriesIds.Select(s => new RefreshSeriesCommand(s)).ToList()); + } } } diff --git a/src/NzbDrone.Core/Tv/SeriesService.cs b/src/NzbDrone.Core/Tv/SeriesService.cs index ad17301ab..62499d58f 100644 --- a/src/NzbDrone.Core/Tv/SeriesService.cs +++ b/src/NzbDrone.Core/Tv/SeriesService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -18,6 +18,7 @@ namespace NzbDrone.Core.Tv Series GetSeries(int seriesId); List<Series> GetSeries(IEnumerable<int> seriesIds); Series AddSeries(Series newSeries); + List<Series> AddSeries(List<Series> newSeries); Series FindByTvdbId(int tvdbId); Series FindByTvRageId(int tvRageId); Series FindByTitle(string title); @@ -25,6 +26,7 @@ namespace NzbDrone.Core.Tv Series FindByTitleInexact(string title); void DeleteSeries(int seriesId, bool deleteFiles); List<Series> GetAllSeries(); + List<Series> AllForTag(int tagId); Series UpdateSeries(Series series); List<Series> UpdateSeries(List<Series> series); bool SeriesPathExists(string folder); @@ -73,6 +75,14 @@ namespace NzbDrone.Core.Tv return newSeries; } + public List<Series> AddSeries(List<Series> newSeries) + { + _seriesRepository.InsertMany(newSeries); + _eventAggregator.PublishEvent(new SeriesImportedEvent(newSeries.Select(s => s.Id).ToList())); + + return newSeries; + } + public Series FindByTvdbId(int tvRageId) { return _seriesRepository.FindByTvdbId(tvRageId); @@ -154,6 +164,13 @@ namespace NzbDrone.Core.Tv return _seriesRepository.All().ToList(); } + public List<Series> AllForTag(int tagId) + { + return GetAllSeries().Where(s => s.Tags.Contains(tagId)) + .ToList(); + } + + public Series UpdateSeries(Series series) { var storedSeries = GetSeries(series.Id); diff --git a/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs b/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs new file mode 100644 index 000000000..90450e3e5 --- /dev/null +++ b/src/NzbDrone.Core/Validation/LanguageProfileExistsValidator.cs @@ -0,0 +1,23 @@ +using FluentValidation.Validators; +using NzbDrone.Core.Profiles.Languages; + +namespace NzbDrone.Core.Validation +{ + public class LanguageProfileExistsValidator : PropertyValidator + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileExistsValidator(ILanguageProfileService profileService) + : base("Language profile does not exist") + { + _profileService = profileService; + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return true; + + return _profileService.Exists((int)context.PropertyValue); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/LanguageValidator.cs b/src/NzbDrone.Core/Validation/LanguageValidator.cs deleted file mode 100644 index 9edfc9085..000000000 --- a/src/NzbDrone.Core/Validation/LanguageValidator.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FluentValidation.Validators; - -namespace NzbDrone.Core.Validation -{ - public class LanguageValidator : PropertyValidator - { - public LanguageValidator() - : base("Unknown Language") - { - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return false; - - if ((int) context.PropertyValue == 0) return false; - - return true; - } - } -} diff --git a/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs b/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs deleted file mode 100644 index 7637b74d2..000000000 --- a/src/NzbDrone.Core/Validation/Paths/DroneFactoryValidator.cs +++ /dev/null @@ -1,28 +0,0 @@ -using FluentValidation.Validators; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Configuration; - -namespace NzbDrone.Core.Validation.Paths -{ - public class DroneFactoryValidator : PropertyValidator - { - private readonly IConfigService _configService; - - public DroneFactoryValidator(IConfigService configService) - : base("Path is already used for drone factory") - { - _configService = configService; - } - - protected override bool IsValid(PropertyValidatorContext context) - { - if (context.PropertyValue == null) return false; - - var droneFactory = _configService.DownloadedAlbumsFolder; - - if (string.IsNullOrWhiteSpace(droneFactory)) return true; - - return !droneFactory.PathEquals(context.PropertyValue.ToString()); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs b/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs index e7ff62b67..0eb5293dd 100644 --- a/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs +++ b/src/NzbDrone.Core/Validation/ProfileExistsValidator.cs @@ -1,5 +1,5 @@ using FluentValidation.Validators; -using NzbDrone.Core.Profiles; +using NzbDrone.Core.Profiles.Qualities; namespace NzbDrone.Core.Validation { diff --git a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs index fe4d97860..fb4f14c32 100644 --- a/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs +++ b/src/NzbDrone.Core/Validation/RuleBuilderExtensions.cs @@ -1,4 +1,4 @@ -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; using NzbDrone.Core.Parser; @@ -34,9 +34,9 @@ namespace NzbDrone.Core.Validation return ruleBuilder.SetValidator(new RegularExpressionValidator("^https?://[-_a-z0-9.]+", RegexOptions.IgnoreCase)).WithMessage("must be valid URL that starts with http(s)://"); } - public static IRuleBuilderOptions<T, string> ValidUrlBase<T>(this IRuleBuilder<T, string> ruleBuilder) + public static IRuleBuilderOptions<T, string> ValidUrlBase<T>(this IRuleBuilder<T, string> ruleBuilder, string example = "/lidarr") { - return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!\/?https?://[-_a-z0-9.]+)", RegexOptions.IgnoreCase)).WithMessage("Must be a valid URL path (ie: '/Lidarr')"); + return ruleBuilder.SetValidator(new RegularExpressionValidator(@"^(?!\/?https?://[-_a-z0-9.]+)", RegexOptions.IgnoreCase)).WithMessage($"Must be a valid URL path (ie: '{example}')"); } public static IRuleBuilderOptions<T, int> ValidPort<T>(this IRuleBuilder<T, int> ruleBuilder) @@ -58,14 +58,9 @@ namespace NzbDrone.Core.Validation }); } - public static IRuleBuilderOptions<T, Language> ValidLanguage<T>(this IRuleBuilder<T, Language> ruleBuilder) - { - return ruleBuilder.SetValidator(new LanguageValidator()); - } - public static IRuleBuilderOptions<T, TProp> AsWarning<T, TProp>(this IRuleBuilderOptions<T, TProp> ruleBuilder) { return ruleBuilder.WithState(v => NzbDroneValidationState.Warning); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/MainAppContainerBuilder.cs b/src/NzbDrone.Host/MainAppContainerBuilder.cs index 23ba6a0dd..ae470e087 100644 --- a/src/NzbDrone.Host/MainAppContainerBuilder.cs +++ b/src/NzbDrone.Host/MainAppContainerBuilder.cs @@ -1,6 +1,6 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Nancy.Bootstrapper; -using NzbDrone.Api; +using Lidarr.Http; using NzbDrone.Common.Composition; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Http.Dispatchers; @@ -17,7 +17,9 @@ namespace NzbDrone.Host "NzbDrone.Host", "NzbDrone.Core", "NzbDrone.Api", - "NzbDrone.SignalR" + "NzbDrone.SignalR", + "Sonarr.Api.V3", + "Sonarr.Http" }; return new MainAppContainerBuilder(args, assemblies).Container; @@ -28,8 +30,8 @@ namespace NzbDrone.Host { AutoRegisterImplementations<NzbDronePersistentConnection>(); - Container.Register<INancyBootstrapper, NancyBootstrapper>(); + Container.Register<INancyBootstrapper, LidarrBootstrapper>(); Container.Register<IHttpDispatcher, FallbackHttpDispatcher>(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Host/NzbDrone.Host.csproj b/src/NzbDrone.Host/NzbDrone.Host.csproj index 6a203bae9..294773c83 100644 --- a/src/NzbDrone.Host/NzbDrone.Host.csproj +++ b/src/NzbDrone.Host/NzbDrone.Host.csproj @@ -180,6 +180,14 @@ <Project>{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}</Project> <Name>NzbDrone.SignalR</Name> </ProjectReference> + <ProjectReference Include="..\Sonarr.Api.V3\Lidarr.Api.V3.csproj"> + <Project>{7140ff1f-79be-492f-9188-b21a050bf708}</Project> + <Name>Lidarr.Api.V3</Name> + </ProjectReference> + <ProjectReference Include="..\Sonarr.Http\Lidarr.Http.csproj"> + <Project>{5370bff7-1bd7-46bc-af06-7d9ea5cda1d6}</Project> + <Name>Lidarr.Http</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <Content Include="NzbDrone.ico" /> diff --git a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs index 0df60a326..4e25d61fa 100644 --- a/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs +++ b/src/NzbDrone.Host/Owin/MiddleWare/SignalRMiddleWare.cs @@ -14,6 +14,8 @@ namespace NzbDrone.Host.Owin.MiddleWare { SignalrDependencyResolver.Register(container); + // Half the default time (110s) to get under nginx's default 60 proxy_read_timeout + GlobalHost.Configuration.ConnectionTimeout = TimeSpan.FromSeconds(55); GlobalHost.Configuration.DisconnectTimeout = TimeSpan.FromMinutes(3); } @@ -22,4 +24,4 @@ namespace NzbDrone.Host.Owin.MiddleWare appBuilder.MapConnection("signalr", typeof(NzbDronePersistentConnection), new ConnectionConfiguration { EnableCrossDomain = true }); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/Client/ClientBase.cs b/src/NzbDrone.Integration.Test/Client/ClientBase.cs index 884fe992a..687168e40 100644 --- a/src/NzbDrone.Integration.Test/Client/ClientBase.cs +++ b/src/NzbDrone.Integration.Test/Client/ClientBase.cs @@ -1,12 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net; using FluentAssertions; using NLog; using NzbDrone.Api; -using NzbDrone.Api.REST; +using Lidarr.Http.REST; using NzbDrone.Common.Serializer; using RestSharp; using System.Linq; +using Lidarr.Http; namespace NzbDrone.Integration.Test.Client { @@ -176,4 +177,4 @@ namespace NzbDrone.Integration.Test.Client Execute<object>(request, statusCode); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Integration.Test/CorsFixture.cs b/src/NzbDrone.Integration.Test/CorsFixture.cs index 2d9d8ac4f..f2c0f378a 100644 --- a/src/NzbDrone.Integration.Test/CorsFixture.cs +++ b/src/NzbDrone.Integration.Test/CorsFixture.cs @@ -1,6 +1,6 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; using RestSharp; namespace NzbDrone.Integration.Test diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 488a081c6..65ff22e8b 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; using NLog; using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Test.Common; +using Lidarr.Http.ClientSchema; namespace NzbDrone.Integration.Test { @@ -33,7 +33,7 @@ namespace NzbDrone.Integration.Test Implementation = nameof(Newznab), Name = "NewznabTest", Protocol = Core.Indexers.DownloadProtocol.Usenet, - Fields = Api.ClientSchema.SchemaBuilder.ToSchema(new NewznabSettings()) + Fields = SchemaBuilder.ToSchema(new NewznabSettings()) }); } diff --git a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj index 52c43a3c6..49f6b285c 100644 --- a/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj +++ b/src/NzbDrone.Integration.Test/NzbDrone.Integration.Test.csproj @@ -164,6 +164,10 @@ <Project>{CADDFCE0-7509-4430-8364-2074E1EEFCA2}</Project> <Name>NzbDrone.Test.Common</Name> </ProjectReference> + <ProjectReference Include="..\Sonarr.Http\Lidarr.Http.csproj"> + <Project>{5370bff7-1bd7-46bc-af06-7d9ea5cda1d6}</Project> + <Name>Lidarr.Http</Name> + </ProjectReference> </ItemGroup> <ItemGroup> <Content Include="..\Libraries\Sqlite\sqlite3.dll"> diff --git a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs index b3342ffba..ff1a2dca8 100644 --- a/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs +++ b/src/NzbDrone.SignalR/NzbDronePersistentConnection.cs @@ -1,7 +1,12 @@ -using Microsoft.AspNet.SignalR; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Infrastructure; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Serializer; using NzbDrone.Common.Extensions; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore.Events; namespace NzbDrone.SignalR { @@ -15,17 +20,31 @@ namespace NzbDrone.SignalR private IPersistentConnectionContext Context => ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); private static string API_KEY; + private readonly Dictionary<string, string> _messageHistory; public NzbDronePersistentConnection(IConfigFileProvider configFileProvider) { API_KEY = configFileProvider.ApiKey; + _messageHistory = new Dictionary<string, string>(); } + public void BroadcastMessage(SignalRMessage message) { + string lastMessage; + if (_messageHistory.TryGetValue(message.Name, out lastMessage)) + { + if (message.Action == ModelAction.Updated && message.Body.ToJson() == lastMessage) + { + return; + } + } + + _messageHistory[message.Name] = message.Body.ToJson(); + Context.Connection.Broadcast(message); } - + protected override bool AuthorizeRequest(IRequest request) { var apiKey = request.QueryString["apiKey"]; @@ -37,5 +56,27 @@ namespace NzbDrone.SignalR return false; } + + protected override Task OnConnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + protected override Task OnReconnected(IRequest request, string connectionId) + { + return SendVersion(connectionId); + } + + private Task SendVersion(string connectionId) + { + return Context.Connection.Send(connectionId, new SignalRMessage + { + Name = "version", + Body = new + { + Version = BuildInfo.Version.ToString() + } + }); + } } } \ No newline at end of file diff --git a/src/NzbDrone.SignalR/SignalRMessage.cs b/src/NzbDrone.SignalR/SignalRMessage.cs index e8993c286..17a7d4187 100644 --- a/src/NzbDrone.SignalR/SignalRMessage.cs +++ b/src/NzbDrone.SignalR/SignalRMessage.cs @@ -1,8 +1,14 @@ +using Newtonsoft.Json; +using NzbDrone.Core.Datastore.Events; + namespace NzbDrone.SignalR { public class SignalRMessage { public object Body { get; set; } public string Name { get; set; } + + [JsonIgnore] + public ModelAction Action { get; set; } } } \ No newline at end of file diff --git a/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs new file mode 100644 index 000000000..e1c43ebc5 --- /dev/null +++ b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioArtistResource.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Lidarr.Api.V3.Albums; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioArtistResource + { + public int Id { get; set; } + public bool? Monitored { get; set; } + public List<AlbumResource> Albums { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioModule.cs b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioModule.cs new file mode 100644 index 000000000..71dc76920 --- /dev/null +++ b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioModule.cs @@ -0,0 +1,55 @@ +using System.Linq; +using Nancy; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioModule : SonarrV3Module + { + private readonly IArtistService _artistService; + private readonly IAlbumMonitoredService _episodeMonitoredService; + + public AlbumStudioModule(IArtistService artistService, IAlbumMonitoredService episodeMonitoredService) + : base("/albumstudio") + { + _artistService = artistService; + _episodeMonitoredService = episodeMonitoredService; + Post["/"] = artist => UpdateAll(); + } + + private Response UpdateAll() + { + //Read from request + var request = Request.Body.FromJson<AlbumStudioResource>(); + var artistToUpdate = _artistService.GetArtists(request.Artist.Select(s => s.Id)); + + foreach (var s in request.Artist) + { + var artist = artistToUpdate.Single(c => c.Id == s.Id); + + if (s.Monitored.HasValue) + { + artist.Monitored = s.Monitored.Value; + } + + if (s.Albums != null && s.Albums.Any()) + { + foreach (var artistAlbum in artist.Albums) + { + var album = s.Albums.FirstOrDefault(c => c.Id == artistAlbum.Id); + + if (album != null) + { + artistAlbum.Monitored = album.Monitored; + } + } + } + + _episodeMonitoredService.SetAlbumMonitoredStatus(artist, request.MonitoringOptions); + } + + return "ok".AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioResource.cs b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioResource.cs new file mode 100644 index 000000000..b03f0629e --- /dev/null +++ b/src/Sonarr.Api.V3/AlbumStudio/AlbumStudioResource.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using NzbDrone.Core.Music; + +namespace Lidarr.Api.V3.AlbumStudio +{ + public class AlbumStudioResource + { + public List<AlbumStudioArtistResource> Artist { get; set; } + public MonitoringOptions MonitoringOptions { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Albums/AlbumModule.cs b/src/Sonarr.Api.V3/Albums/AlbumModule.cs new file mode 100644 index 000000000..0f9a181e9 --- /dev/null +++ b/src/Sonarr.Api.V3/Albums/AlbumModule.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using NzbDrone.Core.ArtistStats; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumModule : AlbumModuleWithSignalR + { + public AlbumModule(IArtistService artistService, + IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetAlbums; + Put[@"/(?<id>[\d]{1,10})"] = x => SetAlbumMonitored(x.Id); + Put["/monitor"] = x => SetAlbumsMonitored(); + } + + private List<AlbumResource> GetAlbums() + { + if (!Request.Query.ArtistId.HasValue) + { + throw new BadRequestException("artistId is missing"); + } + + var artistId = (int)Request.Query.ArtistId; + + var resources = MapToResource(_albumService.GetAlbumsByArtist(artistId), false); + + return resources; + } + + private Response SetAlbumMonitored(int id) + { + var resource = Request.Body.FromJson<AlbumResource>(); + _albumService.SetAlbumMonitored(id, resource.Monitored); + + return MapToResource(_albumService.GetAlbum(id), false).AsResponse(HttpStatusCode.Accepted); + } + + private Response SetAlbumsMonitored() + { + var resource = Request.Body.FromJson<AlbumsMonitoredResource>(); + + _albumService.SetMonitored(resource.AlbumIds, resource.Monitored); + + return MapToResource(_albumService.GetAlbums(resource.AlbumIds), false).AsResponse(HttpStatusCode.Accepted); + } + } +} diff --git a/src/Sonarr.Api.V3/Albums/AlbumModuleWithSignalR.cs b/src/Sonarr.Api.V3/Albums/AlbumModuleWithSignalR.cs new file mode 100644 index 000000000..a12c965b5 --- /dev/null +++ b/src/Sonarr.Api.V3/Albums/AlbumModuleWithSignalR.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using Lidarr.Api.V3.Artist; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Albums +{ + public abstract class AlbumModuleWithSignalR : LidarrRestModuleWithSignalR<AlbumResource, Album> + { + protected readonly IAlbumService _albumService; + protected readonly IArtistStatisticsService _artistStatisticsService; + protected readonly IArtistService _artistService; + protected readonly IUpgradableSpecification _qualityUpgradableSpecification; + + protected AlbumModuleWithSignalR(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + _albumService = albumService; + _artistStatisticsService = artistStatisticsService; + _artistService = artistService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetAlbum; + } + + protected AlbumModuleWithSignalR(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification qualityUpgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster, + string resource) + : base(signalRBroadcaster, resource) + { + _albumService = albumService; + _artistStatisticsService = artistStatisticsService; + _artistService = artistService; + _qualityUpgradableSpecification = qualityUpgradableSpecification; + + GetResourceById = GetAlbum; + } + + protected AlbumResource GetAlbum(int id) + { + var album = _albumService.GetAlbum(id); + var resource = MapToResource(album, true); + return resource; + } + + protected AlbumResource MapToResource(Album album, bool includeArtist) + { + var resource = album.ToResource(); + + if (includeArtist) + { + var artist = album.Artist ?? _artistService.GetArtist(album.ArtistId); + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + } + + FetchAndLinkAlbumStatistics(resource); + + return resource; + } + + protected List<AlbumResource> MapToResource(List<Album> albums, bool includeArtist) + { + var result = albums.ToResource(); + + if (includeArtist) + { + var artistDict = new Dictionary<int, NzbDrone.Core.Music.Artist>(); + for (var i = 0; i < albums.Count; i++) + { + var album = albums[i]; + var resource = result[i]; + + var artist = album.Artist ?? artistDict.GetValueOrDefault(albums[i].ArtistId) ?? _artistService.GetArtist(albums[i].ArtistId); + artistDict[artist.Id] = artist; + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + } + } + + for (var i = 0; i < albums.Count; i++) + { + var resource = result[i]; + FetchAndLinkAlbumStatistics(resource); + } + + + return result; + } + + private void FetchAndLinkAlbumStatistics(AlbumResource resource) + { + LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.ArtistId)); + } + + private void LinkArtistStatistics(AlbumResource resource, ArtistStatistics artistStatistics) + { + if (artistStatistics.AlbumStatistics != null) + { + var dictAlbumStats = artistStatistics.AlbumStatistics.ToDictionary(v => v.AlbumId); + + resource.Statistics = dictAlbumStats.GetValueOrDefault(resource.Id).ToResource(); + + } + } + + //TODO: Implement Track or Album Grabbed/Dowloaded Events + + //public void Handle(TrackGrabbedEvent message) + //{ + // foreach (var track in message.Track.Tracks) + // { + // var resource = track.ToResource(); + // resource.Grabbed = true; + + // BroadcastResourceChange(ModelAction.Updated, resource); + // } + //} + + //public void Handle(TrackDownloadedEvent message) + //{ + // foreach (var album in message.Album.Albums) + // { + // BroadcastResourceChange(ModelAction.Updated, album.Id); + // } + //} + } +} diff --git a/src/Sonarr.Api.V3/Albums/AlbumResource.cs b/src/Sonarr.Api.V3/Albums/AlbumResource.cs new file mode 100644 index 000000000..4ac54ed7a --- /dev/null +++ b/src/Sonarr.Api.V3/Albums/AlbumResource.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Music; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; +using NzbDrone.Core.MediaCover; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumResource : RestResource + { + public string Title { get; set; } + public int ArtistId { get; set; } + public string Label { get; set; } + public bool Monitored { get; set; } + public string Path { get; set; } + public int ProfileId { get; set; } + public int Duration { get; set; } + public string AlbumType { get; set; } + public Ratings Ratings { get; set; } + public DateTime? ReleaseDate { get; set; } + public List<string> Genres { get; set; } + public ArtistResource Artist { get; set; } + public List<MediaCover> Images { get; set; } + public AlbumStatisticsResource Statistics { get; set; } + + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Grabbed { get; set; } + } + + public static class EpisodeResourceMapper + { + public static AlbumResource ToResource(this Album model) + { + if (model == null) return null; + + return new AlbumResource + { + Id = model.Id, + ArtistId = model.ArtistId, + Label = model.Label, + Path = model.Path, + ProfileId = model.ProfileId, + Monitored = model.Monitored, + ReleaseDate = model.ReleaseDate, + Genres = model.Genres, + Title = model.Title, + Images = model.Images, + Ratings = model.Ratings, + Duration = model.Duration, + AlbumType = model.AlbumType + }; + } + + public static List<AlbumResource> ToResource(this IEnumerable<Album> models) + { + if (models == null) return null; + + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Albums/AlbumStatisticsResource.cs b/src/Sonarr.Api.V3/Albums/AlbumStatisticsResource.cs new file mode 100644 index 000000000..e53450ed5 --- /dev/null +++ b/src/Sonarr.Api.V3/Albums/AlbumStatisticsResource.cs @@ -0,0 +1,39 @@ +using System; +using NzbDrone.Core.ArtistStats; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumStatisticsResource + { + public int TrackFileCount { get; set; } + public int TrackCount { get; set; } + public int TotalTrackCount { get; set; } + public long SizeOnDisk { get; set; } + + public decimal PercentOfEpisodes + { + get + { + if (TrackCount == 0) return 0; + + return (decimal)TrackFileCount / (decimal)TrackCount * 100; + } + } + } + + public static class AlbumStatisticsResourceMapper + { + public static AlbumStatisticsResource ToResource(this AlbumStatistics model) + { + if (model == null) return null; + + return new AlbumStatisticsResource + { + TrackFileCount = model.TrackFileCount, + TrackCount = model.TrackCount, + TotalTrackCount = model.TotalTrackCount, + SizeOnDisk = model.SizeOnDisk + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Albums/AlbumsMonitoredResource.cs b/src/Sonarr.Api.V3/Albums/AlbumsMonitoredResource.cs new file mode 100644 index 000000000..9c0120eda --- /dev/null +++ b/src/Sonarr.Api.V3/Albums/AlbumsMonitoredResource.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Albums +{ + public class AlbumsMonitoredResource + { + public List<int> AlbumIds { get; set; } + public bool Monitored { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Artist/AlternateTitleResource.cs b/src/Sonarr.Api.V3/Artist/AlternateTitleResource.cs new file mode 100644 index 000000000..d3df621b9 --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/AlternateTitleResource.cs @@ -0,0 +1,9 @@ +namespace Lidarr.Api.V3.Series +{ + public class AlternateTitleResource + { + public string Title { get; set; } + public int? SeasonNumber { get; set; } + public int? SceneSeasonNumber { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistEditorDeleteResource.cs b/src/Sonarr.Api.V3/Artist/ArtistEditorDeleteResource.cs new file mode 100644 index 000000000..09c5b892c --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistEditorDeleteResource.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorDeleteResource + { + public List<int> ArtistIds { get; set; } + public bool DeleteFiles { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistEditorModule.cs b/src/Sonarr.Api.V3/Artist/ArtistEditorModule.cs new file mode 100644 index 000000000..235952e40 --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistEditorModule.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorModule : SonarrV3Module + { + private readonly IArtistService _artistService; + + public ArtistEditorModule(IArtistService artistService) + : base("/artist/editor") + { + _artistService = artistService; + Put["/"] = artist => SaveAll(); + Delete["/"] = artist => DeleteSeries(); + } + + private Response SaveAll() + { + var resource = Request.Body.FromJson<ArtistEditorResource>(); + var artistToUpdate = _artistService.GetArtists(resource.ArtistIds); + + foreach (var artist in artistToUpdate) + { + if (resource.Monitored.HasValue) + { + artist.Monitored = resource.Monitored.Value; + } + + if (resource.QualityProfileId.HasValue) + { + artist.ProfileId = resource.QualityProfileId.Value; + } + + if (resource.AlbumFolder.HasValue) + { + artist.AlbumFolder = resource.AlbumFolder.Value; + } + + if (resource.RootFolderPath.IsNotNullOrWhiteSpace()) + { + artist.RootFolderPath = resource.RootFolderPath; + } + + if (resource.Tags != null) + { + var newTags = resource.Tags; + var applyTags = resource.ApplyTags; + + switch (applyTags) + { + case ApplyTags.Add: + newTags.ForEach(t => artist.Tags.Add(t)); + break; + case ApplyTags.Remove: + newTags.ForEach(t => artist.Tags.Remove(t)); + break; + case ApplyTags.Replace: + artist.Tags = new HashSet<int>(newTags); + break; + } + } + } + + return _artistService.UpdateArtists(artistToUpdate) + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + + private Response DeleteSeries() + { + var resource = Request.Body.FromJson<ArtistEditorResource>(); + + foreach (var artistId in resource.ArtistIds) + { + _artistService.DeleteArtist(artistId, false); + } + + return new object().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistEditorResource.cs b/src/Sonarr.Api.V3/Artist/ArtistEditorResource.cs new file mode 100644 index 000000000..649b9910a --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistEditorResource.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using NzbDrone.Core.Music; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistEditorResource + { + public List<int> ArtistIds { get; set; } + public bool? Monitored { get; set; } + public int? QualityProfileId { get; set; } + public int? LanguageProfileId { get; set; } + //public SeriesTypes? SeriesType { get; set; } + public bool? AlbumFolder { get; set; } + public string RootFolderPath { get; set; } + public List<int> Tags { get; set; } + public ApplyTags ApplyTags { get; set; } + } + + public enum ApplyTags + { + Add, + Remove, + Replace + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistImportModule.cs b/src/Sonarr.Api.V3/Artist/ArtistImportModule.cs new file mode 100644 index 000000000..2a3e1519e --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistImportModule.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Music; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistImportModule : LidarrRestModule<ArtistResource> + { + private readonly IAddArtistService _addArtistService; + + public ArtistImportModule(IAddArtistService addArtistService) + : base("/artist/import") + { + _addArtistService = addArtistService; + Post["/"] = x => Import(); + } + + + private Response Import() + { + var resource = Request.Body.FromJson<List<ArtistResource>>(); + var newArtist = resource.ToModel(); + + return _addArtistService.AddArtists(newArtist).ToResource().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistLookupModule.cs b/src/Sonarr.Api.V3/Artist/ArtistLookupModule.cs new file mode 100644 index 000000000..d0cbc6349 --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistLookupModule.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MetadataSource; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistLookupModule : LidarrRestModule<ArtistResource> + { + private readonly ISearchForNewArtist _searchProxy; + + public ArtistLookupModule(ISearchForNewArtist searchProxy) + : base("/artist/lookup") + { + _searchProxy = searchProxy; + Get["/"] = x => Search(); + } + + + private Response Search() + { + var tvDbResults = _searchProxy.SearchForNewArtist((string)Request.Query.term); + return MapToResource(tvDbResults).AsResponse(); + } + + + private static IEnumerable<ArtistResource> MapToResource(IEnumerable<NzbDrone.Core.Music.Artist> artist) + { + foreach (var currentArtist in artist) + { + var resource = currentArtist.ToResource(); + var poster = currentArtist.Images.FirstOrDefault(c => c.CoverType == MediaCoverTypes.Poster); + if (poster != null) + { + resource.RemotePoster = poster.Url; + } + + yield return resource; + } + } + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistModule.cs b/src/Sonarr.Api.V3/Artist/ArtistModule.cs new file mode 100644 index 000000000..550b1f5d5 --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistModule.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.DataAugmentation.Scene; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ArtistStats; +using NzbDrone.Core.Music; +using NzbDrone.Core.Music.Events; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Mapping; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistModule : LidarrRestModuleWithSignalR<ArtistResource, NzbDrone.Core.Music.Artist>, + IHandle<TrackImportedEvent>, + IHandle<TrackFileDeletedEvent>, + IHandle<ArtistUpdatedEvent>, + IHandle<ArtistEditedEvent>, + IHandle<ArtistDeletedEvent>, + IHandle<ArtistRenamedEvent> + //IHandle<MediaCoversUpdatedEvent> + + { + private readonly IArtistService _artistService; + private readonly IAddArtistService _addArtistService; + private readonly IArtistStatisticsService _artistStatisticsService; + private readonly IMapCoversToLocal _coverMapper; + + public ArtistModule(IBroadcastSignalRMessage signalRBroadcaster, + IArtistService artistService, + IAddArtistService addArtistService, + IArtistStatisticsService artistStatisticsService, + IMapCoversToLocal coverMapper, + RootFolderValidator rootFolderValidator, + ArtistPathValidator artistPathValidator, + ArtistExistsValidator artistExistsValidator, + SeriesAncestorValidator seriesAncestorValidator, + ProfileExistsValidator profileExistsValidator, + LanguageProfileExistsValidator languageProfileExistsValidator + ) + : base(signalRBroadcaster) + { + _artistService = artistService; + _addArtistService = addArtistService; + _artistStatisticsService = artistStatisticsService; + + _coverMapper = coverMapper; + + GetResourceAll = AllArtists; + GetResourceById = GetArtist; + CreateResource = AddArtist; + UpdateResource = UpdateArtist; + DeleteResource = DeleteArtist; + + Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId)); + + SharedValidator.RuleFor(s => s.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(artistPathValidator) + .SetValidator(seriesAncestorValidator) + .When(s => !s.Path.IsNullOrWhiteSpace()); + + SharedValidator.RuleFor(s => s.QualityProfileId).SetValidator(profileExistsValidator); + SharedValidator.RuleFor(s => s.LanguageProfileId).SetValidator(languageProfileExistsValidator); + + PostValidator.RuleFor(s => s.Path).IsValidPath().When(s => s.RootFolderPath.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.RootFolderPath).IsValidPath().When(s => s.Path.IsNullOrWhiteSpace()); + PostValidator.RuleFor(s => s.ArtistName).NotEmpty(); + PostValidator.RuleFor(s => s.ForeignArtistId).NotEmpty().SetValidator(artistExistsValidator); + + PutValidator.RuleFor(s => s.Path).IsValidPath(); + } + + private ArtistResource GetArtist(int id) + { + var artist = _artistService.GetArtist(id); + return GetArtistResource(artist); + } + + private ArtistResource GetArtistResource(NzbDrone.Core.Music.Artist artist) + { + if (artist == null) return null; + + var resource = artist.ToResource(); + MapCoversToLocal(resource); + FetchAndLinkArtistStatistics(resource); + //PopulateAlternateTitles(resource); + + return resource; + } + + private List<ArtistResource> AllArtists() + { + var artistStats = _artistStatisticsService.ArtistStatistics(); + var artistsResources = _artistService.GetAllArtists().ToResource(); + + MapCoversToLocal(artistsResources.ToArray()); + LinkArtistStatistics(artistsResources, artistStats); + //PopulateAlternateTitles(seriesResources); + + return artistsResources; + } + + private int AddArtist(ArtistResource artistResource) + { + var artist = _addArtistService.AddArtist(artistResource.ToModel()); + + return artist.Id; + } + + private void UpdateArtist(ArtistResource artistResource) + { + var model = artistResource.ToModel(_artistService.GetArtist(artistResource.Id)); + + _artistService.UpdateArtist(model); + + BroadcastResourceChange(ModelAction.Updated, artistResource); + } + + private void DeleteArtist(int id) + { + var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); + + _artistService.DeleteArtist(id, deleteFiles); + } + + private void MapCoversToLocal(params ArtistResource[] artists) + { + foreach (var artistResource in artists) + { + _coverMapper.ConvertToLocalUrls(artistResource.Id, artistResource.Images); + } + } + + private void FetchAndLinkArtistStatistics(ArtistResource resource) + { + LinkArtistStatistics(resource, _artistStatisticsService.ArtistStatistics(resource.Id)); + } + + private void LinkArtistStatistics(List<ArtistResource> resources, List<ArtistStatistics> artistStatistics) + { + foreach (var artist in resources) + { + var stats = artistStatistics.SingleOrDefault(ss => ss.ArtistId == artist.Id); + if (stats == null) continue; + + LinkArtistStatistics(artist, stats); + } + } + + private void LinkArtistStatistics(ArtistResource resource, ArtistStatistics artistStatistics) + { + resource.TotalTrackCount = artistStatistics.TotalTrackCount; + resource.TrackCount = artistStatistics.TrackCount; + resource.TrackFileCount = artistStatistics.TrackFileCount; + resource.SizeOnDisk = artistStatistics.SizeOnDisk; + resource.AlbumCount = artistStatistics.AlbumCount; + + //if (seriesStatistics.AlbumStatistics != null) + //{ + // foreach (var album in resource.Albums) + // { + // album.Statistics = seriesStatistics.AlbumStatistics.SingleOrDefault(s => s.AlbumId == album.Id).ToResource(); + // } + //} + } + + //private void PopulateAlternateTitles(List<ArtistResource> resources) + //{ + // foreach (var resource in resources) + // { + // PopulateAlternateTitles(resource); + // } + //} + + //private void PopulateAlternateTitles(ArtistResource resource) + //{ + // var mappings = _sceneMappingService.FindByTvdbId(resource.TvdbId); + + // if (mappings == null) return; + + // resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource { Title = v.Title, SeasonNumber = v.SeasonNumber, SceneSeasonNumber = v.SceneSeasonNumber }).ToList(); + //} + + public void Handle(TrackImportedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.ImportedTrack.ArtistId); + } + + public void Handle(TrackFileDeletedEvent message) + { + if (message.Reason == DeleteMediaFileReason.Upgrade) return; + + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.ArtistId); + } + + public void Handle(ArtistUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistEditedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + public void Handle(ArtistDeletedEvent message) + { + BroadcastResourceChange(ModelAction.Deleted, message.Artist.ToResource()); + } + + public void Handle(ArtistRenamedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.Artist.Id); + } + + //public void Handle(MediaCoversUpdatedEvent message) + //{ + // BroadcastResourceChange(ModelAction.Updated, message.Series.Id); + //} + } +} diff --git a/src/Sonarr.Api.V3/Artist/ArtistResource.cs b/src/Sonarr.Api.V3/Artist/ArtistResource.cs new file mode 100644 index 000000000..94b851dad --- /dev/null +++ b/src/Sonarr.Api.V3/Artist/ArtistResource.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaCover; +using NzbDrone.Core.Music; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Artist +{ + public class ArtistResource : RestResource + { + //Todo: Sorters should be done completely on the client + //Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing? + //Todo: We should get the entire Profile instead of ID and Name separately + + ////View Only + //public string Title { get; set; } + ////public List<AlternateTitleResource> AlternateTitles { get; set; } + //public string SortTitle { get; set; } + + //public int SeasonCount + //{ + // get + // { + // if (Seasons == null) return 0; + + // return Seasons.Where(s => s.SeasonNumber > 0).Count(); + // } + //} + + //public int? TotalEpisodeCount { get; set; } + //public int? EpisodeCount { get; set; } + //public int? EpisodeFileCount { get; set; } + //public long? SizeOnDisk { get; set; } + + //// V3: replace with Ended + + public ArtistStatusType Status { get; set; } + + public bool Ended => Status == ArtistStatusType.Ended; + + //public string ProfileName { get; set; } + //public string Overview { get; set; } + //public DateTime? NextAiring { get; set; } + //public DateTime? PreviousAiring { get; set; } + //public string Network { get; set; } + //public string AirTime { get; set; } + //public List<MediaCover> Images { get; set; } + + //public string RemotePoster { get; set; } + //public int Year { get; set; } + + ////View & Edit + //public string Path { get; set; } + //public int QualityProfileId { get; set; } + //public int LanguageProfileId { get; set; } + + ////Editing Only + //public bool SeasonFolder { get; set; } + //public bool Monitored { get; set; } + + //public bool UseSceneNumbering { get; set; } + //public int Runtime { get; set; } + //public int TvdbId { get; set; } + //public int TvRageId { get; set; } + //public int TvMazeId { get; set; } + //public DateTime? FirstAired { get; set; } + public DateTime? LastInfoSync { get; set; } + ////public SeriesTypes SeriesType { get; set; } + //public string CleanTitle { get; set; } + //public string ImdbId { get; set; } + //public string TitleSlug { get; set; } + //public string RootFolderPath { get; set; } + //public string Certification { get; set; } + //public List<string> Genres { get; set; } + //public HashSet<int> Tags { get; set; } + //public DateTime Added { get; set; } + //public AddSeriesOptions AddOptions { get; set; } + //public Ratings Ratings { get; set; } + + public string ArtistName { get; set; } + public string ForeignArtistId { get; set; } + public string MBId { get; set; } + public int TADBId { get; set; } + public int DiscogsId { get; set; } + public string AllMusicId { get; set; } + public string Overview { get; set; } + + public int? AlbumCount { get; set; } + public int? TotalTrackCount { get; set; } + public int? TrackCount { get; set; } + public int? TrackFileCount { get; set; } + public long? SizeOnDisk { get; set; } + //public SeriesStatusType Status { get; set; } + + public List<MediaCover> Images { get; set; } + public List<Member> Members { get; set; } + + public string RemotePoster { get; set; } + //public List<AlbumResource> Albums { get; set; } + + + //View & Edit + public string Path { get; set; } + public int QualityProfileId { get; set; } + public int LanguageProfileId { get; set; } + + //Editing Only + public bool AlbumFolder { get; set; } + public bool Monitored { get; set; } + + public string RootFolderPath { get; set; } + //public string Certification { get; set; } + public List<string> Genres { get; set; } + public string CleanName { get; set; } + public string SortName { get; set; } + public HashSet<int> Tags { get; set; } + public DateTime Added { get; set; } + public AddArtistOptions AddOptions { get; set; } + public Ratings Ratings { get; set; } + public string NameSlug { get; set; } + + //TODO: Add series statistics as a property of the series (instead of individual properties) + } + + public static class SeriesResourceMapper + { + public static ArtistResource ToResource(this NzbDrone.Core.Music.Artist model) + { + if (model == null) return null; + + return new ArtistResource + { + Id = model.Id, + + ArtistName = model.Name, + //AlternateTitles + SortName = model.SortName, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = model.Status, + Overview = model.Overview, + //NextAiring + //PreviousAiring + //Network = model.Network, + //AirTime = model.AirTime, + Images = model.Images, + + //Albums = model.Albums.ToResource(), + //Year = model.Year, + + Path = model.Path, + QualityProfileId = model.ProfileId, + LanguageProfileId = model.LanguageProfileId, + + AlbumFolder = model.AlbumFolder, + Monitored = model.Monitored, + + //UseSceneNumbering = model.UseSceneNumbering, + //Runtime = model.Runtime, + //TvdbId = model.TvdbId, + //TvRageId = model.TvRageId, + //TvMazeId = model.TvMazeId, + //FirstAired = model.FirstAired, + LastInfoSync = model.LastInfoSync, + //SeriesType = model.SeriesType, + CleanName = model.CleanName, + ForeignArtistId = model.ForeignArtistId, + NameSlug = model.NameSlug, + RootFolderPath = model.RootFolderPath, + //Certification = model.Certification, + Genres = model.Genres, + Tags = model.Tags, + Added = model.Added, + AddOptions = model.AddOptions, + Ratings = model.Ratings + }; + } + + public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource) + { + if (resource == null) return null; + + return new NzbDrone.Core.Music.Artist + { + Id = resource.Id, + + Name = resource.ArtistName, + //AlternateTitles + SortName = resource.SortName, + + //TotalEpisodeCount + //EpisodeCount + //EpisodeFileCount + //SizeOnDisk + Status = resource.Status, + Overview = resource.Overview, + //NextAiring + //PreviousAiring + // Network = resource.Network, + //AirTime = resource.AirTime, + Images = resource.Images, + + //Albums = resource.Albums.ToModel(), + //Year = resource.Year, + + Path = resource.Path, + ProfileId = resource.QualityProfileId, + LanguageProfileId = resource.LanguageProfileId, + + AlbumFolder = resource.AlbumFolder, + Monitored = resource.Monitored, + + //UseSceneNumbering = resource.UseSceneNumbering, + //Runtime = resource.Runtime, + //TvdbId = resource.TvdbId, + //TvRageId = resource.TvRageId, + //TvMazeId = resource.TvMazeId, + //FirstAired = resource.FirstAired, + LastInfoSync = resource.LastInfoSync, + //SeriesType = resource.SeriesType, + CleanName = resource.CleanName, + ForeignArtistId = resource.ForeignArtistId, + NameSlug = resource.NameSlug, + RootFolderPath = resource.RootFolderPath, + //Certification = resource.Certification, + Genres = resource.Genres, + Tags = resource.Tags, + Added = resource.Added, + AddOptions = resource.AddOptions, + Ratings = resource.Ratings + }; + } + + public static NzbDrone.Core.Music.Artist ToModel(this ArtistResource resource, NzbDrone.Core.Music.Artist artist) + { + var updatedArtist = resource.ToModel(); + + artist.ApplyChanges(updatedArtist); + + return artist; + } + + public static List<ArtistResource> ToResource(this IEnumerable<NzbDrone.Core.Music.Artist> artist) + { + return artist.Select(ToResource).ToList(); + } + + public static List<NzbDrone.Core.Music.Artist> ToModel(this IEnumerable<ArtistResource> resources) + { + return resources.Select(ToModel).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs new file mode 100644 index 000000000..7fecda1ca --- /dev/null +++ b/src/Sonarr.Api.V3/Blacklist/BlacklistModule.cs @@ -0,0 +1,36 @@ +using NzbDrone.Core.Blacklisting; +using NzbDrone.Core.Datastore; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Blacklist +{ + public class BlacklistModule : LidarrRestModule<BlacklistResource> + { + private readonly IBlacklistService _blacklistService; + + public BlacklistModule(IBlacklistService blacklistService) + { + _blacklistService = blacklistService; + GetResourcePaged = GetBlacklist; + DeleteResource = DeleteBlacklist; + } + + private PagingResource<BlacklistResource> GetBlacklist(PagingResource<BlacklistResource> pagingResource) + { + var pagingSpec = new PagingSpec<NzbDrone.Core.Blacklisting.Blacklist> + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + return ApplyToPage(_blacklistService.Paged, pagingSpec, BlacklistResourceMapper.MapToResource); + } + + private void DeleteBlacklist(int id) + { + _blacklistService.Delete(id); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs b/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs new file mode 100644 index 000000000..d47926b97 --- /dev/null +++ b/src/Sonarr.Api.V3/Blacklist/BlacklistResource.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Blacklist +{ + public class BlacklistResource : RestResource + { + public int ArtistId { get; set; } + public List<int> AlbumIds { get; set; } + public string SourceTitle { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public DateTime Date { get; set; } + public DownloadProtocol Protocol { get; set; } + public string Indexer { get; set; } + public string Message { get; set; } + + public ArtistResource Artist { get; set; } + } + + public static class BlacklistResourceMapper + { + public static BlacklistResource MapToResource(this NzbDrone.Core.Blacklisting.Blacklist model) + { + if (model == null) return null; + + return new BlacklistResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumIds = model.AlbumIds, + SourceTitle = model.SourceTitle, + Language = model.Language, + Quality = model.Quality, + Date = model.Date, + Protocol = model.Protocol, + Indexer = model.Indexer, + Message = model.Message, + + Artist = model.Artist.ToResource() + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs new file mode 100644 index 000000000..5ebf7644f --- /dev/null +++ b/src/Sonarr.Api.V3/Calendar/CalendarFeedModule.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Ical.Net; +using Ical.Net.DataTypes; +using Ical.Net.Interfaces.Serialization; +using Ical.Net.Serialization; +using Ical.Net.Serialization.iCalendar.Factory; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Tags; +using NzbDrone.Core.Music; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Calendar +{ + public class CalendarFeedModule : SonarrV3FeedModule + { + private readonly IAlbumService _albumService; + private readonly ITagService _tagService; + + public CalendarFeedModule(IAlbumService albumService, ITagService tagService) + : base("calendar") + { + _albumService = albumService; + _tagService = tagService; + + Get["/Sonarr.ics"] = options => GetCalendarFeed(); + } + + private Response GetCalendarFeed() + { + var pastDays = 7; + var futureDays = 28; + var start = DateTime.Today.AddDays(-pastDays); + var end = DateTime.Today.AddDays(futureDays); + var unmonitored = Request.GetBooleanQueryParameter("unmonitored"); + var premiersOnly = Request.GetBooleanQueryParameter("premiersOnly"); + var asAllDay = Request.GetBooleanQueryParameter("asAllDay"); + var tags = new List<int>(); + + var queryPastDays = Request.Query.PastDays; + var queryFutureDays = Request.Query.FutureDays; + var queryTags = Request.Query.Tags; + + if (queryPastDays.HasValue) + { + pastDays = int.Parse(queryPastDays.Value); + start = DateTime.Today.AddDays(-pastDays); + } + + if (queryFutureDays.HasValue) + { + futureDays = int.Parse(queryFutureDays.Value); + end = DateTime.Today.AddDays(futureDays); + } + + if (queryTags.HasValue) + { + var tagInput = (string)queryTags.Value.ToString(); + tags.AddRange(tagInput.Split(',').Select(_tagService.GetTag).Select(t => t.Id)); + } + + var episodes = _albumService.AlbumsBetweenDates(start, end, unmonitored); + var calendar = new Ical.Net.Calendar + { + ProductId = "-//sonarr.tv//Sonarr//EN" + }; + + + foreach (var album in episodes.OrderBy(v => v.ReleaseDate.Value)) + { + //if (premiersOnly && (album.SeasonNumber == 0 || album.EpisodeNumber != 1)) + //{ + // continue; + //} + + if (tags.Any() && tags.None(album.Artist.Tags.Contains)) + { + continue; + } + + var occurrence = calendar.Create<Event>(); + occurrence.Uid = "NzbDrone_album_" + album.Id; + //occurrence.Status = album.HasFile ? EventStatus.Confirmed : EventStatus.Tentative; + //occurrence.Description = album.Overview; + //occurrence.Categories = new List<string>() { album.Series.Network }; + + occurrence.Start = new CalDateTime(album.ReleaseDate.Value) { HasTime = false }; + + occurrence.Summary = $"{album.Artist.Name} - {album.Title}"; + } + + var serializer = (IStringSerializer)new SerializerFactory().Build(calendar.GetType(), new SerializationContext()); + var icalendar = serializer.SerializeToString(calendar); + + return new TextResponse(icalendar, "text/calendar"); + } + } +} diff --git a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs new file mode 100644 index 000000000..db4e78722 --- /dev/null +++ b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Calendar +{ + public class CalendarModule : AlbumModuleWithSignalR + { + public CalendarModule(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification ugradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, ugradableSpecification, signalRBroadcaster, "calendar") + { + GetResourceAll = GetCalendar; + } + + private List<AlbumResource> GetCalendar() + { + var start = DateTime.Today; + var end = DateTime.Today.AddDays(2); + var includeUnmonitored = Request.GetBooleanQueryParameter("unmonitored"); + var includeArtist = Request.GetBooleanQueryParameter("includeArtist"); + var includeEpisodeFile = Request.GetBooleanQueryParameter("includeEpisodeFile"); + + var queryStart = Request.Query.Start; + var queryEnd = Request.Query.End; + + if (queryStart.HasValue) start = DateTime.Parse(queryStart.Value); + if (queryEnd.HasValue) end = DateTime.Parse(queryEnd.Value); + + var resources = MapToResource(_albumService.AlbumsBetweenDates(start, end, includeUnmonitored), includeArtist); + + return resources.OrderBy(e => e.ReleaseDate).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Commands/CommandModule.cs b/src/Sonarr.Api.V3/Commands/CommandModule.cs new file mode 100644 index 000000000..c4ceba3ed --- /dev/null +++ b/src/Sonarr.Api.V3/Commands/CommandModule.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.ProgressMessaging; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Validation; + +namespace Lidarr.Api.V3.Commands +{ + public class CommandModule : LidarrRestModuleWithSignalR<CommandResource, CommandModel>, IHandle<CommandUpdatedEvent> + { + private readonly IManageCommandQueue _commandQueueManager; + private readonly IServiceFactory _serviceFactory; + + public CommandModule(IManageCommandQueue commandQueueManager, + IBroadcastSignalRMessage signalRBroadcaster, + IServiceFactory serviceFactory) + : base(signalRBroadcaster) + { + _commandQueueManager = commandQueueManager; + _serviceFactory = serviceFactory; + + GetResourceById = GetCommand; + CreateResource = StartCommand; + GetResourceAll = GetStartedCommands; + + PostValidator.RuleFor(c => c.Name).NotBlank(); + } + + private CommandResource GetCommand(int id) + { + return _commandQueueManager.Get(id).ToResource(); + } + + private int StartCommand(CommandResource commandResource) + { + var commandType = + _serviceFactory.GetImplementations(typeof (Command)) + .Single(c => c.Name.Replace("Command", "") + .Equals(commandResource.Name, StringComparison.InvariantCultureIgnoreCase)); + + dynamic command = Request.Body.FromJson(commandType); + command.Trigger = CommandTrigger.Manual; + command.SuppressMessages = !command.SendUpdatesToClient; + command.SendUpdatesToClient = true; + + var trackedCommand = _commandQueueManager.Push(command, CommandPriority.Normal, CommandTrigger.Manual); + return trackedCommand.Id; + } + + private List<CommandResource> GetStartedCommands() + { + return _commandQueueManager.GetStarted().ToResource(); + } + + public void Handle(CommandUpdatedEvent message) + { + if (message.Command.Body.SendUpdatesToClient) + { + BroadcastResourceChange(ModelAction.Updated, message.Command.ToResource()); + } + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Commands/CommandResource.cs b/src/Sonarr.Api.V3/Commands/CommandResource.cs new file mode 100644 index 000000000..743db720c --- /dev/null +++ b/src/Sonarr.Api.V3/Commands/CommandResource.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Messaging.Commands; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Commands +{ + public class CommandResource : RestResource + { + public string Name { get; set; } + public string Message { get; set; } + public Command Body { get; set; } + public CommandPriority Priority { get; set; } + public CommandStatus Status { get; set; } + public DateTime Queued { get; set; } + public DateTime? Started { get; set; } + public DateTime? Ended { get; set; } + public TimeSpan? Duration { get; set; } + public string Exception { get; set; } + public CommandTrigger Trigger { get; set; } + + [JsonIgnore] + public string CompletionMessage { get; set; } + + //Legacy + public CommandStatus State + { + get + { + return Status; + } + + set { } + } + + public bool Manual + { + get + { + return Trigger == CommandTrigger.Manual; + } + + set { } + } + + public DateTime StartedOn + { + get + { + return Queued; + } + + set { } + } + + public DateTime? StateChangeTime + { + get + { + + if (Started.HasValue) return Started.Value; + + return Ended; + } + + set { } + } + + public bool SendUpdatesToClient + { + get + { + if (Body != null) return Body.SendUpdatesToClient; + + return false; + } + + set { } + } + + public bool UpdateScheduledTask + { + get + { + if (Body != null) return Body.UpdateScheduledTask; + + return false; + } + + set { } + } + + public DateTime? LastExecutionTime { get; set; } + } + + public static class CommandResourceMapper + { + public static CommandResource ToResource(this CommandModel model) + { + if (model == null) return null; + + return new CommandResource + { + Id = model.Id, + + Name = model.Name, + Message = model.Message, + Body = model.Body, + Priority = model.Priority, + Status = model.Status, + Queued = model.QueuedAt, + Started = model.StartedAt, + Ended = model.EndedAt, + Duration = model.Duration, + Exception = model.Exception, + Trigger = model.Trigger, + + CompletionMessage = model.Body.CompletionMessage, + LastExecutionTime = model.Body.LastExecutionTime + }; + } + + public static List<CommandResource> ToResource(this IEnumerable<CommandModel> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs new file mode 100644 index 000000000..262b378c7 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigModule.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.Configuration; + +namespace Lidarr.Api.V3.Config +{ + public class DownloadClientConfigModule : SonarrConfigModule<DownloadClientConfigResource> + { + public DownloadClientConfigModule(IConfigService configService) + : base(configService) + { + } + + protected override DownloadClientConfigResource ToResource(IConfigService model) + { + return DownloadClientConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs new file mode 100644 index 000000000..49340c01b --- /dev/null +++ b/src/Sonarr.Api.V3/Config/DownloadClientConfigResource.cs @@ -0,0 +1,33 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class DownloadClientConfigResource : RestResource + { + public string DownloadClientWorkingFolders { get; set; } + + public bool EnableCompletedDownloadHandling { get; set; } + public bool RemoveCompletedDownloads { get; set; } + + public bool AutoRedownloadFailed { get; set; } + public bool RemoveFailedDownloads { get; set; } + } + + public static class DownloadClientConfigResourceMapper + { + public static DownloadClientConfigResource ToResource(IConfigService model) + { + return new DownloadClientConfigResource + { + DownloadClientWorkingFolders = model.DownloadClientWorkingFolders, + + EnableCompletedDownloadHandling = model.EnableCompletedDownloadHandling, + RemoveCompletedDownloads = model.RemoveCompletedDownloads, + + AutoRedownloadFailed = model.AutoRedownloadFailed, + RemoveFailedDownloads = model.RemoveFailedDownloads + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/HostConfigModule.cs b/src/Sonarr.Api.V3/Config/HostConfigModule.cs new file mode 100644 index 000000000..081ef9f5c --- /dev/null +++ b/src/Sonarr.Api.V3/Config/HostConfigModule.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Reflection; +using FluentValidation; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; +using NzbDrone.Core.Validation; +using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Config +{ + public class HostConfigModule : LidarrRestModule<HostConfigResource> + { + private readonly IConfigFileProvider _configFileProvider; + private readonly IConfigService _configService; + private readonly IUserService _userService; + + public HostConfigModule(IConfigFileProvider configFileProvider, IConfigService configService, IUserService userService) + : base("/config/host") + { + _configFileProvider = configFileProvider; + _configService = configService; + _userService = userService; + + GetResourceSingle = GetHostConfig; + GetResourceById = GetHostConfig; + UpdateResource = SaveHostConfig; + + SharedValidator.RuleFor(c => c.BindAddress) + .ValidIp4Address() + .NotListenAllIp4Address() + .When(c => c.BindAddress != "*"); + + SharedValidator.RuleFor(c => c.Port).ValidPort(); + + SharedValidator.RuleFor(c => c.UrlBase).ValidUrlBase(); + + SharedValidator.RuleFor(c => c.Username).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None); + SharedValidator.RuleFor(c => c.Password).NotEmpty().When(c => c.AuthenticationMethod != AuthenticationType.None); + + SharedValidator.RuleFor(c => c.SslPort).ValidPort().When(c => c.EnableSsl); + SharedValidator.RuleFor(c => c.SslCertHash).NotEmpty().When(c => c.EnableSsl && OsInfo.IsWindows); + + SharedValidator.RuleFor(c => c.Branch).NotEmpty().WithMessage("Branch name is required, 'master' is the default"); + SharedValidator.RuleFor(c => c.UpdateScriptPath).IsValidPath().When(c => c.UpdateMechanism == UpdateMechanism.Script); + } + + private HostConfigResource GetHostConfig() + { + var resource = _configFileProvider.ToResource(_configService); + resource.Id = 1; + + var user = _userService.FindUser(); + if (user != null) + { + resource.Username = user.Username; + resource.Password = user.Password; + } + + return resource; + } + + private HostConfigResource GetHostConfig(int id) + { + return GetHostConfig(); + } + + private void SaveHostConfig(HostConfigResource resource) + { + var dictionary = resource.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + + _configFileProvider.SaveConfigDictionary(dictionary); + + if (resource.Username.IsNotNullOrWhiteSpace() && resource.Password.IsNotNullOrWhiteSpace()) + { + _userService.Upsert(resource.Username, resource.Password); + } + } + } +} diff --git a/src/Sonarr.Api.V3/Config/HostConfigResource.cs b/src/Sonarr.Api.V3/Config/HostConfigResource.cs new file mode 100644 index 000000000..8d45f2a23 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/HostConfigResource.cs @@ -0,0 +1,73 @@ +using NzbDrone.Common.Http.Proxy; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Update; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class HostConfigResource : RestResource + { + public string BindAddress { get; set; } + public int Port { get; set; } + public int SslPort { get; set; } + public bool EnableSsl { get; set; } + public bool LaunchBrowser { get; set; } + public AuthenticationType AuthenticationMethod { get; set; } + public bool AnalyticsEnabled { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string LogLevel { get; set; } + public string Branch { get; set; } + public string ApiKey { get; set; } + public string SslCertHash { get; set; } + public string UrlBase { get; set; } + public bool UpdateAutomatically { get; set; } + public UpdateMechanism UpdateMechanism { get; set; } + public string UpdateScriptPath { get; set; } + public bool ProxyEnabled { get; set; } + public ProxyType ProxyType { get; set; } + public string ProxyHostname { get; set; } + public int ProxyPort { get; set; } + public string ProxyUsername { get; set; } + public string ProxyPassword { get; set; } + public string ProxyBypassFilter { get; set; } + public bool ProxyBypassLocalAddresses { get; set; } + } + + public static class HostConfigResourceMapper + { + public static HostConfigResource ToResource(this IConfigFileProvider model, IConfigService configService) + { + // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? + return new HostConfigResource + { + BindAddress = model.BindAddress, + Port = model.Port, + SslPort = model.SslPort, + EnableSsl = model.EnableSsl, + LaunchBrowser = model.LaunchBrowser, + AuthenticationMethod = model.AuthenticationMethod, + AnalyticsEnabled = model.AnalyticsEnabled, + //Username + //Password + LogLevel = model.LogLevel, + Branch = model.Branch, + ApiKey = model.ApiKey, + SslCertHash = model.SslCertHash, + UrlBase = model.UrlBase, + UpdateAutomatically = model.UpdateAutomatically, + UpdateMechanism = model.UpdateMechanism, + UpdateScriptPath = model.UpdateScriptPath, + ProxyEnabled = configService.ProxyEnabled, + ProxyType = configService.ProxyType, + ProxyHostname = configService.ProxyHostname, + ProxyPort = configService.ProxyPort, + ProxyUsername = configService.ProxyUsername, + ProxyPassword = configService.ProxyPassword, + ProxyBypassFilter = configService.ProxyBypassFilter, + ProxyBypassLocalAddresses = configService.ProxyBypassLocalAddresses + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs b/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs new file mode 100644 index 000000000..b7a066539 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/IndexerConfigModule.cs @@ -0,0 +1,28 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using Lidarr.Http.Validation; + +namespace Lidarr.Api.V3.Config +{ + public class IndexerConfigModule : SonarrConfigModule<IndexerConfigResource> + { + + public IndexerConfigModule(IConfigService configService) + : base(configService) + { + SharedValidator.RuleFor(c => c.MinimumAge) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.Retention) + .GreaterThanOrEqualTo(0); + + SharedValidator.RuleFor(c => c.RssSyncInterval) + .IsValidRssSyncInterval(); + } + + protected override IndexerConfigResource ToResource(IConfigService model) + { + return IndexerConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs new file mode 100644 index 000000000..ffeb6d76d --- /dev/null +++ b/src/Sonarr.Api.V3/Config/IndexerConfigResource.cs @@ -0,0 +1,25 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class IndexerConfigResource : RestResource + { + public int MinimumAge { get; set; } + public int Retention { get; set; } + public int RssSyncInterval { get; set; } + } + + public static class IndexerConfigResourceMapper + { + public static IndexerConfigResource ToResource(IConfigService model) + { + return new IndexerConfigResource + { + MinimumAge = model.MinimumAge, + Retention = model.Retention, + RssSyncInterval = model.RssSyncInterval, + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs new file mode 100644 index 000000000..dbf8ff9a4 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigModule.cs @@ -0,0 +1,22 @@ +using FluentValidation; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Validation.Paths; + +namespace Lidarr.Api.V3.Config +{ + public class MediaManagementConfigModule : SonarrConfigModule<MediaManagementConfigResource> + { + public MediaManagementConfigModule(IConfigService configService, PathExistsValidator pathExistsValidator) + : base(configService) + { + SharedValidator.RuleFor(c => c.FileChmod).NotEmpty(); + SharedValidator.RuleFor(c => c.FolderChmod).NotEmpty(); + SharedValidator.RuleFor(c => c.RecycleBin).IsValidPath().SetValidator(pathExistsValidator).When(c => !string.IsNullOrWhiteSpace(c.RecycleBin)); + } + + protected override MediaManagementConfigResource ToResource(IConfigService model) + { + return MediaManagementConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs new file mode 100644 index 000000000..857e8baa0 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/MediaManagementConfigResource.cs @@ -0,0 +1,54 @@ +using NzbDrone.Core.Configuration; +using NzbDrone.Core.MediaFiles; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class MediaManagementConfigResource : RestResource + { + public bool AutoUnmonitorPreviouslyDownloadedTracks { get; set; } + public string RecycleBin { get; set; } + public bool AutoDownloadPropers { get; set; } + public bool CreateEmptyArtistFolders { get; set; } + public FileDateType FileDate { get; set; } + + public bool SetPermissionsLinux { get; set; } + public string FileChmod { get; set; } + public string FolderChmod { get; set; } + public string ChownUser { get; set; } + public string ChownGroup { get; set; } + + public bool SkipFreeSpaceCheckWhenImporting { get; set; } + public bool CopyUsingHardlinks { get; set; } + public bool ImportExtraFiles { get; set; } + public string ExtraFileExtensions { get; set; } + public bool EnableMediaInfo { get; set; } + } + + public static class MediaManagementConfigResourceMapper + { + public static MediaManagementConfigResource ToResource(IConfigService model) + { + return new MediaManagementConfigResource + { + AutoUnmonitorPreviouslyDownloadedTracks = model.AutoUnmonitorPreviouslyDownloadedTracks, + RecycleBin = model.RecycleBin, + AutoDownloadPropers = model.AutoDownloadPropers, + CreateEmptyArtistFolders = model.CreateEmptyArtistFolders, + FileDate = model.FileDate, + + SetPermissionsLinux = model.SetPermissionsLinux, + FileChmod = model.FileChmod, + FolderChmod = model.FolderChmod, + ChownUser = model.ChownUser, + ChownGroup = model.ChownGroup, + + SkipFreeSpaceCheckWhenImporting = model.SkipFreeSpaceCheckWhenImporting, + CopyUsingHardlinks = model.CopyUsingHardlinks, + ImportExtraFiles = model.ImportExtraFiles, + ExtraFileExtensions = model.ExtraFileExtensions, + EnableMediaInfo = model.EnableMediaInfo + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/NamingConfigModule.cs b/src/Sonarr.Api.V3/Config/NamingConfigModule.cs new file mode 100644 index 000000000..8dd3c9c74 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingConfigModule.cs @@ -0,0 +1,114 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Nancy.ModelBinding; +using Nancy.Responses; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Organizer; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.Mapping; + +namespace Lidarr.Api.V3.Config +{ + public class NamingConfigModule : LidarrRestModule<NamingConfigResource> + { + private readonly INamingConfigService _namingConfigService; + private readonly IFilenameSampleService _filenameSampleService; + private readonly IFilenameValidationService _filenameValidationService; + private readonly IBuildFileNames _filenameBuilder; + + public NamingConfigModule(INamingConfigService namingConfigService, + IFilenameSampleService filenameSampleService, + IFilenameValidationService filenameValidationService, + IBuildFileNames filenameBuilder) + : base("config/naming") + { + _namingConfigService = namingConfigService; + _filenameSampleService = filenameSampleService; + _filenameValidationService = filenameValidationService; + _filenameBuilder = filenameBuilder; + GetResourceSingle = GetNamingConfig; + GetResourceById = GetNamingConfig; + UpdateResource = UpdateNamingConfig; + + Get["/examples"] = x => GetExamples(this.Bind<NamingConfigResource>()); + + + SharedValidator.RuleFor(c => c.StandardTrackFormat).ValidEpisodeFormat(); + SharedValidator.RuleFor(c => c.ArtistFolderFormat).ValidSeriesFolderFormat(); + SharedValidator.RuleFor(c => c.AlbumFolderFormat).ValidSeasonFolderFormat(); + } + + private void UpdateNamingConfig(NamingConfigResource resource) + { + var nameSpec = resource.ToModel(); + ValidateFormatResult(nameSpec); + + _namingConfigService.Save(nameSpec); + } + + private NamingConfigResource GetNamingConfig() + { + var nameSpec = _namingConfigService.GetConfig(); + var resource = nameSpec.ToResource(); + + if (resource.StandardTrackFormat.IsNotNullOrWhiteSpace()) + { + var basicConfig = _filenameBuilder.GetBasicNamingConfig(nameSpec); + basicConfig.AddToResource(resource); + } + + return resource; + } + + private NamingConfigResource GetNamingConfig(int id) + { + return GetNamingConfig(); + } + + private JsonResponse<NamingExampleResource> GetExamples(NamingConfigResource config) + { + if (config.Id == 0) + { + config = GetNamingConfig(); + } + + var nameSpec = config.ToModel(); + var sampleResource = new NamingExampleResource(); + + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); + + sampleResource.SingleTrackExample = _filenameValidationService.ValidateTrackFilename(singleTrackSampleResult) != null + ? null + : singleTrackSampleResult.FileName; + + sampleResource.ArtistFolderExample = nameSpec.ArtistFolderFormat.IsNullOrWhiteSpace() + ? null + : _filenameSampleService.GetArtistFolderSample(nameSpec); + + sampleResource.AlbumFolderExample = nameSpec.AlbumFolderFormat.IsNullOrWhiteSpace() + ? null + : _filenameSampleService.GetAlbumFolderSample(nameSpec); + + return sampleResource.AsResponse(); + } + + private void ValidateFormatResult(NamingConfig nameSpec) + { + var singleTrackSampleResult = _filenameSampleService.GetStandardTrackSample(nameSpec); + + var singleTrackValidationResult = _filenameValidationService.ValidateStandardFilename(singleTrackSampleResult); + + var validationFailures = new List<ValidationFailure>(); + + validationFailures.AddIfNotNull(singleTrackValidationResult); + + if (validationFailures.Any()) + { + throw new ValidationException(validationFailures.DistinctBy(v => v.PropertyName).ToArray()); + } + } + } +} diff --git a/src/Sonarr.Api.V3/Config/NamingConfigResource.cs b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs new file mode 100644 index 000000000..f081d65a6 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingConfigResource.cs @@ -0,0 +1,19 @@ +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class NamingConfigResource : RestResource + { + public bool RenameTracks { get; set; } + public bool ReplaceIllegalCharacters { get; set; } + public string StandardTrackFormat { get; set; } + public string ArtistFolderFormat { get; set; } + public string AlbumFolderFormat { get; set; } + public bool IncludeArtistName { get; set; } + public bool IncludeAlbumTitle { get; set; } + public bool IncludeQuality { get; set; } + public bool ReplaceSpaces { get; set; } + public string Separator { get; set; } + public string NumberStyle { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Config/NamingExampleResource.cs b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs new file mode 100644 index 000000000..b086a3f06 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/NamingExampleResource.cs @@ -0,0 +1,63 @@ +using NzbDrone.Core.Organizer; + +namespace Lidarr.Api.V3.Config +{ + public class NamingExampleResource + { + public string SingleTrackExample { get; set; } + public string MultiEpisodeExample { get; set; } + public string DailyEpisodeExample { get; set; } + public string AnimeEpisodeExample { get; set; } + public string AnimeMultiEpisodeExample { get; set; } + public string ArtistFolderExample { get; set; } + public string AlbumFolderExample { get; set; } + } + + public static class NamingConfigResourceMapper + { + public static NamingConfigResource ToResource(this NamingConfig model) + { + return new NamingConfigResource + { + Id = model.Id, + + RenameTracks = model.RenameTracks, + ReplaceIllegalCharacters = model.ReplaceIllegalCharacters, + StandardTrackFormat = model.StandardTrackFormat, + ArtistFolderFormat = model.ArtistFolderFormat, + AlbumFolderFormat = model.AlbumFolderFormat + //IncludeSeriesTitle + //IncludeEpisodeTitle + //IncludeQuality + //ReplaceSpaces + //Separator + //NumberStyle + }; + } + + public static void AddToResource(this BasicNamingConfig basicNamingConfig, NamingConfigResource resource) + { + resource.IncludeArtistName = basicNamingConfig.IncludeArtistName; + resource.IncludeAlbumTitle = basicNamingConfig.IncludeAlbumTitle; + resource.IncludeQuality = basicNamingConfig.IncludeQuality; + resource.ReplaceSpaces = basicNamingConfig.ReplaceSpaces; + resource.Separator = basicNamingConfig.Separator; + resource.NumberStyle = basicNamingConfig.NumberStyle; + } + + public static NamingConfig ToModel(this NamingConfigResource resource) + { + return new NamingConfig + { + Id = resource.Id, + + RenameTracks = resource.RenameTracks, + ReplaceIllegalCharacters = resource.ReplaceIllegalCharacters, + StandardTrackFormat = resource.StandardTrackFormat, + + ArtistFolderFormat = resource.ArtistFolderFormat, + AlbumFolderFormat = resource.AlbumFolderFormat + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs b/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs new file mode 100644 index 000000000..b294ef7bb --- /dev/null +++ b/src/Sonarr.Api.V3/Config/SonarrConfigModule.cs @@ -0,0 +1,52 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public abstract class SonarrConfigModule<TResource> : LidarrRestModule<TResource> where TResource : RestResource, new() + { + private readonly IConfigService _configService; + + protected SonarrConfigModule(IConfigService configService) + : this(new TResource().ResourceName.Replace("config", ""), configService) + { + } + + protected SonarrConfigModule(string resource, IConfigService configService) : + base("config/" + resource.Trim('/')) + { + _configService = configService; + + GetResourceSingle = GetConfig; + GetResourceById = GetConfig; + UpdateResource = SaveConfig; + } + + private TResource GetConfig() + { + var resource = ToResource(_configService); + resource.Id = 1; + + return resource; + } + + protected abstract TResource ToResource(IConfigService model); + + private TResource GetConfig(int id) + { + return GetConfig(); + } + + private void SaveConfig(TResource resource) + { + var dictionary = resource.GetType() + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .ToDictionary(prop => prop.Name, prop => prop.GetValue(resource, null)); + + _configService.SaveConfigDictionary(dictionary); + } + } +} diff --git a/src/Sonarr.Api.V3/Config/UiConfigModule.cs b/src/Sonarr.Api.V3/Config/UiConfigModule.cs new file mode 100644 index 000000000..2bc1fd4f9 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/UiConfigModule.cs @@ -0,0 +1,21 @@ +using System.Linq; +using System.Reflection; +using NzbDrone.Core.Configuration; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Config +{ + public class UiConfigModule : SonarrConfigModule<UiConfigResource> + { + public UiConfigModule(IConfigService configService) + : base(configService) + { + + } + + protected override UiConfigResource ToResource(IConfigService model) + { + return UiConfigResourceMapper.ToResource(model); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Config/UiConfigResource.cs b/src/Sonarr.Api.V3/Config/UiConfigResource.cs new file mode 100644 index 000000000..28ed7ce28 --- /dev/null +++ b/src/Sonarr.Api.V3/Config/UiConfigResource.cs @@ -0,0 +1,39 @@ +using NzbDrone.Core.Configuration; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Config +{ + public class UiConfigResource : RestResource + { + //Calendar + public int FirstDayOfWeek { get; set; } + public string CalendarWeekColumnHeader { get; set; } + + //Dates + public string ShortDateFormat { get; set; } + public string LongDateFormat { get; set; } + public string TimeFormat { get; set; } + public bool ShowRelativeDates { get; set; } + + public bool EnableColorImpairedMode { get; set; } + } + + public static class UiConfigResourceMapper + { + public static UiConfigResource ToResource(IConfigService model) + { + return new UiConfigResource + { + FirstDayOfWeek = model.FirstDayOfWeek, + CalendarWeekColumnHeader = model.CalendarWeekColumnHeader, + + ShortDateFormat = model.ShortDateFormat, + LongDateFormat = model.LongDateFormat, + TimeFormat = model.TimeFormat, + ShowRelativeDates = model.ShowRelativeDates, + + EnableColorImpairedMode = model.EnableColorImpairedMode, + }; + } + } +} diff --git a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs new file mode 100644 index 000000000..15806fb59 --- /dev/null +++ b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceModule.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using NzbDrone.Core.DiskSpace; +using Lidarr.Http; + +namespace Lidarr.Api.V3.DiskSpace +{ + public class DiskSpaceModule :LidarrRestModule<DiskSpaceResource> + { + private readonly IDiskSpaceService _diskSpaceService; + + public DiskSpaceModule(IDiskSpaceService diskSpaceService) + :base("diskspace") + { + _diskSpaceService = diskSpaceService; + GetResourceAll = GetFreeSpace; + } + + public List<DiskSpaceResource> GetFreeSpace() + { + return _diskSpaceService.GetFreeSpace().ConvertAll(DiskSpaceResourceMapper.MapToResource); + } + } +} diff --git a/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs new file mode 100644 index 000000000..fc236f1c7 --- /dev/null +++ b/src/Sonarr.Api.V3/DiskSpace/DiskSpaceResource.cs @@ -0,0 +1,28 @@ +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.DiskSpace +{ + public class DiskSpaceResource : RestResource + { + public string Path { get; set; } + public string Label { get; set; } + public long FreeSpace { get; set; } + public long TotalSpace { get; set; } + } + + public static class DiskSpaceResourceMapper + { + public static DiskSpaceResource MapToResource(this NzbDrone.Core.DiskSpace.DiskSpace model) + { + if (model == null) return null; + + return new DiskSpaceResource + { + Path = model.Path, + Label = model.Label, + FreeSpace = model.FreeSpace, + TotalSpace = model.TotalSpace + }; + } + } +} diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs new file mode 100644 index 000000000..af12eb21d --- /dev/null +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Download; + +namespace Lidarr.Api.V3.DownloadClient +{ + public class DownloadClientModule : ProviderModuleBase<DownloadClientResource, IDownloadClient, DownloadClientDefinition> + { + public static readonly DownloadClientResourceMapper ResourceMapper = new DownloadClientResourceMapper(); + + public DownloadClientModule(IDownloadClientFactory downloadClientFactory) + : base(downloadClientFactory, "downloadclient", ResourceMapper) + { + } + + protected override void Validate(DownloadClientDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs new file mode 100644 index 000000000..6205f8813 --- /dev/null +++ b/src/Sonarr.Api.V3/DownloadClient/DownloadClientResource.cs @@ -0,0 +1,38 @@ +using NzbDrone.Core.Download; +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.DownloadClient +{ + public class DownloadClientResource : ProviderResource + { + public bool Enable { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class DownloadClientResourceMapper : ProviderResourceMapper<DownloadClientResource, DownloadClientDefinition> + { + public override DownloadClientResource ToResource(DownloadClientDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override DownloadClientDefinition ToModel(DownloadClientResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + definition.Protocol = resource.Protocol; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs b/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs new file mode 100644 index 000000000..f6a9bb24a --- /dev/null +++ b/src/Sonarr.Api.V3/FileSystem/FileSystemModule.cs @@ -0,0 +1,71 @@ +using System; +using System.IO; +using System.Linq; +using Nancy; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.MediaFiles; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.FileSystem +{ + public class FileSystemModule : SonarrV3Module + { + private readonly IFileSystemLookupService _fileSystemLookupService; + private readonly IDiskProvider _diskProvider; + private readonly IDiskScanService _diskScanService; + + public FileSystemModule(IFileSystemLookupService fileSystemLookupService, + IDiskProvider diskProvider, + IDiskScanService diskScanService) + : base("/filesystem") + { + _fileSystemLookupService = fileSystemLookupService; + _diskProvider = diskProvider; + _diskScanService = diskScanService; + Get["/"] = x => GetContents(); + Get["/type"] = x => GetEntityType(); + Get["/mediafiles"] = x => GetMediaFiles(); + } + + private Response GetContents() + { + var pathQuery = Request.Query.path; + var includeFiles = Request.GetBooleanQueryParameter("includeFiles"); + + + return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse(); + } + + private Response GetEntityType() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (_diskProvider.FileExists(path)) + { + return new { type = "file" }.AsResponse(); + } + + //Return folder even if it doesn't exist on disk to avoid leaking anything from the UI about the underlying system + return new { type = "folder" }.AsResponse(); + } + + private Response GetMediaFiles() + { + var pathQuery = Request.Query.path; + var path = (string)pathQuery.Value; + + if (!_diskProvider.FolderExists(path)) + { + return new string[0].AsResponse(); + } + + return _diskScanService.GetAudioFiles(path).Select(f => new { + Path = f, + RelativePath = path.GetRelativePath(f), + Name = Path.GetFileName(f) + }).AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Health/HealthModule.cs b/src/Sonarr.Api.V3/Health/HealthModule.cs new file mode 100644 index 000000000..d299b40f7 --- /dev/null +++ b/src/Sonarr.Api.V3/Health/HealthModule.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.HealthCheck; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Health +{ + public class HealthModule : LidarrRestModuleWithSignalR<HealthResource, HealthCheck>, + IHandle<HealthCheckCompleteEvent> + { + private readonly IHealthCheckService _healthCheckService; + + public HealthModule(IBroadcastSignalRMessage signalRBroadcaster, IHealthCheckService healthCheckService) + : base(signalRBroadcaster) + { + _healthCheckService = healthCheckService; + GetResourceAll = GetHealth; + } + + private List<HealthResource> GetHealth() + { + return _healthCheckService.Results().ToResource(); + } + + public void Handle(HealthCheckCompleteEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Health/HealthResource.cs b/src/Sonarr.Api.V3/Health/HealthResource.cs new file mode 100644 index 000000000..feebcb230 --- /dev/null +++ b/src/Sonarr.Api.V3/Health/HealthResource.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.Http; +using NzbDrone.Core.HealthCheck; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Health +{ + public class HealthResource : RestResource + { + public string Source { get; set; } + public HealthCheckResult Type { get; set; } + public string Message { get; set; } + public HttpUri WikiUrl { get; set; } + } + + public static class HealthResourceMapper + { + public static HealthResource ToResource(this HealthCheck model) + { + if (model == null) return null; + + return new HealthResource + { + Id = model.Id, + Source = model.Source.Name, + Type = model.Type, + Message = model.Message, + WikiUrl = model.WikiUrl + }; + } + + public static List<HealthResource> ToResource(this IEnumerable<HealthCheck> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/History/HistoryModule.cs b/src/Sonarr.Api.V3/History/HistoryModule.cs new file mode 100644 index 000000000..601c6364c --- /dev/null +++ b/src/Sonarr.Api.V3/History/HistoryModule.cs @@ -0,0 +1,76 @@ +using System; +using Nancy; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.History; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.History +{ + public class HistoryModule : LidarrRestModule<HistoryResource> + { + private readonly IHistoryService _historyService; + private readonly IUpgradableSpecification _upgradableSpecification; + private readonly IFailedDownloadService _failedDownloadService; + + public HistoryModule(IHistoryService historyService, + IUpgradableSpecification upgradableSpecification, + IFailedDownloadService failedDownloadService) + { + _historyService = historyService; + _upgradableSpecification = upgradableSpecification; + _failedDownloadService = failedDownloadService; + GetResourcePaged = GetHistory; + + Post["/failed"] = x => MarkAsFailed(); + } + + protected HistoryResource MapToResource(NzbDrone.Core.History.History model) + { + var resource = model.ToResource(); + + resource.Artist = model.Artist.ToResource(); + resource.Album = model.Album.ToResource(); + + if (model.Artist != null) + { + resource.QualityCutoffNotMet = _upgradableSpecification.CutoffNotMet(model.Artist.Profile.Value, + model.Artist.LanguageProfile, + model.Quality, + model.Language); + } + + return resource; + } + + private PagingResource<HistoryResource> GetHistory(PagingResource<HistoryResource> pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending); + + if (pagingResource.FilterKey == "eventType") + { + var filterValue = (HistoryEventType)Convert.ToInt32(pagingResource.FilterValue); + pagingSpec.FilterExpression = v => v.EventType == filterValue; + } + + if (pagingResource.FilterKey == "albumId") + { + int albumId = Convert.ToInt32(pagingResource.FilterValue); + pagingSpec.FilterExpression = h => h.AlbumId == albumId; + } + + return ApplyToPage(_historyService.Paged, pagingSpec, MapToResource); + } + + private Response MarkAsFailed() + { + var id = (int)Request.Form.Id; + _failedDownloadService.MarkAsFailed(id); + return new object().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/History/HistoryResource.cs b/src/Sonarr.Api.V3/History/HistoryResource.cs new file mode 100644 index 000000000..05e1bb522 --- /dev/null +++ b/src/Sonarr.Api.V3/History/HistoryResource.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using NzbDrone.Core.History; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.History +{ + public class HistoryResource : RestResource + { + public int AlbumId { get; set; } + public int ArtistId { get; set; } + public string SourceTitle { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public bool QualityCutoffNotMet { get; set; } + public DateTime Date { get; set; } + public string DownloadId { get; set; } + + public HistoryEventType EventType { get; set; } + + public Dictionary<string, string> Data { get; set; } + + public AlbumResource Album { get; set; } + public ArtistResource Artist { get; set; } + } + + public static class HistoryResourceMapper + { + public static HistoryResource ToResource(this NzbDrone.Core.History.History model) + { + if (model == null) return null; + + return new HistoryResource + { + Id = model.Id, + + AlbumId = model.AlbumId, + ArtistId = model.ArtistId, + SourceTitle = model.SourceTitle, + Language = model.Language, + Quality = model.Quality, + //QualityCutoffNotMet + Date = model.Date, + DownloadId = model.DownloadId, + + EventType = model.EventType, + + Data = model.Data + //Episode + //Series + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/IndexerModule.cs b/src/Sonarr.Api.V3/Indexers/IndexerModule.cs new file mode 100644 index 000000000..c3451b61b --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.Indexers +{ + public class IndexerModule : ProviderModuleBase<IndexerResource, IIndexer, IndexerDefinition> + { + public static readonly IndexerResourceMapper ResourceMapper = new IndexerResourceMapper(); + + public IndexerModule(IndexerFactory indexerFactory) + : base(indexerFactory, "indexer", ResourceMapper) + { + } + + protected override void Validate(IndexerDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Indexers/IndexerResource.cs b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs new file mode 100644 index 000000000..cfe23ea61 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/IndexerResource.cs @@ -0,0 +1,43 @@ +using NzbDrone.Core.Indexers; + +namespace Lidarr.Api.V3.Indexers +{ + public class IndexerResource : ProviderResource + { + public bool EnableRss { get; set; } + public bool EnableSearch { get; set; } + public bool SupportsRss { get; set; } + public bool SupportsSearch { get; set; } + public DownloadProtocol Protocol { get; set; } + } + + public class IndexerResourceMapper : ProviderResourceMapper<IndexerResource, IndexerDefinition> + { + public override IndexerResource ToResource(IndexerDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.EnableRss = definition.EnableRss; + resource.EnableSearch = definition.EnableSearch; + resource.SupportsRss = definition.SupportsRss; + resource.SupportsSearch = definition.SupportsSearch; + resource.Protocol = definition.Protocol; + + return resource; + } + + public override IndexerDefinition ToModel(IndexerResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.EnableRss = resource.EnableRss; + definition.EnableSearch = resource.EnableSearch; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs b/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs new file mode 100644 index 000000000..68b386797 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseModule.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using Nancy; +using Nancy.ModelBinding; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Exceptions; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.Parser.Model; +using Lidarr.Http.Extensions; +using HttpStatusCode = System.Net.HttpStatusCode; + +namespace Lidarr.Api.V3.Indexers +{ + public class ReleaseModule : ReleaseModuleBase + { + private readonly IFetchAndParseRss _rssFetcherAndParser; + private readonly ISearchForNzb _nzbSearchService; + private readonly IMakeDownloadDecision _downloadDecisionMaker; + private readonly IPrioritizeDownloadDecision _prioritizeDownloadDecision; + private readonly IDownloadService _downloadService; + private readonly Logger _logger; + + private readonly ICached<RemoteAlbum> _remoteAlbumCache; + + public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, + ISearchForNzb nzbSearchService, + IMakeDownloadDecision downloadDecisionMaker, + IPrioritizeDownloadDecision prioritizeDownloadDecision, + IDownloadService downloadService, + ICacheManager cacheManager, + Logger logger) + { + _rssFetcherAndParser = rssFetcherAndParser; + _nzbSearchService = nzbSearchService; + _downloadDecisionMaker = downloadDecisionMaker; + _prioritizeDownloadDecision = prioritizeDownloadDecision; + _downloadService = downloadService; + _logger = logger; + + GetResourceAll = GetReleases; + Post["/"] = x => DownloadRelease(this.Bind<ReleaseResource>()); + + PostValidator.RuleFor(s => s.DownloadAllowed).Equal(true); + PostValidator.RuleFor(s => s.Guid).NotEmpty(); + + _remoteAlbumCache = cacheManager.GetCache<RemoteAlbum>(GetType(), "remoteAlbums"); + } + + private Response DownloadRelease(ReleaseResource release) + { + var remoteEpisode = _remoteAlbumCache.Find(release.Guid); + + if (remoteEpisode == null) + { + _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); + + throw new NzbDroneClientException(HttpStatusCode.NotFound, "Couldn't find requested release in cache, try searching again"); + } + + try + { + _downloadService.DownloadReport(remoteEpisode); + } + catch (ReleaseDownloadException ex) + { + _logger.ErrorException(ex.Message, ex); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); + } + + return release.AsResponse(); + } + + private List<ReleaseResource> GetReleases() + { + if (Request.Query.episodeId.HasValue) + { + return GetEpisodeReleases(Request.Query.episodeId); + } + + return GetRss(); + } + + private List<ReleaseResource> GetEpisodeReleases(int albumId) + { + try + { + var decisions = _nzbSearchService.AlbumSearch(albumId, true, true); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); + + return MapDecisions(prioritizedDecisions); + } + catch (Exception ex) + { + _logger.ErrorException("Episode search failed: " + ex.Message, ex); + } + + return new List<ReleaseResource>(); + } + + private List<ReleaseResource> GetRss() + { + var reports = _rssFetcherAndParser.Fetch(); + var decisions = _downloadDecisionMaker.GetRssDecision(reports); + var prioritizedDecisions = _prioritizeDownloadDecision.PrioritizeDecisions(decisions); + + return MapDecisions(prioritizedDecisions); + } + + protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) + { + _remoteAlbumCache.Set(decision.RemoteAlbum.Release.Guid, decision.RemoteAlbum, TimeSpan.FromMinutes(30)); + return base.MapDecision(decision, initialWeight); + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs new file mode 100644 index 000000000..5d0e79ec5 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using NzbDrone.Core.DecisionEngine; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Indexers +{ + public abstract class ReleaseModuleBase : LidarrRestModule<ReleaseResource> + { + protected virtual List<ReleaseResource> MapDecisions(IEnumerable<DownloadDecision> decisions) + { + var result = new List<ReleaseResource>(); + + foreach (var downloadDecision in decisions) + { + var release = MapDecision(downloadDecision, result.Count); + + result.Add(release); + } + + return result; + } + + protected virtual ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) + { + var release = decision.ToResource(); + + release.ReleaseWeight = initialWeight; + + if (decision.RemoteAlbum.Artist != null) + { + release.QualityWeight = decision.RemoteAlbum.Artist + .Profile.Value + .Items.FindIndex(v => v.Quality == release.Quality.Quality) * 100; + } + + release.QualityWeight += release.Quality.Revision.Real * 10; + release.QualityWeight += release.Quality.Revision.Version; + + return release; + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs b/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs new file mode 100644 index 000000000..817602f12 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleasePushModule.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using Nancy; +using Nancy.ModelBinding; +using NLog; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.Parser.Model; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Indexers +{ + class ReleasePushModule : ReleaseModuleBase + { + private readonly IMakeDownloadDecision _downloadDecisionMaker; + private readonly IProcessDownloadDecisions _downloadDecisionProcessor; + private readonly Logger _logger; + + public ReleasePushModule(IMakeDownloadDecision downloadDecisionMaker, + IProcessDownloadDecisions downloadDecisionProcessor, + Logger logger) + { + _downloadDecisionMaker = downloadDecisionMaker; + _downloadDecisionProcessor = downloadDecisionProcessor; + _logger = logger; + + Post["/push"] = x => ProcessRelease(this.Bind<ReleaseResource>()); + + PostValidator.RuleFor(s => s.Title).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadUrl).NotEmpty(); + PostValidator.RuleFor(s => s.DownloadProtocol).NotEmpty(); + PostValidator.RuleFor(s => s.PublishDate).NotEmpty(); + } + + private Response ProcessRelease(ReleaseResource release) + { + _logger.Info("Release pushed: {0} - {1}", release.Title, release.DownloadUrl); + + var info = release.ToModel(); + + info.Guid = "PUSH-" + info.DownloadUrl; + + var decisions = _downloadDecisionMaker.GetRssDecision(new List<ReleaseInfo> { info }); + _downloadDecisionProcessor.ProcessDecisions(decisions); + + return MapDecisions(decisions).First().AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs new file mode 100644 index 000000000..373cf71f0 --- /dev/null +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Indexers +{ + public class ReleaseResource : RestResource + { + public string Guid { get; set; } + public QualityModel Quality { get; set; } + public int QualityWeight { get; set; } + public int Age { get; set; } + public double AgeHours { get; set; } + public double AgeMinutes { get; set; } + public long Size { get; set; } + public int IndexerId { get; set; } + public string Indexer { get; set; } + public string ReleaseGroup { get; set; } + public string SubGroup { get; set; } + public string ReleaseHash { get; set; } + public string Title { get; set; } + public bool FullSeason { get; set; } + public bool SceneSource { get; set; } + public int SeasonNumber { get; set; } + public Language Language { get; set; } + public string AirDate { get; set; } + public string ArtistName { get; set; } + public int[] EpisodeNumbers { get; set; } + public int[] AbsoluteEpisodeNumbers { get; set; } + public bool Approved { get; set; } + public bool TemporarilyRejected { get; set; } + public bool Rejected { get; set; } + public IEnumerable<string> Rejections { get; set; } + public DateTime PublishDate { get; set; } + public string CommentUrl { get; set; } + public string DownloadUrl { get; set; } + public string InfoUrl { get; set; } + public bool DownloadAllowed { get; set; } + public int ReleaseWeight { get; set; } + + public string MagnetUrl { get; set; } + public string InfoHash { get; set; } + public int? Seeders { get; set; } + public int? Leechers { get; set; } + public DownloadProtocol Protocol { get; set; } + + //TODO: besides a test I don't think this is used... + public DownloadProtocol DownloadProtocol { get; set; } + + //public bool IsDaily { get; set; } + //public bool IsAbsoluteNumbering { get; set; } + //public bool IsPossibleSpecialEpisode { get; set; } + //public bool Special { get; set; } + } + + public static class ReleaseResourceMapper + { + public static ReleaseResource ToResource(this DownloadDecision model) + { + var releaseInfo = model.RemoteAlbum.Release; + var parsedEpisodeInfo = model.RemoteAlbum.ParsedAlbumInfo; + var remoteEpisode = model.RemoteAlbum; + var torrentInfo = (model.RemoteAlbum.Release as TorrentInfo) ?? new TorrentInfo(); + + // TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?) + return new ReleaseResource + { + Guid = releaseInfo.Guid, + Quality = parsedEpisodeInfo.Quality, + //QualityWeight + Age = releaseInfo.Age, + AgeHours = releaseInfo.AgeHours, + AgeMinutes = releaseInfo.AgeMinutes, + Size = releaseInfo.Size, + IndexerId = releaseInfo.IndexerId, + Indexer = releaseInfo.Indexer, + ReleaseGroup = parsedEpisodeInfo.ReleaseGroup, + ReleaseHash = parsedEpisodeInfo.ReleaseHash, + Title = releaseInfo.Title, + //FullSeason = parsedEpisodeInfo.FullSeason, + //SeasonNumber = parsedEpisodeInfo.SeasonNumber, + Language = parsedEpisodeInfo.Language, + //AirDate = parsedEpisodeInfo.AirDate, + ArtistName = parsedEpisodeInfo.ArtistName, + + //EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers, + //AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers, + Approved = model.Approved, + TemporarilyRejected = model.TemporarilyRejected, + Rejected = model.Rejected, + Rejections = model.Rejections.Select(r => r.Reason).ToList(), + PublishDate = releaseInfo.PublishDate, + CommentUrl = releaseInfo.CommentUrl, + DownloadUrl = releaseInfo.DownloadUrl, + InfoUrl = releaseInfo.InfoUrl, + DownloadAllowed = remoteEpisode.DownloadAllowed, + //ReleaseWeight + + + MagnetUrl = torrentInfo.MagnetUrl, + InfoHash = torrentInfo.InfoHash, + Seeders = torrentInfo.Seeders, + Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null, + Protocol = releaseInfo.DownloadProtocol, + + //IsDaily = parsedEpisodeInfo.IsDaily, + //IsAbsoluteNumbering = parsedEpisodeInfo.IsAbsoluteNumbering, + //IsPossibleSpecialEpisode = parsedEpisodeInfo.IsPossibleSpecialEpisode, + //Special = parsedEpisodeInfo.Special, + }; + + } + + public static ReleaseInfo ToModel(this ReleaseResource resource) + { + ReleaseInfo model; + + if (resource.Protocol == DownloadProtocol.Torrent) + { + model = new TorrentInfo + { + MagnetUrl = resource.MagnetUrl, + InfoHash = resource.InfoHash, + Seeders = resource.Seeders, + Peers = (resource.Seeders.HasValue && resource.Leechers.HasValue) ? (resource.Seeders + resource.Leechers) : null + }; + } + else + { + model = new ReleaseInfo(); + } + + model.Guid = resource.Guid; + model.Title = resource.Title; + model.Size = resource.Size; + model.DownloadUrl = resource.DownloadUrl; + model.InfoUrl = resource.InfoUrl; + model.CommentUrl = resource.CommentUrl; + model.IndexerId = resource.IndexerId; + model.Indexer = resource.Indexer; + model.DownloadProtocol = resource.DownloadProtocol; + model.PublishDate = resource.PublishDate; + + return model; + } + } +} diff --git a/src/Sonarr.Api.V3/Lidarr.Api.V3.csproj b/src/Sonarr.Api.V3/Lidarr.Api.V3.csproj new file mode 100644 index 000000000..3775dc7ce --- /dev/null +++ b/src/Sonarr.Api.V3/Lidarr.Api.V3.csproj @@ -0,0 +1,242 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">x86</Platform> + <ProjectGuid>{7140FF1F-79BE-492F-9188-B21A050BF708}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Sonarr.Api.V3</RootNamespace> + <AssemblyName>Sonarr.Api.V3</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> + <RestorePackages>true</RestorePackages> + <TargetFrameworkProfile> + </TargetFrameworkProfile> + <ProductVersion>12.0.0</ProductVersion> + <SchemaVersion>2.0</SchemaVersion> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> + <DebugSymbols>true</DebugSymbols> + <OutputPath>..\..\_output\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <DebugType>full</DebugType> + <PlatformTarget>x86</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <WarningLevel>4</WarningLevel> + <Optimize>false</Optimize> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> + <OutputPath>..\..\_output\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <Optimize>true</Optimize> + <DebugType>pdbonly</DebugType> + <PlatformTarget>x86</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="antlr.runtime, Version=2.7.6.2, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Ical.Net.2.2.32\lib\net40\antlr.runtime.dll</HintPath> + </Reference> + <Reference Include="FluentValidation, Version=6.2.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Ical.Net, Version=2.1.0.18776, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.dll</HintPath> + </Reference> + <Reference Include="Ical.Net.Collections, Version=2.1.0.18775, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Ical.Net.2.2.32\lib\net40\Ical.Net.Collections.dll</HintPath> + </Reference> + <Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Nancy.Authentication.Basic, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Nancy.Authentication.Forms, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> + <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + </Reference> + <Reference Include="NodaTime, Version=1.3.0.0, Culture=neutral, PublicKeyToken=4226afe0d9b296d1, processorArchitecture=MSIL"> + <HintPath>..\packages\Ical.Net.2.2.32\lib\net40\NodaTime.dll</HintPath> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="Microsoft.CSharp" /> + </ItemGroup> + <ItemGroup> + <Compile Include="..\NzbDrone.Common\Properties\SharedAssemblyInfo.cs"> + <Link>Properties\SharedAssemblyInfo.cs</Link> + </Compile> + <Compile Include="Albums\AlbumModule.cs" /> + <Compile Include="Albums\AlbumModuleWithSignalR.cs" /> + <Compile Include="Albums\AlbumResource.cs" /> + <Compile Include="Albums\AlbumsMonitoredResource.cs" /> + <Compile Include="Albums\AlbumStatisticsResource.cs" /> + <Compile Include="Blacklist\BlacklistModule.cs" /> + <Compile Include="Blacklist\BlacklistResource.cs" /> + <Compile Include="Calendar\CalendarFeedModule.cs" /> + <Compile Include="Calendar\CalendarModule.cs" /> + <Compile Include="Commands\CommandModule.cs" /> + <Compile Include="Commands\CommandResource.cs" /> + <Compile Include="TrackFiles\TrackFileListResource.cs" /> + <Compile Include="TrackFiles\MediaInfoResource.cs" /> + <Compile Include="Indexers\ReleaseModuleBase.cs" /> + <Compile Include="Indexers\ReleasePushModule.cs" /> + <Compile Include="Parse\ParseModule.cs" /> + <Compile Include="Parse\ParseResource.cs" /> + <Compile Include="ManualImport\ManualImportModule.cs" /> + <Compile Include="ManualImport\ManualImportResource.cs" /> + <Compile Include="Profiles\Delay\DelayProfileModule.cs" /> + <Compile Include="Profiles\Delay\DelayProfileResource.cs" /> + <Compile Include="Profiles\Language\LanguageProfileModule.cs" /> + <Compile Include="Profiles\Language\LanguageProfileResource.cs" /> + <Compile Include="Profiles\Language\LanguageProfileSchemaModule.cs" /> + <Compile Include="Profiles\Language\LanguageValidator.cs" /> + <Compile Include="Queue\QueueActionModule.cs" /> + <Compile Include="Queue\QueueBulkResource.cs" /> + <Compile Include="Queue\QueueDetailsModule.cs" /> + <Compile Include="Queue\QueueStatusModule.cs" /> + <Compile Include="Queue\QueueStatusResource.cs" /> + <Compile Include="RemotePathMappings\RemotePathMappingModule.cs" /> + <Compile Include="RemotePathMappings\RemotePathMappingResource.cs" /> + <Compile Include="Config\UiConfigModule.cs" /> + <Compile Include="Config\UiConfigResource.cs" /> + <Compile Include="Config\DownloadClientConfigModule.cs" /> + <Compile Include="Config\DownloadClientConfigResource.cs" /> + <Compile Include="Config\HostConfigModule.cs" /> + <Compile Include="Config\HostConfigResource.cs" /> + <Compile Include="Config\IndexerConfigModule.cs" /> + <Compile Include="Config\IndexerConfigResource.cs" /> + <Compile Include="Config\MediaManagementConfigModule.cs" /> + <Compile Include="Config\MediaManagementConfigResource.cs" /> + <Compile Include="Config\NamingConfigModule.cs" /> + <Compile Include="Config\NamingConfigResource.cs" /> + <Compile Include="Config\NamingExampleResource.cs" /> + <Compile Include="Config\SonarrConfigModule.cs" /> + <Compile Include="FileSystem\FileSystemModule.cs" /> + <Compile Include="DiskSpace\DiskSpaceModule.cs" /> + <Compile Include="DiskSpace\DiskSpaceResource.cs" /> + <Compile Include="DownloadClient\DownloadClientModule.cs" /> + <Compile Include="DownloadClient\DownloadClientResource.cs" /> + <Compile Include="TrackFiles\TrackFileModule.cs" /> + <Compile Include="TrackFiles\TrackFileResource.cs" /> + <Compile Include="Tracks\TrackModule.cs" /> + <Compile Include="Tracks\TrackModuleWithSignalR.cs" /> + <Compile Include="Tracks\TrackResource.cs" /> + <Compile Include="Tracks\RenameTrackModule.cs" /> + <Compile Include="Tracks\RenameTrackResource.cs" /> + <Compile Include="Health\HealthModule.cs" /> + <Compile Include="Health\HealthResource.cs" /> + <Compile Include="History\HistoryModule.cs" /> + <Compile Include="History\HistoryResource.cs" /> + <Compile Include="Indexers\IndexerModule.cs" /> + <Compile Include="Indexers\IndexerResource.cs" /> + <Compile Include="Indexers\ReleaseModule.cs" /> + <Compile Include="Indexers\ReleaseResource.cs" /> + <Compile Include="Logs\LogFileModule.cs" /> + <Compile Include="Logs\LogFileModuleBase.cs" /> + <Compile Include="Logs\LogFileResource.cs" /> + <Compile Include="Logs\LogModule.cs" /> + <Compile Include="Logs\LogResource.cs" /> + <Compile Include="Logs\UpdateLogFileModule.cs" /> + <Compile Include="MediaCovers\MediaCoverModule.cs" /> + <Compile Include="Metadata\MetadataModule.cs" /> + <Compile Include="Metadata\MetadataResource.cs" /> + <Compile Include="Notifications\NotificationModule.cs" /> + <Compile Include="Notifications\NotificationResource.cs" /> + <Compile Include="AlbumStudio\AlbumStudioArtistResource.cs" /> + <Compile Include="Artist\ArtistEditorDeleteResource.cs" /> + <Compile Include="Artist\ArtistEditorResource.cs" /> + <Compile Include="Artist\ArtistImportModule.cs" /> + <Compile Include="SonarrV3FeedModule.cs" /> + <Compile Include="SonarrV3Module.cs" /> + <Compile Include="Profiles\Quality\QualityProfileModule.cs" /> + <Compile Include="Profiles\Quality\QualityProfileResource.cs" /> + <Compile Include="Profiles\Quality\QualityProfileSchemaModule.cs" /> + <Compile Include="Profiles\Quality\QualityProfileValidation.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="ProviderModuleBase.cs" /> + <Compile Include="ProviderResource.cs" /> + <Compile Include="Qualities\QualityDefinitionModule.cs" /> + <Compile Include="Qualities\QualityDefinitionResource.cs" /> + <Compile Include="Queue\QueueModule.cs" /> + <Compile Include="Queue\QueueResource.cs" /> + <Compile Include="Restrictions\RestrictionModule.cs" /> + <Compile Include="Restrictions\RestrictionResource.cs" /> + <Compile Include="RootFolders\RootFolderModule.cs" /> + <Compile Include="RootFolders\RootFolderResource.cs" /> + <Compile Include="AlbumStudio\AlbumStudioResource.cs" /> + <Compile Include="Artist\AlternateTitleResource.cs" /> + <Compile Include="AlbumStudio\AlbumStudioModule.cs" /> + <Compile Include="Artist\ArtistEditorModule.cs" /> + <Compile Include="Artist\ArtistLookupModule.cs" /> + <Compile Include="Artist\ArtistModule.cs" /> + <Compile Include="Artist\ArtistResource.cs" /> + <Compile Include="System\Backup\BackupModule.cs" /> + <Compile Include="System\Backup\BackupResource.cs" /> + <Compile Include="System\Tasks\TaskModule.cs" /> + <Compile Include="System\Tasks\TaskResource.cs" /> + <Compile Include="System\SystemModule.cs" /> + <Compile Include="Tags\TagDetailsModule.cs" /> + <Compile Include="Tags\TagModule.cs" /> + <Compile Include="Tags\TagDetailsResource.cs" /> + <Compile Include="Tags\TagResource.cs" /> + <Compile Include="Update\UpdateModule.cs" /> + <Compile Include="Update\UpdateResource.cs" /> + <Compile Include="Wanted\CutoffModule.cs" /> + <Compile Include="Wanted\MissingModule.cs" /> + </ItemGroup> + <ItemGroup> + <None Include="app.config" /> + <None Include="packages.config"> + <SubType>Designer</SubType> + </None> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Marr.Data\Marr.Data.csproj"> + <Project>{F6FC6BE7-0847-4817-A1ED-223DC647C3D7}</Project> + <Name>Marr.Data</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> + <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> + <Name>NzbDrone.Common</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.Core\NzbDrone.Core.csproj"> + <Project>{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}</Project> + <Name>NzbDrone.Core</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.SignalR\NzbDrone.SignalR.csproj"> + <Project>{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}</Project> + <Name>NzbDrone.SignalR</Name> + </ProjectReference> + <ProjectReference Include="..\Sonarr.Http\Lidarr.Http.csproj"> + <Project>{5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}</Project> + <Name>Lidarr.Http</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup /> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogFileModule.cs b/src/Sonarr.Api.V3/Logs/LogFileModule.cs new file mode 100644 index 000000000..19676d523 --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Api.V3.Logs +{ + public class LogFileModule : LogFileModuleBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public LogFileModule(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider) + : base(diskProvider, configFileProvider, "") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + } + + protected override IEnumerable<string> GetLogFiles() + { + return _diskProvider.GetFiles(_appFolderInfo.GetLogFolder(), SearchOption.TopDirectoryOnly); + } + + protected override string GetLogFilePath(string filename) + { + return Path.Combine(_appFolderInfo.GetLogFolder(), filename); + } + + protected override string DownloadUrlRoot + { + get + { + return "logfile"; + } + } + + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs b/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs new file mode 100644 index 000000000..47a8eab6d --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileModuleBase.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Configuration; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Logs +{ + public abstract class LogFileModuleBase : LidarrRestModule<LogFileResource> + { + protected const string LOGFILE_ROUTE = @"/(?<filename>[-.a-zA-Z0-9]+?\.txt)"; + + private readonly IDiskProvider _diskProvider; + private readonly IConfigFileProvider _configFileProvider; + + public LogFileModuleBase(IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + string route) + : base("log/file" + route) + { + _diskProvider = diskProvider; + _configFileProvider = configFileProvider; + GetResourceAll = GetLogFilesResponse; + + Get[LOGFILE_ROUTE] = options => GetLogFileResponse(options.filename); + } + + private List<LogFileResource> GetLogFilesResponse() + { + var result = new List<LogFileResource>(); + + var files = GetLogFiles().ToList(); + + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + var filename = Path.GetFileName(file); + + result.Add(new LogFileResource + { + Id = i + 1, + Filename = filename, + LastWriteTime = _diskProvider.FileGetLastWrite(file), + ContentsUrl = string.Format("{0}/api/v3/{1}/{2}", _configFileProvider.UrlBase, Resource, filename), + DownloadUrl = string.Format("{0}/{1}/{2}", _configFileProvider.UrlBase, DownloadUrlRoot, filename) + }); + } + + return result.OrderByDescending(l => l.LastWriteTime).ToList(); + } + + private Response GetLogFileResponse(string filename) + { + var filePath = GetLogFilePath(filename); + + if (!_diskProvider.FileExists(filePath)) + return new NotFoundResponse(); + + var data = _diskProvider.ReadAllText(filePath); + + return new TextResponse(data); + } + + protected abstract IEnumerable<string> GetLogFiles(); + protected abstract string GetLogFilePath(string filename); + + protected abstract string DownloadUrlRoot { get; } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogFileResource.cs b/src/Sonarr.Api.V3/Logs/LogFileResource.cs new file mode 100644 index 000000000..2bfb7da7c --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogFileResource.cs @@ -0,0 +1,13 @@ +using System; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Logs +{ + public class LogFileResource : RestResource + { + public string Filename { get; set; } + public DateTime LastWriteTime { get; set; } + public string ContentsUrl { get; set; } + public string DownloadUrl { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Logs/LogModule.cs b/src/Sonarr.Api.V3/Logs/LogModule.cs new file mode 100644 index 000000000..a1b9b79e3 --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogModule.cs @@ -0,0 +1,60 @@ +using NzbDrone.Core.Instrumentation; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Logs +{ + public class LogModule : LidarrRestModule<LogResource> + { + private readonly ILogService _logService; + + public LogModule(ILogService logService) + { + _logService = logService; + GetResourcePaged = GetLogs; + } + + private PagingResource<LogResource> GetLogs(PagingResource<LogResource> pagingResource) + { + var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(); + + if (pageSpec.SortKey == "time") + { + pageSpec.SortKey = "id"; + } + + if (pagingResource.FilterKey == "level") + { + switch (pagingResource.FilterValue) + { + case "Fatal": + pageSpec.FilterExpression = h => h.Level == "Fatal"; + break; + case "Error": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error"; + break; + case "Warn": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn"; + break; + case "Info": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info"; + break; + case "Debug": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug"; + break; + case "Trace": + pageSpec.FilterExpression = h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace"; + break; + } + } + + var response = ApplyToPage(_logService.Paged, pageSpec, LogResourceMapper.ToResource); + + if (pageSpec.SortKey == "id") + { + response.SortKey = "time"; + } + + return response; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Logs/LogResource.cs b/src/Sonarr.Api.V3/Logs/LogResource.cs new file mode 100644 index 000000000..e3eef3836 --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/LogResource.cs @@ -0,0 +1,37 @@ +using System; +using NzbDrone.Core.Instrumentation; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Logs +{ + public class LogResource : RestResource + { + public DateTime Time { get; set; } + public string Exception { get; set; } + public string ExceptionType { get; set; } + public string Level { get; set; } + public string Logger { get; set; } + public string Message { get; set; } + public string Method { get; set; } + } + + public static class LogResourceMapper + { + public static LogResource ToResource(this Log model) + { + if (model == null) return null; + + return new LogResource + { + Id = model.Id, + + Time = model.Time, + Exception = model.Exception, + ExceptionType = model.ExceptionType, + Level = model.Level, + Logger = model.Logger, + Message = model.Message + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs b/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs new file mode 100644 index 000000000..f9ac8311c --- /dev/null +++ b/src/Sonarr.Api.V3/Logs/UpdateLogFileModule.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Api.V3.Logs +{ + public class UpdateLogFileModule : LogFileModuleBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public UpdateLogFileModule(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider) + : base(diskProvider, configFileProvider, "/update") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + } + + protected override IEnumerable<string> GetLogFiles() + { + if (!_diskProvider.FolderExists(_appFolderInfo.GetUpdateLogFolder())) return Enumerable.Empty<string>(); + + return _diskProvider.GetFiles(_appFolderInfo.GetUpdateLogFolder(), SearchOption.TopDirectoryOnly) + .Where(f => Regex.IsMatch(Path.GetFileName(f), LOGFILE_ROUTE.TrimStart('/'), RegexOptions.IgnoreCase)) + .ToList(); + } + + protected override string GetLogFilePath(string filename) + { + return Path.Combine(_appFolderInfo.GetUpdateLogFolder(), filename); + } + + protected override string DownloadUrlRoot + { + get + { + return "updatelogfile"; + } + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs new file mode 100644 index 000000000..11a3e4924 --- /dev/null +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportModule.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.MediaFiles.TrackImport.Manual; +using NzbDrone.Core.Qualities; +using Lidarr.Http; + +namespace Lidarr.Api.V3.ManualImport +{ + public class ManualImportModule : LidarrRestModule<ManualImportResource> + { + private readonly IManualImportService _manualImportService; + + public ManualImportModule(IManualImportService manualImportService) + : base("/manualimport") + { + _manualImportService = manualImportService; + + GetResourceAll = GetMediaFiles; + } + + private List<ManualImportResource> GetMediaFiles() + { + var folder = (string)Request.Query.folder; + var downloadId = (string)Request.Query.downloadId; + + return _manualImportService.GetMediaFiles(folder, downloadId).ToResource().Select(AddQualityWeight).ToList(); + } + + private ManualImportResource AddQualityWeight(ManualImportResource item) + { + if (item.Quality != null) + { + item.QualityWeight = Quality.DefaultQualityDefinitions.Single(q => q.Quality == item.Quality.Quality).Weight; + item.QualityWeight += item.Quality.Revision.Real * 10; + item.QualityWeight += item.Quality.Revision.Version; + } + + return item; + } + } +} diff --git a/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs new file mode 100644 index 000000000..c27d6e7f6 --- /dev/null +++ b/src/Sonarr.Api.V3/ManualImport/ManualImportResource.cs @@ -0,0 +1,56 @@ +using NzbDrone.Common.Crypto; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles.TrackImport.Manual; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Artist; +using Lidarr.Api.V3.Tracks; +using Lidarr.Http.REST; +using System.Collections.Generic; +using System.Linq; + +namespace Lidarr.Api.V3.ManualImport +{ + public class ManualImportResource : RestResource + { + public string Path { get; set; } + public string RelativePath { get; set; } + public string Name { get; set; } + public long Size { get; set; } + //public ArtistResource Artist { get; set; } + public int? SeasonNumber { get; set; } + public List<TrackResource> Episodes { get; set; } + public QualityModel Quality { get; set; } + public int QualityWeight { get; set; } + public string DownloadId { get; set; } + public IEnumerable<Rejection> Rejections { get; set; } + } + + public static class ManualImportResourceMapper + { + public static ManualImportResource ToResource(this ManualImportItem model) + { + if (model == null) return null; + + return new ManualImportResource + { + Id = HashConverter.GetHashInt31(model.Path), + Path = model.Path, + RelativePath = model.RelativePath, + Name = model.Name, + Size = model.Size, + //Artist = model., + SeasonNumber = model.SeasonNumber, + //Episodes = model.Episodes.ToResource(), + Quality = model.Quality, + //QualityWeight + DownloadId = model.DownloadId, + Rejections = model.Rejections + }; + } + + public static List<ManualImportResource> ToResource(this IEnumerable<ManualImportItem> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs b/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs new file mode 100644 index 000000000..922cf54a4 --- /dev/null +++ b/src/Sonarr.Api.V3/MediaCovers/MediaCoverModule.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Text.RegularExpressions; +using Nancy; +using Nancy.Responses; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace Lidarr.Api.V3.MediaCovers +{ + public class MediaCoverModule : SonarrV3Module + { + private static readonly Regex RegexResizedImage = new Regex(@"-\d+\.jpg$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private const string MEDIA_COVER_ROUTE = @"/(?<seriesId>\d+)/(?<filename>(.+)\.(jpg|png|gif))"; + + private readonly IAppFolderInfo _appFolderInfo; + private readonly IDiskProvider _diskProvider; + + public MediaCoverModule(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider) : base("MediaCover") + { + _appFolderInfo = appFolderInfo; + _diskProvider = diskProvider; + + Get[MEDIA_COVER_ROUTE] = options => GetMediaCover(options.seriesId, options.filename); + } + + private Response GetMediaCover(int seriesId, string filename) + { + var filePath = Path.Combine(_appFolderInfo.GetAppDataPath(), "MediaCover", seriesId.ToString(), filename); + + if (!_diskProvider.FileExists(filePath) || _diskProvider.GetFileSize(filePath) == 0) + { + // Return the full sized image if someone requests a non-existing resized one. + // TODO: This code can be removed later once everyone had the update for a while. + var basefilePath = RegexResizedImage.Replace(filePath, ".jpg"); + if (basefilePath == filePath || !_diskProvider.FileExists(basefilePath)) + { + return new NotFoundResponse(); + } + filePath = basefilePath; + } + + return new StreamResponse(() => File.OpenRead(filePath), MimeTypes.GetMimeType(filePath)); + } + } +} diff --git a/src/Sonarr.Api.V3/Metadata/MetadataModule.cs b/src/Sonarr.Api.V3/Metadata/MetadataModule.cs new file mode 100644 index 000000000..c8de3fc02 --- /dev/null +++ b/src/Sonarr.Api.V3/Metadata/MetadataModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Lidarr.Api.V3.Metadata +{ + public class MetadataModule : ProviderModuleBase<MetadataResource, IMetadata, MetadataDefinition> + { + public static readonly MetadataResourceMapper ResourceMapper = new MetadataResourceMapper(); + + public MetadataModule(IMetadataFactory metadataFactory) + : base(metadataFactory, "metadata", ResourceMapper) + { + } + + protected override void Validate(MetadataDefinition definition, bool includeWarnings) + { + if (!definition.Enable) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Metadata/MetadataResource.cs b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs new file mode 100644 index 000000000..3920b46f9 --- /dev/null +++ b/src/Sonarr.Api.V3/Metadata/MetadataResource.cs @@ -0,0 +1,34 @@ +using NzbDrone.Core.Extras.Metadata; + +namespace Lidarr.Api.V3.Metadata +{ + public class MetadataResource : ProviderResource + { + public bool Enable { get; set; } + } + + public class MetadataResourceMapper : ProviderResourceMapper<MetadataResource, MetadataDefinition> + { + public override MetadataResource ToResource(MetadataDefinition definition) + { + if (definition == null) return null; + + var resource = base.ToResource(definition); + + resource.Enable = definition.Enable; + + return resource; + } + + public override MetadataDefinition ToModel(MetadataResource resource) + { + if (resource == null) return null; + + var definition = base.ToModel(resource); + + definition.Enable = resource.Enable; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Notifications/NotificationModule.cs b/src/Sonarr.Api.V3/Notifications/NotificationModule.cs new file mode 100644 index 000000000..edf57eaa8 --- /dev/null +++ b/src/Sonarr.Api.V3/Notifications/NotificationModule.cs @@ -0,0 +1,20 @@ +using NzbDrone.Core.Notifications; + +namespace Lidarr.Api.V3.Notifications +{ + public class NotificationModule : ProviderModuleBase<NotificationResource, INotification, NotificationDefinition> + { + public static readonly NotificationResourceMapper ResourceMapper = new NotificationResourceMapper(); + + public NotificationModule(NotificationFactory notificationFactory) + : base(notificationFactory, "notification", ResourceMapper) + { + } + + protected override void Validate(NotificationDefinition definition, bool includeWarnings) + { + if (!definition.OnGrab && !definition.OnDownload) return; + base.Validate(definition, includeWarnings); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Notifications/NotificationResource.cs b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs new file mode 100644 index 000000000..1cdb4e176 --- /dev/null +++ b/src/Sonarr.Api.V3/Notifications/NotificationResource.cs @@ -0,0 +1,57 @@ +using NzbDrone.Core.Notifications; + +namespace Lidarr.Api.V3.Notifications +{ + public class NotificationResource : ProviderResource + { + public string Link { get; set; } + public bool OnGrab { get; set; } + public bool OnDownload { get; set; } + public bool OnUpgrade { get; set; } + public bool OnRename { get; set; } + public bool SupportsOnGrab { get; set; } + public bool SupportsOnDownload { get; set; } + public bool SupportsOnUpgrade { get; set; } + public bool SupportsOnRename { get; set; } + public string TestCommand { get; set; } + } + + public class NotificationResourceMapper : ProviderResourceMapper<NotificationResource, NotificationDefinition> + { + public override NotificationResource ToResource(NotificationDefinition definition) + { + if (definition == null) return default(NotificationResource); + + var resource = base.ToResource(definition); + + resource.OnGrab = definition.OnGrab; + resource.OnDownload = definition.OnDownload; + resource.OnUpgrade = definition.OnUpgrade; + resource.OnRename = definition.OnRename; + resource.SupportsOnGrab = definition.SupportsOnGrab; + resource.SupportsOnDownload = definition.SupportsOnDownload; + resource.SupportsOnUpgrade = definition.SupportsOnUpgrade; + resource.SupportsOnRename = definition.SupportsOnRename; + + return resource; + } + + public override NotificationDefinition ToModel(NotificationResource resource) + { + if (resource == null) return default(NotificationDefinition); + + var definition = base.ToModel(resource); + + definition.OnGrab = resource.OnGrab; + definition.OnDownload = resource.OnDownload; + definition.OnUpgrade = resource.OnUpgrade; + definition.OnRename = resource.OnRename; + definition.SupportsOnGrab = resource.SupportsOnGrab; + definition.SupportsOnDownload = resource.SupportsOnDownload; + definition.SupportsOnUpgrade = resource.SupportsOnUpgrade; + definition.SupportsOnRename = resource.SupportsOnRename; + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Parse/ParseModule.cs b/src/Sonarr.Api.V3/Parse/ParseModule.cs new file mode 100644 index 000000000..e6f3b15b6 --- /dev/null +++ b/src/Sonarr.Api.V3/Parse/ParseModule.cs @@ -0,0 +1,59 @@ +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Parser; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Parse +{ + public class ParseModule : LidarrRestModule<ParseResource> + { + private readonly IParsingService _parsingService; + + public ParseModule(IParsingService parsingService) + { + _parsingService = parsingService; + + GetResourceSingle = Parse; + } + + private ParseResource Parse() + { + var title = Request.Query.Title.Value as string; + var path = Request.Query.Path.Value as string; + var parsedEpisodeInfo = path.IsNotNullOrWhiteSpace() ? Parser.ParseMusicPath(path) : Parser.ParseMusicTitle(title); + + if (parsedEpisodeInfo == null) + { + return null; + } + + return new ParseResource + { + Title = title, + ParsedAlbumInfo = parsedEpisodeInfo + }; + + //var remoteEpisode = null //_parsingService.Map(parsedEpisodeInfo, 0, 0); + + //if (remoteEpisode != null) + //{ + // return new ParseResource + // { + // Title = title, + // ParsedAlbumInfo = remoteEpisode.ParsedEpisodeInfo, + // Artist = remoteEpisode.Series.ToResource(), + // Albums = remoteEpisode.Episodes.ToResource() + // }; + //} + //else + //{ + // return new ParseResource + // { + // Title = title, + // ParsedAlbumInfo = parsedEpisodeInfo + // }; + //} + } + } +} diff --git a/src/Sonarr.Api.V3/Parse/ParseResource.cs b/src/Sonarr.Api.V3/Parse/ParseResource.cs new file mode 100644 index 000000000..85a335186 --- /dev/null +++ b/src/Sonarr.Api.V3/Parse/ParseResource.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using NzbDrone.Core.Parser.Model; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Parse +{ + public class ParseResource : RestResource + { + public string Title { get; set; } + public ParsedTrackInfo ParsedAlbumInfo { get; set; } + public ArtistResource Artist { get; set; } + public List<AlbumResource> Albums { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs new file mode 100644 index 000000000..217a50bc6 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileModule.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using FluentValidation; +using FluentValidation.Results; +using Nancy; +using NzbDrone.Core.Profiles.Delay; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; +using Lidarr.Http.Validation; + +namespace Lidarr.Api.V3.Profiles.Delay +{ + public class DelayProfileModule : LidarrRestModule<DelayProfileResource> + { + private readonly IDelayProfileService _delayProfileService; + + public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator) + { + _delayProfileService = delayProfileService; + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + Put[@"/reorder/(?<id>[\d]{1,10})"] = options => Reorder(options.Id); + + SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1); + SharedValidator.RuleFor(d => d.Tags).EmptyCollection<DelayProfileResource, int>().When(d => d.Id == 1); + SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator); + SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0); + + SharedValidator.Custom(delayProfile => + { + if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent) + { + return new ValidationFailure("", "Either Usenet or Torrent should be enabled"); + } + + return null; + }); + } + + private int Create(DelayProfileResource resource) + { + var model = resource.ToModel(); + model = _delayProfileService.Add(model); + + return model.Id; + } + + private void DeleteProfile(int id) + { + if (id == 1) + { + throw new MethodNotAllowedException("Cannot delete global delay profile"); + } + + _delayProfileService.Delete(id); + } + + private void Update(DelayProfileResource resource) + { + var model = resource.ToModel(); + _delayProfileService.Update(model); + } + + private DelayProfileResource GetById(int id) + { + return _delayProfileService.Get(id).ToResource(); + } + + private List<DelayProfileResource> GetAll() + { + return _delayProfileService.All().ToResource(); + } + + private Response Reorder(int id) + { + ValidateId(id); + + var afterIdQuery = Request.Query.After; + int? afterId = afterIdQuery.HasValue ? Convert.ToInt32(afterIdQuery.Value) : null; + + return _delayProfileService.Reorder(id, afterId).ToResource().AsResponse(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs new file mode 100644 index 000000000..6ec92a6cd --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Delay/DelayProfileResource.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Profiles.Delay; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Profiles.Delay +{ + public class DelayProfileResource : RestResource + { + public bool EnableUsenet { get; set; } + public bool EnableTorrent { get; set; } + public DownloadProtocol PreferredProtocol { get; set; } + public int UsenetDelay { get; set; } + public int TorrentDelay { get; set; } + public int Order { get; set; } + public HashSet<int> Tags { get; set; } + } + + public static class DelayProfileResourceMapper + { + public static DelayProfileResource ToResource(this DelayProfile model) + { + if (model == null) return null; + + return new DelayProfileResource + { + Id = model.Id, + + EnableUsenet = model.EnableUsenet, + EnableTorrent = model.EnableTorrent, + PreferredProtocol = model.PreferredProtocol, + UsenetDelay = model.UsenetDelay, + TorrentDelay = model.TorrentDelay, + Order = model.Order, + Tags = new HashSet<int>(model.Tags) + }; + } + + public static DelayProfile ToModel(this DelayProfileResource resource) + { + if (resource == null) return null; + + return new DelayProfile + { + Id = resource.Id, + + EnableUsenet = resource.EnableUsenet, + EnableTorrent = resource.EnableTorrent, + PreferredProtocol = resource.PreferredProtocol, + UsenetDelay = resource.UsenetDelay, + TorrentDelay = resource.TorrentDelay, + Order = resource.Order, + Tags = new HashSet<int>(resource.Tags) + }; + } + + public static List<DelayProfileResource> ToResource(this IEnumerable<DelayProfile> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs new file mode 100644 index 000000000..458eede53 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileModule : LidarrRestModule<LanguageProfileResource> + { + private readonly ILanguageProfileService _profileService; + + public LanguageProfileModule(ILanguageProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Languages).MustHaveAllowedLanguage(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(LanguageProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(LanguageProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + } + + private LanguageProfileResource GetById(int id) + { + return _profileService.Get(id).ToResource(); + } + + private List<LanguageProfileResource> GetAll() + { + var profiles = _profileService.All().ToResource(); + + return profiles; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileResource.cs new file mode 100644 index 000000000..fabfc40f7 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileResource.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileResource : RestResource + { + public string Name { get; set; } + public NzbDrone.Core.Languages.Language Cutoff { get; set; } + public List<ProfileLanguageItemResource> Languages { get; set; } + } + + public class ProfileLanguageItemResource : RestResource + { + public NzbDrone.Core.Languages.Language Language { get; set; } + public bool Allowed { get; set; } + } + + public static class LanguageProfileResourceMapper + { + public static LanguageProfileResource ToResource(this LanguageProfile model) + { + if (model == null) return null; + + return new LanguageProfileResource + { + Id = model.Id, + Name = model.Name, + Cutoff = model.Cutoff, + Languages = model.Languages.ConvertAll(ToResource) + }; + } + + public static ProfileLanguageItemResource ToResource(this ProfileLanguageItem model) + { + if (model == null) return null; + + return new ProfileLanguageItemResource + { + Language = model.Language, + Allowed = model.Allowed + }; + } + + public static LanguageProfile ToModel(this LanguageProfileResource resource) + { + if (resource == null) return null; + + return new LanguageProfile + { + Id = resource.Id, + Name = resource.Name, + Cutoff = (NzbDrone.Core.Languages.Language)resource.Cutoff.Id, + Languages = resource.Languages.ConvertAll(ToModel) + }; + } + + public static ProfileLanguageItem ToModel(this ProfileLanguageItemResource resource) + { + if (resource == null) return null; + + return new ProfileLanguageItem + { + Language = (NzbDrone.Core.Languages.Language)resource.Language.Id, + Allowed = resource.Allowed + }; + } + + public static List<LanguageProfileResource> ToResource(this IEnumerable<LanguageProfile> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs new file mode 100644 index 000000000..899658441 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageProfileSchemaModule.cs @@ -0,0 +1,37 @@ +using System.Linq; +using NzbDrone.Core.Profiles.Languages; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public class LanguageProfileSchemaModule : LidarrRestModule<LanguageProfileResource> + { + + public LanguageProfileSchemaModule() + : base("/languageprofile/schema") + { + GetResourceSingle = GetAll; + } + + private LanguageProfileResource GetAll() + { + var orderedLanguages = NzbDrone.Core.Languages.Language.All + .Where(l => l != NzbDrone.Core.Languages.Language.Unknown) + .OrderByDescending(l => l.Name) + .ToList(); + + orderedLanguages.Insert(0, NzbDrone.Core.Languages.Language.Unknown); + + var languages = orderedLanguages.Select(v => new ProfileLanguageItem {Language = v, Allowed = false}) + .ToList(); + + var profile = new LanguageProfile + { + Cutoff = NzbDrone.Core.Languages.Language.Unknown, + Languages = languages + }; + + return profile.ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Language/LanguageValidator.cs b/src/Sonarr.Api.V3/Profiles/Language/LanguageValidator.cs new file mode 100644 index 000000000..6aa5de220 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Language/LanguageValidator.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Lidarr.Api.V3.Profiles.Language +{ + public static class LanguageValidation + { + public static IRuleBuilderOptions<T, IList<ProfileLanguageItemResource>> MustHaveAllowedLanguage<T>(this IRuleBuilder<T, IList<ProfileLanguageItemResource>> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new LanguageValidator<T>()); + } + } + + + public class LanguageValidator<T> : PropertyValidator + { + public LanguageValidator() + : base("Must have at least one allowed language") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList<ProfileLanguageItemResource>; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs new file mode 100644 index 000000000..dbc7d8a1d --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileModule.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.Profiles.Qualities; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class ProfileModule : LidarrRestModule<QualityProfileResource> + { + private readonly IProfileService _profileService; + + public ProfileModule(IProfileService profileService) + { + _profileService = profileService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Cutoff).NotNull(); + SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality(); + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + CreateResource = Create; + DeleteResource = DeleteProfile; + } + + private int Create(QualityProfileResource resource) + { + var model = resource.ToModel(); + model = _profileService.Add(model); + return model.Id; + } + + private void DeleteProfile(int id) + { + _profileService.Delete(id); + } + + private void Update(QualityProfileResource resource) + { + var model = resource.ToModel(); + + _profileService.Update(model); + } + + private QualityProfileResource GetById(int id) + { + return _profileService.Get(id).ToResource(); + } + + private List<QualityProfileResource> GetAll() + { + return _profileService.All().ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs new file mode 100644 index 000000000..111fbec79 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Profiles.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class QualityProfileResource : RestResource + { + public string Name { get; set; } + public NzbDrone.Core.Qualities.Quality Cutoff { get; set; } + public List<QualityProfileQualityItemResource> Items { get; set; } + } + + public class QualityProfileQualityItemResource : RestResource + { + public NzbDrone.Core.Qualities.Quality Quality { get; set; } + public bool Allowed { get; set; } + } + + public static class ProfileResourceMapper + { + public static QualityProfileResource ToResource(this Profile model) + { + if (model == null) return null; + + return new QualityProfileResource + { + Id = model.Id, + + Name = model.Name, + Cutoff = model.Cutoff, + Items = model.Items.ConvertAll(ToResource), + }; + } + + public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model) + { + if (model == null) return null; + + return new QualityProfileQualityItemResource + { + Quality = model.Quality, + Allowed = model.Allowed + }; + } + + public static Profile ToModel(this QualityProfileResource resource) + { + if (resource == null) return null; + + return new Profile + { + Id = resource.Id, + + Name = resource.Name, + Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id, + Items = resource.Items.ConvertAll(ToModel) + }; + } + + public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource) + { + if (resource == null) return null; + + return new ProfileQualityItem + { + Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id, + Allowed = resource.Allowed + }; + } + + public static List<QualityProfileResource> ToResource(this IEnumerable<Profile> models) + { + return models.Select(ToResource).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs new file mode 100644 index 000000000..4aebb9fb2 --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileSchemaModule.cs @@ -0,0 +1,34 @@ +using System.Linq; +using NzbDrone.Core.Profiles.Qualities; +using NzbDrone.Core.Qualities; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public class QualityProfileSchemaModule : LidarrRestModule<QualityProfileResource> + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService) + : base("/qualityprofile/schema") + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceSingle = GetSchema; + } + + private QualityProfileResource GetSchema() + { + var items = _qualityDefinitionService.All() + .OrderBy(v => v.Weight) + .Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false }) + .ToList(); + + var qualityProfile = new Profile(); + qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown; + qualityProfile.Items = items; + + return qualityProfile.ToResource(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs new file mode 100644 index 000000000..7382dbefb --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileValidation.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Validators; + +namespace Lidarr.Api.V3.Profiles.Quality +{ + public static class QualityProfileValidation + { + public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder) + { + ruleBuilder.SetValidator(new NotEmptyValidator(null)); + + return ruleBuilder.SetValidator(new AllowedValidator<T>()); + } + } + + public class AllowedValidator<T> : PropertyValidator + { + public AllowedValidator() + : base("Must contain at least one allowed quality") + { + + } + + protected override bool IsValid(PropertyValidatorContext context) + { + var list = context.PropertyValue as IList<QualityProfileQualityItemResource>; + + if (list == null) + { + return false; + } + + if (!list.Any(c => c.Allowed)) + { + return false; + } + + return true; + } + } +} diff --git a/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs b/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6149a06c4 --- /dev/null +++ b/src/Sonarr.Api.V3/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("NzbDrone.Api")] + +[assembly: Guid("4c0922d7-979e-4ff7-b44b-b8ac2100eeb5")] + +[assembly: AssemblyVersion("10.0.0.*")] + +[assembly: InternalsVisibleTo("NzbDrone.Core")] diff --git a/src/Sonarr.Api.V3/ProviderModuleBase.cs b/src/Sonarr.Api.V3/ProviderModuleBase.cs new file mode 100644 index 000000000..43d7228a0 --- /dev/null +++ b/src/Sonarr.Api.V3/ProviderModuleBase.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Nancy; +using Newtonsoft.Json; +using NzbDrone.Core.ThingiProvider; +using NzbDrone.Core.Validation; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3 +{ + public abstract class ProviderModuleBase<TProviderResource, TProvider, TProviderDefinition> : LidarrRestModule<TProviderResource> + where TProviderDefinition : ProviderDefinition, new() + where TProvider : IProvider + where TProviderResource : ProviderResource, new() + { + private readonly IProviderFactory<TProvider, TProviderDefinition> _providerFactory; + private readonly ProviderResourceMapper<TProviderResource, TProviderDefinition> _resourceMapper; + + protected ProviderModuleBase(IProviderFactory<TProvider, TProviderDefinition> providerFactory, string resource, ProviderResourceMapper<TProviderResource, TProviderDefinition> resourceMapper) + : base(resource) + { + _providerFactory = providerFactory; + _resourceMapper = resourceMapper; + + Get["schema"] = x => GetTemplates(); + Post["test"] = x => Test(ReadResourceFromRequest(true)); + Post["action/{action}"] = x => RequestAction(x.action, ReadResourceFromRequest(true)); + + GetResourceAll = GetAll; + GetResourceById = GetProviderById; + CreateResource = CreateProvider; + UpdateResource = UpdateProvider; + DeleteResource = DeleteProvider; + + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name).Must((v,c) => !_providerFactory.All().Any(p => p.Name == c && p.Id != v.Id)).WithMessage("Should be unique"); + SharedValidator.RuleFor(c => c.Implementation).NotEmpty(); + SharedValidator.RuleFor(c => c.ConfigContract).NotEmpty(); + + PostValidator.RuleFor(c => c.Fields).NotNull(); + } + + private TProviderResource GetProviderById(int id) + { + var definition = _providerFactory.Get(id); + _providerFactory.SetProviderCharacteristics(definition); + + return _resourceMapper.ToResource(definition); + } + + private List<TProviderResource> GetAll() + { + var providerDefinitions = _providerFactory.All().OrderBy(p => p.ImplementationName); + + var result = new List<TProviderResource>(providerDefinitions.Count()); + + foreach (var definition in providerDefinitions) + { + _providerFactory.SetProviderCharacteristics(definition); + + result.Add(_resourceMapper.ToResource(definition)); + } + + return result.OrderBy(p => p.Name).ToList(); + } + + private int CreateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + providerDefinition = _providerFactory.Create(providerDefinition); + + return providerDefinition.Id; + } + + private void UpdateProvider(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, false); + + if (providerDefinition.Enable) + { + Test(providerDefinition, false); + } + + _providerFactory.Update(providerDefinition); + } + + private TProviderDefinition GetDefinition(TProviderResource providerResource, bool includeWarnings = false, bool validate = true) + { + var definition = _resourceMapper.ToModel(providerResource); + + if (validate) + { + Validate(definition, includeWarnings); + } + + return definition; + } + + private void DeleteProvider(int id) + { + _providerFactory.Delete(id); + } + + private Response GetTemplates() + { + var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList(); + + var result = new List<TProviderResource>(defaultDefinitions.Count()); + + foreach (var providerDefinition in defaultDefinitions) + { + var providerResource = _resourceMapper.ToResource(providerDefinition); + var presetDefinitions = _providerFactory.GetPresetDefinitions(providerDefinition); + + providerResource.Presets = presetDefinitions.Select(v => + { + var presetResource = _resourceMapper.ToResource(v); + + return presetResource as ProviderResource; + }).ToList(); + + result.Add(providerResource); + } + + return result.AsResponse(); + } + + private Response Test(TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true); + + Test(providerDefinition, true); + + return "{}"; + } + + private Response RequestAction(string action, TProviderResource providerResource) + { + var providerDefinition = GetDefinition(providerResource, true, false); + + var query = ((IDictionary<string, object>)Request.Query.ToDictionary()).ToDictionary(k => k.Key, k => k.Value.ToString()); + + var data = _providerFactory.RequestAction(providerDefinition, action, query); + Response resp = JsonConvert.SerializeObject(data); + resp.ContentType = "application/json"; + return resp; + } + + protected virtual void Validate(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = definition.Settings.Validate(); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected virtual void Test(TProviderDefinition definition, bool includeWarnings) + { + var validationResult = _providerFactory.Test(definition); + + VerifyValidationResult(validationResult, includeWarnings); + } + + protected void VerifyValidationResult(ValidationResult validationResult, bool includeWarnings) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (includeWarnings && (!result.IsValid || result.HasWarnings)) + { + throw new ValidationException(result.Failures); + } + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } + } +} diff --git a/src/Sonarr.Api.V3/ProviderResource.cs b/src/Sonarr.Api.V3/ProviderResource.cs new file mode 100644 index 000000000..e9aa78c80 --- /dev/null +++ b/src/Sonarr.Api.V3/ProviderResource.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using NzbDrone.Common.Reflection; +using NzbDrone.Core.ThingiProvider; +using Lidarr.Http.ClientSchema; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3 +{ + public class ProviderResource : RestResource + { + public string Name { get; set; } + public List<Field> Fields { get; set; } + public string ImplementationName { get; set; } + public string Implementation { get; set; } + public string ConfigContract { get; set; } + public string InfoLink { get; set; } + public ProviderMessage Message { get; set; } + public HashSet<int> Tags { get; set; } + + public List<ProviderResource> Presets { get; set; } + } + + public class ProviderResourceMapper<TProviderResource, TProviderDefinition> + where TProviderResource : ProviderResource, new() + where TProviderDefinition : ProviderDefinition, new() + { + public virtual TProviderResource ToResource(TProviderDefinition definition) + + { + return new TProviderResource + { + Id = definition.Id, + + Name = definition.Name, + ImplementationName = definition.ImplementationName, + Implementation = definition.Implementation, + ConfigContract = definition.ConfigContract, + Message = definition.Message, + Tags = definition.Tags, + Fields = SchemaBuilder.ToSchema(definition.Settings), + + InfoLink = string.Format("https://github.com/Sonarr/Sonarr/wiki/Supported-{0}#{1}", + typeof(TProviderResource).Name.Replace("Resource", "s"), + definition.Implementation.ToLower()) + }; + } + + public virtual TProviderDefinition ToModel(TProviderResource resource) + { + if (resource == null) return default(TProviderDefinition); + + var definition = new TProviderDefinition + { + Id = resource.Id, + + Name = resource.Name, + ImplementationName = resource.ImplementationName, + Implementation = resource.Implementation, + ConfigContract = resource.ConfigContract, + Message = resource.Message, + Tags = resource.Tags + }; + + var configContract = ReflectionExtensions.CoreAssembly.FindTypeByName(definition.ConfigContract); + definition.Settings = (IProviderConfig)SchemaBuilder.ReadFromSchema(resource.Fields, configContract); + + return definition; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs new file mode 100644 index 000000000..1da32a774 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionModule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.Qualities; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Qualities +{ + public class QualityDefinitionModule : LidarrRestModule<QualityDefinitionResource> + { + private readonly IQualityDefinitionService _qualityDefinitionService; + + public QualityDefinitionModule(IQualityDefinitionService qualityDefinitionService) + { + _qualityDefinitionService = qualityDefinitionService; + + GetResourceAll = GetAll; + GetResourceById = GetById; + UpdateResource = Update; + Put["/update"] = d => UpdateMany(); + } + + private void Update(QualityDefinitionResource resource) + { + var model = resource.ToModel(); + _qualityDefinitionService.Update(model); + } + + private QualityDefinitionResource GetById(int id) + { + return _qualityDefinitionService.GetById(id).ToResource(); + } + + private List<QualityDefinitionResource> GetAll() + { + return _qualityDefinitionService.All().ToResource(); + } + + private Response UpdateMany() + { + //Read from request + var qualityDefinitions = Request.Body.FromJson<List<QualityDefinitionResource>>() + .ToModel() + .ToList(); + + _qualityDefinitionService.UpdateMany(qualityDefinitions); + + return _qualityDefinitionService.All() + .ToResource() + .AsResponse(HttpStatusCode.Accepted); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs new file mode 100644 index 000000000..7d2000c79 --- /dev/null +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Qualities +{ + public class QualityDefinitionResource : RestResource + { + public Quality Quality { get; set; } + + public string Title { get; set; } + + public int Weight { get; set; } + + public double? MinSize { get; set; } + public double? MaxSize { get; set; } + } + + public static class QualityDefinitionResourceMapper + { + public static QualityDefinitionResource ToResource(this QualityDefinition model) + { + if (model == null) return null; + + return new QualityDefinitionResource + { + Id = model.Id, + Quality = model.Quality, + Title = model.Title, + Weight = model.Weight, + MinSize = model.MinSize, + MaxSize = model.MaxSize + }; + } + + public static QualityDefinition ToModel(this QualityDefinitionResource resource) + { + if (resource == null) return null; + + return new QualityDefinition + { + Id = resource.Id, + Quality = resource.Quality, + Title = resource.Title, + Weight = resource.Weight, + MinSize = resource.MinSize, + MaxSize = resource.MaxSize + }; + } + + public static List<QualityDefinitionResource> ToResource(this IEnumerable<QualityDefinition> models) + { + return models.Select(ToResource).ToList(); + } + + public static List<QualityDefinition> ToModel(this IEnumerable<QualityDefinitionResource> resources) + { + return resources.Select(ToModel).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Queue/QueueActionModule.cs b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs new file mode 100644 index 000000000..d0ff7e818 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueActionModule.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using Nancy; +using NzbDrone.Core.Download; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Queue; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueActionModule : LidarrRestModule<QueueResource> + { + private readonly IQueueService _queueService; + private readonly ITrackedDownloadService _trackedDownloadService; + private readonly IFailedDownloadService _failedDownloadService; + private readonly IProvideDownloadClient _downloadClientProvider; + private readonly IPendingReleaseService _pendingReleaseService; + private readonly IDownloadService _downloadService; + + public QueueActionModule(IQueueService queueService, + ITrackedDownloadService trackedDownloadService, + IFailedDownloadService failedDownloadService, + IProvideDownloadClient downloadClientProvider, + IPendingReleaseService pendingReleaseService, + IDownloadService downloadService) + { + _queueService = queueService; + _trackedDownloadService = trackedDownloadService; + _failedDownloadService = failedDownloadService; + _downloadClientProvider = downloadClientProvider; + _pendingReleaseService = pendingReleaseService; + _downloadService = downloadService; + + Post[@"/grab/(?<id>[\d]{1,10})"] = x => Grab((int)x.Id); + Post["/grab/bulk"] = x => Grab(); + + Delete[@"/(?<id>[\d]{1,10})"] = x => Remove((int)x.Id); + Delete["/bulk"] = x => Remove(); + } + + private Response Grab(int id) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteAlbum); + + return new object().AsResponse(); + } + + private Response Grab() + { + var resource = Request.Body.FromJson<QueueBulkResource>(); + + foreach (var id in resource.Ids) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease == null) + { + throw new NotFoundException(); + } + + _downloadService.DownloadReport(pendingRelease.RemoteAlbum); + } + + return new object().AsResponse(); + } + + private Response Remove(int id) + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + _trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId); + } + + return new object().AsResponse(); + } + + private Response Remove() + { + var blacklist = Request.GetBooleanQueryParameter("blacklist"); + + var resource = Request.Body.FromJson<QueueBulkResource>(); + var trackedDownloadIds = new List<string>(); + + foreach (var id in resource.Ids) + { + var trackedDownload = Remove(id, blacklist); + + if (trackedDownload != null) + { + trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId); + } + } + + _trackedDownloadService.StopTracking(trackedDownloadIds); + + return new object().AsResponse(); + } + + private TrackedDownload Remove(int id, bool blacklist) + { + var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id); + + if (pendingRelease != null) + { + _pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id); + + return null; + } + + var trackedDownload = GetTrackedDownload(id); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient); + + if (downloadClient == null) + { + throw new BadRequestException(); + } + + downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true); + + if (blacklist) + { + _failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId); + } + + return trackedDownload; + } + + private TrackedDownload GetTrackedDownload(int queueId) + { + var queueItem = _queueService.Find(queueId); + + if (queueItem == null) + { + throw new NotFoundException(); + } + + var trackedDownload = _trackedDownloadService.Find(queueItem.DownloadId); + + if (trackedDownload == null) + { + throw new NotFoundException(); + } + + return trackedDownload; + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs b/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs new file mode 100644 index 000000000..b1024846e --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueBulkResource.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueBulkResource + { + public List<int> Ids { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs b/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs new file mode 100644 index 000000000..37bc6dd09 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueDetailsModule.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueDetailsModule : LidarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>, + IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueDetailsModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/details") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourceAll = GetQueue; + } + + private List<QueueResource> GetQueue() + { + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode", true); + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending); + + var seriesIdQuery = Request.Query.SeriesId; + var episodeIdsQuery = Request.Query.EpisodeIds; + + if (seriesIdQuery.HasValue) + { + return fullQueue.Where(q => q.Artist.Id == (int)seriesIdQuery).ToResource(includeSeries, includeEpisode); + } + + if (episodeIdsQuery.HasValue) + { + string episodeIdsValue = episodeIdsQuery.Value.ToString(); + + var episodeIds = episodeIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return fullQueue.Where(q => episodeIds.Contains(q.Episode.Id)).ToResource(includeSeries, includeEpisode); + } + + return fullQueue.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueModule.cs b/src/Sonarr.Api.V3/Queue/QueueModule.cs new file mode 100644 index 000000000..abc21122c --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueModule.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueModule : LidarrRestModuleWithSignalR<QueueResource, NzbDrone.Core.Queue.Queue>, + IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage) + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + GetResourcePaged = GetQueue; + } + + private PagingResource<QueueResource> GetQueue(PagingResource<QueueResource> pagingResource) + { + var pagingSpec = pagingResource.MapToPagingSpec<QueueResource, NzbDrone.Core.Queue.Queue>("timeleft", SortDirection.Ascending); + var includeSeries = Request.GetBooleanQueryParameter("includeSeries"); + var includeEpisode = Request.GetBooleanQueryParameter("includeEpisode", true); + + return ApplyToPage(GetQueue, pagingSpec, (q) => MapToResource(q, includeSeries, includeEpisode)); + } + + private PagingSpec<NzbDrone.Core.Queue.Queue> GetQueue(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec) + { + var ascending = pagingSpec.SortDirection == SortDirection.Ascending; + var orderByFunc = GetOrderByFunc(pagingSpec); + + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + var fullQueue = queue.Concat(pending).ToList(); + IOrderedEnumerable<NzbDrone.Core.Queue.Queue> ordered; + + if (pagingSpec.SortKey == "timeleft") + { + ordered = ascending ? fullQueue.OrderBy(q => q.Timeleft, new TimeleftComparer()) : + fullQueue.OrderByDescending(q => q.Timeleft, new TimeleftComparer()); + } + + else if (pagingSpec.SortKey == "estimatedCompletionTime") + { + ordered = ascending ? fullQueue.OrderBy(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()) : + fullQueue.OrderByDescending(q => q.EstimatedCompletionTime, new EstimatedCompletionTimeComparer()); + } + + else + { + ordered = ascending ? fullQueue.OrderBy(orderByFunc) : fullQueue.OrderByDescending(orderByFunc); + } + + ordered = ordered.ThenByDescending(q => 100 - q.Sizeleft / q.Size * 100); + + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + pagingSpec.TotalRecords = fullQueue.Count; + + if (pagingSpec.Records.Empty() && pagingSpec.Page > 1) + { + pagingSpec.Page = (int)Math.Max(Math.Ceiling((decimal)(pagingSpec.TotalRecords / pagingSpec.PageSize)), 1); + pagingSpec.Records = ordered.Skip((pagingSpec.Page - 1) * pagingSpec.PageSize).Take(pagingSpec.PageSize).ToList(); + } + + return pagingSpec; + } + + private Func<NzbDrone.Core.Queue.Queue, Object> GetOrderByFunc(PagingSpec<NzbDrone.Core.Queue.Queue> pagingSpec) + { + switch (pagingSpec.SortKey) + { + case "series.sortTitle": + return q => q.Artist.SortName; + case "episode": + return q => q.Album; + case "episode.title": + return q => q.Album.Title; + case "quality": + return q => q.Quality; + case "progress": + return q => 100 - q.Sizeleft / q.Size * 100; + default: + return q => q.Timeleft; + } + } + + private QueueResource MapToResource(NzbDrone.Core.Queue.Queue queueItem, bool includeSeries, bool includeEpisode) + { + return queueItem.ToResource(includeSeries, includeEpisode); + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueResource.cs b/src/Sonarr.Api.V3/Queue/QueueResource.cs new file mode 100644 index 000000000..e37a0155b --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueResource.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Qualities; +using Lidarr.Api.V3.Albums; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public ArtistResource Artist { get; set; } + public AlbumResource Album { get; set; } + public QualityModel Quality { get; set; } + public decimal Size { get; set; } + public string Title { get; set; } + public decimal Sizeleft { get; set; } + public TimeSpan? Timeleft { get; set; } + public DateTime? EstimatedCompletionTime { get; set; } + public string Status { get; set; } + public string TrackedDownloadStatus { get; set; } + public List<TrackedDownloadStatusMessage> StatusMessages { get; set; } + public string ErrorMessage { get; set; } + public string DownloadId { get; set; } + public DownloadProtocol Protocol { get; set; } + public string DownloadClient { get; set; } + public string Indexer { get; set; } + } + + public static class QueueResourceMapper + { + public static QueueResource ToResource(this NzbDrone.Core.Queue.Queue model, bool includeSeries, bool includeEpisode) + { + if (model == null) return null; + + return new QueueResource + { + Id = model.Id, + ArtistId = model.Artist.Id, + AlbumId = model.Album.Id, + Artist = includeSeries ? model.Artist.ToResource() : null, + Album = includeEpisode ? model.Album.ToResource() : null, + Quality = model.Quality, + Size = model.Size, + Title = model.Title, + Sizeleft = model.Sizeleft, + Timeleft = model.Timeleft, + EstimatedCompletionTime = model.EstimatedCompletionTime, + Status = model.Status, + TrackedDownloadStatus = model.TrackedDownloadStatus, + StatusMessages = model.StatusMessages, + ErrorMessage = model.ErrorMessage, + DownloadId = model.DownloadId, + Protocol = model.Protocol, + DownloadClient = model.DownloadClient, + Indexer = model.Indexer + }; + } + + public static List<QueueResource> ToResource(this IEnumerable<NzbDrone.Core.Queue.Queue> models, bool includeSeries, bool includeEpisode) + { + return models.Select((m) => ToResource(m, includeSeries, includeEpisode)).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs b/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs new file mode 100644 index 000000000..09b59fc33 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueStatusModule.cs @@ -0,0 +1,56 @@ +using System; +using System.Linq; +using Nancy.Responses; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Download.Pending; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Queue; +using NzbDrone.SignalR; +using Lidarr.Http; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueStatusModule : LidarrRestModuleWithSignalR<QueueStatusResource, NzbDrone.Core.Queue.Queue>, + IHandle<QueueUpdatedEvent>, IHandle<PendingReleasesUpdatedEvent> + { + private readonly IQueueService _queueService; + private readonly IPendingReleaseService _pendingReleaseService; + + public QueueStatusModule(IBroadcastSignalRMessage broadcastSignalRMessage, IQueueService queueService, IPendingReleaseService pendingReleaseService) + : base(broadcastSignalRMessage, "queue/status") + { + _queueService = queueService; + _pendingReleaseService = pendingReleaseService; + Get["/"] = x => GetQueueStatusResponse(); + } + + private JsonResponse<QueueStatusResource> GetQueueStatusResponse() + { + return GetQueueStatus().AsResponse(); + } + + private QueueStatusResource GetQueueStatus() + { + var queue = _queueService.GetQueue(); + var pending = _pendingReleaseService.GetPendingQueue(); + + return new QueueStatusResource + { + Count = queue.Count + pending.Count, + Errors = queue.Any(q => q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)), + Warnings = queue.Any(q => q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)) + }; + } + + public void Handle(QueueUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + + public void Handle(PendingReleasesUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, GetQueueStatus()); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs b/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs new file mode 100644 index 000000000..82261abc3 --- /dev/null +++ b/src/Sonarr.Api.V3/Queue/QueueStatusResource.cs @@ -0,0 +1,11 @@ +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Queue +{ + public class QueueStatusResource : RestResource + { + public int Count { get; set; } + public bool Errors { get; set; } + public bool Warnings { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs new file mode 100644 index 000000000..cbdffa155 --- /dev/null +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingModule.cs @@ -0,0 +1,68 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Core.Validation.Paths; +using Lidarr.Http; + +namespace Lidarr.Api.V3.RemotePathMappings +{ + public class RemotePathMappingModule : LidarrRestModule<RemotePathMappingResource> + { + private readonly IRemotePathMappingService _remotePathMappingService; + + public RemotePathMappingModule(IRemotePathMappingService remotePathMappingService, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + { + _remotePathMappingService = remotePathMappingService; + + GetResourceAll = GetMappings; + GetResourceById = GetMappingById; + CreateResource = CreateMapping; + DeleteResource = DeleteMapping; + UpdateResource = UpdateMapping; + + SharedValidator.RuleFor(c => c.Host) + .NotEmpty(); + + // We cannot use IsValidPath here, because it's a remote path, possibly other OS. + SharedValidator.RuleFor(c => c.RemotePath) + .NotEmpty(); + + SharedValidator.RuleFor(c => c.LocalPath) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator); + } + + private RemotePathMappingResource GetMappingById(int id) + { + return _remotePathMappingService.Get(id).ToResource(); + } + + private int CreateMapping(RemotePathMappingResource resource) + { + var model = resource.ToModel(); + + return _remotePathMappingService.Add(model).Id; + } + + private List<RemotePathMappingResource> GetMappings() + { + return _remotePathMappingService.All().ToResource(); + } + + private void DeleteMapping(int id) + { + _remotePathMappingService.Remove(id); + } + + private void UpdateMapping(RemotePathMappingResource resource) + { + var mapping = resource.ToModel(); + + _remotePathMappingService.Update(mapping); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs new file mode 100644 index 000000000..e1b716ba6 --- /dev/null +++ b/src/Sonarr.Api.V3/RemotePathMappings/RemotePathMappingResource.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.RemotePathMappings; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.RemotePathMappings +{ + public class RemotePathMappingResource : RestResource + { + public string Host { get; set; } + public string RemotePath { get; set; } + public string LocalPath { get; set; } + } + + public static class RemotePathMappingResourceMapper + { + public static RemotePathMappingResource ToResource(this RemotePathMapping model) + { + if (model == null) return null; + + return new RemotePathMappingResource + { + Id = model.Id, + + Host = model.Host, + RemotePath = model.RemotePath, + LocalPath = model.LocalPath + }; + } + + public static RemotePathMapping ToModel(this RemotePathMappingResource resource) + { + if (resource == null) return null; + + return new RemotePathMapping + { + Id = resource.Id, + + Host = resource.Host, + RemotePath = resource.RemotePath, + LocalPath = resource.LocalPath + }; + } + + public static List<RemotePathMappingResource> ToResource(this IEnumerable<RemotePathMapping> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs b/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs new file mode 100644 index 000000000..810eb821a --- /dev/null +++ b/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Restrictions; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Restrictions +{ + public class RestrictionModule : LidarrRestModule<RestrictionResource> + { + private readonly IRestrictionService _restrictionService; + + + public RestrictionModule(IRestrictionService restrictionService) + { + _restrictionService = restrictionService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + + SharedValidator.Custom(restriction => + { + if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) + { + return new ValidationFailure("", "Either 'Must contain' or 'Must not contain' is required"); + } + + return null; + }); + } + + private RestrictionResource Get(int id) + { + return _restrictionService.Get(id).ToResource(); + } + + private List<RestrictionResource> GetAll() + { + return _restrictionService.All().ToResource(); + } + + private int Create(RestrictionResource resource) + { + return _restrictionService.Add(resource.ToModel()).Id; + } + + private void Update(RestrictionResource resource) + { + _restrictionService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _restrictionService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs b/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs new file mode 100644 index 000000000..8c8689cd6 --- /dev/null +++ b/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Restrictions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Restrictions +{ + public class RestrictionResource : RestResource + { + public string Required { get; set; } + public string Preferred { get; set; } + public string Ignored { get; set; } + public HashSet<int> Tags { get; set; } + + public RestrictionResource() + { + Tags = new HashSet<int>(); + } + } + + public static class RestrictionResourceMapper + { + public static RestrictionResource ToResource(this Restriction model) + { + if (model == null) return null; + + return new RestrictionResource + { + Id = model.Id, + + Required = model.Required, + Preferred = model.Preferred, + Ignored = model.Ignored, + Tags = new HashSet<int>(model.Tags) + }; + } + + public static Restriction ToModel(this RestrictionResource resource) + { + if (resource == null) return null; + + return new Restriction + { + Id = resource.Id, + + Required = resource.Required, + Preferred = resource.Preferred, + Ignored = resource.Ignored, + Tags = new HashSet<int>(resource.Tags) + }; + } + + public static List<RestrictionResource> ToResource(this IEnumerable<Restriction> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs new file mode 100644 index 000000000..9258db3f6 --- /dev/null +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderModule.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using FluentValidation; +using NzbDrone.Core.RootFolders; +using NzbDrone.Core.Validation.Paths; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.RootFolders +{ + public class RootFolderModule : LidarrRestModuleWithSignalR<RootFolderResource, RootFolder> + { + private readonly IRootFolderService _rootFolderService; + + public RootFolderModule(IRootFolderService rootFolderService, + IBroadcastSignalRMessage signalRBroadcaster, + RootFolderValidator rootFolderValidator, + PathExistsValidator pathExistsValidator, + MappedNetworkDriveValidator mappedNetworkDriveValidator) + : base(signalRBroadcaster) + { + _rootFolderService = rootFolderService; + + GetResourceAll = GetRootFolders; + GetResourceById = GetRootFolder; + CreateResource = CreateRootFolder; + DeleteResource = DeleteFolder; + + SharedValidator.RuleFor(c => c.Path) + .Cascade(CascadeMode.StopOnFirstFailure) + .IsValidPath() + .SetValidator(rootFolderValidator) + .SetValidator(mappedNetworkDriveValidator) + .SetValidator(pathExistsValidator); + } + + private RootFolderResource GetRootFolder(int id) + { + return _rootFolderService.Get(id).ToResource(); + } + + private int CreateRootFolder(RootFolderResource rootFolderResource) + { + var model = rootFolderResource.ToModel(); + + return _rootFolderService.Add(model).Id; + } + + private List<RootFolderResource> GetRootFolders() + { + return _rootFolderService.AllWithUnmappedFolders().ToResource(); + } + + private void DeleteFolder(int id) + { + _rootFolderService.Remove(id); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs b/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs new file mode 100644 index 000000000..992f112be --- /dev/null +++ b/src/Sonarr.Api.V3/RootFolders/RootFolderResource.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.RootFolders; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.RootFolders +{ + public class RootFolderResource : RestResource + { + public string Path { get; set; } + public long? FreeSpace { get; set; } + + public List<UnmappedFolder> UnmappedFolders { get; set; } + } + + public static class RootFolderResourceMapper + { + public static RootFolderResource ToResource(this RootFolder model) + { + if (model == null) return null; + + return new RootFolderResource + { + Id = model.Id, + + Path = model.Path, + FreeSpace = model.FreeSpace, + UnmappedFolders = model.UnmappedFolders + }; + } + + public static RootFolder ToModel(this RootFolderResource resource) + { + if (resource == null) return null; + + return new RootFolder + { + Id = resource.Id, + + Path = resource.Path, + //FreeSpace + //UnmappedFolders + }; + } + + public static List<RootFolderResource> ToResource(this IEnumerable<RootFolder> models) + { + return models.Select(ToResource).ToList(); + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/SonarrV3FeedModule.cs b/src/Sonarr.Api.V3/SonarrV3FeedModule.cs new file mode 100644 index 000000000..0c6e838b2 --- /dev/null +++ b/src/Sonarr.Api.V3/SonarrV3FeedModule.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Lidarr.Api.V3 +{ + public abstract class SonarrV3FeedModule : NancyModule + { + protected SonarrV3FeedModule(string resource) + : base("/feed/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/SonarrV3Module.cs b/src/Sonarr.Api.V3/SonarrV3Module.cs new file mode 100644 index 000000000..112368f9f --- /dev/null +++ b/src/Sonarr.Api.V3/SonarrV3Module.cs @@ -0,0 +1,12 @@ +using Nancy; + +namespace Lidarr.Api.V3 +{ + public abstract class SonarrV3Module : NancyModule + { + protected SonarrV3Module(string resource) + : base("/api/v3/" + resource.Trim('/')) + { + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/System/Backup/BackupModule.cs b/src/Sonarr.Api.V3/System/Backup/BackupModule.cs new file mode 100644 index 000000000..9bf93146d --- /dev/null +++ b/src/Sonarr.Api.V3/System/Backup/BackupModule.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Core.Backup; +using Lidarr.Http; + +namespace Lidarr.Api.V3.System.Backup +{ + public class BackupModule : LidarrRestModule<BackupResource> + { + private readonly IBackupService _backupService; + + public BackupModule(IBackupService backupService) : base("system/backup") + { + _backupService = backupService; + GetResourceAll = GetBackupFiles; + } + + public List<BackupResource> GetBackupFiles() + { + var backups = _backupService.GetBackups(); + + return backups.Select(b => new BackupResource + { + Id = b.Name.GetHashCode(), + Name = b.Name, + Path = $"/backup/{b.Type.ToString().ToLower()}/{b.Name}", + Type = b.Type, + Time = b.Time + }) + .OrderByDescending(b => b.Time) + .ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Backup/BackupResource.cs b/src/Sonarr.Api.V3/System/Backup/BackupResource.cs new file mode 100644 index 000000000..243ccd5bf --- /dev/null +++ b/src/Sonarr.Api.V3/System/Backup/BackupResource.cs @@ -0,0 +1,14 @@ +using System; +using NzbDrone.Core.Backup; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.System.Backup +{ + public class BackupResource : RestResource + { + public string Name { get; set; } + public string Path { get; set; } + public BackupType Type { get; set; } + public DateTime Time { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/System/SystemModule.cs b/src/Sonarr.Api.V3/System/SystemModule.cs new file mode 100644 index 000000000..5017c5ed4 --- /dev/null +++ b/src/Sonarr.Api.V3/System/SystemModule.cs @@ -0,0 +1,93 @@ +using Nancy; +using Nancy.Routing; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Lifecycle; +using Lidarr.Http.Extensions; + +namespace Lidarr.Api.V3.System +{ + public class SystemModule : SonarrV3Module + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IRuntimeInfo _runtimeInfo; + private readonly IPlatformInfo _platformInfo; + private readonly IOsInfo _osInfo; + private readonly IRouteCacheProvider _routeCacheProvider; + private readonly IConfigFileProvider _configFileProvider; + private readonly IMainDatabase _database; + private readonly ILifecycleService _lifecycleService; + + public SystemModule(IAppFolderInfo appFolderInfo, + IRuntimeInfo runtimeInfo, + IPlatformInfo platformInfo, + IOsInfo osInfo, + IRouteCacheProvider routeCacheProvider, + IConfigFileProvider configFileProvider, + IMainDatabase database, + ILifecycleService lifecycleService) + : base("system") + { + _appFolderInfo = appFolderInfo; + _runtimeInfo = runtimeInfo; + _platformInfo = platformInfo; + _osInfo = osInfo; + _routeCacheProvider = routeCacheProvider; + _configFileProvider = configFileProvider; + _database = database; + _lifecycleService = lifecycleService; + Get["/status"] = x => GetStatus(); + Get["/routes"] = x => GetRoutes(); + Post["/shutdown"] = x => Shutdown(); + Post["/restart"] = x => Restart(); + } + + private Response GetStatus() + { + return new + { + Version = BuildInfo.Version.ToString(), + BuildTime = BuildInfo.BuildDateTime, + IsDebug = BuildInfo.IsDebug, + IsProduction = RuntimeInfo.IsProduction, + IsAdmin = _runtimeInfo.IsAdmin, + IsUserInteractive = RuntimeInfo.IsUserInteractive, + StartupPath = _appFolderInfo.StartUpFolder, + AppData = _appFolderInfo.GetAppDataPath(), + OsName = _osInfo.Name, + OsVersion = _osInfo.Version, + IsMonoRuntime = PlatformInfo.IsMono, + IsMono = PlatformInfo.IsMono, + IsLinux = OsInfo.IsLinux, + IsOsx = OsInfo.IsOsx, + IsWindows = OsInfo.IsWindows, + Mode = _runtimeInfo.Mode, + Branch = _configFileProvider.Branch, + Authentication = _configFileProvider.AuthenticationMethod, + SqliteVersion = _database.Version, + UrlBase = _configFileProvider.UrlBase, + RuntimeVersion = _platformInfo.Version, + RuntimeName = PlatformInfo.Platform + }.AsResponse(); + } + + private Response GetRoutes() + { + return _routeCacheProvider.GetCache().Values.AsResponse(); + } + + private Response Shutdown() + { + _lifecycleService.Shutdown(); + return "".AsResponse(); + } + + private Response Restart() + { + _lifecycleService.Restart(); + return "".AsResponse(); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs b/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs new file mode 100644 index 000000000..e23a8567e --- /dev/null +++ b/src/Sonarr.Api.V3/System/Tasks/TaskModule.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Jobs; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.System.Tasks +{ + public class TaskModule : LidarrRestModuleWithSignalR<TaskResource, ScheduledTask>, IHandle<CommandExecutedEvent> + { + private readonly ITaskManager _taskManager; + + private static readonly Regex NameRegex = new Regex("(?<!^)[A-Z]", RegexOptions.Compiled); + + public TaskModule(ITaskManager taskManager, IBroadcastSignalRMessage broadcastSignalRMessage) + : base(broadcastSignalRMessage, "system/task") + { + _taskManager = taskManager; + GetResourceAll = GetAll; + GetResourceById = GetTask; + } + + private List<TaskResource> GetAll() + { + return _taskManager.GetAll() + .Select(ConvertToResource) + .OrderBy(t => t.Name) + .ToList(); + } + + private TaskResource GetTask(int id) + { + var task = _taskManager.GetAll() + .SingleOrDefault(t => t.Id == id); + + if (task == null) + { + return null; + } + + return ConvertToResource(task); + } + + private static TaskResource ConvertToResource(ScheduledTask scheduledTask) + { + var taskName = scheduledTask.TypeName.Split('.').Last().Replace("Command", ""); + + return new TaskResource + { + Id = scheduledTask.Id, + Name = NameRegex.Replace(taskName, match => " " + match.Value), + TaskName = taskName, + Interval = scheduledTask.Interval, + LastExecution = scheduledTask.LastExecution, + NextExecution = scheduledTask.LastExecution.AddMinutes(scheduledTask.Interval) + }; + } + + public void Handle(CommandExecutedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs b/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs new file mode 100644 index 000000000..9c6d6385b --- /dev/null +++ b/src/Sonarr.Api.V3/System/Tasks/TaskResource.cs @@ -0,0 +1,14 @@ +using System; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.System.Tasks +{ + public class TaskResource : RestResource + { + public string Name { get; set; } + public string TaskName { get; set; } + public int Interval { get; set; } + public DateTime LastExecution { get; set; } + public DateTime NextExecution { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs b/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs new file mode 100644 index 000000000..a8a4f173c --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagDetailsModule.cs @@ -0,0 +1,23 @@ +using NzbDrone.Core.Tags; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tags +{ + public class TagDetailsModule : LidarrRestModule<TagDetailsResource> + { + private readonly ITagService _tagService; + + public TagDetailsModule(ITagService tagService) + : base("/tag/details") + { + _tagService = tagService; + + GetResourceById = Get; + } + + private TagDetailsResource Get(int id) + { + return _tagService.Details(id).ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs new file mode 100644 index 000000000..297a87ff4 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagDetailsResource.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Lidarr.Api.V3.Notifications; +using Lidarr.Api.V3.Profiles.Delay; +using Lidarr.Api.V3.Restrictions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tags +{ + public class TagDetailsResource : RestResource + { + public string Label { get; set; } + public List<DelayProfileResource> DelayProfiles { get; set; } + public List<NotificationResource> Notifications { get; set; } + public List<RestrictionResource> Restrictions { get; set; } + public List<int> SeriesIds { get; set; } + } + + public static class TagDetailsResourceMapper + { + private static readonly NotificationResourceMapper NotificationResourceMapper = new NotificationResourceMapper(); + + public static TagDetailsResource ToResource(this TagDetails model) + { + if (model == null) return null; + + return new TagDetailsResource + { + Id = model.Id, + Label = model.Label, + DelayProfiles = model.DelayProfiles.ToResource(), + Notifications = model.Notifications.Select(NotificationResourceMapper.ToResource).ToList(), + Restrictions = model.Restrictions.ToResource(), + SeriesIds = model.Series.Select(s => s.Id).ToList() + }; + } + + public static List<TagDetailsResource> ToResource(this IEnumerable<TagDetails> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagModule.cs b/src/Sonarr.Api.V3/Tags/TagModule.cs new file mode 100644 index 000000000..045fe723d --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagModule.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Tags; +using NzbDrone.SignalR; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tags +{ + public class TagModule : LidarrRestModuleWithSignalR<TagResource, Tag>, IHandle<TagsUpdatedEvent> + { + private readonly ITagService _tagService; + + public TagModule(IBroadcastSignalRMessage signalRBroadcaster, + ITagService tagService) + : base(signalRBroadcaster) + { + _tagService = tagService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + } + + private TagResource Get(int id) + { + return _tagService.GetTag(id).ToResource(); + } + + private List<TagResource> GetAll() + { + return _tagService.All().ToResource(); + } + + private int Create(TagResource resource) + { + return _tagService.Add(resource.ToModel()).Id; + } + + private void Update(TagResource resource) + { + _tagService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _tagService.Delete(id); + } + + public void Handle(TagsUpdatedEvent message) + { + BroadcastResourceChange(ModelAction.Sync); + } + } +} diff --git a/src/Sonarr.Api.V3/Tags/TagResource.cs b/src/Sonarr.Api.V3/Tags/TagResource.cs new file mode 100644 index 000000000..1c95024f2 --- /dev/null +++ b/src/Sonarr.Api.V3/Tags/TagResource.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.Tags; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tags +{ + public class TagResource : RestResource + { + public string Label { get; set; } + } + + public static class TagResourceMapper + { + public static TagResource ToResource(this Tag model) + { + if (model == null) return null; + + return new TagResource + { + Id = model.Id, + Label = model.Label + }; + } + + public static Tag ToModel(this TagResource resource) + { + if (resource == null) return null; + + return new Tag + { + Id = resource.Id, + Label = resource.Label + }; + } + + public static List<TagResource> ToResource(this IEnumerable<Tag> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/TrackFiles/MediaInfoResource.cs b/src/Sonarr.Api.V3/TrackFiles/MediaInfoResource.cs new file mode 100644 index 000000000..7ebdd974b --- /dev/null +++ b/src/Sonarr.Api.V3/TrackFiles/MediaInfoResource.cs @@ -0,0 +1,28 @@ +using NzbDrone.Core.MediaFiles.MediaInfo; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class MediaInfoResource : RestResource + { + public decimal AudioChannels { get; set; } + public string AudioCodec { get; set; } + } + + public static class MediaInfoResourceMapper + { + public static MediaInfoResource ToResource(this MediaInfoModel model, string sceneName) + { + if (model == null) + { + return null; + } + + return new MediaInfoResource + { + AudioChannels = MediaInfoFormatter.FormatAudioChannels(model), + AudioCodec = MediaInfoFormatter.FormatAudioCodec(model) + }; + } + } +} diff --git a/src/Sonarr.Api.V3/TrackFiles/TrackFileListResource.cs b/src/Sonarr.Api.V3/TrackFiles/TrackFileListResource.cs new file mode 100644 index 000000000..d8bf09018 --- /dev/null +++ b/src/Sonarr.Api.V3/TrackFiles/TrackFileListResource.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using NzbDrone.Core.Languages; +using NzbDrone.Core.Qualities; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackFileListResource + { + public List<int> TrackFileIds { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + } +} diff --git a/src/Sonarr.Api.V3/TrackFiles/TrackFileModule.cs b/src/Sonarr.Api.V3/TrackFiles/TrackFileModule.cs new file mode 100644 index 000000000..ab0ea125f --- /dev/null +++ b/src/Sonarr.Api.V3/TrackFiles/TrackFileModule.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nancy; +using NLog; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Series; +using Lidarr.Http; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackModule : LidarrRestModuleWithSignalR<TrackFileResource, TrackFile>, + IHandle<TrackFileAddedEvent> + { + private readonly IMediaFileService _mediaFileService; + private readonly IRecycleBinProvider _recycleBinProvider; + private readonly IArtistService _artistService; + private readonly IUpgradableSpecification _upgradableSpecification; + private readonly Logger _logger; + + public TrackModule(IBroadcastSignalRMessage signalRBroadcaster, + IMediaFileService mediaFileService, + IRecycleBinProvider recycleBinProvider, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + Logger logger) + : base(signalRBroadcaster) + { + _mediaFileService = mediaFileService; + _recycleBinProvider = recycleBinProvider; + _artistService = artistService; + _upgradableSpecification = upgradableSpecification; + _logger = logger; + + GetResourceById = GetTrackFile; + GetResourceAll = GetTrackFiles; + UpdateResource = SetQuality; + DeleteResource = DeleteTrackFile; + + Put["/editor"] = episodeFiles => SetQuality(); + Delete["/bulk"] = episodeFiles => DeleteTrackFiles(); + } + + private TrackFileResource GetTrackFile(int id) + { + var trackFile = _mediaFileService.Get(id); + var series = _artistService.GetArtist(trackFile.ArtistId); + + return trackFile.ToResource(series, _upgradableSpecification); + } + + private List<TrackFileResource> GetTrackFiles() + { + var artistIdQuery = Request.Query.ArtistId; + var trackFileIdsQuery = Request.Query.TrackFileIds; + + if (!artistIdQuery.HasValue && !trackFileIdsQuery.HasValue) + { + throw new BadRequestException("artistId or trackFileIds must be provided"); + } + + if (artistIdQuery.HasValue) + { + int artistId = Convert.ToInt32(artistIdQuery.Value); + var artist = _artistService.GetArtist(artistId); + + return _mediaFileService.GetFilesByArtist(artistId).ConvertAll(f => f.ToResource(artist, _upgradableSpecification)); + } + + else + { + string episodeFileIdsValue = trackFileIdsQuery.Value.ToString(); + + var trackFileIds = episodeFileIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + var trackFiles = _mediaFileService.Get(trackFileIds); + + return trackFiles.GroupBy(e => e.ArtistId) + .SelectMany(f => f.ToList() + .ConvertAll( e => e.ToResource(_artistService.GetArtist(f.Key), _upgradableSpecification))) + .ToList(); + } + } + + private void SetQuality(TrackFileResource trackFileResource) + { + var trackFile = _mediaFileService.Get(trackFileResource.Id); + trackFile.Quality = trackFileResource.Quality; + _mediaFileService.Update(trackFile); + } + + private Response SetQuality() + { + var resource = Request.Body.FromJson<TrackFileListResource>(); + var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds); + + foreach (var trackFile in trackFiles) + { + if (resource.Language != null) + { + trackFile.Language = resource.Language; + } + + if (resource.Quality != null) + { + trackFile.Quality = resource.Quality; + } + } + + _mediaFileService.Update(trackFiles); + + var series = _artistService.GetArtist(trackFiles.First().ArtistId); + + return trackFiles.ConvertAll(f => f.ToResource(series, _upgradableSpecification)) + .AsResponse(HttpStatusCode.Accepted); + } + + private void DeleteTrackFile(int id) + { + var trackFile = _mediaFileService.Get(id); + var artist = _artistService.GetArtist(trackFile.ArtistId); + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + + _logger.Info("Deleting track file: {0}", fullPath); + _recycleBinProvider.DeleteFile(fullPath); + _mediaFileService.Delete(trackFile, DeleteMediaFileReason.Manual); + } + + private Response DeleteTrackFiles() + { + var resource = Request.Body.FromJson<TrackFileListResource>(); + var trackFiles = _mediaFileService.GetFiles(resource.TrackFileIds); + var artist = _artistService.GetArtist(trackFiles.First().ArtistId); + + foreach (var trackFile in trackFiles) + { + var fullPath = Path.Combine(artist.Path, trackFile.RelativePath); + + _logger.Info("Deleting track file: {0}", fullPath); + _recycleBinProvider.DeleteFile(fullPath); + _mediaFileService.Delete(trackFile, DeleteMediaFileReason.Manual); + } + + return new object().AsResponse(); + } + + public void Handle(TrackFileAddedEvent message) + { + BroadcastResourceChange(ModelAction.Updated, message.TrackFile.Id); + } + } +} diff --git a/src/Sonarr.Api.V3/TrackFiles/TrackFileResource.cs b/src/Sonarr.Api.V3/TrackFiles/TrackFileResource.cs new file mode 100644 index 000000000..be6deaabe --- /dev/null +++ b/src/Sonarr.Api.V3/TrackFiles/TrackFileResource.cs @@ -0,0 +1,78 @@ +using System; +using System.IO; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Languages; +using NzbDrone.Core.MediaFiles; +using NzbDrone.Core.Qualities; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.TrackFiles +{ + public class TrackFileResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public string RelativePath { get; set; } + public string Path { get; set; } + public long Size { get; set; } + public DateTime DateAdded { get; set; } + //public string SceneName { get; set; } + public Language Language { get; set; } + public QualityModel Quality { get; set; } + public MediaInfoResource MediaInfo { get; set; } + + public bool QualityCutoffNotMet { get; set; } + } + + public static class TrackFileResourceMapper + { + private static TrackFileResource ToResource(this TrackFile model) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + //Path + Size = model.Size, + DateAdded = model.DateAdded, + // SceneName = model.SceneName, + Language = model.Language, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName) + //QualityCutoffNotMet + }; + + } + + public static TrackFileResource ToResource(this TrackFile model, NzbDrone.Core.Music.Artist artist, IUpgradableSpecification upgradableSpecification) + { + if (model == null) return null; + + return new TrackFileResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + RelativePath = model.RelativePath, + Path = Path.Combine(artist.Path, model.RelativePath), + Size = model.Size, + DateAdded = model.DateAdded, + //SceneName = model.SceneName, + Language = model.Language, + Quality = model.Quality, + MediaInfo = model.MediaInfo.ToResource(model.SceneName), + + QualityCutoffNotMet = upgradableSpecification.CutoffNotMet(artist.Profile.Value, + artist.LanguageProfile.Value, + model.Quality, + model.Language) + }; + } + } +} diff --git a/src/Sonarr.Api.V3/Tracks/RenameTrackModule.cs b/src/Sonarr.Api.V3/Tracks/RenameTrackModule.cs new file mode 100644 index 000000000..5cf86e304 --- /dev/null +++ b/src/Sonarr.Api.V3/Tracks/RenameTrackModule.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using NzbDrone.Core.MediaFiles; +using Lidarr.Http; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class RenameTrackModule : LidarrRestModule<RenameTrackResource> + { + private readonly IRenameTrackFileService _renameTrackFileService; + + public RenameTrackModule(IRenameTrackFileService renameTrackFileService) + : base("rename") + { + _renameTrackFileService = renameTrackFileService; + + GetResourceAll = GetTracks; + } + + private List<RenameTrackResource> GetTracks() + { + int artistId; + + if (Request.Query.SeriesId.HasValue) + { + artistId = (int)Request.Query.SeriesId; + } + + else + { + throw new BadRequestException("artistId is missing"); + } + + if (Request.Query.albumId.HasValue) + { + var albumId = (int)Request.Query.albumId; + return _renameTrackFileService.GetRenamePreviews(artistId, albumId).ToResource(); + } + + return _renameTrackFileService.GetRenamePreviews(artistId).ToResource(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tracks/RenameTrackResource.cs b/src/Sonarr.Api.V3/Tracks/RenameTrackResource.cs new file mode 100644 index 000000000..9bfe676cb --- /dev/null +++ b/src/Sonarr.Api.V3/Tracks/RenameTrackResource.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class RenameTrackResource : RestResource + { + public int ArtistId { get; set; } + public int AlbumId { get; set; } + public List<int> TrackNumbers { get; set; } + public int TrackFileId { get; set; } + public string ExistingPath { get; set; } + public string NewPath { get; set; } + } + + public static class RenameTrackResourceMapper + { + public static RenameTrackResource ToResource(this NzbDrone.Core.MediaFiles.RenameTrackFilePreview model) + { + if (model == null) return null; + + return new RenameTrackResource + { + ArtistId = model.ArtistId, + AlbumId = model.AlbumId, + TrackNumbers = model.TrackNumbers.ToList(), + TrackFileId = model.TrackFileId, + ExistingPath = model.ExistingPath, + NewPath = model.NewPath + }; + } + + public static List<RenameTrackResource> ToResource(this IEnumerable<NzbDrone.Core.MediaFiles.RenameTrackFilePreview> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Tracks/TrackModule.cs b/src/Sonarr.Api.V3/Tracks/TrackModule.cs new file mode 100644 index 000000000..ad3120515 --- /dev/null +++ b/src/Sonarr.Api.V3/Tracks/TrackModule.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Http.Extensions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class TrackModule : TrackModuleWithSignalR + { + public TrackModule(IArtistService artistService, + ITrackService trackService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(trackService, artistService, upgradableSpecification, signalRBroadcaster) + { + GetResourceAll = GetEpisodes; + } + + private List<TrackResource> GetEpisodes() + { + var artistIdQuery = Request.Query.ArtistId; + var trackIdsQuery = Request.Query.TrackIds; + + if (!artistIdQuery.HasValue && !trackIdsQuery.HasValue) + { + throw new BadRequestException("artistId or trackIds must be provided"); + } + + if (artistIdQuery.HasValue) + { + int artistId = Convert.ToInt32(artistIdQuery.Value); + var albumId = Request.Query.AlbumId.HasValue ? (int)Request.Query.AlbumId : (int?)null; + + if (albumId.HasValue) + { + return MapToResource(_trackService.GetTracksByAlbum(artistId, albumId.Value), false, false); + } + + return MapToResource(_trackService.GetTracksByArtist(artistId), false, false); + } + + string trackIdsValue = trackIdsQuery.Value.ToString(); + + var trackIds = trackIdsValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(e => Convert.ToInt32(e)) + .ToList(); + + return MapToResource(_trackService.GetTracks(trackIds), false, false); + } + } +} diff --git a/src/Sonarr.Api.V3/Tracks/TrackModuleWithSignalR.cs b/src/Sonarr.Api.V3/Tracks/TrackModuleWithSignalR.cs new file mode 100644 index 000000000..bd4f3f654 --- /dev/null +++ b/src/Sonarr.Api.V3/Tracks/TrackModuleWithSignalR.cs @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Download; +using NzbDrone.Core.MediaFiles.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music; +using NzbDrone.SignalR; +using Lidarr.Api.V3.TrackFiles; +using Lidarr.Api.V3.Artist; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Tracks +{ + public abstract class TrackModuleWithSignalR : LidarrRestModuleWithSignalR<TrackResource, Track> + //IHandle<EpisodeGrabbedEvent>, + //IHandle<EpisodeImportedEvent> + { + protected readonly ITrackService _trackService; + protected readonly IArtistService _artistService; + protected readonly IUpgradableSpecification _upgradableSpecification; + + protected TrackModuleWithSignalR(ITrackService trackService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(signalRBroadcaster) + { + _trackService = trackService; + _artistService = artistService; + _upgradableSpecification = upgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected TrackModuleWithSignalR(ITrackService episodeService, + IArtistService seriesService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster, + string resource) + : base(signalRBroadcaster, resource) + { + _trackService = episodeService; + _artistService = seriesService; + _upgradableSpecification = upgradableSpecification; + + GetResourceById = GetEpisode; + } + + protected TrackResource GetEpisode(int id) + { + var episode = _trackService.GetTrack(id); + var resource = MapToResource(episode, true, true); + return resource; + } + + protected TrackResource MapToResource(Track track, bool includeArtist, bool includeTrackFile) + { + var resource = track.ToResource(); + + if (includeArtist || includeTrackFile) + { + var artist = track.Artist ?? _artistService.GetArtist(track.ArtistId); + + if (includeArtist) + { + resource.Artist = artist.ToResource(); + } + if (includeTrackFile && track.TrackFileId != 0) + { + resource.TrackFile = track.TrackFile.Value.ToResource(artist, _upgradableSpecification); + } + } + + return resource; + } + + protected List<TrackResource> MapToResource(List<Track> episodes, bool includeSeries, bool includeEpisodeFile) + { + var result = episodes.ToResource(); + + if (includeSeries || includeEpisodeFile) + { + var seriesDict = new Dictionary<int, NzbDrone.Core.Music.Artist>(); + for (var i = 0; i < episodes.Count; i++) + { + var episode = episodes[i]; + var resource = result[i]; + + var series = episode.Artist ?? seriesDict.GetValueOrDefault(episodes[i].ArtistId) ?? _artistService.GetArtist(episodes[i].ArtistId); + seriesDict[series.Id] = series; + + if (includeSeries) + { + resource.Artist = series.ToResource(); + } + if (includeEpisodeFile && episodes[i].TrackFileId != 0) + { + resource.TrackFile = episodes[i].TrackFile.Value.ToResource(series, _upgradableSpecification); + } + } + } + + return result; + } + + //public void Handle(EpisodeGrabbedEvent message) + //{ + // foreach (var episode in message.Episode.Episodes) + // { + // var resource = episode.ToResource(); + // resource.Grabbed = true; + + // BroadcastResourceChange(ModelAction.Updated, resource); + // } + //} + + //public void Handle(EpisodeImportedEvent message) + //{ + // if (!message.NewDownload) + // { + // return; + // } + + // foreach (var episode in message.EpisodeInfo.Episodes) + // { + // BroadcastResourceChange(ModelAction.Updated, episode.Id); + // } + //} + } +} diff --git a/src/Sonarr.Api.V3/Tracks/TrackResource.cs b/src/Sonarr.Api.V3/Tracks/TrackResource.cs new file mode 100644 index 000000000..222394c52 --- /dev/null +++ b/src/Sonarr.Api.V3/Tracks/TrackResource.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Music; +using Lidarr.Api.V3.TrackFiles; +using Lidarr.Api.V3.Artist; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Tracks +{ + public class TrackResource : RestResource + { + public int ArtistId { get; set; } + public int TrackFileId { get; set; } + public int AlbumId { get; set; } + public bool Explicit { get; set; } + public int TrackNumber { get; set; } + public string Title { get; set; } + public int Duration { get; set; } + public TrackFileResource TrackFile { get; set; } + + public bool HasFile { get; set; } + public bool Monitored { get; set; } + //public string SeriesTitle { get; set; } + public ArtistResource Artist { get; set; } + public Ratings Ratings { get; set; } + + //Hiding this so people don't think its usable (only used to set the initial state) + [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool Grabbed { get; set; } + } + + public static class TrackResourceMapper + { + public static TrackResource ToResource(this Track model) + { + if (model == null) return null; + + return new TrackResource + { + Id = model.Id, + + ArtistId = model.ArtistId, + TrackFileId = model.TrackFileId, + AlbumId = model.AlbumId, + Explicit = model.Explicit, + TrackNumber = model.TrackNumber, + Title = model.Title, + Duration = model.Duration, + //EpisodeFile + + HasFile = model.HasFile, + Monitored = model.Monitored, + Ratings = model.Ratings, + //SeriesTitle = model.SeriesTitle, + //Series = model.Series.MapToResource(), + }; + } + + public static List<TrackResource> ToResource(this IEnumerable<Track> models) + { + if (models == null) return null; + + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Update/UpdateModule.cs b/src/Sonarr.Api.V3/Update/UpdateModule.cs new file mode 100644 index 000000000..9aa4c2c94 --- /dev/null +++ b/src/Sonarr.Api.V3/Update/UpdateModule.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Update; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Update +{ + public class UpdateModule : LidarrRestModule<UpdateResource> + { + private readonly IRecentUpdateProvider _recentUpdateProvider; + + public UpdateModule(IRecentUpdateProvider recentUpdateProvider) + { + _recentUpdateProvider = recentUpdateProvider; + GetResourceAll = GetRecentUpdates; + } + + private List<UpdateResource> GetRecentUpdates() + { + var resources = _recentUpdateProvider.GetRecentUpdatePackages() + .OrderByDescending(u => u.Version) + .ToResource(); + + if (resources.Any()) + { + var first = resources.First(); + first.Latest = true; + + if (first.Version > BuildInfo.Version) + { + first.Installable = true; + } + + var installed = resources.SingleOrDefault(r => r.Version == BuildInfo.Version); + + if (installed != null) + { + installed.Installed = true; + } + } + + return resources; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Api.V3/Update/UpdateResource.cs b/src/Sonarr.Api.V3/Update/UpdateResource.cs new file mode 100644 index 000000000..2ed916cdc --- /dev/null +++ b/src/Sonarr.Api.V3/Update/UpdateResource.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using NzbDrone.Core.Update; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V3.Update +{ + public class UpdateResource : RestResource + { + [JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))] + public Version Version { get; set; } + + public string Branch { get; set; } + public DateTime ReleaseDate { get; set; } + public string FileName { get; set; } + public string Url { get; set; } + public bool Installed { get; set; } + public bool Installable { get; set; } + public bool Latest { get; set; } + public UpdateChanges Changes { get; set; } + public string Hash { get; set; } + } + + public static class UpdateResourceMapper + { + public static UpdateResource ToResource(this UpdatePackage model) + { + if (model == null) return null; + + return new UpdateResource + { + Version = model.Version, + + Branch = model.Branch, + ReleaseDate = model.ReleaseDate, + FileName = model.FileName, + Url = model.Url, + //Installed + //Installable + //Latest + Changes = model.Changes, + Hash = model.Hash, + }; + } + + public static List<UpdateResource> ToResource(this IEnumerable<UpdatePackage> models) + { + return models.Select(ToResource).ToList(); + } + } +} diff --git a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs new file mode 100644 index 000000000..f1445d61d --- /dev/null +++ b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs @@ -0,0 +1,52 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.Tv; //TODO Remove after EpisodeCutoffService is Refactored +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Wanted +{ + public class CutoffModule : AlbumModuleWithSignalR + { + private readonly IEpisodeCutoffService _episodeCutoffService; + + public CutoffModule(IEpisodeCutoffService episodeCutoffService, + IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "wanted/cutoff") + { + _episodeCutoffService = episodeCutoffService; + GetResourcePaged = GetCutoffUnmetAlbums; + } + + private PagingResource<AlbumResource> GetCutoffUnmetAlbums(PagingResource<AlbumResource> pagingResource) + { + var pagingSpec = new PagingSpec<Album> + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + { + pagingSpec.FilterExpression = v => v.Monitored == false || v.Artist.Monitored == false; + } + else + { + pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; + } + + //var resource = ApplyToPage(_episodeCutoffService.EpisodesWhereCutoffUnmet, pagingSpec, v => MapToResource(v, true)); + return null; + //return resource; + } + } +} diff --git a/src/Sonarr.Api.V3/Wanted/MissingModule.cs b/src/Sonarr.Api.V3/Wanted/MissingModule.cs new file mode 100644 index 000000000..05ddf6e21 --- /dev/null +++ b/src/Sonarr.Api.V3/Wanted/MissingModule.cs @@ -0,0 +1,47 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.Music; +using NzbDrone.Core.ArtistStats; +using NzbDrone.SignalR; +using Lidarr.Api.V3.Albums; +using Lidarr.Http; + +namespace Lidarr.Api.V3.Wanted +{ + public class MissingModule : AlbumModuleWithSignalR + { + public MissingModule(IAlbumService albumService, + IArtistStatisticsService artistStatisticsService, + IArtistService artistService, + IUpgradableSpecification upgradableSpecification, + IBroadcastSignalRMessage signalRBroadcaster) + : base(albumService, artistStatisticsService, artistService, upgradableSpecification, signalRBroadcaster, "wanted/missing") + { + GetResourcePaged = GetMissingAlbums; + } + + private PagingResource<AlbumResource> GetMissingAlbums(PagingResource<AlbumResource> pagingResource) + { + var pagingSpec = new PagingSpec<Album> + { + Page = pagingResource.Page, + PageSize = pagingResource.PageSize, + SortKey = pagingResource.SortKey, + SortDirection = pagingResource.SortDirection + }; + + if (pagingResource.FilterKey == "monitored" && pagingResource.FilterValue == "false") + { + pagingSpec.FilterExpression = v => v.Monitored == false || v.Artist.Monitored == false; + } + else + { + pagingSpec.FilterExpression = v => v.Monitored == true && v.Artist.Monitored == true; + } + + var resource = ApplyToPage(_albumService.AlbumsWithoutFiles, pagingSpec, v => MapToResource(v, true)); + + return resource; + } + } +} diff --git a/src/Sonarr.Api.V3/app.config b/src/Sonarr.Api.V3/app.config new file mode 100644 index 000000000..c1684a7be --- /dev/null +++ b/src/Sonarr.Api.V3/app.config @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <runtime> + <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> + <dependentAssembly> + <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" /> + </dependentAssembly> + <dependentAssembly> + <assemblyIdentity name="FluentMigrator" publicKeyToken="aacfc7de5acabf05" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-1.3.1.0" newVersion="1.3.1.0" /> + </dependentAssembly> + </assemblyBinding> + </runtime> +</configuration> \ No newline at end of file diff --git a/src/Sonarr.Api.V3/packages.config b/src/Sonarr.Api.V3/packages.config new file mode 100644 index 000000000..f68f69f6b --- /dev/null +++ b/src/Sonarr.Api.V3/packages.config @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="FluentValidation" version="6.2.1.0" targetFramework="net40" /> + <package id="Ical.Net" version="2.2.32" targetFramework="net40" /> + <package id="Nancy" version="1.4.3" targetFramework="net40" /> + <package id="Nancy.Authentication.Basic" version="1.4.1" targetFramework="net40" /> + <package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net40" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> + <package id="NLog" version="4.4.3" targetFramework="net40" /> +</packages> \ No newline at end of file diff --git a/src/NzbDrone.Api/Authentication/1tews5g3.gd1~ b/src/Sonarr.Http/Authentication/1tews5g3.gd1~ similarity index 100% rename from src/NzbDrone.Api/Authentication/1tews5g3.gd1~ rename to src/Sonarr.Http/Authentication/1tews5g3.gd1~ diff --git a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs b/src/Sonarr.Http/Authentication/AuthenticationModule.cs similarity index 89% rename from src/NzbDrone.Api/Authentication/AuthenticationModule.cs rename to src/Sonarr.Http/Authentication/AuthenticationModule.cs index df940a947..8feee4589 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationModule.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationModule.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class AuthenticationModule : NancyModule { @@ -33,7 +33,8 @@ namespace NzbDrone.Api.Authentication if (user == null) { - return Context.GetRedirect("~/login?returnUrl=" + (string)Request.Query.returnUrl); + var returnUrl = (string)Request.Query.returnUrl; + return Context.GetRedirect($"~/login?returnUrl={returnUrl}&loginFailed=true"); } DateTime? expiry = null; diff --git a/src/NzbDrone.Api/Authentication/AuthenticationService.cs b/src/Sonarr.Http/Authentication/AuthenticationService.cs similarity index 95% rename from src/NzbDrone.Api/Authentication/AuthenticationService.cs rename to src/Sonarr.Http/Authentication/AuthenticationService.cs index beb908b11..97436e979 100644 --- a/src/NzbDrone.Api/Authentication/AuthenticationService.cs +++ b/src/Sonarr.Http/Authentication/AuthenticationService.cs @@ -4,12 +4,12 @@ using Nancy; using Nancy.Authentication.Basic; using Nancy.Authentication.Forms; using Nancy.Security; -using NzbDrone.Api.Extensions; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public interface IAuthenticationService : IUserValidator, IUserMapper { @@ -18,7 +18,6 @@ namespace NzbDrone.Api.Authentication public class AuthenticationService : IAuthenticationService { - private readonly IConfigFileProvider _configFileProvider; private readonly IUserService _userService; private static readonly NzbDroneUser AnonymousUser = new NzbDroneUser { UserName = "Anonymous" }; @@ -27,7 +26,6 @@ namespace NzbDrone.Api.Authentication public AuthenticationService(IConfigFileProvider configFileProvider, IUserService userService) { - _configFileProvider = configFileProvider; _userService = userService; API_KEY = configFileProvider.ApiKey; AUTH_METHOD = configFileProvider.AuthenticationMethod; diff --git a/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs new file mode 100644 index 000000000..1d619583d --- /dev/null +++ b/src/Sonarr.Http/Authentication/EnableAuthInNancy.cs @@ -0,0 +1,136 @@ +using System; +using System.Text; +using Nancy; +using Nancy.Authentication.Basic; +using Nancy.Authentication.Forms; +using Nancy.Bootstrapper; +using Nancy.Cookies; +using Nancy.Cryptography; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Authentication; +using NzbDrone.Core.Configuration; +using Lidarr.Http.Extensions; +using Lidarr.Http.Extensions.Pipelines; + +namespace Lidarr.Http.Authentication +{ + public class EnableAuthInNancy : IRegisterNancyPipeline + { + private readonly IAuthenticationService _authenticationService; + private readonly IConfigService _configService; + private readonly IConfigFileProvider _configFileProvider; + private FormsAuthenticationConfiguration FormsAuthConfig; + + public EnableAuthInNancy(IAuthenticationService authenticationService, + IConfigService configService, + IConfigFileProvider configFileProvider) + { + _authenticationService = authenticationService; + _configService = configService; + _configFileProvider = configFileProvider; + } + + public int Order => 10; + + public void Register(IPipelines pipelines) + { + if (_configFileProvider.AuthenticationMethod == AuthenticationType.Forms) + { + RegisterFormsAuth(pipelines); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>)SlidingAuthenticationForFormsAuth); + } + + else if (_configFileProvider.AuthenticationMethod == AuthenticationType.Basic) + { + pipelines.EnableBasicAuthentication(new BasicAuthenticationConfiguration(_authenticationService, "Lidarr")); + } + + pipelines.BeforeRequest.AddItemToEndOfPipeline((Func<NancyContext, Response>)RequiresAuthentication); + pipelines.AfterRequest.AddItemToEndOfPipeline((Action<NancyContext>)RemoveLoginHooksForApiCalls); + } + + private Response RequiresAuthentication(NancyContext context) + { + Response response = null; + + if (!_authenticationService.IsAuthenticated(context)) + { + response = new Response { StatusCode = HttpStatusCode.Unauthorized }; + } + + return response; + } + + private void RegisterFormsAuth(IPipelines pipelines) + { + FormsAuthentication.FormsAuthenticationCookieName = "LidarrAuth"; + + var cryptographyConfiguration = new CryptographyConfiguration( + new RijndaelEncryptionProvider(new PassphraseKeyGenerator(_configService.RijndaelPassphrase, Encoding.ASCII.GetBytes(_configService.RijndaelSalt))), + new DefaultHmacProvider(new PassphraseKeyGenerator(_configService.HmacPassphrase, Encoding.ASCII.GetBytes(_configService.HmacSalt))) + ); + + FormsAuthConfig = new FormsAuthenticationConfiguration + + { + RedirectUrl = _configFileProvider.UrlBase + "/login", + UserMapper = _authenticationService, + Path = GetCookiePath(), + CryptographyConfiguration = cryptographyConfiguration + }; + + FormsAuthentication.Enable(pipelines, FormsAuthConfig); + } + + private void RemoveLoginHooksForApiCalls(NancyContext context) + { + if (context.Request.IsApiRequest()) + { + if ((context.Response.StatusCode == HttpStatusCode.SeeOther && + context.Response.Headers["Location"].StartsWith($"{_configFileProvider.UrlBase}/login", StringComparison.InvariantCultureIgnoreCase)) || + context.Response.StatusCode == HttpStatusCode.Unauthorized) + { + context.Response = new { Error = "Unauthorized" }.AsResponse(HttpStatusCode.Unauthorized); + } + } + } + + private void SlidingAuthenticationForFormsAuth(NancyContext context) + { + if (context.CurrentUser == null) + { + return; + } + + var formsAuthCookieName = FormsAuthentication.FormsAuthenticationCookieName; + + if (!context.Request.Path.Equals("/logout") && + context.Request.Cookies.ContainsKey(formsAuthCookieName)) + { + var formsAuthCookieValue = context.Request.Cookies[formsAuthCookieName]; + + if (FormsAuthentication.DecryptAndValidateAuthenticationCookie(formsAuthCookieValue, FormsAuthConfig).IsNotNullOrWhiteSpace()) + { + var formsAuthCookie = new NancyCookie(formsAuthCookieName, formsAuthCookieValue, true, false, DateTime.UtcNow.AddDays(7)) + { + Path = GetCookiePath() + }; + + context.Response.WithCookie(formsAuthCookie); + } + } + } + + private string GetCookiePath() + { + var urlBase = _configFileProvider.UrlBase; + + if (urlBase.IsNullOrWhiteSpace()) + { + return "/"; + } + + return urlBase; + } + } +} diff --git a/src/NzbDrone.Api/Authentication/LoginResource.cs b/src/Sonarr.Http/Authentication/LoginResource.cs similarity index 81% rename from src/NzbDrone.Api/Authentication/LoginResource.cs rename to src/Sonarr.Http/Authentication/LoginResource.cs index 5d6a5c9f5..db1b94513 100644 --- a/src/NzbDrone.Api/Authentication/LoginResource.cs +++ b/src/Sonarr.Http/Authentication/LoginResource.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class LoginResource { diff --git a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs b/src/Sonarr.Http/Authentication/NzbDroneUser.cs similarity index 85% rename from src/NzbDrone.Api/Authentication/NzbDroneUser.cs rename to src/Sonarr.Http/Authentication/NzbDroneUser.cs index c8fce02fd..a83a0fda5 100644 --- a/src/NzbDrone.Api/Authentication/NzbDroneUser.cs +++ b/src/Sonarr.Http/Authentication/NzbDroneUser.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Nancy.Security; -namespace NzbDrone.Api.Authentication +namespace Lidarr.Http.Authentication { public class NzbDroneUser : IUserIdentity { diff --git a/src/NzbDrone.Api/ClientSchema/Field.cs b/src/Sonarr.Http/ClientSchema/Field.cs similarity index 92% rename from src/NzbDrone.Api/ClientSchema/Field.cs rename to src/Sonarr.Http/ClientSchema/Field.cs index ec611e8d6..332ed871d 100644 --- a/src/NzbDrone.Api/ClientSchema/Field.cs +++ b/src/Sonarr.Http/ClientSchema/Field.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public class Field { diff --git a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs similarity index 99% rename from src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs rename to src/Sonarr.Http/ClientSchema/SchemaBuilder.cs index 0a7acb9e1..c6b8240d5 100644 --- a/src/NzbDrone.Api/ClientSchema/SchemaBuilder.cs +++ b/src/Sonarr.Http/ClientSchema/SchemaBuilder.cs @@ -7,7 +7,7 @@ using NzbDrone.Common.Extensions; using NzbDrone.Common.Reflection; using NzbDrone.Core.Annotations; -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public static class SchemaBuilder { diff --git a/src/NzbDrone.Api/ClientSchema/SelectOption.cs b/src/Sonarr.Http/ClientSchema/SelectOption.cs similarity index 76% rename from src/NzbDrone.Api/ClientSchema/SelectOption.cs rename to src/Sonarr.Http/ClientSchema/SelectOption.cs index fe42f46dd..58029e8fa 100644 --- a/src/NzbDrone.Api/ClientSchema/SelectOption.cs +++ b/src/Sonarr.Http/ClientSchema/SelectOption.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.ClientSchema +namespace Lidarr.Http.ClientSchema { public class SelectOption { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs b/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs similarity index 93% rename from src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs rename to src/Sonarr.Http/ErrorManagement/ErrorHandler.cs index 6ba25d741..4307f9d53 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorHandler.cs +++ b/src/Sonarr.Http/ErrorManagement/ErrorHandler.cs @@ -1,8 +1,8 @@ using Nancy; using Nancy.ErrorHandling; -using NzbDrone.Api.Extensions; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Lidarr.Http.ErrorManagement { public class ErrorHandler : IStatusCodeHandler { diff --git a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs similarity index 83% rename from src/NzbDrone.Api/ErrorManagement/ErrorModel.cs rename to src/Sonarr.Http/ErrorManagement/ErrorModel.cs index b88600717..cc2b95562 100644 --- a/src/NzbDrone.Api/ErrorManagement/ErrorModel.cs +++ b/src/Sonarr.Http/ErrorManagement/ErrorModel.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Api.ErrorManagement +using Lidarr.Http.Exceptions; + +namespace Lidarr.Http.ErrorManagement { public class ErrorModel { diff --git a/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs new file mode 100644 index 000000000..afb759952 --- /dev/null +++ b/src/Sonarr.Http/ErrorManagement/SonarrErrorPipeline.cs @@ -0,0 +1,79 @@ +using System; +using System.Data.SQLite; +using FluentValidation; +using Nancy; +using NLog; +using NzbDrone.Core.Exceptions; +using Lidarr.Http.Exceptions; +using Lidarr.Http.Extensions; +using HttpStatusCode = Nancy.HttpStatusCode; + +namespace Lidarr.Http.ErrorManagement +{ + public class SonarrErrorPipeline + { + private readonly Logger _logger; + + public SonarrErrorPipeline(Logger logger) + { + _logger = logger; + } + + public Response HandleException(NancyContext context, Exception exception) + { + _logger.Trace("Handling Exception"); + + var apiException = exception as ApiException; + + if (apiException != null) + { + _logger.Warn(apiException, "API Error"); + return apiException.ToErrorResponse(); + } + + var validationException = exception as ValidationException; + + if (validationException != null) + { + _logger.Warn("Invalid request {0}", validationException.Message); + + return validationException.Errors.AsResponse(HttpStatusCode.BadRequest); + } + + var clientException = exception as NzbDroneClientException; + + if (clientException != null) + { + return new ErrorModel + { + Message = exception.Message, + Description = exception.ToString() + }.AsResponse((HttpStatusCode)clientException.StatusCode); + } + + var sqLiteException = exception as SQLiteException; + + if (sqLiteException != null) + { + if (context.Request.Method == "PUT" || context.Request.Method == "POST") + { + if (sqLiteException.Message.Contains("constraint failed")) + return new ErrorModel + { + Message = exception.Message, + }.AsResponse(HttpStatusCode.Conflict); + } + + _logger.Error(sqLiteException, "[{0} {1}]", context.Request.Method, context.Request.Path); + } + + _logger.Fatal(exception, "Request Failed. {0} {1}", context.Request.Method, context.Request.Path); + + return new ErrorModel + { + Message = exception.Message, + Description = exception.ToString() + }.AsResponse(HttpStatusCode.InternalServerError); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/ErrorManagement/ApiException.cs b/src/Sonarr.Http/Exceptions/ApiException.cs similarity index 90% rename from src/NzbDrone.Api/ErrorManagement/ApiException.cs rename to src/Sonarr.Http/Exceptions/ApiException.cs index 2a9f2678f..7823c552f 100644 --- a/src/NzbDrone.Api/ErrorManagement/ApiException.cs +++ b/src/Sonarr.Http/Exceptions/ApiException.cs @@ -1,9 +1,10 @@ using System; using Nancy; using Nancy.Responses; -using NzbDrone.Api.Extensions; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.ErrorManagement +namespace Lidarr.Http.Exceptions { public abstract class ApiException : Exception { diff --git a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs b/src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs similarity index 87% rename from src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs rename to src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs index 8c16e8133..067f0ccc4 100644 --- a/src/NzbDrone.Api/Exceptions/InvalidApiKeyException.cs +++ b/src/Sonarr.Http/Exceptions/InvalidApiKeyException.cs @@ -1,6 +1,6 @@ using System; -namespace NzbDrone.Api.Exceptions +namespace Lidarr.Http.Exceptions { public class InvalidApiKeyException : Exception { diff --git a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs b/src/Sonarr.Http/Extensions/AccessControlHeaders.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/AccessControlHeaders.cs rename to src/Sonarr.Http/Extensions/AccessControlHeaders.cs index 5a32395cb..5ffc632d7 100644 --- a/src/NzbDrone.Api/Extensions/AccessControlHeaders.cs +++ b/src/Sonarr.Http/Extensions/AccessControlHeaders.cs @@ -1,4 +1,4 @@ -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public static class AccessControlHeaders { diff --git a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs rename to src/Sonarr.Http/Extensions/NancyJsonSerializer.cs index 00b3c3b2c..ff3459de9 100644 --- a/src/NzbDrone.Api/Extensions/NancyJsonSerializer.cs +++ b/src/Sonarr.Http/Extensions/NancyJsonSerializer.cs @@ -3,7 +3,7 @@ using System.IO; using Nancy; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public class NancyJsonSerializer : ISerializer { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs similarity index 92% rename from src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs index 94c738d9b..febfc17c3 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CacheHeaderPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/CacheHeaderPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Lidarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class CacheHeaderPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs similarity index 97% rename from src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs index b8c83298a..3f980524f 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/CorsPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/CorsPipeline.cs @@ -3,7 +3,7 @@ using System.Linq; using Nancy; using Nancy.Bootstrapper; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class CorsPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs index 12293f23c..782dd5b15 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs @@ -7,7 +7,7 @@ using Nancy.Bootstrapper; using NLog; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class GzipCompressionPipeline : IRegisterNancyPipeline { @@ -63,20 +63,24 @@ namespace NzbDrone.Api.Extensions.Pipelines private static bool ContentLengthIsTooSmall(Response response) { var contentLength = response.Headers.GetValueOrDefault("Content-Length"); + if (contentLength != null && long.Parse(contentLength) < 1024) { return true; } + return false; } private static bool AlreadyGzipEncoded(Response response) { var contentEncoding = response.Headers.GetValueOrDefault("Content-Encoding"); + if (contentEncoding == "gzip") { return true; } + return false; } } diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs similarity index 78% rename from src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs index 0376ccc70..84105c46b 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IRegisterNancyPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/IRegisterNancyPipeline.cs @@ -1,6 +1,6 @@ using Nancy.Bootstrapper; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public interface IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs similarity index 93% rename from src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs index 68abf4ade..cf619745d 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/IfModifiedPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/IfModifiedPipeline.cs @@ -1,9 +1,9 @@ using System; using Nancy; using Nancy.Bootstrapper; -using NzbDrone.Api.Frontend; +using Lidarr.Http.Frontend; -namespace NzbDrone.Api.Extensions.Pipelines +namespace Lidarr.Http.Extensions.Pipelines { public class IfModifiedPipeline : IRegisterNancyPipeline { diff --git a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs similarity index 91% rename from src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs index 73668bc81..a650ad90f 100644 --- a/src/NzbDrone.Api/Extensions/Pipelines/RequestLoggingPipeline.cs +++ b/src/Sonarr.Http/Extensions/Pipelines/RequestLoggingPipeline.cs @@ -1,91 +1,93 @@ -using System; -using System.Threading; -using Nancy; -using Nancy.Bootstrapper; -using NLog; -using NzbDrone.Api.ErrorManagement; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Api.Extensions.Pipelines -{ - public class RequestLoggingPipeline : IRegisterNancyPipeline - { - private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); - private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); - - private static int _requestSequenceID; - - private readonly NzbDroneErrorPipeline _errorPipeline; - - public RequestLoggingPipeline(NzbDroneErrorPipeline errorPipeline) - { - _errorPipeline = errorPipeline; - } - - public int Order => 100; - - public void Register(IPipelines pipelines) - { - pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); - pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); - pipelines.OnError.AddItemToEndOfPipeline(LogError); - } - - private Response LogStart(NancyContext context) - { - var id = Interlocked.Increment(ref _requestSequenceID); - - context.Items["ApiRequestSequenceID"] = id; - context.Items["ApiRequestStartTime"] = DateTime.UtcNow; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath); - - return null; - } - - private void LogEnd(NancyContext context) - { - var id = (int)context.Items["ApiRequestSequenceID"]; - var startTime = (DateTime)context.Items["ApiRequestStartTime"]; - - var endTime = DateTime.UtcNow; - var duration = endTime - startTime; - - var reqPath = GetRequestPathAndQuery(context.Request); - - _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - - if (context.Request.IsApiRequest()) - { - _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); - } - } - - private Response LogError(NancyContext context, Exception exception) - { - var response = _errorPipeline.HandleException(context, exception); - - context.Response = response; - - LogEnd(context); - - context.Response = null; - - return response; - } - - private static string GetRequestPathAndQuery(Request request) - { - if (request.Url.Query.IsNotNullOrWhiteSpace()) - { - return string.Concat(request.Url.Path, request.Url.Query); - } - else - { - return request.Url.Path; - } - } - } -} +using System; +using System.Threading; +using Nancy; +using Nancy.Bootstrapper; +using NLog; +using NzbDrone.Common.Extensions; +using Lidarr.Http.ErrorManagement; +using Lidarr.Http.Extensions; +using Lidarr.Http.Extensions.Pipelines; + +namespace NzbDrone.Api.Extensions.Pipelines +{ + public class RequestLoggingPipeline : IRegisterNancyPipeline + { + private static readonly Logger _loggerHttp = LogManager.GetLogger("Http"); + private static readonly Logger _loggerApi = LogManager.GetLogger("Api"); + + private static int _requestSequenceID; + + private readonly SonarrErrorPipeline _errorPipeline; + + public RequestLoggingPipeline(SonarrErrorPipeline errorPipeline) + { + _errorPipeline = errorPipeline; + } + + public int Order => 100; + + public void Register(IPipelines pipelines) + { + pipelines.BeforeRequest.AddItemToStartOfPipeline(LogStart); + pipelines.AfterRequest.AddItemToEndOfPipeline(LogEnd); + pipelines.OnError.AddItemToEndOfPipeline(LogError); + } + + private Response LogStart(NancyContext context) + { + var id = Interlocked.Increment(ref _requestSequenceID); + + context.Items["ApiRequestSequenceID"] = id; + context.Items["ApiRequestStartTime"] = DateTime.UtcNow; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Req: {0} [{1}] {2}", id, context.Request.Method, reqPath); + + return null; + } + + private void LogEnd(NancyContext context) + { + var id = (int)context.Items["ApiRequestSequenceID"]; + var startTime = (DateTime)context.Items["ApiRequestStartTime"]; + + var endTime = DateTime.UtcNow; + var duration = endTime - startTime; + + var reqPath = GetRequestPathAndQuery(context.Request); + + _loggerHttp.Trace("Res: {0} [{1}] {2}: {3}.{4} ({5} ms)", id, context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); + + if (context.Request.IsApiRequest()) + { + _loggerApi.Debug("[{0}] {1}: {2}.{3} ({4} ms)", context.Request.Method, reqPath, (int)context.Response.StatusCode, context.Response.StatusCode, (int)duration.TotalMilliseconds); + } + } + + private Response LogError(NancyContext context, Exception exception) + { + var response = _errorPipeline.HandleException(context, exception); + + context.Response = response; + + LogEnd(context); + + context.Response = null; + + return response; + } + + private static string GetRequestPathAndQuery(Request request) + { + if (request.Url.Query.IsNotNullOrWhiteSpace()) + { + return string.Concat(request.Url.Path, request.Url.Query); + } + else + { + return request.Url.Path; + } + } + } +} diff --git a/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs new file mode 100644 index 000000000..96f99328d --- /dev/null +++ b/src/Sonarr.Http/Extensions/Pipelines/SonarrVersionPipeline.cs @@ -0,0 +1,25 @@ +using System; +using Nancy; +using Nancy.Bootstrapper; +using NzbDrone.Common.EnvironmentInfo; + +namespace Lidarr.Http.Extensions.Pipelines +{ + public class SonarrVersionPipeline : IRegisterNancyPipeline + { + public int Order => 0; + + public void Register(IPipelines pipelines) + { + pipelines.AfterRequest.AddItemToStartOfPipeline((Action<NancyContext>) Handle); + } + + private void Handle(NancyContext context) + { + if (!context.Response.Headers.ContainsKey("X-ApplicationVersion")) + { + context.Response.Headers.Add("X-ApplicationVersion", BuildInfo.Version.ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs b/src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs similarity index 100% rename from src/NzbDrone.Api/Extensions/Pipelines/UrlBasePipeline.cs rename to src/Sonarr.Http/Extensions/Pipelines/UrlBasePipeline.cs diff --git a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs b/src/Sonarr.Http/Extensions/ReqResExtensions.cs similarity index 98% rename from src/NzbDrone.Api/Extensions/ReqResExtensions.cs rename to src/Sonarr.Http/Extensions/ReqResExtensions.cs index 1f1d89180..78a3d911a 100644 --- a/src/NzbDrone.Api/Extensions/ReqResExtensions.cs +++ b/src/Sonarr.Http/Extensions/ReqResExtensions.cs @@ -6,7 +6,7 @@ using Nancy.Responses; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Serializer; -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public static class ReqResExtensions { diff --git a/src/NzbDrone.Api/Extensions/RequestExtensions.cs b/src/Sonarr.Http/Extensions/RequestExtensions.cs similarity index 76% rename from src/NzbDrone.Api/Extensions/RequestExtensions.cs rename to src/Sonarr.Http/Extensions/RequestExtensions.cs index 6c112c900..d9dc691a1 100644 --- a/src/NzbDrone.Api/Extensions/RequestExtensions.cs +++ b/src/Sonarr.Http/Extensions/RequestExtensions.cs @@ -1,7 +1,7 @@ using System; using Nancy; -namespace NzbDrone.Api.Extensions +namespace Lidarr.Http.Extensions { public static class RequestExtensions { @@ -36,5 +36,17 @@ namespace NzbDrone.Api.Extensions { return request.Path.StartsWith("/Content/", StringComparison.InvariantCultureIgnoreCase); } + + public static bool GetBooleanQueryParameter(this Request request, string parameter, bool defaultValue = false) + { + var parameterValue = request.Query[parameter]; + + if (parameterValue.HasValue) + { + return bool.Parse(parameterValue.Value); + } + + return defaultValue; + } } } diff --git a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs b/src/Sonarr.Http/Frontend/CacheableSpecification.cs similarity index 93% rename from src/NzbDrone.Api/Frontend/CacheableSpecification.cs rename to src/Sonarr.Http/Frontend/CacheableSpecification.cs index 7995c7da1..7d934530f 100644 --- a/src/NzbDrone.Api/Frontend/CacheableSpecification.cs +++ b/src/Sonarr.Http/Frontend/CacheableSpecification.cs @@ -3,7 +3,7 @@ using Nancy; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend +namespace Lidarr.Http.Frontend { public interface ICacheableSpecification { @@ -29,7 +29,7 @@ namespace NzbDrone.Api.Frontend } if (context.Request.Path.StartsWith("/signalr", StringComparison.CurrentCultureIgnoreCase)) return false; - if (context.Request.Path.EndsWith("main.js")) return false; + if (context.Request.Path.EndsWith("index.js")) return false; if (context.Request.Path.StartsWith("/feed", StringComparison.CurrentCultureIgnoreCase)) return false; if (context.Request.Path.StartsWith("/log", StringComparison.CurrentCultureIgnoreCase) && diff --git a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs index 9e4912524..bc79a53fb 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/BackupFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/BackupFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class BackupFileMapper : StaticResourceMapperBase { diff --git a/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs new file mode 100644 index 000000000..4d7bb0808 --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/BrowserConfig.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class BrowserConfig : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public BrowserConfig(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "xml"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/Content/Images/Icons/browserconfig"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs rename to src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs index 53ebb2986..f165594a8 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/CacheBreakerProvider.cs +++ b/src/Sonarr.Http/Frontend/Mappers/CacheBreakerProvider.cs @@ -3,7 +3,7 @@ using System.Linq; using NzbDrone.Common.Crypto; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public interface ICacheBreakerProvider { diff --git a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs similarity index 88% rename from src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs index 002ffa7ce..c269fa1b3 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/FaviconMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/FaviconMapper.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class FaviconMapper : StaticResourceMapperBase { @@ -27,7 +27,7 @@ namespace NzbDrone.Api.Frontend.Mappers fileName = "favicon-debug.ico"; } - var path = Path.Combine("Content", "Images", fileName); + var path = Path.Combine("Content", "Images", "Icons", fileName); return Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path); } diff --git a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs similarity index 84% rename from src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs rename to src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs index 6390a2545..1bfd458ad 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IMapHttpRequestsToDisk.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs @@ -1,7 +1,7 @@  using Nancy; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public interface IMapHttpRequestsToDisk { diff --git a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs similarity index 87% rename from src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs index ae66b2aa2..aefe17fb6 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/IndexHtmlMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text.RegularExpressions; using Nancy; @@ -8,7 +8,7 @@ using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Analytics; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class IndexHtmlMapper : StaticResourceMapperBase { @@ -17,7 +17,7 @@ namespace NzbDrone.Api.Frontend.Mappers private readonly IAnalyticsService _analyticsService; private readonly Func<ICacheBreakerProvider> _cacheBreakProviderFactory; private readonly string _indexPath; - private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ReplaceRegex = new Regex(@"(?:(?<attribute>href|src)=\"")(?<path>.*?(?<extension>css|js|png|ico|ics|svg))(?:\"")(?:\s(?<nohash>data-no-hash))?", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static string API_KEY; private static string URL_BASE; @@ -49,7 +49,9 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { - return !resourceUrl.Contains(".") && !resourceUrl.StartsWith("/login"); + return !resourceUrl.Contains(".") && + !resourceUrl.StartsWith("/login") && + !resourceUrl.StartsWith("/Content"); } public override Response GetResponse(string resourceUrl) @@ -100,13 +102,14 @@ namespace NzbDrone.Api.Frontend.Mappers return string.Format("{0}=\"{1}{2}\"", match.Groups["attribute"].Value, URL_BASE, url); }); - text = text.Replace("API_ROOT", URL_BASE + "/api"); + text = text.Replace("API_ROOT", URL_BASE + "/api/v3"); text = text.Replace("API_KEY", API_KEY); + text = text.Replace("RELEASE", BuildInfo.Release); text = text.Replace("APP_VERSION", BuildInfo.Version.ToString()); text = text.Replace("APP_BRANCH", _configFileProvider.Branch.ToLower()); text = text.Replace("APP_ANALYTICS", _analyticsService.IsEnabled.ToString().ToLowerInvariant()); text = text.Replace("URL_BASE", URL_BASE); - text = text.Replace("PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); + text = text.Replace("IS_PRODUCTION", RuntimeInfo.IsProduction.ToString().ToLowerInvariant()); _generatedContent = text; diff --git a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs index 5fda7d483..c6f73732f 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/LogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/LogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class UpdateLogFileMapper : StaticResourceMapperBase { diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs new file mode 100644 index 000000000..22b4071da --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs @@ -0,0 +1,71 @@ +using System.IO; +using Nancy; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class LoginHtmlMapper : StaticResourceMapperBase + { + private readonly IDiskProvider _diskProvider; + private readonly string _indexPath; + + private string _generatedContent; + + public LoginHtmlMapper(IAppFolderInfo appFolderInfo, + IDiskProvider diskProvider, + IConfigFileProvider configFileProvider, + Logger logger) + : base(diskProvider, logger) + { + _diskProvider = diskProvider; + _indexPath = Path.Combine(appFolderInfo.StartUpFolder, configFileProvider.UiFolder, "login.html"); + } + + public override string Map(string resourceUrl) + { + return _indexPath; + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/login"); + } + + public override Response GetResponse(string resourceUrl) + { + var response = base.GetResponse(resourceUrl); + response.Headers["X-UA-Compatible"] = "IE=edge"; + + return response; + } + + protected override Stream GetContentStream(string filePath) + { + var text = GetLoginText(); + + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(text); + writer.Flush(); + stream.Position = 0; + return stream; + } + + private string GetLoginText() + { + if (RuntimeInfo.IsProduction && _generatedContent != null) + { + return _generatedContent; + } + + var text = _diskProvider.ReadAllText(_indexPath); + + _generatedContent = text; + + return _generatedContent; + } + } +} diff --git a/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs new file mode 100644 index 000000000..92afc08ca --- /dev/null +++ b/src/Sonarr.Http/Frontend/Mappers/ManifestMapper.cs @@ -0,0 +1,34 @@ +using System.IO; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.Configuration; + +namespace Lidarr.Http.Frontend.Mappers +{ + public class ManifestMapper : StaticResourceMapperBase + { + private readonly IAppFolderInfo _appFolderInfo; + private readonly IConfigFileProvider _configFileProvider; + + public ManifestMapper(IAppFolderInfo appFolderInfo, IDiskProvider diskProvider, IConfigFileProvider configFileProvider, Logger logger) + : base(diskProvider, logger) + { + _appFolderInfo = appFolderInfo; + _configFileProvider = configFileProvider; + } + + public override string Map(string resourceUrl) + { + var path = resourceUrl.Replace('/', Path.DirectorySeparatorChar); + path = path.Trim(Path.DirectorySeparatorChar); + + return Path.ChangeExtension(Path.Combine(_appFolderInfo.StartUpFolder, _configFileProvider.UiFolder, path), "json"); + } + + public override bool CanHandle(string resourceUrl) + { + return resourceUrl.StartsWith("/Content/Images/Icons/manifest"); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs similarity index 97% rename from src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs index a4e5fb8f2..9aa9dfc49 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/MediaCoverMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverMapper.cs @@ -5,7 +5,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class MediaCoverMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs similarity index 96% rename from src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs index 60b3131c6..d6bfedb5f 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/RobotsTxtMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/RobotsTxtMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class RobotsTxtMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs similarity index 83% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs index 61ed14e9b..32518a935 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapper.cs @@ -1,10 +1,10 @@ -using System.IO; +using System.IO; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Core.Configuration; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class StaticResourceMapper : StaticResourceMapperBase { @@ -28,6 +28,12 @@ namespace NzbDrone.Api.Frontend.Mappers public override bool CanHandle(string resourceUrl) { + if (resourceUrl.StartsWith("/Content/Images/Icons/manifest") || + resourceUrl.StartsWith("/Content/Images/Icons/browserconfig")) + { + return false; + } + return resourceUrl.StartsWith("/Content") || resourceUrl.EndsWith(".js") || resourceUrl.EndsWith(".map") || diff --git a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs similarity index 97% rename from src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs rename to src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs index 5d4b57d50..5698d60f2 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/StaticResourceMapperBase.cs +++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs @@ -1,12 +1,12 @@ using System; using System.IO; -using NLog; using Nancy; using Nancy.Responses; +using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public abstract class StaticResourceMapperBase : IMapHttpRequestsToDisk { diff --git a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs rename to src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs index 021bdba58..13fe24b58 100644 --- a/src/NzbDrone.Api/Frontend/Mappers/UpdateLogFileMapper.cs +++ b/src/Sonarr.Http/Frontend/Mappers/UpdateLogFileMapper.cs @@ -4,7 +4,7 @@ using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Frontend.Mappers +namespace Lidarr.Http.Frontend.Mappers { public class LogFileMapper : StaticResourceMapperBase { diff --git a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs b/src/Sonarr.Http/Frontend/StaticResourceModule.cs similarity index 95% rename from src/NzbDrone.Api/Frontend/StaticResourceModule.cs rename to src/Sonarr.Http/Frontend/StaticResourceModule.cs index f58667c6c..627ec28b5 100644 --- a/src/NzbDrone.Api/Frontend/StaticResourceModule.cs +++ b/src/Sonarr.Http/Frontend/StaticResourceModule.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using Nancy; using Nancy.Responses; using NLog; -using Nancy; -using NzbDrone.Api.Frontend.Mappers; using NzbDrone.Core.Configuration; +using Lidarr.Http.Frontend.Mappers; -namespace NzbDrone.Api.Frontend +namespace Lidarr.Http.Frontend { public class StaticResourceModule : NancyModule { diff --git a/src/Sonarr.Http/LIdarrRestModuleWithSignalR.cs b/src/Sonarr.Http/LIdarrRestModuleWithSignalR.cs new file mode 100644 index 000000000..dd5efe0ee --- /dev/null +++ b/src/Sonarr.Http/LIdarrRestModuleWithSignalR.cs @@ -0,0 +1,71 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Events; +using NzbDrone.Core.Messaging.Events; +using NzbDrone.SignalR; +using Lidarr.Http.REST; + +namespace Lidarr.Http +{ + public abstract class LidarrRestModuleWithSignalR<TResource, TModel> : LidarrRestModule<TResource>, IHandle<ModelEvent<TModel>> + where TResource : RestResource, new() + where TModel : ModelBase, new() + { + private readonly IBroadcastSignalRMessage _signalRBroadcaster; + + protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster) + { + _signalRBroadcaster = signalRBroadcaster; + } + + protected LidarrRestModuleWithSignalR(IBroadcastSignalRMessage signalRBroadcaster, string resource) + : base(resource) + { + _signalRBroadcaster = signalRBroadcaster; + } + + public void Handle(ModelEvent<TModel> message) + { + if (message.Action == ModelAction.Deleted || message.Action == ModelAction.Sync) + { + BroadcastResourceChange(message.Action); + } + + BroadcastResourceChange(message.Action, message.Model.Id); + } + + protected void BroadcastResourceChange(ModelAction action, int id) + { + var resource = GetResourceById(id); + BroadcastResourceChange(action, resource); + } + + + protected void BroadcastResourceChange(ModelAction action, TResource resource) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage<TResource>(resource, action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + + + protected void BroadcastResourceChange(ModelAction action) + { + if (GetType().Namespace.Contains("V3")) + { + var signalRMessage = new SignalRMessage + { + Name = Resource, + Body = new ResourceChangeMessage<TResource>(action), + Action = action + }; + + _signalRBroadcaster.BroadcastMessage(signalRMessage); + } + } + } +} diff --git a/src/Sonarr.Http/Lidarr.Http.csproj b/src/Sonarr.Http/Lidarr.Http.csproj new file mode 100644 index 000000000..fc0c41f80 --- /dev/null +++ b/src/Sonarr.Http/Lidarr.Http.csproj @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{5370BFF7-1BD7-46BC-AF06-7D9EA5CDA1D6}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Sonarr.Http</RootNamespace> + <AssemblyName>Sonarr.Http</AssemblyName> + <TargetFrameworkVersion>v4.0</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'"> + <DebugSymbols>true</DebugSymbols> + <OutputPath>bin\x86\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <DebugType>full</DebugType> + <PlatformTarget>x86</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x86'"> + <OutputPath>bin\x86\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <Optimize>true</Optimize> + <DebugType>pdbonly</DebugType> + <PlatformTarget>x86</PlatformTarget> + <ErrorReport>prompt</ErrorReport> + <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet> + </PropertyGroup> + <ItemGroup> + <Reference Include="FluentValidation, Version=6.2.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\FluentValidation.6.2.1.0\lib\portable-net40+sl50+wp80+win8+wpa81\FluentValidation.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Nancy, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.1.4.3\lib\net40\Nancy.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Nancy.Authentication.Basic, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.Authentication.Basic.1.4.1\lib\net40\Nancy.Authentication.Basic.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Nancy.Authentication.Forms, Version=1.4.1.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\Nancy.Authentication.Forms.1.4.1\lib\net40\Nancy.Authentication.Forms.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="Newtonsoft.Json, Version=9.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> + <HintPath>..\packages\Newtonsoft.Json.9.0.1\lib\net40\Newtonsoft.Json.dll</HintPath> + <Private>True</Private> + </Reference> + <Reference Include="NLog, Version=4.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> + <HintPath>..\packages\NLog.4.4.3\lib\net40\NLog.dll</HintPath> + </Reference> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Data.SQLite"> + <HintPath>..\Libraries\Sqlite\System.Data.SQLite.dll</HintPath> + </Reference> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + </ItemGroup> + <ItemGroup> + <Compile Include="Exceptions\ApiException.cs" /> + <Compile Include="Authentication\AuthenticationModule.cs" /> + <Compile Include="Authentication\AuthenticationService.cs" /> + <Compile Include="Authentication\EnableAuthInNancy.cs" /> + <Compile Include="Authentication\LoginResource.cs" /> + <Compile Include="Authentication\NzbDroneUser.cs" /> + <Compile Include="ClientSchema\Field.cs" /> + <Compile Include="ClientSchema\SchemaBuilder.cs" /> + <Compile Include="ClientSchema\SelectOption.cs" /> + <Compile Include="ErrorManagement\ErrorHandler.cs" /> + <Compile Include="ErrorManagement\ErrorModel.cs" /> + <Compile Include="ErrorManagement\SonarrErrorPipeline.cs" /> + <Compile Include="Exceptions\InvalidApiKeyException.cs" /> + <Compile Include="Extensions\AccessControlHeaders.cs" /> + <Compile Include="Extensions\NancyJsonSerializer.cs" /> + <Compile Include="Extensions\Pipelines\CacheHeaderPipeline.cs" /> + <Compile Include="Extensions\Pipelines\CorsPipeline.cs" /> + <Compile Include="Extensions\Pipelines\GZipPipeline.cs" /> + <Compile Include="Extensions\Pipelines\IfModifiedPipeline.cs" /> + <Compile Include="Extensions\Pipelines\IRegisterNancyPipeline.cs" /> + <Compile Include="Extensions\Pipelines\RequestLoggingPipeline.cs" /> + <Compile Include="Extensions\Pipelines\SonarrVersionPipeline.cs" /> + <Compile Include="Extensions\ReqResExtensions.cs" /> + <Compile Include="Extensions\RequestExtensions.cs" /> + <Compile Include="Frontend\CacheableSpecification.cs" /> + <Compile Include="Frontend\Mappers\BackupFileMapper.cs" /> + <Compile Include="Frontend\Mappers\CacheBreakerProvider.cs" /> + <Compile Include="Frontend\Mappers\FaviconMapper.cs" /> + <Compile Include="Frontend\Mappers\IMapHttpRequestsToDisk.cs" /> + <Compile Include="Frontend\Mappers\IndexHtmlMapper.cs" /> + <Compile Include="Frontend\Mappers\LogFileMapper.cs" /> + <Compile Include="Frontend\Mappers\LoginHtmlMapper.cs" /> + <Compile Include="Frontend\Mappers\BrowserConfig.cs" /> + <Compile Include="Frontend\Mappers\MediaCoverMapper.cs" /> + <Compile Include="Frontend\Mappers\RobotsTxtMapper.cs" /> + <Compile Include="Frontend\Mappers\ManifestMapper.cs" /> + <Compile Include="Frontend\Mappers\StaticResourceMapper.cs" /> + <Compile Include="Frontend\Mappers\StaticResourceMapperBase.cs" /> + <Compile Include="Frontend\Mappers\UpdateLogFileMapper.cs" /> + <Compile Include="Frontend\StaticResourceModule.cs" /> + <Compile Include="Mapping\MappingValidation.cs" /> + <Compile Include="Mapping\ResourceMappingException.cs" /> + <Compile Include="PagingResource.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="ResourceChangeMessage.cs" /> + <Compile Include="REST\BadRequestException.cs" /> + <Compile Include="REST\MethodNotAllowedException.cs" /> + <Compile Include="REST\NotFoundException.cs" /> + <Compile Include="REST\ResourceValidator.cs" /> + <Compile Include="REST\RestModule.cs" /> + <Compile Include="REST\RestResource.cs" /> + <Compile Include="LidarrBootstrapper.cs" /> + <Compile Include="LidarrRestModule.cs" /> + <Compile Include="LidarrRestModuleWithSignalR.cs" /> + <Compile Include="TinyIoCNancyBootstrapper.cs" /> + <Compile Include="Validation\EmptyCollectionValidator.cs" /> + <Compile Include="Validation\RssSyncIntervalValidator.cs" /> + <Compile Include="Validation\RuleBuilderExtensions.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\Marr.Data\Marr.Data.csproj"> + <Project>{F6FC6BE7-0847-4817-A1ED-223DC647C3D7}</Project> + <Name>Marr.Data</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.Common\NzbDrone.Common.csproj"> + <Project>{F2BE0FDF-6E47-4827-A420-DD4EF82407F8}</Project> + <Name>NzbDrone.Common</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.Core\NzbDrone.Core.csproj"> + <Project>{FF5EE3B6-913B-47CE-9CEB-11C51B4E1205}</Project> + <Name>NzbDrone.Core</Name> + </ProjectReference> + <ProjectReference Include="..\NzbDrone.SignalR\NzbDrone.SignalR.csproj"> + <Project>{7C2CC69F-5CA0-4E5C-85CB-983F9F6C3B36}</Project> + <Name>NzbDrone.SignalR</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <None Include="app.config" /> + <None Include="packages.config" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/src/Sonarr.Http/LidarrBootstrapper.cs b/src/Sonarr.Http/LidarrBootstrapper.cs new file mode 100644 index 000000000..65c528f56 --- /dev/null +++ b/src/Sonarr.Http/LidarrBootstrapper.cs @@ -0,0 +1,59 @@ +using System.Linq; +using Nancy.Bootstrapper; +using Nancy.Diagnostics; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Instrumentation; +using NzbDrone.Core.Instrumentation; +using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.Messaging.Events; +using Lidarr.Http.Extensions.Pipelines; +using TinyIoC; + +namespace Lidarr.Http +{ + public class LidarrBootstrapper : TinyIoCNancyBootstrapper + { + private readonly TinyIoCContainer _tinyIoCContainer; + private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(LidarrBootstrapper)); + + public LidarrBootstrapper(TinyIoCContainer tinyIoCContainer) + { + _tinyIoCContainer = tinyIoCContainer; + } + + protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) + { + Logger.Info("Starting Web Server"); + + if (RuntimeInfo.IsProduction) + { + DiagnosticsHook.Disable(pipelines); + } + + RegisterPipelines(pipelines); + + container.Resolve<DatabaseTarget>().Register(); + container.Resolve<IEventAggregator>().PublishEvent(new ApplicationStartedEvent()); + } + + private void RegisterPipelines(IPipelines pipelines) + { + var pipelineRegistrars = _tinyIoCContainer.ResolveAll<IRegisterNancyPipeline>().OrderBy(v => v.Order).ToList(); + + foreach (var registerNancyPipeline in pipelineRegistrars) + { + registerNancyPipeline.Register(pipelines); + } + } + + protected override TinyIoCContainer GetApplicationContainer() + { + return _tinyIoCContainer; + } + + protected override DiagnosticsConfiguration DiagnosticsConfiguration => new DiagnosticsConfiguration { Password = @"password" }; + + protected override byte[] FavIcon => null; + } +} diff --git a/src/Sonarr.Http/LidarrRestModule.cs b/src/Sonarr.Http/LidarrRestModule.cs new file mode 100644 index 000000000..d5f148a06 --- /dev/null +++ b/src/Sonarr.Http/LidarrRestModule.cs @@ -0,0 +1,56 @@ +using System; +using NzbDrone.Core.Datastore; +using Lidarr.Http.REST; +using Lidarr.Http.Validation; + +namespace Lidarr.Http +{ + public abstract class LidarrRestModule<TResource> : RestModule<TResource> where TResource : RestResource, new() + { + protected string Resource { get; private set; } + + + private static string BaseUrl() + { + var isV3 = typeof(TResource).Namespace.Contains(".V3."); + if (isV3) + { + return "/api/v3/"; + } + return "/api/"; + } + + private static string ResourceName() + { + return new TResource().ResourceName.Trim('/').ToLower(); + } + + protected LidarrRestModule() + : this(ResourceName()) + { + } + + protected LidarrRestModule(string resource) + : base(BaseUrl() + resource.Trim('/').ToLower()) + { + Resource = resource; + PostValidator.RuleFor(r => r.Id).IsZero(); + PutValidator.RuleFor(r => r.Id).ValidId(); + } + + protected PagingResource<TResource> ApplyToPage<TModel>(Func<PagingSpec<TModel>, PagingSpec<TModel>> function, PagingSpec<TModel> pagingSpec, Converter<TModel, TResource> mapper) + { + pagingSpec = function(pagingSpec); + + return new PagingResource<TResource> + { + Page = pagingSpec.Page, + PageSize = pagingSpec.PageSize, + SortDirection = pagingSpec.SortDirection, + SortKey = pagingSpec.SortKey, + TotalRecords = pagingSpec.TotalRecords, + Records = pagingSpec.Records.ConvertAll(mapper) + }; + } + } +} diff --git a/src/Sonarr.Http/Mapping/MappingValidation.cs b/src/Sonarr.Http/Mapping/MappingValidation.cs new file mode 100644 index 000000000..e4f056520 --- /dev/null +++ b/src/Sonarr.Http/Mapping/MappingValidation.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using System.Reflection; +using NzbDrone.Common.Reflection; +using Lidarr.Http.REST; + +namespace Lidarr.Http.Mapping +{ + public static class MappingValidation + { + public static void ValidateMapping(Type modelType, Type resourceType) + { + var errors = modelType.GetSimpleProperties().Where(c=>!c.GetGetMethod().IsStatic).Select(p => GetError(resourceType, p)).Where(c => c != null).ToList(); + + if (errors.Any()) + { + throw new ResourceMappingException(errors); + } + + PrintExtraProperties(modelType, resourceType); + } + + private static void PrintExtraProperties(Type modelType, Type resourceType) + { + var resourceBaseProperties = typeof(RestResource).GetProperties().Select(c => c.Name); + var resourceProperties = resourceType.GetProperties().Select(c => c.Name).Except(resourceBaseProperties); + var modelProperties = modelType.GetProperties().Select(c => c.Name); + + var extra = resourceProperties.Except(modelProperties); + + foreach (var extraProp in extra) + { + Console.WriteLine("Extra: [{0}]", extraProp); + } + } + + private static string GetError(Type resourceType, PropertyInfo modelProperty) + { + var resourceProperty = resourceType.GetProperties().FirstOrDefault(c => c.Name == modelProperty.Name); + + if (resourceProperty == null) + { + return string.Format("public {0} {1} {{ get; set; }}", modelProperty.PropertyType.Name, modelProperty.Name); + } + + if (resourceProperty.PropertyType != modelProperty.PropertyType && !typeof(RestResource).IsAssignableFrom(resourceProperty.PropertyType)) + { + return string.Format("Expected {0}.{1} to have type of {2} but found {3}", resourceType.Name, resourceProperty.Name, modelProperty.PropertyType, resourceProperty.PropertyType); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/Sonarr.Http/Mapping/ResourceMappingException.cs b/src/Sonarr.Http/Mapping/ResourceMappingException.cs new file mode 100644 index 000000000..3744d3d5e --- /dev/null +++ b/src/Sonarr.Http/Mapping/ResourceMappingException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Lidarr.Http.Mapping +{ + public class ResourceMappingException : ApplicationException + { + public ResourceMappingException(IEnumerable<string> error) + : base(Environment.NewLine + string.Join(Environment.NewLine, error.OrderBy(c => c))) + { + + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/PagingResource.cs b/src/Sonarr.Http/PagingResource.cs similarity index 95% rename from src/NzbDrone.Api/PagingResource.cs rename to src/Sonarr.Http/PagingResource.cs index b8025efc4..8b648fa4f 100644 --- a/src/NzbDrone.Api/PagingResource.cs +++ b/src/Sonarr.Http/PagingResource.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.Datastore; -namespace NzbDrone.Api +namespace Lidarr.Http { public class PagingResource<TResource> { diff --git a/src/Sonarr.Http/Properties/AssemblyInfo.cs b/src/Sonarr.Http/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9d340b973 --- /dev/null +++ b/src/Sonarr.Http/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Sonarr.Nancy")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Sonarr.Nancy")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5370bff7-1bd7-46bc-af06-7d9ea5cda1d6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/NzbDrone.Api/REST/BadRequestException.cs b/src/Sonarr.Http/REST/BadRequestException.cs similarity index 76% rename from src/NzbDrone.Api/REST/BadRequestException.cs rename to src/Sonarr.Http/REST/BadRequestException.cs index 450f484e5..5f61a1d49 100644 --- a/src/NzbDrone.Api/REST/BadRequestException.cs +++ b/src/Sonarr.Http/REST/BadRequestException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class BadRequestException : ApiException { diff --git a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs b/src/Sonarr.Http/REST/MethodNotAllowedException.cs similarity index 78% rename from src/NzbDrone.Api/REST/MethodNotAllowedException.cs rename to src/Sonarr.Http/REST/MethodNotAllowedException.cs index 44d2065c6..e9dc7b74c 100644 --- a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs +++ b/src/Sonarr.Http/REST/MethodNotAllowedException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class MethodNotAllowedException : ApiException { diff --git a/src/NzbDrone.Api/REST/NotFoundException.cs b/src/Sonarr.Http/REST/NotFoundException.cs similarity index 76% rename from src/NzbDrone.Api/REST/NotFoundException.cs rename to src/Sonarr.Http/REST/NotFoundException.cs index 92b4016a9..e8377ced4 100644 --- a/src/NzbDrone.Api/REST/NotFoundException.cs +++ b/src/Sonarr.Http/REST/NotFoundException.cs @@ -1,7 +1,7 @@ using Nancy; -using NzbDrone.Api.ErrorManagement; +using Lidarr.Http.Exceptions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class NotFoundException : ApiException { diff --git a/src/NzbDrone.Api/REST/ResourceValidator.cs b/src/Sonarr.Http/REST/ResourceValidator.cs similarity index 95% rename from src/NzbDrone.Api/REST/ResourceValidator.cs rename to src/Sonarr.Http/REST/ResourceValidator.cs index 8062e6fd0..e052470d1 100644 --- a/src/NzbDrone.Api/REST/ResourceValidator.cs +++ b/src/Sonarr.Http/REST/ResourceValidator.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; using FluentValidation; using FluentValidation.Internal; using FluentValidation.Resources; -using NzbDrone.Api.ClientSchema; -using System.Linq; +using Lidarr.Http.ClientSchema; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public class ResourceValidator<TResource> : AbstractValidator<TResource> { diff --git a/src/NzbDrone.Api/REST/RestModule.cs b/src/Sonarr.Http/REST/RestModule.cs similarity index 92% rename from src/NzbDrone.Api/REST/RestModule.cs rename to src/Sonarr.Http/REST/RestModule.cs index 7c6ba37a4..040658aa9 100644 --- a/src/NzbDrone.Api/REST/RestModule.cs +++ b/src/Sonarr.Http/REST/RestModule.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using FluentValidation; using Nancy; -using NzbDrone.Api.Extensions; -using System.Linq; using NzbDrone.Core.Datastore; +using Lidarr.Http.Extensions; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public abstract class RestModule<TResource> : NancyModule where TResource : RestResource, new() @@ -232,6 +232,7 @@ namespace NzbDrone.Api.REST { pagingResource.SortKey = Request.Query.SortKey.ToString(); + // For backwards compatibility with v2 if (Request.Query.SortDir != null) { pagingResource.SortDirection = Request.Query.SortDir.ToString() @@ -239,6 +240,15 @@ namespace NzbDrone.Api.REST ? SortDirection.Ascending : SortDirection.Descending; } + + // v3 uses SortDirection instead of SortDir to be consistent with every other use of it + if (Request.Query.SortDirection != null) + { + pagingResource.SortDirection = Request.Query.SortDirection.ToString() + .Equals("ascending", StringComparison.InvariantCultureIgnoreCase) + ? SortDirection.Ascending + : SortDirection.Descending; + } } if (Request.Query.FilterKey != null) diff --git a/src/NzbDrone.Api/REST/RestResource.cs b/src/Sonarr.Http/REST/RestResource.cs similarity index 91% rename from src/NzbDrone.Api/REST/RestResource.cs rename to src/Sonarr.Http/REST/RestResource.cs index ec9f195c6..b08aa6eef 100644 --- a/src/NzbDrone.Api/REST/RestResource.cs +++ b/src/Sonarr.Http/REST/RestResource.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace NzbDrone.Api.REST +namespace Lidarr.Http.REST { public abstract class RestResource { diff --git a/src/NzbDrone.Api/ResourceChangeMessage.cs b/src/Sonarr.Http/ResourceChangeMessage.cs similarity index 93% rename from src/NzbDrone.Api/ResourceChangeMessage.cs rename to src/Sonarr.Http/ResourceChangeMessage.cs index 6319dcc39..3d2e67c78 100644 --- a/src/NzbDrone.Api/ResourceChangeMessage.cs +++ b/src/Sonarr.Http/ResourceChangeMessage.cs @@ -1,8 +1,8 @@ using System; -using NzbDrone.Api.REST; using NzbDrone.Core.Datastore.Events; +using Lidarr.Http.REST; -namespace NzbDrone.Api +namespace Lidarr.Http { public class ResourceChangeMessage<TResource> where TResource : RestResource { diff --git a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs b/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs similarity index 99% rename from src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs rename to src/Sonarr.Http/TinyIoCNancyBootstrapper.cs index d938b0c6e..c4b3f5339 100644 --- a/src/NzbDrone.Api/TinyIoCNancyBootstrapper.cs +++ b/src/Sonarr.Http/TinyIoCNancyBootstrapper.cs @@ -7,7 +7,7 @@ using Nancy; using Nancy.Diagnostics; using Nancy.Bootstrapper; -namespace NzbDrone.Api +namespace Lidarr.Http { diff --git a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs b/src/Sonarr.Http/Validation/EmptyCollectionValidator.cs similarity index 94% rename from src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs rename to src/Sonarr.Http/Validation/EmptyCollectionValidator.cs index 432eb1ed9..1ad5e20e2 100644 --- a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs +++ b/src/Sonarr.Http/Validation/EmptyCollectionValidator.cs @@ -2,7 +2,7 @@ using FluentValidation.Validators; using NzbDrone.Common.Extensions; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public class EmptyCollectionValidator<T> : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs b/src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs similarity index 95% rename from src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs rename to src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs index 8a3f2d54c..797103b2b 100644 --- a/src/NzbDrone.Api/Validation/RssSyncIntervalValidator.cs +++ b/src/Sonarr.Http/Validation/RssSyncIntervalValidator.cs @@ -1,6 +1,6 @@ using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public class RssSyncIntervalValidator : PropertyValidator { diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/Sonarr.Http/Validation/RuleBuilderExtensions.cs similarity index 97% rename from src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs rename to src/Sonarr.Http/Validation/RuleBuilderExtensions.cs index 01a3a4f75..01f1608e2 100644 --- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs +++ b/src/Sonarr.Http/Validation/RuleBuilderExtensions.cs @@ -3,7 +3,7 @@ using System.Text.RegularExpressions; using FluentValidation; using FluentValidation.Validators; -namespace NzbDrone.Api.Validation +namespace Lidarr.Http.Validation { public static class RuleBuilderExtensions { diff --git a/src/Sonarr.Http/app.config b/src/Sonarr.Http/app.config new file mode 100644 index 000000000..8460dd432 --- /dev/null +++ b/src/Sonarr.Http/app.config @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <runtime> + <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> + <dependentAssembly> + <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> + <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" /> + </dependentAssembly> + </assemblyBinding> + </runtime> +</configuration> \ No newline at end of file diff --git a/src/Sonarr.Http/packages.config b/src/Sonarr.Http/packages.config new file mode 100644 index 000000000..1d49009e6 --- /dev/null +++ b/src/Sonarr.Http/packages.config @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="FluentValidation" version="6.2.1.0" targetFramework="net40" /> + <package id="Nancy" version="1.4.3" targetFramework="net40" /> + <package id="Nancy.Authentication.Basic" version="1.4.1" targetFramework="net40" /> + <package id="Nancy.Authentication.Forms" version="1.4.1" targetFramework="net40" /> + <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net40" /> + <package id="NLog" version="4.4.3" targetFramework="net40" /> +</packages> \ No newline at end of file diff --git a/src/UI/.idea/.name b/src/UI/.idea/.name deleted file mode 100644 index 78ec2c0fe..000000000 --- a/src/UI/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -NzbDrone.UI \ No newline at end of file diff --git a/src/UI/.idea/NzbDrone.UI.iml b/src/UI/.idea/NzbDrone.UI.iml deleted file mode 100644 index 2184ad470..000000000 --- a/src/UI/.idea/NzbDrone.UI.iml +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module type="WEB_MODULE" version="4"> - <component name="NewModuleRootManager"> - <content url="file://$MODULE_DIR$" /> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="jQuery-1.9.1" level="application" /> - <orderEntry type="library" name="backbone.backgrid.filter.js" level="project" /> - </component> -</module> - diff --git a/src/UI/.idea/codeStyleSettings.xml b/src/UI/.idea/codeStyleSettings.xml deleted file mode 100644 index 7598f4c8e..000000000 --- a/src/UI/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,59 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectCodeStyleSettingsManager"> - <option name="PER_PROJECT_SETTINGS"> - <value> - <option name="LINE_SEPARATOR" value=" " /> - <option name="RIGHT_MARGIN" value="190" /> - <option name="HTML_ATTRIBUTE_WRAP" value="0" /> - <option name="HTML_KEEP_LINE_BREAKS" value="false" /> - <option name="HTML_KEEP_BLANK_LINES" value="1" /> - <option name="HTML_ALIGN_ATTRIBUTES" value="false" /> - <option name="HTML_INLINE_ELEMENTS" value="" /> - <option name="HTML_DONT_ADD_BREAKS_IF_INLINE_CONTENT" value="" /> - <CssCodeStyleSettings> - <option name="HEX_COLOR_LOWER_CASE" value="true" /> - <option name="HEX_COLOR_LONG_FORMAT" value="true" /> - <option name="VALUE_ALIGNMENT" value="1" /> - </CssCodeStyleSettings> - <JSCodeStyleSettings> - <option name="SPACE_BEFORE_PROPERTY_COLON" value="true" /> - <option name="ALIGN_OBJECT_PROPERTIES" value="2" /> - <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> - <option name="OBJECT_LITERAL_WRAP" value="2" /> - <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> - </JSCodeStyleSettings> - <XML> - <option name="XML_LEGACY_SETTINGS_IMPORTED" value="true" /> - </XML> - <codeStyleSettings language="CSS"> - <indentOptions> - <option name="SMART_TABS" value="true" /> - </indentOptions> - </codeStyleSettings> - <codeStyleSettings language="JavaScript"> - <option name="LINE_COMMENT_AT_FIRST_COLUMN" value="true" /> - <option name="KEEP_LINE_BREAKS" value="false" /> - <option name="KEEP_FIRST_COLUMN_COMMENT" value="false" /> - <option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> - <option name="CATCH_ON_NEW_LINE" value="true" /> - <option name="FINALLY_ON_NEW_LINE" value="true" /> - <option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> - <option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" /> - <option name="SPACE_BEFORE_METHOD_PARENTHESES" value="true" /> - <option name="CALL_PARAMETERS_WRAP" value="1" /> - <option name="BINARY_OPERATION_WRAP" value="1" /> - <option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" /> - <option name="ARRAY_INITIALIZER_WRAP" value="2" /> - <option name="ARRAY_INITIALIZER_LBRACE_ON_NEXT_LINE" value="true" /> - <option name="ARRAY_INITIALIZER_RBRACE_ON_NEXT_LINE" value="true" /> - <option name="IF_BRACE_FORCE" value="3" /> - <option name="DOWHILE_BRACE_FORCE" value="3" /> - <option name="WHILE_BRACE_FORCE" value="3" /> - <option name="FOR_BRACE_FORCE" value="3" /> - </codeStyleSettings> - </value> - </option> - <option name="USE_PER_PROJECT_SETTINGS" value="true" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Keivan.xml b/src/UI/.idea/dictionaries/Keivan.xml deleted file mode 100644 index fb034b3af..000000000 --- a/src/UI/.idea/dictionaries/Keivan.xml +++ /dev/null @@ -1,20 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Keivan"> - <words> - <w>deps</w> - <w>mixins</w> - <w>nzbdrone</w> - <w>rootdir</w> - <w>rootfolder</w> - <w>rootfolders</w> - <w>signalr</w> - <w>lidarr</w> - <w>templated</w> - <w>thetvdb</w> - <w>trakt</w> - <w>tvdb</w> - <w>xlarge</w> - <w>yyyy</w> - </words> - </dictionary> -</component> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Keivan_Beigi.xml b/src/UI/.idea/dictionaries/Keivan_Beigi.xml deleted file mode 100644 index 00d8e4cec..000000000 --- a/src/UI/.idea/dictionaries/Keivan_Beigi.xml +++ /dev/null @@ -1,13 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Keivan.Beigi"> - <words> - <w>backgrid</w> - <w>bnzbd</w> - <w>clickable</w> - <w>couldn</w> - <w>mouseenter</w> - <w>mouseleave</w> - <w>navbar</w> - </words> - </dictionary> -</component> \ No newline at end of file diff --git a/src/UI/.idea/dictionaries/Mark.xml b/src/UI/.idea/dictionaries/Mark.xml deleted file mode 100644 index ecbbe884c..000000000 --- a/src/UI/.idea/dictionaries/Mark.xml +++ /dev/null @@ -1,3 +0,0 @@ -<component name="ProjectDictionaryState"> - <dictionary name="Mark" /> -</component> \ No newline at end of file diff --git a/src/UI/.idea/encodings.xml b/src/UI/.idea/encodings.xml deleted file mode 100644 index e55d06786..000000000 --- a/src/UI/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false"> - <file url="file://$PROJECT_DIR$/System/Logs/Files/LogFileModel.js" charset="UTF-8" /> - <file url="PROJECT" charset="UTF-8" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/inspectionProfiles/Project_Default.xml b/src/UI/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 7aba4e3c2..000000000 --- a/src/UI/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,117 +0,0 @@ -<component name="InspectionProjectProfileManager"> - <profile version="1.0" is_locked="false"> - <option name="myName" value="Project Default" /> - <option name="myLocal" value="false" /> - <inspection_tool class="AssignmentResultUsedJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="AssignmentToForLoopParameterJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="AssignmentToFunctionParameterJS" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="BadExpressionStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="BreakStatementJS" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" /> - <inspection_tool class="BreakStatementWithLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ChainedEqualityJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="CheckEmptyScriptTag" enabled="false" level="WARNING" enabled_by_default="false" /> - <inspection_tool class="ConditionalExpressionWithIdenticalBranchesJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ConstantIfStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ConstantOnLHSOfComparisonJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ContinueStatementWithLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="CssMissingSemicolonInspection" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="CyclomaticComplexityJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="10" /> - </inspection_tool> - <inspection_tool class="DefaultNotLastCaseInSwitchJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="DuplicateCaseLabelJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="DuplicateConditionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="DynamicallyGeneratedCodeJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="EmptyTryBlockJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ExceptionCaughtLocallyJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ForLoopReplaceableByWhileJS" enabled="true" level="INFO" enabled_by_default="true"> - <option name="m_ignoreLoopsWithoutConditions" value="false" /> - </inspection_tool> - <inspection_tool class="FunctionNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="4" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="FunctionWithInconsistentReturnsJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="HtmlFormInputWithoutLabel" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="HtmlPresentationalElement" enabled="false" level="WEAK WARNING" enabled_by_default="false" /> - <inspection_tool class="HtmlUnknownAttribute" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myValues"> - <value> - <list size="2"> - <item index="0" class="java.lang.String" itemvalue="name" /> - <item index="1" class="java.lang.String" itemvalue="validation-name" /> - </list> - </value> - </option> - <option name="myCustomValuesEnabled" value="true" /> - </inspection_tool> - <inspection_tool class="HtmlUnknownTag" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myValues"> - <value> - <list size="8"> - <item index="0" class="java.lang.String" itemvalue="nobr" /> - <item index="1" class="java.lang.String" itemvalue="noembed" /> - <item index="2" class="java.lang.String" itemvalue="comment" /> - <item index="3" class="java.lang.String" itemvalue="noscript" /> - <item index="4" class="java.lang.String" itemvalue="embed" /> - <item index="5" class="java.lang.String" itemvalue="script" /> - <item index="6" class="java.lang.String" itemvalue="icon" /> - <item index="7" class="java.lang.String" itemvalue="p" /> - </list> - </value> - </option> - <option name="myCustomValuesEnabled" value="true" /> - </inspection_tool> - <inspection_tool class="IfStatementWithIdenticalBranchesJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="IfStatementWithTooManyBranchesJS" enabled="true" level="ERROR" enabled_by_default="true"> - <option name="m_limit" value="3" /> - </inspection_tool> - <inspection_tool class="JSDuplicatedDeclaration" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSHint" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSLastCommaInArrayLiteral" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSPotentiallyInvalidUsageOfThis" enabled="true" level="SERVER PROBLEM" enabled_by_default="true" /> - <inspection_tool class="JSUndeclaredVariable" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSUnnecessarySemicolon" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="JSUnresolvedFunction" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="JSUnresolvedVariable" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="JSUnusedGlobalSymbols" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="myReportUnusedDefinitions" value="true" /> - <option name="myReportUnusedProperties" value="true" /> - </inspection_tool> - <inspection_tool class="LabeledStatementJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="LocalVariableNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="1" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="NegatedIfStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestedAssignmentJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestedFunctionCallJS" enabled="false" level="ERROR" enabled_by_default="false" /> - <inspection_tool class="NestedSwitchStatementJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="NestingDepthJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="5" /> - </inspection_tool> - <inspection_tool class="NonBlockStatementBodyJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ParameterNamingConventionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_regex" value="[a-z][A-Za-z]*" /> - <option name="m_minLength" value="1" /> - <option name="m_maxLength" value="32" /> - </inspection_tool> - <inspection_tool class="ParametersPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="4" /> - </inspection_tool> - <inspection_tool class="ReservedWordUsedAsNameJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ReuseOfLocalVariableJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="StatementsPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true"> - <option name="m_limit" value="30" /> - </inspection_tool> - <inspection_tool class="SwitchStatementWithNoDefaultBranchJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="TailRecursionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="ThisExpressionReferencesGlobalObjectJS" enabled="true" level="ERROR" enabled_by_default="true" /> - <inspection_tool class="ThreeNegationsPerFunctionJS" enabled="true" level="WARNING" enabled_by_default="true" /> - <inspection_tool class="UnterminatedStatementJS" enabled="true" level="ERROR" enabled_by_default="true"> - <option name="ignoreSemicolonAtEndOfBlock" value="true" /> - </inspection_tool> - </profile> -</component> \ No newline at end of file diff --git a/src/UI/.idea/inspectionProfiles/profiles_settings.xml b/src/UI/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b312839b..000000000 --- a/src/UI/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ -<component name="InspectionProjectProfileManager"> - <settings> - <option name="PROJECT_PROFILE" value="Project Default" /> - <option name="USE_PROJECT_PROFILE" value="true" /> - <version value="1.0" /> - </settings> -</component> \ No newline at end of file diff --git a/src/UI/.idea/jsLibraryMappings.xml b/src/UI/.idea/jsLibraryMappings.xml deleted file mode 100644 index 62c621f94..000000000 --- a/src/UI/.idea/jsLibraryMappings.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JavaScriptLibraryMappings"> - <excludedPredefinedLibrary name="HTML" /> - <excludedPredefinedLibrary name="HTML5 / EcmaScript 5" /> - </component> -</project> - diff --git a/src/UI/.idea/jsLinters/jshint.xml b/src/UI/.idea/jsLinters/jshint.xml deleted file mode 100644 index 0b5c0e41e..000000000 --- a/src/UI/.idea/jsLinters/jshint.xml +++ /dev/null @@ -1,72 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JSHintConfiguration" version="2.6.0" use-config-file="false"> - <option asi="false" /> - <option bitwise="true" /> - <option boss="false" /> - <option browser="true" /> - <option camelcase="true" /> - <option couch="false" /> - <option curly="true" /> - <option debug="false" /> - <option devel="true" /> - <option dojo="false" /> - <option eqeqeq="true" /> - <option eqnull="false" /> - <option es3="false" /> - <option esnext="false" /> - <option evil="false" /> - <option expr="false" /> - <option forin="true" /> - <option freeze="false" /> - <option funcscope="false" /> - <option gcl="false" /> - <option globalstrict="true" /> - <option immed="true" /> - <option iterator="false" /> - <option jquery="false" /> - <option lastsemic="false" /> - <option latedef="true" /> - <option laxbreak="false" /> - <option laxcomma="false" /> - <option loopfunc="false" /> - <option maxdepth="3" /> - <option maxerr="50" /> - <option mootools="false" /> - <option moz="false" /> - <option multistr="false" /> - <option newcap="true" /> - <option noarg="true" /> - <option node="true" /> - <option noempty="false" /> - <option nomen="false" /> - <option nonbsp="false" /> - <option nonew="true" /> - <option nonstandard="false" /> - <option notypeof="false" /> - <option noyield="false" /> - <option onevar="false" /> - <option passfail="false" /> - <option phantom="false" /> - <option plusplus="false" /> - <option predef="window, define, require, module" /> - <option proto="false" /> - <option prototypejs="false" /> - <option quotmark="single" /> - <option rhino="false" /> - <option scripturl="false" /> - <option shadow="false" /> - <option smarttabs="false" /> - <option strict="false" /> - <option sub="false" /> - <option supernew="false" /> - <option trailing="false" /> - <option undef="true" /> - <option unused="true" /> - <option validthis="false" /> - <option white="false" /> - <option worker="false" /> - <option wsh="false" /> - <option yui="false" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/jsLinters/jslint.xml b/src/UI/.idea/jsLinters/jslint.xml deleted file mode 100644 index 822a7aa5e..000000000 --- a/src/UI/.idea/jsLinters/jslint.xml +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="JSLintConfiguration" html="true" json="true"> - <option browser="true" /> - <option devel="true" /> - <option indent="4" /> - <option maxerr="50" /> - <option plusplus="true" /> - <option todo="true" /> - <option white="true" /> - </component> -</project> - diff --git a/src/UI/.idea/misc.xml b/src/UI/.idea/misc.xml deleted file mode 100644 index e9e9ba1c3..000000000 --- a/src/UI/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectKey"> - <option name="state" value="git@github.com:NzbDrone/NzbDrone.git" /> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/modules.xml b/src/UI/.idea/modules.xml deleted file mode 100644 index ab774833e..000000000 --- a/src/UI/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="ProjectModuleManager"> - <modules> - <module fileurl="file://$PROJECT_DIR$/.idea/NzbDrone.UI.iml" filepath="$PROJECT_DIR$/.idea/NzbDrone.UI.iml" /> - </modules> - </component> -</project> \ No newline at end of file diff --git a/src/UI/.idea/runConfigurations/Debug___Chrome.xml b/src/UI/.idea/runConfigurations/Debug___Chrome.xml deleted file mode 100644 index 2ff8dbf6b..000000000 --- a/src/UI/.idea/runConfigurations/Debug___Chrome.xml +++ /dev/null @@ -1,23 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="Debug - Chrome" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" uri="http://localhost:8686"> - <mapping url="http://localhost:8686/Calendar" local-file="$PROJECT_DIR$/Calendar" /> - <mapping url="http://localhost:8686/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> - <mapping url="http://localhost:8686/Settings" local-file="$PROJECT_DIR$/Settings" /> - <mapping url="http://localhost:8686/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> - <mapping url="http://localhost:8686/app.js" local-file="$PROJECT_DIR$/app.js" /> - <mapping url="http://localhost:8686/Mixins" local-file="$PROJECT_DIR$/Mixins" /> - <mapping url="http://localhost:8686/Wanted" local-file="$PROJECT_DIR$/Wanted" /> - <mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" /> - <mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" /> - <mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" /> - <mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" /> - <mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> - <mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" /> - <mapping url="http://localhost:8686/Routing.js" local-file="$PROJECT_DIR$/Routing.js" /> - <mapping url="http://localhost:8686/Controller.js" local-file="$PROJECT_DIR$/Controller.js" /> - <mapping url="http://localhost:8686/Series" local-file="$PROJECT_DIR$/Series" /> - <RunnerSettings RunnerId="JavascriptDebugRunner" /> - <ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> - <method /> - </configuration> -</component> \ No newline at end of file diff --git a/src/UI/.idea/runConfigurations/Debug___Firefox.xml b/src/UI/.idea/runConfigurations/Debug___Firefox.xml deleted file mode 100644 index dbbdebbe4..000000000 --- a/src/UI/.idea/runConfigurations/Debug___Firefox.xml +++ /dev/null @@ -1,23 +0,0 @@ -<component name="ProjectRunConfigurationManager"> - <configuration default="false" name="Debug - Firefox" type="JavascriptDebugType" factoryName="JavaScript Debug" singleton="true" engineId="firefox" uri="http://localhost:8686"> - <mapping url="http://localhost:8686/Calendar" local-file="$PROJECT_DIR$/Calendar" /> - <mapping url="http://localhost:8686/MainMenuView.js" local-file="$PROJECT_DIR$/MainMenuView.js" /> - <mapping url="http://localhost:8686/Settings" local-file="$PROJECT_DIR$/Settings" /> - <mapping url="http://localhost:8686/Upcoming" local-file="$PROJECT_DIR$/Upcoming" /> - <mapping url="http://localhost:8686/app.js" local-file="$PROJECT_DIR$/app.js" /> - <mapping url="http://localhost:8686/Mixins" local-file="$PROJECT_DIR$/Mixins" /> - <mapping url="http://localhost:8686/Wanted" local-file="$PROJECT_DIR$/Wanted" /> - <mapping url="http://localhost:8686/Config.js" local-file="$PROJECT_DIR$/Config.js" /> - <mapping url="http://localhost:8686/Quality" local-file="$PROJECT_DIR$/Quality" /> - <mapping url="http://localhost:8686/AddArtist" local-file="$PROJECT_DIR$/AddArtist" /> - <mapping url="http://localhost:8686/Shared" local-file="$PROJECT_DIR$/Shared" /> - <mapping url="http://localhost:8686/HeaderView.js" local-file="$PROJECT_DIR$/HeaderView.js" /> - <mapping url="http://localhost:8686" local-file="$PROJECT_DIR$" /> - <mapping url="http://localhost:8686/Routing.js" local-file="$PROJECT_DIR$/Routing.js" /> - <mapping url="http://localhost:8686/Controller.js" local-file="$PROJECT_DIR$/Controller.js" /> - <mapping url="http://localhost:8686/Series" local-file="$PROJECT_DIR$/Series" /> - <RunnerSettings RunnerId="JavascriptDebugRunner" /> - <ConfigurationWrapper RunnerId="JavascriptDebugRunner" /> - <method /> - </configuration> -</component> \ No newline at end of file diff --git a/src/UI/.idea/scopes/NzbDrone.xml b/src/UI/.idea/scopes/NzbDrone.xml deleted file mode 100644 index 17c1c9c5e..000000000 --- a/src/UI/.idea/scopes/NzbDrone.xml +++ /dev/null @@ -1,3 +0,0 @@ -<component name="DependencyValidationManager"> - <scope name="NzbDrone" pattern="!file:JsLibraries//*" /> -</component> \ No newline at end of file diff --git a/src/UI/.idea/scopes/scope_settings.xml b/src/UI/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b84..000000000 --- a/src/UI/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ -<component name="DependencyValidationManager"> - <state> - <option name="SKIP_IMPORT_STATEMENTS" value="false" /> - </state> -</component> \ No newline at end of file diff --git a/src/UI/.idea/vcs.xml b/src/UI/.idea/vcs.xml deleted file mode 100644 index 9ab281ac8..000000000 --- a/src/UI/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project version="4"> - <component name="VcsDirectoryMappings"> - <mapping directory="$PROJECT_DIR$/../.." vcs="Git" /> - </component> -</project> - diff --git a/src/UI/.jshintrc b/src/UI/.jshintrc deleted file mode 100644 index 888afe448..000000000 --- a/src/UI/.jshintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "-W030": false, - "-W064": false, - "-W097": false, - "-W100": false, - "undef": true, - "curly": true, - "immed": true, - "eqeqeq": true, - "latedef": true, - "globals": { - "module": true, - "require": true, - "define": true, - "window": true, - "document": true, - "console": true - } -} diff --git a/src/UI/Activity/ActivityLayout.js b/src/UI/Activity/ActivityLayout.js deleted file mode 100644 index a8826a714..000000000 --- a/src/UI/Activity/ActivityLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var Backgrid = require('backgrid'); -var HistoryLayout = require('./History/HistoryLayout'); -var BlacklistLayout = require('./Blacklist/BlacklistLayout'); -var QueueLayout = require('./Queue/QueueLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/ActivityLayoutTemplate', - - regions : { - queueRegion : '#queue', - history : '#history', - blacklist : '#blacklist' - }, - - ui : { - queueTab : '.x-queue-tab', - historyTab : '.x-history-tab', - blacklistTab : '.x-blacklist-tab' - }, - - events : { - 'click .x-queue-tab' : '_showQueue', - 'click .x-history-tab' : '_showHistory', - 'click .x-blacklist-tab' : '_showBlacklist' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - }, - - onShow : function() { - switch (this.action) { - case 'history': - this._showHistory(); - break; - case 'blacklist': - this._showBlacklist(); - break; - default: - this._showQueue(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.history.show(new HistoryLayout()); - this.ui.historyTab.tab('show'); - this._navigate('/activity/history'); - }, - - _showBlacklist : function(e) { - if (e) { - e.preventDefault(); - } - - this.blacklist.show(new BlacklistLayout()); - this.ui.blacklistTab.tab('show'); - this._navigate('/activity/blacklist'); - }, - - _showQueue : function(e) { - if (e) { - e.preventDefault(); - } - - this.queueRegion.show(new QueueLayout()); - this.ui.queueTab.tab('show'); - this._navigate('/activity/queue'); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/ActivityLayoutTemplate.hbs b/src/UI/Activity/ActivityLayoutTemplate.hbs deleted file mode 100644 index c9c08ecf7..000000000 --- a/src/UI/Activity/ActivityLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#queue" class="x-queue-tab no-router">Queue</a></li> - <li><a href="#history" class="x-history-tab no-router">History</a></li> - <li><a href="#blacklist" class="x-blacklist-tab no-router">Blacklist</a></li> -</ul> - -<div class="tab-content"> - <div class="tab-pane" id="queue"></div> - <div class="tab-pane" id="history"></div> - <div class="tab-pane" id="blacklist"></div> -</div> \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/BlacklistActionsCell.js b/src/UI/Activity/Blacklist/BlacklistActionsCell.js deleted file mode 100644 index ed013db1d..000000000 --- a/src/UI/Activity/Blacklist/BlacklistActionsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var BlacklistDetailsLayout = require('./Details/BlacklistDetailsLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'blacklist-actions-cell', - - events : { - 'click .x-details' : '_details', - 'click .x-delete' : '_delete' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-info x-details"></i>' + - '<i class="icon-lidarr-delete x-delete"></i>'); - - return this; - }, - - _details : function() { - vent.trigger(vent.Commands.OpenModalCommand, new BlacklistDetailsLayout({ model : this.model })); - }, - - _delete : function() { - this.model.destroy(); - } -}); diff --git a/src/UI/Activity/Blacklist/BlacklistCollection.js b/src/UI/Activity/Blacklist/BlacklistCollection.js deleted file mode 100644 index 86b177065..000000000 --- a/src/UI/Activity/Blacklist/BlacklistCollection.js +++ /dev/null @@ -1,47 +0,0 @@ -var BlacklistModel = require('./BlacklistModel'); -var PageableCollection = require('backbone.pageable'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/blacklist', - model : BlacklistModel, - - state : { - pageSize : 15, - sortKey : 'date', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/BlacklistLayout.js b/src/UI/Activity/Blacklist/BlacklistLayout.js deleted file mode 100644 index 1be8fed68..000000000 --- a/src/UI/Activity/Blacklist/BlacklistLayout.js +++ /dev/null @@ -1,114 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BlacklistCollection = require('./BlacklistCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var BlacklistActionsCell = require('./BlacklistActionsCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var LoadingView = require('../../Shared/LoadingView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Blacklist/BlacklistLayoutTemplate', - - regions : { - blacklist : '#x-blacklist', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - columns : [ - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : BlacklistActionsCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new BlacklistCollection({ tableName : 'blacklist' }); - - this.listenTo(this.collection, 'sync', this._showTable); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onShow : function() { - this.blacklist.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function(collection) { - - this.blacklist.show(new Backgrid.Grid({ - columns : this.columns, - collection : collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Clear Blacklist', - icon : 'icon-lidarr-clear', - command : 'clearBlacklist' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [ - leftSideButtons - ], - context : this - })); - }, - - _refreshTable : function(buttonContext) { - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'clearblacklist') { - this._refreshTable(); - } - } -}); diff --git a/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs b/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs deleted file mode 100644 index 8f78eb0db..000000000 --- a/src/UI/Activity/Blacklist/BlacklistLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-blacklist" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Activity/Blacklist/BlacklistModel.js b/src/UI/Activity/Blacklist/BlacklistModel.js deleted file mode 100644 index e3c4b5ab0..000000000 --- a/src/UI/Activity/Blacklist/BlacklistModel.js +++ /dev/null @@ -1,17 +0,0 @@ -var Backbone = require('backbone'); -var ArtistCollection = require('../../Artist/ArtistCollection'); - -module.exports = Backbone.Model.extend({ - - //Hack to deal with Backbone 1.0's bug - initialize : function() { - this.url = function() { - return this.collection.url + '/' + this.get('id'); - }; - }, - - parse : function(model) { - model.artist = ArtistCollection.get(model.artistId); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js deleted file mode 100644 index cdcbf25f0..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayout.js +++ /dev/null @@ -1,14 +0,0 @@ -var Marionette = require('marionette'); -var BlacklistDetailsView = require('./BlacklistDetailsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate', - - regions : { - bodyRegion : '.modal-body' - }, - - onShow : function() { - this.bodyRegion.show(new BlacklistDetailsView({ model : this.model })); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs deleted file mode 100644 index 3cdfa99c7..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="modal-content"> - <div class="history-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Blacklisted - </h3> - - </div> - <div class="modal-body"> - - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js b/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js deleted file mode 100644 index 1b7bc883d..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/Blacklist/Details/BlacklistDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs b/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs deleted file mode 100644 index d29a878fc..000000000 --- a/src/UI/Activity/Blacklist/Details/BlacklistDetailsViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<dl class="dl-horizontal info"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#if protocol}} - {{#unless_eq protocol compare="unknown"}} - <dt>Protocol:</dt> - <dd>{{protocol}}</dd> - {{/unless_eq}} - {{/if}} - - {{#if indexer}} - <dt>Indexer:</dt> - <dd>{{indexer}}</dd> - {{/if}} - - - {{#if message}} - <dt>Message:</dt> - <dd>{{message}}</dd> - {{/if}} -</dl> diff --git a/src/UI/Activity/History/Details/HistoryDetailsAge.js b/src/UI/Activity/History/Details/HistoryDetailsAge.js deleted file mode 100644 index a7c40f69a..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsAge.js +++ /dev/null @@ -1,22 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); - -Handlebars.registerHelper('historyAge', function() { - - var age = this.age; - var unit = FormatHelpers.plural(Math.round(age), 'day'); - var ageHours = parseFloat(this.ageHours); - var ageMinutes = this.ageMinutes ? parseFloat(this.ageMinutes) : null; - - if (age < 2) { - age = ageHours.toFixed(1); - unit = FormatHelpers.plural(Math.round(ageHours), 'hour'); - } - - if (age < 2 && ageMinutes) { - age = parseFloat(ageMinutes).toFixed(1); - unit = FormatHelpers.plural(Math.round(ageMinutes), 'minute'); - } - - return new Handlebars.SafeString('<dt>Age (when grabbed):</dt><dd>{0} {1}</dd>'.format(age, unit)); -}); diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayout.js b/src/UI/Activity/History/Details/HistoryDetailsLayout.js deleted file mode 100644 index 5654a3e72..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsLayout.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var HistoryDetailsView = require('./HistoryDetailsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/History/Details/HistoryDetailsLayoutTemplate', - - regions : { - bodyRegion : '.modal-body' - }, - - events : { - 'click .x-mark-as-failed' : '_markAsFailed' - }, - - onShow : function() { - this.bodyRegion.show(new HistoryDetailsView({ model : this.model })); - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs b/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs deleted file mode 100644 index e24b3b861..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,22 +0,0 @@ -<div class="modal-content"> - <div class="history-detail-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - {{#if_eq eventType compare="grabbed"}}Grabbed{{/if_eq}} - {{#if_eq eventType compare="downloadFailed"}}Download Failed{{/if_eq}} - {{#if_eq eventType compare="downloadFolderImported"}}Album Imported{{/if_eq}} - {{#if_eq eventType compare="episodeFileDeleted"}}Album Files Deleted{{/if_eq}} - </h3> - - </div> - <div class="modal-body"> - - </div> - <div class="modal-footer"> - {{#if_eq eventType compare="grabbed"}}<button class="btn btn-danger x-mark-as-failed">Mark As Failed</button>{{/if_eq}} - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Activity/History/Details/HistoryDetailsView.js b/src/UI/Activity/History/Details/HistoryDetailsView.js deleted file mode 100644 index a883b0cb4..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); -require('./HistoryDetailsAge'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/History/Details/HistoryDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs b/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs deleted file mode 100644 index 147504fc0..000000000 --- a/src/UI/Activity/History/Details/HistoryDetailsViewTemplate.hbs +++ /dev/null @@ -1,103 +0,0 @@ -{{#if_eq eventType compare="grabbed"}} -<dl class="dl-horizontal info"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - {{#if indexer}} - <dt>Indexer:</dt> - <dd>{{indexer}}</dd> - {{/if}} - - {{#if releaseGroup}} - <dt>Release Group:</dt> - <dd>{{releaseGroup}}</dd> - {{/if}} - - {{#if nzbInfoUrl}} - <dt>Info:</dt> - <dd><a href="{{nzbInfoUrl}}">{{nzbInfoUrl}}</a></dd> - {{/if}} - - {{#if downloadClient}} - <dt>Download Client:</dt> - <dd>{{downloadClient}}</dd> - {{/if}} - - {{#if downloadId}} - <dt>Grab ID:</dt> - <dd>{{downloadId}}</dd> - {{/if}} - - {{#if age}} - {{historyAge}} - {{/if}} - - {{#if publishedDate}} - <dt>Published Date:</dt> - <dd>{{ShortDate publishedDate}} {{LTS publishedDate}}</dd> - {{/if}} - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="downloadFailed"}} -<dl class="dl-horizontal"> - - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - <dt>Message:</dt> - <dd>{{message}}</dd> - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="downloadFolderImported"}} -<dl class="dl-horizontal"> - - {{#if sourceTitle}} - <dt>Name:</dt> - <dd>{{sourceTitle}}</dd> - {{/if}} - - {{#with data}} - {{#if droppedPath}} - <dt>Source:</dt> - <dd>{{droppedPath}}</dd> - {{/if}} - - {{#if importedPath}} - <dt>Imported To:</dt> - <dd>{{importedPath}}</dd> - {{/if}} - {{/with}} -</dl> -{{/if_eq}} - -{{#if_eq eventType compare="episodeFileDeleted"}} -<dl class="dl-horizontal"> - - <dt>Path:</dt> - <dd>{{sourceTitle}}</dd> - - {{#with data}} - <dt>Reason:</dt> - <dd> - {{#if_eq reason compare="Manual"}} - File was deleted by via UI - {{/if_eq}} - - {{#if_eq reason compare="MissingFromDisk"}} - Lidarr was unable to find the file on disk so it was removed - {{/if_eq}} - - {{#if_eq reason compare="Upgrade"}} - File was deleted to import an upgrade - {{/if_eq}} - </dd> - {{/with}} -</dl> -{{/if_eq}} \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryCollection.js b/src/UI/Activity/History/HistoryCollection.js deleted file mode 100644 index 93ae84ea4..000000000 --- a/src/UI/Activity/History/HistoryCollection.js +++ /dev/null @@ -1,84 +0,0 @@ -var HistoryModel = require('./HistoryModel'); -var PageableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/history', - model : HistoryModel, - - state : { - pageSize : 15, - sortKey : 'date', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'grabbed' : [ - 'eventType', - '1' - ], - 'imported' : [ - 'eventType', - '3' - ], - 'failed' : [ - 'eventType', - '4' - ], - 'deleted' : [ - 'eventType', - '5' - ] - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' }, - 'album' : { sortKey : 'album.title' } - }, - - initialize : function(options) { - delete this.queryParams.albumId; - - if (options) { - if (options.albumId) { - this.queryParams.albumId = options.albumId; - } - } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryDetailsCell.js b/src/UI/Activity/History/HistoryDetailsCell.js deleted file mode 100644 index 5c8a33b09..000000000 --- a/src/UI/Activity/History/HistoryDetailsCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-details-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-info"></i>'); - - return this; - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowHistoryDetails, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryLayout.js b/src/UI/Activity/History/HistoryLayout.js deleted file mode 100644 index 3223885f7..000000000 --- a/src/UI/Activity/History/HistoryLayout.js +++ /dev/null @@ -1,146 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('./HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var HistoryQualityCell = require('./HistoryQualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var HistoryDetailsCell = require('./HistoryDetailsCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/History/HistoryLayoutTemplate', - - regions : { - history : '#x-history', - toolbar : '#x-history-toolbar', - pager : '#x-history-pager' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'album', - label : 'Album Title', - cell : AlbumTitleCell - }, - { - name : 'this', - label : 'Quality', - cell : HistoryQualityCell, - sortable : false - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : HistoryDetailsCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new HistoryCollection({ tableName : 'history' }); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.history.show(new LoadingView()); - this._showToolbar(); - }, - - _showTable : function(collection) { - - this.history.show(new Backgrid.Grid({ - columns : this.columns, - collection : collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : collection - })); - }, - - _showToolbar : function() { - var filterOptions = { - type : 'radio', - storeState : true, - menuKey : 'history.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'grabbed', - title : '', - tooltip : 'Grabbed', - icon : 'icon-lidarr-downloading', - callback : this._setFilter - }, - { - key : 'imported', - title : '', - tooltip : 'Imported', - icon : 'icon-lidarr-imported', - callback : this._setFilter - }, - { - key : 'failed', - title : '', - tooltip : 'Failed', - icon : 'icon-lidarr-download-failed', - callback : this._setFilter - }, - { - key : 'deleted', - title : '', - tooltip : 'Deleted', - icon : 'icon-lidarr-deleted', - callback : this._setFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - right : [ - filterOptions - ], - context : this - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - } -}); diff --git a/src/UI/Activity/History/HistoryLayoutTemplate.hbs b/src/UI/Activity/History/HistoryLayoutTemplate.hbs deleted file mode 100644 index bffb274fe..000000000 --- a/src/UI/Activity/History/HistoryLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-history-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-history" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-history-pager"/> - </div> -</div> diff --git a/src/UI/Activity/History/HistoryModel.js b/src/UI/Activity/History/HistoryModel.js deleted file mode 100644 index e37fbedd5..000000000 --- a/src/UI/Activity/History/HistoryModel.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AlbumModel = require('../../Artist/AlbumModel'); - -module.exports = Backbone.Model.extend({ - parse : function(model) { - model.artist = new ArtistModel(model.artist); - model.album = new AlbumModel(model.album); - model.album.set('artist', model.artist); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/History/HistoryQualityCell.js b/src/UI/Activity/History/HistoryQualityCell.js deleted file mode 100644 index c65aa042b..000000000 --- a/src/UI/Activity/History/HistoryQualityCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-quality-cell', - - render : function() { - - var title = ''; - var quality = this.model.get('quality'); - var revision = quality.revision; - - if (revision.real && revision.real > 0) { - title += ' REAL'; - } - - if (revision.version && revision.version > 1) { - title += ' PROPER'; - } - - title = title.trim(); - - if (this.model.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/ProgressCell.js b/src/UI/Activity/Queue/ProgressCell.js deleted file mode 100644 index 1f69bf017..000000000 --- a/src/UI/Activity/Queue/ProgressCell.js +++ /dev/null @@ -1,23 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'progress-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - - var status = this.model.get('status').toLowerCase(); - - if (status === 'downloading') { - var progress = 100 - (this.model.get('sizeleft') / this.model.get('size') * 100); - - this.$el.html('<div class="progress" title="{0}%">'.format(progress.toFixed(1)) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - } - } - - return this; - } -}); diff --git a/src/UI/Activity/Queue/QueueActionsCell.js b/src/UI/Activity/Queue/QueueActionsCell.js deleted file mode 100644 index 9653a71bd..000000000 --- a/src/UI/Activity/Queue/QueueActionsCell.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('../../vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var RemoveFromQueueView = require('./RemoveFromQueueView'); - -module.exports = TemplatedCell.extend({ - - template : 'Activity/Queue/QueueActionsCellTemplate', - className : 'queue-actions-cell', - - events : { - 'click .x-remove' : '_remove', - 'click .x-manual-import' : '_manualImport', - 'click .x-grab' : '_grab' - }, - - ui : { - import : '.x-import', - grab : '.x-grab' - }, - - _remove : function() { - var showBlacklist = this.model.get('status') !== 'Pending'; - - vent.trigger(vent.Commands.OpenModalCommand, new RemoveFromQueueView({ - model : this.model, - showBlacklist : showBlacklist - })); - }, - - _manualImport : function () { - vent.trigger(vent.Commands.ShowManualImport, - { - downloadId: this.model.get('downloadId'), - title: this.model.get('title') - }); - }, - - _grab : function() { - var self = this; - var data = _.omit(this.model.toJSON(), 'artist', 'album'); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/queue/grab', - type : 'POST', - data : JSON.stringify(data) - }); - - this.$(this.ui.grab).spinForPromise(promise); - - promise.success(function() { - //find models that have the same series id and episode ids and remove them - self.model.trigger('destroy', self.model); - }); - } -}); diff --git a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs b/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs deleted file mode 100644 index 0f2301929..000000000 --- a/src/UI/Activity/Queue/QueueActionsCellTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if_eq status compare="Completed"}} - {{#if_eq trackedDownloadStatus compare="Warning"}} - <i class="icon-lidarr-import-manual x-manual-import" title="Manual import"></i> - {{/if_eq}} -{{/if_eq}} - -{{#if_eq status compare="Pending"}} - <i class="icon-lidarr-download x-grab" title="Add to download queue (Override Delay Profile)"></i> - <i class="icon-lidarr-delete x-remove" title="Remove pending release"></i> -{{else}} - <i class="icon-lidarr-delete x-remove" title="Remove from download client"></i> -{{/if_eq}} diff --git a/src/UI/Activity/Queue/QueueCollection.js b/src/UI/Activity/Queue/QueueCollection.js deleted file mode 100644 index b0e47070a..000000000 --- a/src/UI/Activity/Queue/QueueCollection.js +++ /dev/null @@ -1,79 +0,0 @@ -var _ = require('underscore'); -var PageableCollection = require('backbone.pageable'); -//var PageableCollection = require('../../Shared/Grid/LidarrPageableCollection'); -var QueueModel = require('./QueueModel'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPageableCollection = require('../../Mixins/AsPageableCollection'); -var moment = require('moment'); - -require('../../Mixins/backbone.signalr.mixin'); - -var QueueCollection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/queue', - model : QueueModel, - - state : { - pageSize : 15, - sortKey: 'timeleft' - }, - - mode : 'client', - - findEpisode : function(albumId) { - return _.find(this.fullCollection.models, function(queueModel) { - return queueModel.get('album').id === albumId; - }); - }, - - sortMappings : { - artist : { - sortValue : function(model, attr) { - var artist = model.get(attr); - - return artist.get('sortName'); - } - }, - - albumTitle : { - sortValue : function(model, attr) { - var album = model.get('album'); - - return album.get('title'); - } - }, - - timeleft : { - sortValue : function(model, attr) { - var eta = model.get('estimatedCompletionTime'); - - if (eta) { - return moment(eta).unix(); - } - - return Number.MAX_VALUE; - } - }, - - sizeleft : { - sortValue : function(model, attr) { - var size = model.get('size'); - var sizeleft = model.get('sizeleft'); - - if (size && sizeleft) { - return sizeleft / size; - } - - return 0; - } - } - } -}); - -QueueCollection = AsSortedCollection.call(QueueCollection); -QueueCollection = AsPageableCollection.call(QueueCollection); - -var collection = new QueueCollection().bindSignalR(); -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueLayout.js b/src/UI/Activity/Queue/QueueLayout.js deleted file mode 100644 index 028bc40bc..000000000 --- a/src/UI/Activity/Queue/QueueLayout.js +++ /dev/null @@ -1,91 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var QueueCollection = require('./QueueCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var QualityCell = require('../../Cells/QualityCell'); -var QueueStatusCell = require('./QueueStatusCell'); -var QueueActionsCell = require('./QueueActionsCell'); -var TimeleftCell = require('./TimeleftCell'); -var ProgressCell = require('./ProgressCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var GridPager = require('../../Shared/Grid/Pager'); - -module.exports = Marionette.Layout.extend({ - template : 'Activity/Queue/QueueLayoutTemplate', - - regions : { - table : '#x-queue', - pager : '#x-queue-pager' - }, - - columns : [ - { - name : 'status', - label : '', - cell : QueueStatusCell, - cellValue : 'this' - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell - }, - { - name : 'albumTitle', - label : 'Album Title', - cell : AlbumTitleCell, - cellValue : 'album' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false - }, - { - name : 'protocol', - label : 'Protocol', - cell : ProtocolCell - }, - { - name : 'timeleft', - label : 'Time Left', - cell : TimeleftCell, - cellValue : 'this' - }, - { - name : 'sizeleft', - label : 'Progress', - cell : ProgressCell, - cellValue : 'this' - }, - { - name : 'status', - label : '', - cell : QueueActionsCell, - cellValue : 'this' - } - ], - - initialize : function() { - this.listenTo(QueueCollection, 'sync', this._showTable); - }, - - onShow : function() { - this._showTable(); - }, - - _showTable : function() { - this.table.show(new Backgrid.Grid({ - columns : this.columns, - collection : QueueCollection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : QueueCollection - })); - } -}); diff --git a/src/UI/Activity/Queue/QueueLayoutTemplate.hbs b/src/UI/Activity/Queue/QueueLayoutTemplate.hbs deleted file mode 100644 index e8e6a3c12..000000000 --- a/src/UI/Activity/Queue/QueueLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-queue" class="queue table-responsive"/> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-queue-pager"/> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueModel.js b/src/UI/Activity/Queue/QueueModel.js deleted file mode 100644 index e37fbedd5..000000000 --- a/src/UI/Activity/Queue/QueueModel.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AlbumModel = require('../../Artist/AlbumModel'); - -module.exports = Backbone.Model.extend({ - parse : function(model) { - model.artist = new ArtistModel(model.artist); - model.album = new AlbumModel(model.album); - model.album.set('artist', model.artist); - return model; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueStatusCell.js b/src/UI/Activity/Queue/QueueStatusCell.js deleted file mode 100644 index a345cac3c..000000000 --- a/src/UI/Activity/Queue/QueueStatusCell.js +++ /dev/null @@ -1,81 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'queue-status-cell', - template : 'Activity/Queue/QueueStatusCellTemplate', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - var status = this.cellValue.get('status').toLowerCase(); - var trackedDownloadStatus = this.cellValue.has('trackedDownloadStatus') ? this.cellValue.get('trackedDownloadStatus').toLowerCase() : 'ok'; - var icon = 'icon-lidarr-downloading'; - var title = 'Downloading'; - var itemTitle = this.cellValue.get('title'); - var content = itemTitle; - - if (status === 'paused') { - icon = 'icon-lidarr-paused'; - title = 'Paused'; - } - - if (status === 'queued') { - icon = 'icon-lidarr-queued'; - title = 'Queued'; - } - - if (status === 'completed') { - icon = 'icon-lidarr-downloaded'; - title = 'Downloaded'; - } - - if (status === 'pending') { - icon = 'icon-lidarr-pending'; - title = 'Pending'; - } - - if (status === 'failed') { - icon = 'icon-lidarr-download-failed'; - title = 'Download failed'; - } - - if (status === 'warning') { - icon = 'icon-lidarr-download-warning'; - title = 'Download warning: check download client for more details'; - } - - if (trackedDownloadStatus === 'warning') { - icon += ' icon-lidarr-warning'; - - this.templateFunction = Marionette.TemplateCache.get(this.template); - content = this.templateFunction(this.cellValue.toJSON()); - } - - if (trackedDownloadStatus === 'error') { - if (status === 'completed') { - icon = 'icon-lidarr-import-failed'; - title = 'Import failed: ' + itemTitle; - } else { - icon = 'icon-lidarr-download-failed'; - title = 'Download failed'; - } - - this.templateFunction = Marionette.TemplateCache.get(this.template); - content = this.templateFunction(this.cellValue.toJSON()); - } - - this.$el.html('<i class="{0}"></i>'.format(icon)); - this.$el.popover({ - content : content, - html : true, - trigger : 'hover', - title : title, - placement : 'right', - container : this.$el - }); - } - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs b/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs deleted file mode 100644 index 477fdd028..000000000 --- a/src/UI/Activity/Queue/QueueStatusCellTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -{{#each statusMessages}} - {{title}} - <ul> - {{#each messages}} - <li>{{this}}</li> - {{/each}} - </ul> -{{/each}} \ No newline at end of file diff --git a/src/UI/Activity/Queue/QueueView.js b/src/UI/Activity/Queue/QueueView.js deleted file mode 100644 index ccddebbc9..000000000 --- a/src/UI/Activity/Queue/QueueView.js +++ /dev/null @@ -1,40 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var QueueCollection = require('./QueueCollection'); - -module.exports = Marionette.ItemView.extend({ - tagName : 'span', - - initialize : function() { - this.listenTo(QueueCollection, 'sync', this.render); - QueueCollection.fetch(); - }, - - render : function() { - this.$el.empty(); - - if (QueueCollection.length === 0) { - return this; - } - - var count = QueueCollection.fullCollection.length; - var label = 'label-info'; - - var errors = QueueCollection.fullCollection.some(function(model) { - return model.has('trackedDownloadStatus') && model.get('trackedDownloadStatus').toLowerCase() === 'error'; - }); - - var warnings = QueueCollection.fullCollection.some(function(model) { - return model.has('trackedDownloadStatus') && model.get('trackedDownloadStatus').toLowerCase() === 'warning'; - }); - - if (errors) { - label = 'label-danger'; - } else if (warnings) { - label = 'label-warning'; - } - - this.$el.html('<span class="label {0}">{1}</span>'.format(label, count)); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/Queue/RemoveFromQueueView.js b/src/UI/Activity/Queue/RemoveFromQueueView.js deleted file mode 100644 index 571738d7a..000000000 --- a/src/UI/Activity/Queue/RemoveFromQueueView.js +++ /dev/null @@ -1,34 +0,0 @@ -var vent = require('../../vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Activity/Queue/RemoveFromQueueViewTemplate', - - events : { - 'click .x-confirm-remove' : 'removeItem' - }, - - ui : { - blacklist : '.x-blacklist', - indicator : '.x-indicator' - }, - - initialize : function(options) { - this.templateHelpers = { - showBlacklist : options.showBlacklist - }; - }, - - removeItem : function() { - var blacklist = this.ui.blacklist.prop('checked') || false; - - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'blacklist' : blacklist }, - wait : true - }).done(function() { - vent.trigger(vent.Commands.CloseModalCommand); - }); - } -}); diff --git a/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs b/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs deleted file mode 100644 index b7853e2fa..000000000 --- a/src/UI/Activity/Queue/RemoveFromQueueViewTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{title}}</h3> - </div> - <div class="modal-body remove-from-queue-modal"> - - <div class="row"> - <div class="col-sm-12"> - Are you sure you want to remove '{{title}}'? - </div> - </div> - - {{#if showBlacklist}} - <div class="row"> - <div class="col-sm-12"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-4 control-label">Blacklist release</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-blacklist"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to blacklist this release?"/> - </span> - </div> - </div> - </div> - </div> - </div> - </div> - {{/if}} - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-remove">Remove</button> - </div> -</div> diff --git a/src/UI/Activity/Queue/TimeleftCell.js b/src/UI/Activity/Queue/TimeleftCell.js deleted file mode 100644 index 766d9df2d..000000000 --- a/src/UI/Activity/Queue/TimeleftCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); -var UiSettingsModel = require('../../Shared/UiSettingsModel'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'timeleft-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - if (this.cellValue.get('status').toLowerCase() === 'pending') { - var ect = this.cellValue.get('estimatedCompletionTime'); - var time = '{0} at {1}'.format(FormatHelpers.relativeDate(ect), moment(ect).format(UiSettingsModel.time(true, false))); - this.$el.html('<div title="Delaying download till {0}">-</div>'.format(time)); - return this; - } - - var timeleft = this.cellValue.get('timeleft'); - var totalSize = FormatHelpers.bytes(this.cellValue.get('size'), 2); - var remainingSize = FormatHelpers.bytes(this.cellValue.get('sizeleft'), 2); - - if (timeleft === undefined) { - this.$el.html('-'); - } else { - this.$el.html('<span title="{1} / {2}">{0}</span>'.format(timeleft, remainingSize, totalSize)); - } - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Activity/activity.less b/src/UI/Activity/activity.less deleted file mode 100644 index cb4c538cb..000000000 --- a/src/UI/Activity/activity.less +++ /dev/null @@ -1,27 +0,0 @@ - -.queue-status-cell .popover { - max-width : 800px; -} - -.queue { - .protocol-cell { - text-align : center; - width : 80px; - } - - .episode-number-cell { - min-width : 90px; - } -} - -.remove-from-queue-modal { - .form-horizontal { - margin-top : 20px; - } -} - -.history-detail-modal { - .info { - word-wrap: break-word; - } -} diff --git a/src/UI/AddArtist/AddArtistCollection.js b/src/UI/AddArtist/AddArtistCollection.js deleted file mode 100644 index a243649f4..000000000 --- a/src/UI/AddArtist/AddArtistCollection.js +++ /dev/null @@ -1,23 +0,0 @@ -var Backbone = require('backbone'); -var ArtistModel = require('../Artist/ArtistModel'); -var _ = require('underscore'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/artist/lookup', - model : ArtistModel, - - parse : function(response) { - var self = this; - - _.each(response, function(model) { - model.id = undefined; - - if (self.unmappedFolderModel) { - model.path = self.unmappedFolderModel.get('folder').path; - } - }); - console.log('response: ', response); - - return response; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistLayout.js b/src/UI/AddArtist/AddArtistLayout.js deleted file mode 100644 index da539c101..000000000 --- a/src/UI/AddArtist/AddArtistLayout.js +++ /dev/null @@ -1,66 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var RootFolderLayout = require('./RootFolders/RootFolderLayout'); -var ExistingArtistCollectionView = require('./Existing/AddExistingArtistCollectionView'); -var AddArtistView = require('./AddArtistView'); -var ProfileCollection = require('../Profile/ProfileCollection'); -var RootFolderCollection = require('./RootFolders/RootFolderCollection'); -var BulkImportView = require('./BulkImport/BulkImportView'); -require('../Artist/ArtistCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/AddArtistLayoutTemplate', - - regions : { - workspace : '#add-artist-workspace' - }, - - events : { - 'click .x-import' : '_importArtist', - 'click .x-bulk-import' : '_bulkImportArtist', - 'click .x-add-new' : '_addArtist' - }, - - attributes : { - id : 'add-artist-screen' - }, - - initialize : function() { - ProfileCollection.fetch(); - RootFolderCollection.fetch().done(function() { - RootFolderCollection.synced = true; - }); - }, - - onShow : function() { - this.workspace.show(new AddArtistView()); - }, - - _folderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - - this.workspace.show(new ExistingArtistCollectionView({ model : options.model })); - }, - - _bulkFolderSelected : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.workspace.show(new BulkImportView({ model : options.model})); - }, - - _importArtist : function() { - this.rootFolderLayout = new RootFolderLayout(); - this.listenTo(this.rootFolderLayout, 'folderSelected', this._folderSelected); - AppLayout.modalRegion.show(this.rootFolderLayout); - }, - - _addArtist : function() { - this.workspace.show(new AddArtistView()); - }, - - _bulkImportArtist : function() { - this.bulkRootFolderLayout = new RootFolderLayout(); - this.listenTo(this.bulkRootFolderLayout, 'folderSelected', this._bulkFolderSelected); - AppLayout.modalRegion.show(this.bulkRootFolderLayout); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs b/src/UI/AddArtist/AddArtistLayoutTemplate.hbs deleted file mode 100644 index 313cccef4..000000000 --- a/src/UI/AddArtist/AddArtistLayoutTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div class="btn-group add-artist-btn-group btn-group-lg btn-block"> - <button type="button" class="btn btn-default col-md-7 col-xs-4 add-artist-import-btn x-import"> - <i class="icon-lidarr-hdd"/> - Import existing artists on disk - </button> - <button class="btn btn-default col-md-3 col-xs-4 x-bulk-import"><i class="icon-lidarr-view-list hidden-xs"></i> Bulk Import Artists</button> - <button class="btn btn-default col-md-2 col-xs-4 x-add-new"><i class="icon-lidarr-active hidden-xs"></i> Add New Artist</button> - </div> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="add-artist-workspace"></div> - </div> -</div> - diff --git a/src/UI/AddArtist/AddArtistView.js b/src/UI/AddArtist/AddArtistView.js deleted file mode 100644 index 85e47a02d..000000000 --- a/src/UI/AddArtist/AddArtistView.js +++ /dev/null @@ -1,183 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var AddArtistCollection = require('./AddArtistCollection'); -var SearchResultCollectionView = require('./SearchResultCollectionView'); -var EmptyView = require('./EmptyView'); -var NotFoundView = require('./NotFoundView'); -var ErrorView = require('./ErrorView'); -var LoadingView = require('../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/AddArtistViewTemplate', - - regions : { - searchResult : '#search-result' - }, - - ui : { - artistSearch : '.x-artist-search', - searchBar : '.x-search-bar', - loadMore : '.x-load-more' - }, - - events : { - 'click .x-load-more' : '_onLoadMore' - }, - - initialize : function(options) { - this.isExisting = options.isExisting; - this.collection = new AddArtistCollection(); - console.log('this.collection:', this.collection); - - if (this.isExisting) { - this.collection.unmappedFolderModel = this.model; - } - - if (this.isExisting) { - this.className = 'existing-artist'; - } else { - this.className = 'new-artist'; - } - - this.listenTo(vent, vent.Events.ArtistAdded, this._onArtistAdded); - this.listenTo(this.collection, 'sync', this._showResults); - - this.resultCollectionView = new SearchResultCollectionView({ - collection : this.collection, - isExisting : this.isExisting - }); - - this.throttledSearch = _.debounce(this.search, 1000, { trailing : true }).bind(this); - }, - - onRender : function() { - var self = this; - - this.$el.addClass(this.className); - - this.ui.artistSearch.keyup(function(e) { - - if (_.contains([ - 9, - 16, - 17, - 18, - 19, - 20, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 91, - 92, - 93 - ], e.keyCode)) { - return; - } - - self._abortExistingSearch(); - self.throttledSearch({ - term : self.ui.artistSearch.val() - }); - }); - - this._clearResults(); - - if (this.isExisting) { - this.ui.searchBar.hide(); - } - }, - - onShow : function() { - this.ui.artistSearch.focus(); - }, - - search : function(options) { - var self = this; - - this.collection.reset(); - - if (!options.term || options.term === this.collection.term) { - return Marionette.$.Deferred().resolve(); - } - - this.searchResult.show(new LoadingView()); - this.collection.term = options.term; - this.currentSearchPromise = this.collection.fetch({ - data : { term : options.term } - }); - - this.currentSearchPromise.fail(function() { - self._showError(); - }); - - return this.currentSearchPromise; - }, - - _onArtistAdded : function(options) { - if (this.isExisting && options.artist.get('path') === this.model.get('folder').path) { - this.close(); - } - - else if (!this.isExisting) { - this.collection.term = ''; - this.collection.reset(); - this._clearResults(); - this.ui.artistSearch.val(''); - this.ui.artistSearch.focus(); - } - }, - - _onLoadMore : function() { - var showingAll = this.resultCollectionView.showMore(); - this.ui.searchBar.show(); - - if (showingAll) { - this.ui.loadMore.hide(); - } - }, - - _clearResults : function() { - if (!this.isExisting) { - this.searchResult.show(new EmptyView()); - } else { - this.searchResult.close(); - } - }, - - _showResults : function() { - if (!this.isClosed) { - if (this.collection.length === 0) { - this.ui.searchBar.show(); - this.searchResult.show(new NotFoundView({ term : this.collection.term })); - } else { - this.searchResult.show(this.resultCollectionView); - if (!this.showingAll && this.isExisting) { - this.ui.loadMore.show(); - } - } - } - }, - - _abortExistingSearch : function() { - if (this.currentSearchPromise && this.currentSearchPromise.readyState > 0 && this.currentSearchPromise.readyState < 4) { - console.log('aborting previous pending search request.'); - this.currentSearchPromise.abort(); - } else { - this._clearResults(); - } - }, - - _showError : function() { - if (!this.isClosed) { - this.ui.searchBar.show(); - this.searchResult.show(new ErrorView({ term : this.collection.term })); - this.collection.term = ''; - } - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/AddArtistViewTemplate.hbs b/src/UI/AddArtist/AddArtistViewTemplate.hbs deleted file mode 100644 index adadf0569..000000000 --- a/src/UI/AddArtist/AddArtistViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{#if folder.path}} -<div class="unmapped-folder-path"> - <div class="col-md-12"> - {{folder.path}} - </div> -</div>{{/if}} -<div class="x-search-bar"> - <div class="input-group input-group-lg add-artist-search"> - <span class="input-group-addon"><i class="icon-lidarr-search"/></span> - - {{#if folder}} - <input type="text" class="form-control x-artist-search" value="{{folder.name}}"> - {{else}} - <input type="text" class="form-control x-artist-search" placeholder="Start typing the name of an artist or album..."> - {{/if}} - </div> -</div> -<div class="row"> - <div id="search-result" class="result-list col-md-12"/> -</div> -<div class="btn btn-block text-center new-artist-loadmore x-load-more" style="display: none;"> - <i class="icon-lidarr-load-more"/> - more -</div> diff --git a/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs b/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs deleted file mode 100644 index f8cadd547..000000000 --- a/src/UI/AddArtist/ArtistTypeSelectionPartial.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<select class="form-control col-md-2 x-artist-type" name="artistType"> - <option value="standard">Standard</option> -</select> diff --git a/src/UI/AddArtist/BulkImport/ArtistPathCell.js b/src/UI/AddArtist/BulkImport/ArtistPathCell.js deleted file mode 100644 index debe25ae1..000000000 --- a/src/UI/AddArtist/BulkImport/ArtistPathCell.js +++ /dev/null @@ -1,7 +0,0 @@ -var TemplatedCell = require('../../Cells/TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'artist-title-cell', - template : 'AddArtist/BulkImport/ArtistPathTemplate', - -}); diff --git a/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs b/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs deleted file mode 100644 index 53fa29105..000000000 --- a/src/UI/AddArtist/BulkImport/ArtistPathTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -{{path}}<br> diff --git a/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js b/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js deleted file mode 100644 index 5490439e3..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportArtistNameCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var BulkImportCollection = require('./BulkImportCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-title-cell', - - render : function() { - var collection = this.model.collection; - this.listenTo(collection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - this.$el.empty(); - - this.$el.html('<a href="https://www.musicbrainz.org/artist/' + this.cellValue.get('foreignArtistId') +'">' + this.cellValue.get('name') +'</a><br><span class="hint">' + this.cellValue.get('overview') + '</span>'); - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportCollection.js b/src/UI/AddArtist/BulkImport/BulkImportCollection.js deleted file mode 100644 index d6fef1faa..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportCollection.js +++ /dev/null @@ -1,49 +0,0 @@ -var _ = require('underscore'); -var PageableCollection = require('backbone.pageable'); -var ArtistModel = require('../../Artist/ArtistModel'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPageableCollection = require('../../Mixins/AsPageableCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var BulkImportCollection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/artist/bulkimport', - model : ArtistModel, - tableName : 'bulkimport', - - state : { - pageSize : 100000, - sortKey: 'sortName', - firstPage: 1 - }, - - fetch : function(options) { - - options = options || {}; - - var data = options.data || {}; - - if (!data.id || !data.folder) { - data.id = this.folderId; - data.folder = this.folder; - } - - options.data = data; - return PageableCollection.prototype.fetch.call(this, options); - }, - - parseLinks : function(options) { - - return { - first : this.url, - next: this.url, - last : this.url - }; - } -}); - - -BulkImportCollection = AsSortedCollection.call(BulkImportCollection); -BulkImportCollection = AsPageableCollection.call(BulkImportCollection); -BulkImportCollection = AsPersistedStateCollection.call(BulkImportCollection); - -module.exports = BulkImportCollection; diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js b/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js deleted file mode 100644 index db9860ffe..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportMonitorCell.js +++ /dev/null @@ -1,65 +0,0 @@ -var Backgrid = require('backgrid'); -var Config = require('../../Config'); -var _ = require('underscore'); -var vent = require('vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var Marionette = require('marionette'); - -module.exports = TemplatedCell.extend({ - className : 'monitor-cell', - template : 'AddArtist/BulkImport/BulkImportMonitorCell', - - _orig : TemplatedCell.prototype.initialize, - _origRender : TemplatedCell.prototype.initialize, - - ui : { - monitor : '.x-monitor', - }, - - events: { 'change .x-monitor' : '_monitorChanged' }, - - initialize : function () { - this._orig.apply(this, arguments); - - this.defaultMonitor = Config.getValue(Config.Keys.MonitorEpisodes, 'all'); - - this.model.set('monitored', this._convertMonitorToBool(this.defaultMonitor)); - - this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); - }, - - _convertMonitorToBool : function(monitorString) { - return monitorString === 'all' ? true : false; - }, - - _convertBooltoMonitor : function(monitorBool) { - return monitorBool === true ? 'all' : 'none'; - }, - - _monitorChanged : function() { - Config.setValue(Config.Keys.MonitorEpisodes, this.$el.find('.x-monitor').val()); - this.defaultMonitor = this.$el.find('.x-monitor').val(); - this.model.set('monitored', this._convertMonitorToBool(this.$el.find('.x-monitor').val())); - }, - - render : function() { - var templateName = this.column.get('template') || this.template; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - - this.$el.find('.x-monitor').val(this._convertBooltoMonitor(this.model.get('monitored'))); - - return this; - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs deleted file mode 100644 index 5ef509ce1..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportMonitorCellTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<select class="col-md-2 form-control x-monitor"> - <option value="all">Yes</option> - <option value="none">No</option> -</select> diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js deleted file mode 100644 index 682c475f9..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var Config = require('../../Config'); -var _ = require('underscore'); - -module.exports = Backgrid.SelectCell.extend({ - className : 'profile-cell', - - _orig : Backgrid.SelectCell.prototype.initialize, - - initialize : function () { - this._orig.apply(this, arguments); - - this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - if(ProfileCollection.get(this.defaultProfile)) - { - this.profile = this.defaultProfile; - } else { - this.profile = ProfileCollection.get(1); - } - - this.render(); - - }, - - optionValues : function() { - return _.map(ProfileCollection.models, function(model){ - return [model.get('name'), model.get('id')+""]; - }); - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js b/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js deleted file mode 100644 index 4b0bd168b..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCellT.js +++ /dev/null @@ -1,77 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var Config = require('../../Config'); -var _ = require('underscore'); -var vent = require('vent'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var Marionette = require('marionette'); - -module.exports = TemplatedCell.extend({ - className : 'profile-cell', - template : 'AddArtist/BulkImport/BulkImportProfileCell', - - _orig : TemplatedCell.prototype.initialize, - _origRender : TemplatedCell.prototype.initialize, - - ui : { - profile : '.x-profile', - }, - - events: { 'change .x-profile' : '_profileChanged' }, - - initialize : function () { - this._orig.apply(this, arguments); - - this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); - - this.defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - - this.profile = this.defaultProfile; - - if(ProfileCollection.get(this.defaultProfile)) - { - this.profile = this.defaultProfile; - this.model.set('profileId', this.defaultProfile); - } else { - this.profile = 1; - this.model.set('profileId', 1); - } - - this.$('.x-profile').val(this.model.get('profileId')); - - this.cellValue = ProfileCollection; - - }, - - _profileChanged : function() { - Config.setValue(Config.Keys.DefaultProfileId, this.$('.x-profile').val()); - this.model.set('profileId', this.$('.x-profile').val()); - }, - - _onConfigUpdated : function(options) { - if (options.key === Config.Keys.DefaultProfileId) { - this.defaultProfile = options.value; - } - }, - - render : function() { - var templateName = this.column.get('template') || this.template; - - this.cellValue = ProfileCollection; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - this.$('.x-profile').val(this.model.get('profileId')); - return this; - } - -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs deleted file mode 100644 index 7124319eb..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportProfileCellTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<select class="col-md-2 form-control x-profile"> - {{#each this}} - <option value="{{id}}">{{name}}</option> - {{/each}} -</select> diff --git a/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js b/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js deleted file mode 100644 index d1435dd14..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportSelectAllCell.js +++ /dev/null @@ -1,54 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var Backgrid = require('backgrid'); -var FullArtistCollection = require('../../Artist/ArtistCollection'); - - -module.exports = SelectAllCell.extend({ - _originalRender : SelectAllCell.prototype.render, - - _originalInit : SelectAllCell.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - - this._refreshIsDuplicate(); - - this.listenTo(this.model, 'change', this._refresh); - }, - - onChange : function(e) { - if(!this.isDuplicate) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger('backgrid:selected', this.model, checked); - } else { - $(e.target).prop('checked', false); - } - }, - - render : function() { - this._originalRender.apply(this, arguments); - - this.$el.children(':first').prop('disabled', this.isDuplicate); - - if (!this.isDuplicate) { - this.$el.children(':first').prop('checked', this.isChecked); - } - - return this; - }, - - _refresh: function() { - this.isChecked = this.$el.children(':first').prop('checked'); - this._refreshIsDuplicate(); - this.render(); - }, - - _refreshIsDuplicate: function() { - var foreignArtistId = this.model.get('foreignArtistId'); - var existingArtist = FullArtistCollection.where({ foreignArtistId: foreignArtistId }); - this.isDuplicate = existingArtist.length > 0 ? true : false; - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportView.js b/src/UI/AddArtist/BulkImport/BulkImportView.js deleted file mode 100644 index 83fcd41f6..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportView.js +++ /dev/null @@ -1,191 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ArtistNameCell = require('./BulkImportArtistNameCell'); -var BulkImportCollection = require('./BulkImportCollection'); -var ForeignIdCell = require('./ForeignIdCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var SelectAllCell = require('./BulkImportSelectAllCell'); -var ProfileCell = require('./BulkImportProfileCellT'); -var MonitorCell = require('./BulkImportMonitorCell'); -var ArtistPathCell = require('./ArtistPathCell'); -var LoadingView = require('../../Shared/LoadingView'); -var EmptyView = require('./EmptyView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var CommandController = require('../../Commands/CommandController'); -var Messenger = require('../../Shared/Messenger'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var ProfileCollection = require('../../Profile/ProfileCollection'); - -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'AddArtist/BulkImport/BulkImportViewTemplate', - - regions : { - toolbar : '#x-toolbar', - table : '#x-artists-bulk', - }, - - ui : { - addSelectdBtn : '.x-add-selected' - }, - - initialize : function(options) { - ProfileCollection.fetch(); - this.bulkImportCollection = new BulkImportCollection().bindSignalR({ updateOnly : true }); - this.model = options.model; - this.folder = this.model.get('path'); - this.folderId = this.model.get('id'); - this.bulkImportCollection.folderId = this.folderId; - this.bulkImportCollection.folder = this.folder; - this.bulkImportCollection.fetch(); - this.listenTo(this.bulkImportCollection, {'sync' : this._showContent, 'error' : this._showContent, 'backgrid:selected' : this._select}); - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false, - cellValue : 'this' - }, - { - name : 'movie', - label : 'Artist', - cell : ArtistNameCell, - cellValue : 'this', - sortable : false - }, - { - name : 'path', - label : 'Path', - cell : ArtistPathCell, - cellValue : 'this', - sortable : false - }, - { - name : 'foreignArtistId', - label : 'MB Id', - cell : ForeignIdCell, - cellValue : 'this', - sortable: false - }, - { - name :'monitor', - label: 'Monitor', - cell : MonitorCell, - cellValue : 'this', - sortable: false - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell, - cellValue : 'this', - sortable: false - } - ], - - _showContent : function() { - this._showToolbar(); - this._showTable(); - }, - - onShow : function() { - this.table.show(new LoadingView()); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState: false, - collapse : true, - items : [ - { - title : 'Add Selected', - icon : 'icon-lidarr-add', - callback : this._addSelected, - ownerContext : this, - className : 'x-add-selected' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [], - context : this - })); - - $('#x-toolbar').addClass('inline'); - }, - - _addSelected : function() { - var selected = _.filter(this.bulkImportCollection.models, function(elem){ - return elem.selected; - }); - - var promise = ArtistCollection.importFromList(selected); - this.ui.addSelectdBtn.spinForPromise(promise); - this.ui.addSelectdBtn.addClass('disabled'); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No artists selected' - }); - return; - } - - Messenger.show({ - message : 'Importing {0} artists. This can take multiple minutes depending on how many artists should be imported. Don\'t close this browser window until it is finished!'.format(selected.length), - hideOnNavigate : false, - hideAfter : 30, - type : 'error' - }); - - var _this = this; - - promise.done(function() { - Messenger.show({ - message : 'Imported artists from folder.', - hideAfter : 8, - hideOnNavigate : true - }); - - - _.forEach(selected, function(artist) { - artist.destroy(); //update the collection without the added movies - }); - }); - }, - - _handleEvent : function(eventName, data) { - if (eventName === 'sync' || eventName === 'content') { - this._showContent(); - } - }, - - _select : function(model, selected) { - model.selected = selected; - }, - - _showTable : function() { - if (this.bulkImportCollection.length === 0) { - this.table.show(new EmptyView({ folder : this.folder })); - return; - } - - this.importGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.bulkImportCollection, - className : 'table table-hover' - }); - - this.table.show(this.importGrid); - } -}); diff --git a/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs b/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs deleted file mode 100644 index e07b37e8e..000000000 --- a/src/UI/AddArtist/BulkImport/BulkImportViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div id="x-toolbar"/> - -<div class="row"> - <div class="col-md-12"> - <span><b>Disabled artists are possible duplicates. If the match is incorrect, update the MB Id cell to import the proper artist.</b><span> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artists-bulk" class="queue table-responsive"/> - </div> -</div> diff --git a/src/UI/AddArtist/BulkImport/EmptyView.js b/src/UI/AddArtist/BulkImport/EmptyView.js deleted file mode 100644 index a3c635533..000000000 --- a/src/UI/AddArtist/BulkImport/EmptyView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/BulkImport/EmptyViewTemplate', - - initialize : function (options) { - this.templateHelpers = {}; - this.templateHelpers.folder = options.folder; - } -}); diff --git a/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs b/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs deleted file mode 100644 index b2d80a15c..000000000 --- a/src/UI/AddArtist/BulkImport/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center hint col-md-12"> - <span>No artists found in folder {{folder}}. Have you already added all of them?</span> -</div> diff --git a/src/UI/AddArtist/BulkImport/ForeignIdCell.js b/src/UI/AddArtist/BulkImport/ForeignIdCell.js deleted file mode 100644 index 4b22dcd69..000000000 --- a/src/UI/AddArtist/BulkImport/ForeignIdCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var vent = require('vent'); -var _ = require('underscore'); -var $ = require('jquery'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'foreignId-cell', - - events : { - 'blur input.foreignId-input' : '_updateId' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-info hidden"></i><input type="text" class="x-foreignId foreignId-input form-control" value="' + this.cellValue.get('foreignArtistId') + '" />'); - - return this; - }, - - _updateId : function() { - var field = this.$el.find('.x-foreignId'); - var data = field.val(); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/artist/lookup?term=lidarrid:' + data, - type : 'GET', - }); - - field.prop('disabled', true); - - var icon = this.$('.icon-lidarr-info'); - - icon.removeClass('hidden'); - - icon.spinForPromise(promise); - var _self = this; - var cacheMonitored = this.model.get('monitored'); - var cacheProfile = this.model.get('profileId'); - var cachePath = this.model.get('path'); - var cacheRoot = this.model.get('rootFolderPath'); - - promise.success(function(response) { - _self.model.set(response[0]); - _self.model.set('monitored', cacheMonitored); - _self.model.set('profileId', cacheProfile); - _self.model.set('path', cachePath); - field.prop('disabled', false); - }); - - promise.error(function(request, status, error) { - console.error('Status: ' + status, 'Error: ' + error); - field.prop('disabled', false); - }); - } -}); diff --git a/src/UI/AddArtist/EmptyView.js b/src/UI/AddArtist/EmptyView.js deleted file mode 100644 index e07b1647d..000000000 --- a/src/UI/AddArtist/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/AddArtist/EmptyViewTemplate.hbs b/src/UI/AddArtist/EmptyViewTemplate.hbs deleted file mode 100644 index e2a20efdc..000000000 --- a/src/UI/AddArtist/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center hint col-md-12"> - <span>You can also search by MusicBrianzID using the MBID: prefixes.</span> -</div> diff --git a/src/UI/AddArtist/ErrorView.js b/src/UI/AddArtist/ErrorView.js deleted file mode 100644 index 9d53fae8c..000000000 --- a/src/UI/AddArtist/ErrorView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/ErrorViewTemplate', - - initialize : function(options) { - this.options = options; - }, - - templateHelpers : function() { - return this.options; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/ErrorViewTemplate.hbs b/src/UI/AddArtist/ErrorViewTemplate.hbs deleted file mode 100644 index c0b1e3673..000000000 --- a/src/UI/AddArtist/ErrorViewTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="text-center col-md-12"> - <h3> - There was an error searching for '{{term}}'. - </h3> - - If the artist name contains non-alphanumeric characters try removing them, otherwise try your search again later. -</div> diff --git a/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js b/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js deleted file mode 100644 index af57bc1d2..000000000 --- a/src/UI/AddArtist/Existing/AddExistingArtistCollectionView.js +++ /dev/null @@ -1,51 +0,0 @@ -var Marionette = require('marionette'); -var AddArtistView = require('../AddArtistView'); -var UnmappedFolderCollection = require('./UnmappedFolderCollection'); - -module.exports = Marionette.CompositeView.extend({ - itemView : AddArtistView, - itemViewContainer : '.x-loading-folders', - template : 'AddArtist/Existing/AddExistingArtistCollectionViewTemplate', - - ui : { - loadingFolders : '.x-loading-folders' - }, - - initialize : function() { - this.collection = new UnmappedFolderCollection(); - this.collection.importItems(this.model); - }, - - showCollection : function() { - this._showAndSearch(0); - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.loadingFolders.before(itemView.el); - }, - - _showAndSearch : function(index) { - var self = this; - var model = this.collection.at(index); - - if (model) { - var currentIndex = index; - var folderName = model.get('folder').name; - this.addItemView(model, this.getItemView(), index); - this.children.findByModel(model).search({ term : folderName }).always(function() { - if (!self.isClosed) { - self._showAndSearch(currentIndex + 1); - } - }); - } - - else { - this.ui.loadingFolders.hide(); - } - }, - - itemViewOptions : { - isExisting : true - } - -}); \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs b/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs deleted file mode 100644 index 5acbd1ef0..000000000 --- a/src/UI/AddArtist/Existing/AddExistingArtistCollectionViewTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="x-existing-folders"> - <div class="loading-folders x-loading-folders"> - Loading search results from server for your artists, this may take a few minutes. - </div> -</div> \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/UnmappedFolderCollection.js b/src/UI/AddArtist/Existing/UnmappedFolderCollection.js deleted file mode 100644 index bd2a83f49..000000000 --- a/src/UI/AddArtist/Existing/UnmappedFolderCollection.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); -var UnmappedFolderModel = require('./UnmappedFolderModel'); -var _ = require('underscore'); - -module.exports = Backbone.Collection.extend({ - model : UnmappedFolderModel, - - importItems : function(rootFolderModel) { - - this.reset(); - var rootFolder = rootFolderModel; - - _.each(rootFolderModel.get('unmappedFolders'), function(folder) { - this.push(new UnmappedFolderModel({ - rootFolder : rootFolder, - folder : folder - })); - }, this); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/Existing/UnmappedFolderModel.js b/src/UI/AddArtist/Existing/UnmappedFolderModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/AddArtist/Existing/UnmappedFolderModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/AddArtist/MonitoringTooltipTemplate.hbs b/src/UI/AddArtist/MonitoringTooltipTemplate.hbs deleted file mode 100644 index 0c795cf12..000000000 --- a/src/UI/AddArtist/MonitoringTooltipTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<dl class="monitor-tooltip-contents"> - <dt>All</dt> - <dd>Monitor all tracks except specials</dd> - <dt>Future</dt> - <dd>Monitor tracks that have not been released yet</dd> - <dt>Missing</dt> - <dd>Monitor tracks that do not have files or have not aired yet</dd> - <dt>Existing</dt> - <dd>Monitor tracks that have files or have not aired yet</dd> - <dt>First Season</dt> - <dd>Monitor all tracks of the first album. All other albums will be ignored</dd> - <dt>Latest Season</dt> - <dd>Monitor all tracks of the latest album and future albums</dd> - <dt>None</dt> - <dd>No tracks will be monitored.</dd> - <!--<dt>Latest Season</dt>--> - <!--<dd>Monitor all tracks the latest album only, previous albums will be ignored</dd>--> -</dl> \ No newline at end of file diff --git a/src/UI/AddArtist/NotFoundView.js b/src/UI/AddArtist/NotFoundView.js deleted file mode 100644 index d25f339c3..000000000 --- a/src/UI/AddArtist/NotFoundView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/NotFoundViewTemplate', - - initialize : function(options) { - this.options = options; - }, - - templateHelpers : function() { - return this.options; - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/NotFoundViewTemplate.hbs b/src/UI/AddArtist/NotFoundViewTemplate.hbs deleted file mode 100644 index abaca6646..000000000 --- a/src/UI/AddArtist/NotFoundViewTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="text-center col-md-12"> - <h3> - Sorry. We couldn't find any artist matching '{{term}}' - </h3> - <a href="https://github.com/mattman86/Lidarr/wiki/FAQ#wiki-why-cant-i-add-a-new-show-to-nzbdrone-its-on-thetvdb">Why can't I find my artist?</a> - -</div> diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollection.js b/src/UI/AddArtist/RootFolders/RootFolderCollection.js deleted file mode 100644 index 81050c19d..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollection.js +++ /dev/null @@ -1,10 +0,0 @@ -var Backbone = require('backbone'); -var RootFolderModel = require('./RootFolderModel'); -require('../../Mixins/backbone.signalr.mixin'); - -var RootFolderCollection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/rootfolder', - model : RootFolderModel -}); - -module.exports = new RootFolderCollection(); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js b/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js deleted file mode 100644 index 1029de245..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var RootFolderItemView = require('./RootFolderItemView'); - -module.exports = Marionette.CompositeView.extend({ - template : 'AddArtist/RootFolders/RootFolderCollectionViewTemplate', - itemViewContainer : '.x-root-folders', - itemView : RootFolderItemView -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs deleted file mode 100644 index 70755bbca..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderCollectionViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<table class="table table-hover"> - <thead> - <tr> - <th class="col-md-10 "> - Path - </th> - <th class="col-md-3"> - Free Space - </th> - </tr> - </thead> - <tbody class="x-root-folders"></tbody> -</table> \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderItemView.js b/src/UI/AddArtist/RootFolders/RootFolderItemView.js deleted file mode 100644 index c22f6fcf7..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'AddArtist/RootFolders/RootFolderItemViewTemplate', - className : 'recent-folder', - tagName : 'tr', - - initialize : function() { - this.listenTo(this.model, 'change', this.render); - }, - - events : { - 'click .x-delete' : 'removeFolder', - 'click .x-folder' : 'folderSelected' - }, - - removeFolder : function() { - var self = this; - - this.model.destroy().success(function() { - self.close(); - }); - }, - - folderSelected : function() { - this.trigger('folderSelected', this.model); - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs deleted file mode 100644 index c1378207a..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderItemViewTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<td class="col-md-10 x-folder folder-path"> - {{path}} -</td> -<td class="col-md-3 x-folder folder-free-space"> - <span>{{Bytes freeSpace}}</span> -</td> -<td class="col-md-1"> - <i class="icon-lidarr-delete x-delete"></i> -</td> diff --git a/src/UI/AddArtist/RootFolders/RootFolderLayout.js b/src/UI/AddArtist/RootFolders/RootFolderLayout.js deleted file mode 100644 index 7b5036689..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderLayout.js +++ /dev/null @@ -1,80 +0,0 @@ -var Marionette = require('marionette'); -var RootFolderCollectionView = require('./RootFolderCollectionView'); -var RootFolderCollection = require('./RootFolderCollection'); -var RootFolderModel = require('./RootFolderModel'); -var LoadingView = require('../../Shared/LoadingView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -require('../../Mixins/FileBrowser'); - -var Layout = Marionette.Layout.extend({ - template : 'AddArtist/RootFolders/RootFolderLayoutTemplate', - - ui : { - pathInput : '.x-path' - }, - - regions : { - currentDirs : '#current-dirs' - }, - - events : { - 'click .x-add' : '_addFolder', - 'keydown .x-path input' : '_keydown' - }, - - initialize : function() { - this.collection = RootFolderCollection; - this.rootfolderListView = null; - }, - - onShow : function() { - this.listenTo(RootFolderCollection, 'sync', this._showCurrentDirs); - this.currentDirs.show(new LoadingView()); - - if (RootFolderCollection.synced) { - this._showCurrentDirs(); - } - - this.ui.pathInput.fileBrowser(); - }, - - _onFolderSelected : function(options) { - this.trigger('folderSelected', options); - }, - - _addFolder : function() { - var self = this; - - var newDir = new RootFolderModel({ - Path : this.ui.pathInput.val() - }); - - this.bindToModelValidation(newDir); - - newDir.save().done(function() { - RootFolderCollection.add(newDir); - self.trigger('folderSelected', { model : newDir }); - }); - }, - - _showCurrentDirs : function() { - if (!this.rootfolderListView) { - this.rootfolderListView = new RootFolderCollectionView({ collection : RootFolderCollection }); - this.currentDirs.show(this.rootfolderListView); - - this.listenTo(this.rootfolderListView, 'itemview:folderSelected', this._onFolderSelected); - } - }, - - _keydown : function(e) { - if (e.keyCode !== 13) { - return; - } - - this._addFolder(); - } -}); - -var Layout = AsValidatedView.apply(Layout); - -module.exports = Layout; diff --git a/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs deleted file mode 100644 index efb6eb7c9..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderLayoutTemplate.hbs +++ /dev/null @@ -1,36 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Select Folder</h3> - </div> - <div class="modal-body root-folders-modal"> - <div class="validation-errors"></div> - <div class="alert alert-info">Enter the path that contains some or all of your Music, you will be able to choose which artist you want to import<button type="button" class="close" data-dismiss="alert">×</button></div> - - <div class="row"> - <div class="form-group"> - - <div class="col-md-12"> - - <div class="input-group"> - <span class="input-group-addon"> <i class="icon-lidarr-folder-open"></i></span> - <input class="form-control x-path" type="text" validation-name="path" placeholder="Enter path to folder that contains your music"> - <span class="input-group-btn"><button class="btn btn-success x-add"><i class="icon-lidarr-ok"/></button></span> - </div> - </div> - </div> - </div> - - <div class="row root-folders"> - <div class="col-md-12"> - {{#if items}} - <h4>Recent Folders</h4> - {{/if}} - <div id="current-dirs" class="root-folders-list"></div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/AddArtist/RootFolders/RootFolderModel.js b/src/UI/AddArtist/RootFolders/RootFolderModel.js deleted file mode 100644 index 28681768b..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderModel.js +++ /dev/null @@ -1,8 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/rootfolder', - defaults : { - freeSpace : 0 - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs b/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs deleted file mode 100644 index 56729b0dd..000000000 --- a/src/UI/AddArtist/RootFolders/RootFolderSelectionPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<select class="col-md-4 form-control x-root-folder" validation-name="RootFolderPath"> - {{#if this}} - {{#each this}} - <option value="{{id}}">{{path}}</option> - {{/each}} - {{else}} - <option value="">Select Path</option> - {{/if}} - <option value="addNew">Add a different path</option> -</select> - diff --git a/src/UI/AddArtist/SearchResultCollectionView.js b/src/UI/AddArtist/SearchResultCollectionView.js deleted file mode 100644 index e533085ac..000000000 --- a/src/UI/AddArtist/SearchResultCollectionView.js +++ /dev/null @@ -1,29 +0,0 @@ -var Marionette = require('marionette'); -var SearchResultView = require('./SearchResultView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : SearchResultView, - - initialize : function(options) { - this.isExisting = options.isExisting; - this.showing = 1; - }, - - showAll : function() { - this.showingAll = true; - this.render(); - }, - - showMore : function() { - this.showing += 5; - this.render(); - - return this.showing >= this.collection.length; - }, - - appendHtml : function(collectionView, itemView, index) { - if (!this.isExisting || index < this.showing || index === 0) { - collectionView.$el.append(itemView.el); - } - } -}); \ No newline at end of file diff --git a/src/UI/AddArtist/SearchResultView.js b/src/UI/AddArtist/SearchResultView.js deleted file mode 100644 index ee8806a0e..000000000 --- a/src/UI/AddArtist/SearchResultView.js +++ /dev/null @@ -1,297 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var Profiles = require('../Profile/ProfileCollection'); -var RootFolders = require('./RootFolders/RootFolderCollection'); -var RootFolderLayout = require('./RootFolders/RootFolderLayout'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Config = require('../Config'); -var Messenger = require('../Shared/Messenger'); -var AsValidatedView = require('../Mixins/AsValidatedView'); - -require('jquery.dotdotdot'); - -var view = Marionette.ItemView.extend({ - - template : 'AddArtist/SearchResultViewTemplate', - - ui : { - profile : '.x-profile', - rootFolder : '.x-root-folder', - albumFolder : '.x-album-folder', - artistType : '.x-artist-type', - monitor : '.x-monitor', - monitorTooltip : '.x-monitor-tooltip', - addButton : '.x-add', - addAlbumButton : '.x-add-album', - addSearchButton : '.x-add-search', - addAlbumSearchButton : '.x-add-album-search', - overview : '.x-overview' - }, - - events : { - 'click .x-add' : '_addWithoutSearch', - 'click .x-add-album' : '_addWithoutSearch', - 'click .x-add-search' : '_addAndSearch', - 'click .x-add-album-search' : '_addAndSearch', - 'change .x-profile' : '_profileChanged', - 'change .x-root-folder' : '_rootFolderChanged', - 'change .x-album-folder' : '_albumFolderChanged', - 'change .x-artist-type' : '_artistTypeChanged', - 'change .x-monitor' : '_monitorChanged' - }, - - initialize : function() { - - if (!this.model) { - throw 'model is required'; - } - - this.templateHelpers = {}; - this._configureTemplateHelpers(); - - this.listenTo(vent, Config.Events.ConfigUpdatedEvent, this._onConfigUpdated); - this.listenTo(this.model, 'change', this.render); - this.listenTo(RootFolders, 'all', this._rootFoldersUpdated); - }, - - onRender : function() { - - var defaultProfile = Config.getValue(Config.Keys.DefaultProfileId); - var defaultRoot = Config.getValue(Config.Keys.DefaultRootFolderId); - var useAlbumFolder = Config.getValueBoolean(Config.Keys.UseAlbumFolder, true); - var defaultArtistType = Config.getValue(Config.Keys.DefaultSeriesType, 'standard'); - var defaultMonitorEpisodes = Config.getValue(Config.Keys.MonitorEpisodes, 'missing'); - - if (Profiles.get(defaultProfile)) { - this.ui.profile.val(defaultProfile); - } - - if (RootFolders.get(defaultRoot)) { - this.ui.rootFolder.val(defaultRoot); - } - - this.ui.albumFolder.prop('checked', useAlbumFolder); - this.ui.artistType.val(defaultArtistType); - this.ui.monitor.val(defaultMonitorEpisodes); - - //TODO: make this work via onRender, FM? - //works with onShow, but stops working after the first render - this.ui.overview.dotdotdot({ - height : 120 - }); - - this.templateFunction = Marionette.TemplateCache.get('AddArtist/MonitoringTooltipTemplate'); - var content = this.templateFunction(); - - this.ui.monitorTooltip.popover({ - content : content, - html : true, - trigger : 'hover', - title : 'Track Monitoring Options', - placement : 'right', - container : this.$el - }); - }, - - _configureTemplateHelpers : function() { - var existingArtist = ArtistCollection.where({ foreignArtistId : this.model.get('foreignArtistId') }); - - if (existingArtist.length > 0) { - this.templateHelpers.existing = existingArtist[0].toJSON(); - } - - this.templateHelpers.profiles = Profiles.toJSON(); - - if (!this.model.get('isExisting')) { - this.templateHelpers.rootFolders = RootFolders.toJSON(); - } - }, - - _onConfigUpdated : function(options) { - if (options.key === Config.Keys.DefaultProfileId) { - this.ui.profile.val(options.value); - } - - else if (options.key === Config.Keys.DefaultRootFolderId) { - this.ui.rootFolder.val(options.value); - } - - else if (options.key === Config.Keys.UseAlbumFolder) { - this.ui.seasonFolder.prop('checked', options.value); - } - - else if (options.key === Config.Keys.DefaultArtistType) { - this.ui.artistType.val(options.value); - } - - else if (options.key === Config.Keys.MonitorEpisodes) { - this.ui.monitor.val(options.value); - } - }, - - _profileChanged : function() { - Config.setValue(Config.Keys.DefaultProfileId, this.ui.profile.val()); - }, - - _albumFolderChanged : function() { - Config.setValue(Config.Keys.UseAlbumFolder, this.ui.albumFolder.prop('checked')); - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - AppLayout.modalRegion.show(rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _artistTypeChanged : function() { - Config.setValue(Config.Keys.DefaultArtistType, this.ui.artistType.val()); - }, - - _monitorChanged : function() { - Config.setValue(Config.Keys.MonitorEpisodes, this.ui.monitor.val()); - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _addWithoutSearch : function(evt) { - console.log(evt); - this._addArtist(false); - }, - - _addAndSearch : function() { - this._addArtist(true); - }, - - _addArtist : function(searchForMissing) { - // TODO: Refactor to handle multiple add buttons/albums - var addButton = this.ui.addButton; - var addSearchButton = this.ui.addSearchButton; - console.log('_addArtist, searchForMissing=', searchForMissing); - - addButton.addClass('disabled'); - addSearchButton.addClass('disabled'); - - var profile = this.ui.profile.val(); - var rootFolderPath = this.ui.rootFolder.children(':selected').text(); - var artistType = this.ui.artistType.val(); // Perhaps make this a differnitator between artist or Album? - var albumFolder = this.ui.albumFolder.prop('checked'); - - var options = this._getAddArtistOptions(); - options.searchForMissing = searchForMissing; - - this.model.set({ - profileId : profile, - rootFolderPath : rootFolderPath, - albumFolder : albumFolder, - artistType : artistType, - addOptions : options, - monitored : true - }, { silent : true }); - - var self = this; - var promise = this.model.save(); - - if (searchForMissing) { - this.ui.addSearchButton.spinForPromise(promise); - } - - else { - this.ui.addButton.spinForPromise(promise); - } - - promise.always(function() { - addButton.removeClass('disabled'); - addSearchButton.removeClass('disabled'); - }); - - promise.done(function() { - console.log('[SearchResultView] _addArtist promise resolve:', self.model); - ArtistCollection.add(self.model); - - self.close(); - - Messenger.show({ - message : 'Added: ' + self.model.get('name'), - actions : { - goToArtist : { - label : 'Go to Artist', - action : function() { - Backbone.history.navigate('/artist/' + self.model.get('nameSlug'), { trigger : true }); - } - } - }, - hideAfter : 8, - hideOnNavigate : true - }); - - vent.trigger(vent.Events.ArtistAdded, { artist : self.model }); - }); - }, - - _rootFoldersUpdated : function() { - this._configureTemplateHelpers(); - this.render(); - }, - - _getAddArtistOptions : function() { - var monitor = this.ui.monitor.val(); - //[TODO]: Refactor for albums - var lastSeason = _.max(this.model.get('seasons'), 'seasonNumber'); - var firstSeason = _.min(_.reject(this.model.get('seasons'), { seasonNumber : 0 }), 'seasonNumber'); - - //this.model.setSeasonPass(firstSeason.seasonNumber); // TODO - - var options = { - ignoreTracksWithFiles : false, - ignoreTracksWithoutFiles : false - }; - - if (monitor === 'all') { - return options; - } - - else if (monitor === 'future') { - options.ignoreTracksWithFiles = true; - options.ignoreTracksWithoutFiles = true; - } - - /*else if (monitor === 'latest') { - this.model.setSeasonPass(lastSeason.seasonNumber); - } - - else if (monitor === 'first') { - this.model.setSeasonPass(lastSeason.seasonNumber + 1); - this.model.setSeasonMonitored(firstSeason.seasonNumber); - }*/ - - else if (monitor === 'missing') { - options.ignoreTracksWithFiles = true; - } - - else if (monitor === 'existing') { - options.ignoreTracksWithoutFiles = true; - } - - /*else if (monitor === 'none') { - this.model.setSeasonPass(lastSeason.seasonNumber + 1); - }*/ - - return options; - } -}); - -AsValidatedView.apply(view); - -module.exports = view; diff --git a/src/UI/AddArtist/SearchResultViewTemplate.hbs b/src/UI/AddArtist/SearchResultViewTemplate.hbs deleted file mode 100644 index f92eb2d5f..000000000 --- a/src/UI/AddArtist/SearchResultViewTemplate.hbs +++ /dev/null @@ -1,146 +0,0 @@ -<div class="search-item {{#unless isExisting}}search-item-new{{/unless}}"> - <div class="row"> - <div class="col-md-10"> - <div class="row"> - <div class="col-md-12"> - <h2 class="artist-title"> - <!--{{titleWithYear}}--> - {{name}} - - <!--<span class="labels"> - <span class="label label-default">{{network}}</span> - {{#unless_eq status compare="continuing"}} - <span class="label label-danger">Ended</span> - {{/unless_eq}} - </span>--> - </h2> - </div> - </div> - <div class="row new-artist-overview x-overview"> - <div class="col-md-12 overview-internal"> - {{overview}} - </div> - </div> - <div class="row"> - {{#unless existing}} - {{#unless path}} - <div class="form-group col-md-4"> - <label>Path</label> - {{> RootFolderSelectionPartial rootFolders}} - </div> - {{/unless}} - - <div class="form-group col-md-2"> - <label>Monitor <i class="icon-lidarr-form-info monitor-tooltip x-monitor-tooltip"></i></label> - <select class="form-control col-md-2 x-monitor"> - <option value="all">All</option> - <option value="future">Future</option> - <option value="missing">Missing</option> - <option value="existing">Existing</option> - <option value="none">None</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - {{> ProfileSelectionPartial profiles}} - </div> - - <!--<div class="form-group col-md-2"> - - </div>--> - - <div class="form-group col-md-2"> - <label>Album Folders</label> - - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-album-folder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - {{/unless}} - </div> - <div class="row"> - {{#unless existing}} - {{#if name}} - <div class="form-group col-md-2 col-md-offset-10"> - <!--Uncomment if we need to add even more controls to add artist--> - <!--<label style="visibility: hidden">Add</label>--> - <div class="btn-group"> - <button class="btn btn-success add x-add" title="Add" data-artist="{{name}}"> - <i class="icon-lidarr-add"></i> - </button> - - <button class="btn btn-success add x-add-search" title="Add and Search for missing tracks" data-artist="{{name}}"> - <i class="icon-lidarr-search"></i> - </button> - </div> - </div> - {{else}} - <div class="col-md-2 col-md-offset-10"> - <button class="btn add-artist disabled"> - Add - </button> - </div> - {{/if}} - {{else}} - <div class="col-md-2 col-md-offset-10"> - <a class="btn btn-default" href="{{route}}"> - Already Exists - </a> - </div> - {{/unless}} - </div> - </div> - </div> - <div class="row"> - {{#each albums}} - <div class="col-md-12" style="border:1px dashed black;"> - <div class="col-md-2"> - <a href="{{artworkUrl}}" target="_blank"> - <!-- {{poster}} --> - <img class="album-poster" src="{{artworkUrl}}"> - </a> - </div> - <div class="col-md-8"> - <h2>{{albumName}} ({{year}})</h2> - {{#unless existing}} - {{#if albumName}} - <div class="form-group col-md-offset-10"> - <!--Uncomment if we need to add even more controls to add artist--> - <!--<label style="visibility: hidden">Add</label>--> - <div class="btn-group"> - <button class="btn btn-success add x-add-album" title="Add" data-album="{{albumName}}"> - <i class="icon-lidarr-add"></i> - </button> - - <button class="btn btn-success add x-add-album-search" title="Add and Search for missing tracks" data-album="{{albumName}}"> - <i class="icon-lidarr-search"></i> - </button> - </div> - </div> - {{else}} - <div class="col-md-2 col-md-offset-10"> - <button class="btn add-artist disabled"> - Add - </button> - </div> - {{/if}} - {{else}} - <div class="col-md-2 col-md-offset-10"> - <a class="btn btn-default" href="{{route}}"> - Already Exists - </a> - </div> - {{/unless}} - </div> - </div> - {{/each}} - </div> -</div> diff --git a/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs b/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs deleted file mode 100644 index 573599dab..000000000 --- a/src/UI/AddArtist/StartingAlbumSelectionPartial.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<select class="form-control col-md-2 starting-album x-starting-album"> - - - {{#each this}} - {{#if_eq seasonNumber compare="0"}} - <option value="{{seasonNumber}}">Specials</option> - {{else}} - <option value="{{seasonNumber}}">Album {{seasonNumber}}</option> - {{/if_eq}} - {{/each}} - - <option value="5000000">None</option> -</select> diff --git a/src/UI/AddArtist/addArtist.less b/src/UI/AddArtist/addArtist.less deleted file mode 100644 index e53ff8eee..000000000 --- a/src/UI/AddArtist/addArtist.less +++ /dev/null @@ -1,181 +0,0 @@ -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; - -#add-artist-screen { - .existing-artist { - - .card(); - margin : 30px 0px; - - .unmapped-folder-path { - padding: 20px; - margin-left : 0px; - font-weight : 100; - font-size : 25px; - text-align : center; - } - - .new-artist-loadmore { - font-size : 30px; - font-weight : 300; - padding-top : 10px; - padding-bottom : 10px; - } - } - - .new-artist { - .search-item { - .card(); - margin : 40px 0px; - } - } - - .add-artist-search { - margin-top : 20px; - margin-bottom : 20px; - } - - .search-item { - - padding-bottom : 20px; - - .artist-title { - margin-top : 5px; - - .labels { - margin-left : 10px; - - .label { - font-size : 12px; - vertical-align : middle; - } - } - - .year { - font-style : italic; - color : #aaaaaa; - } - } - - .new-artist-overview { - overflow : hidden; - height : 103px; - - .overview-internal { - overflow : hidden; - height : 80px; - } - } - - .artist-poster { - min-width : 138px; - min-height : 203px; - max-width : 138px; - max-height : 203px; - margin : 10px; - } - - .album-poster { - min-width : 100px; - min-height : 100px; - max-width : 138px; - max-height : 203px; - margin : 10px; - } - - a { - color : #343434; - } - - a:hover { - text-decoration : none; - } - - select { - font-size : 14px; - } - - .checkbox { - margin-top : 0px; - } - - .add { - i { - &:before { - color : #ffffff; - } - } - } - - .monitor-tooltip { - margin-left : 5px; - } - } - - .loading-folders { - margin : 30px 0px; - text-align: center; - } - - .hint { - color : #999999; - font-style : italic; - } - - .monitor-tooltip-contents { - padding-bottom : 0px; - - dd { - padding-bottom : 8px; - } - } -} - -li.add-new { - .clickable; - - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: rgb(51, 51, 51); - white-space: nowrap; -} - -li.add-new:hover { - text-decoration: none; - color: rgb(255, 255, 255); - background-color: rgb(0, 129, 194); -} - -.root-folders-modal { - overflow : visible; - - .root-folders-list { - overflow-y : auto; - max-height : 300px; - - i { - .clickable(); - } - } - - .validation-errors { - display : none; - } - - .input-group { - .form-control { - background-color : white; - } - } - - .root-folders { - margin-top : 20px; - } - - .recent-folder { - .clickable(); - } -} diff --git a/src/UI/Album/AlbumDetailsLayout.js b/src/UI/Album/AlbumDetailsLayout.js deleted file mode 100644 index 53bcf941a..000000000 --- a/src/UI/Album/AlbumDetailsLayout.js +++ /dev/null @@ -1,133 +0,0 @@ -var Marionette = require('marionette'); -var SummaryLayout = require('./Summary/AlbumSummaryLayout'); -var SearchLayout = require('./Search/AlbumSearchLayout'); -var AlbumHistoryLayout = require('./History/AlbumHistoryLayout'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Album/AlbumDetailsLayoutTemplate', - - regions : { - summary : '#album-summary', - history : '#album-history', - search : '#album-search' - }, - - ui : { - summary : '.x-album-summary', - history : '.x-album-history', - search : '.x-album-search', - monitored : '.x-album-monitored' - }, - - events : { - - 'click .x-album-summary' : '_showSummary', - 'click .x-album-history' : '_showHistory', - 'click .x-album-search' : '_showSearch', - 'click .x-album-monitored' : '_toggleMonitored' - }, - - templateHelpers : {}, - - initialize : function(options) { - - this.templateHelpers.hideArtistLink = options.hideArtistLink; - - - this.artist = ArtistCollection.get(this.model.get('artistId')); - - this.templateHelpers.artist = this.artist.toJSON(); - this.openingTab = options.openingTab || 'summary'; - - this.listenTo(this.model, 'sync', this._setMonitoredState); - }, - - onShow : function() { - this.searchLayout = new SearchLayout({ model : this.model }); - - if (this.openingTab === 'search') { - this.searchLayout.startManualSearch = true; - this._showSearch(); - } - - else { - this._showSummary(); - } - - this._setMonitoredState(); - - if (this.artist.get('monitored')) { - this.$el.removeClass('artist-not-monitored'); - } - - else { - this.$el.addClass('artist-not-monitored'); - } - }, - - _showSummary : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.summary.tab('show'); - this.summary.show(new SummaryLayout({ - model : this.model, - artist : this.artist - })); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.history.tab('show'); - this.history.show(new AlbumHistoryLayout({ - model : this.model, - artist : this.artist - })); - }, - - _showSearch : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.search.tab('show'); - this.search.show(this.searchLayout); - }, - - _toggleMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name), { silent : true }); - - this.ui.monitored.addClass('icon-lidarr-spinner fa-spin'); - this.model.save(); - }, - - _setMonitoredState : function() { - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (this.model.get('monitored')) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/AlbumDetailsLayoutTemplate.hbs b/src/UI/Album/AlbumDetailsLayoutTemplate.hbs deleted file mode 100644 index f76caa790..000000000 --- a/src/UI/Album/AlbumDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="modal-content"> - <div class="album-detail-modal"> - <div class="modal-header"> - <span class="hidden-artist-title x-artist-title">{{artist.name}}</span> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - <i class="icon-lidarr-monitored x-album-monitored album-monitored" title="Toggle monitored status" /> - {{title}} ({{albumYear}}) - </h3> - - </div> - <div class="modal-body"> - <ul class="nav nav-tabs" id="myTab"> - <li><a href="#album-summary" class="x-album-summary">Summary</a></li> - <li><a href="#album-history" class="x-album-history">History</a></li> - <li><a href="#album-search" class="x-album-search">Search</a></li> - </ul> - <div class="tab-content"> - <div class="tab-pane" id="album-summary"/> - <div class="tab-pane" id="album-history"/> - <div class="tab-pane" id="album-search"/> - </div> - </div> - <div class="modal-footer"> - {{#unless hideArtistLink}} - {{#with artist}} - <a href="{{route}}" class="btn btn-default pull-left" data-dismiss="modal">Go to Artist</a> - {{/with}} - {{/unless}} - - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Album/History/AlbumHistoryActionsCell.js b/src/UI/Album/History/AlbumHistoryActionsCell.js deleted file mode 100644 index 28c7dc840..000000000 --- a/src/UI/Album/History/AlbumHistoryActionsCell.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'album-actions-cell', - - events : { - 'click .x-failed' : '_markAsFailed' - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('eventType') === 'grabbed') { - this.$el.html('<i class="icon-lidarr-delete x-failed" title="Mark download as failed"></i>'); - } - - return this; - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryDetailsCell.js b/src/UI/Album/History/AlbumHistoryDetailsCell.js deleted file mode 100644 index 893aacae8..000000000 --- a/src/UI/Album/History/AlbumHistoryDetailsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); -require('bootstrap'); - -module.exports = NzbDroneCell.extend({ - className : 'album-history-details-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-form-info"></i>'); - - var html = new HistoryDetailsView({ model : this.model }).render().$el; - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Details', - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryLayout.js b/src/UI/Album/History/AlbumHistoryLayout.js deleted file mode 100644 index e9a5b7bb1..000000000 --- a/src/UI/Album/History/AlbumHistoryLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('../../Activity/History/HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var AlbumHistoryActionsCell = require('./AlbumHistoryActionsCell'); -var AlbumHistoryDetailsCell = require('./AlbumHistoryDetailsCell'); -var NoHistoryView = require('./NoHistoryView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/History/AlbumHistoryLayoutTemplate', - - regions : { - historyTable : '.history-table' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : AlbumHistoryDetailsCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : AlbumHistoryActionsCell, - sortable : false - } - ], - - initialize : function(options) { - this.model = options.model; - this.artist = options.artist; - - this.collection = new HistoryCollection({ - albumId : this.model.id, - tableName : 'albumHistory' - }); - this.collection.fetch(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.historyTable.show(new LoadingView()); - }, - - _showTable : function() { - if (this.collection.any()) { - this.historyTable.show(new Backgrid.Grid({ - collection : this.collection, - columns : this.columns, - className : 'table table-hover table-condensed' - })); - } - - else { - this.historyTable.show(new NoHistoryView()); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs b/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs deleted file mode 100644 index 54fb50522..000000000 --- a/src/UI/Album/History/AlbumHistoryLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div class="history-table table-responsive"></div> \ No newline at end of file diff --git a/src/UI/Album/History/NoHistoryView.js b/src/UI/Album/History/NoHistoryView.js deleted file mode 100644 index 42c272b5b..000000000 --- a/src/UI/Album/History/NoHistoryView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/History/NoHistoryViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/History/NoHistoryViewTemplate.hbs b/src/UI/Album/History/NoHistoryViewTemplate.hbs deleted file mode 100644 index f7fd00ec8..000000000 --- a/src/UI/Album/History/NoHistoryViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No history for this album. -</p> \ No newline at end of file diff --git a/src/UI/Album/Search/AlbumSearchLayout.js b/src/UI/Album/Search/AlbumSearchLayout.js deleted file mode 100644 index e4364279b..000000000 --- a/src/UI/Album/Search/AlbumSearchLayout.js +++ /dev/null @@ -1,82 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ButtonsView = require('./ButtonsView'); -var ManualSearchLayout = require('./ManualLayout'); -var ReleaseCollection = require('../../Release/ReleaseCollection'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var NoResultsView = require('./NoResultsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Search/AlbumSearchLayoutTemplate', - - regions : { - main : '#album-search-region' - }, - - events : { - 'click .x-search-auto' : '_searchAuto', - 'click .x-search-manual' : '_searchManual', - 'click .x-search-back' : '_showButtons' - }, - - initialize : function() { - this.mainView = new ButtonsView(); - this.releaseCollection = new ReleaseCollection(); - - this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - }, - - onShow : function() { - if (this.startManualSearch) { - this._searchManual(); - } - - else { - this._showMainView(); - } - }, - - _searchAuto : function(e) { - if (e) { - e.preventDefault(); - } - - CommandController.Execute('albumSearch', { - albumId : this.model.get('id') - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _searchManual : function(e) { - if (e) { - e.preventDefault(); - } - - this.mainView = new LoadingView(); - this._showMainView(); - this.releaseCollection.fetchAlbumReleases(this.model.id); - }, - - _showMainView : function() { - this.main.show(this.mainView); - }, - - _showButtons : function() { - this.mainView = new ButtonsView(); - this._showMainView(); - }, - - _showSearchResults : function() { - if (this.releaseCollection.length === 0) { - this.mainView = new NoResultsView(); - } - - else { - this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); - } - - this._showMainView(); - } -}); \ No newline at end of file diff --git a/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs b/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs deleted file mode 100644 index 236159842..000000000 --- a/src/UI/Album/Search/AlbumSearchLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="album-search-region"></div> \ No newline at end of file diff --git a/src/UI/Album/Search/ButtonsView.js b/src/UI/Album/Search/ButtonsView.js deleted file mode 100644 index 43c6b5e3a..000000000 --- a/src/UI/Album/Search/ButtonsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Search/ButtonsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Search/ButtonsViewTemplate.hbs b/src/UI/Album/Search/ButtonsViewTemplate.hbs deleted file mode 100644 index 9e578f9db..000000000 --- a/src/UI/Album/Search/ButtonsViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="search-buttons"> - <button class="btn btn-lg btn-block x-search-auto"><i class="icon-lidarr-search-automatic"/> Automatic Search</button> - <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-lidarr-search-manual"/> Manual Search</button> -</div> \ No newline at end of file diff --git a/src/UI/Album/Search/ManualLayout.js b/src/UI/Album/Search/ManualLayout.js deleted file mode 100644 index f1f1683d7..000000000 --- a/src/UI/Album/Search/ManualLayout.js +++ /dev/null @@ -1,86 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); -var DownloadReportCell = require('../../Release/DownloadReportCell'); -var AgeCell = require('../../Release/AgeCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var PeersCell = require('../../Release/PeersCell'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Search/ManualLayoutTemplate', - - regions : { - grid : '#album-release-grid' - }, - - columns : [ - { - name : 'protocol', - label : 'Source', - cell : ProtocolCell - }, - { - name : 'age', - label : 'Age', - cell : AgeCell - }, - { - name : 'title', - label : 'Title', - cell : ReleaseTitleCell - }, - { - name : 'indexer', - label : 'Indexer', - cell : Backgrid.StringCell - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell - }, - { - name : 'seeders', - label : 'Peers', - cell : PeersCell - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending', - title : 'Release Rejected' - }, - { - name : 'download', - label : '<i class="icon-lidarr-download" />', - tooltip : 'Auto-Search Prioritization', - cell : DownloadReportCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending' - } - ], - - onShow : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Album/Search/ManualLayoutTemplate.hbs b/src/UI/Album/Search/ManualLayoutTemplate.hbs deleted file mode 100644 index 30513dbd2..000000000 --- a/src/UI/Album/Search/ManualLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div id="album-release-grid" class="table-responsive"></div> -<button class="btn x-search-back">Back</button> \ No newline at end of file diff --git a/src/UI/Album/Search/NoResultsView.js b/src/UI/Album/Search/NoResultsView.js deleted file mode 100644 index 4622e235f..000000000 --- a/src/UI/Album/Search/NoResultsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Search/NoResultsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Search/NoResultsViewTemplate.hbs b/src/UI/Album/Search/NoResultsViewTemplate.hbs deleted file mode 100644 index 7904e5520..000000000 --- a/src/UI/Album/Search/NoResultsViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Album/Summary/AlbumSummaryLayout.js b/src/UI/Album/Summary/AlbumSummaryLayout.js deleted file mode 100644 index f031de8af..000000000 --- a/src/UI/Album/Summary/AlbumSummaryLayout.js +++ /dev/null @@ -1,119 +0,0 @@ -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var TrackFileModel = require('../../Artist/TrackFileModel'); -var TrackFileCollection = require('../../Artist/TrackFileCollection'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var DeleteEpisodeFileCell = require('../../Cells/DeleteEpisodeFileCell'); -var NoFileView = require('./NoFileView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Album/Summary/AlbumSummaryLayoutTemplate', - - regions : { - overview : '.album-overview', - activity : '.album-file-info' - }, - - columns : [ - { - name : 'path', - label : 'Path', - cell : 'string', - sortable : false - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false, - editable : true - }, - { - name : 'this', - label : '', - cell : DeleteEpisodeFileCell, - sortable : false - } - ], - - templateHelpers : {}, - - initialize : function(options) { - if (!this.model.artist) { - this.templateHelpers.artist = options.artist.toJSON(); - } - }, - - onShow : function() { - if (this.model.get('hasFile')) { //TODO Refactor for Albums - var episodeFileId = this.model.get('episodeFileId'); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - this.trackFileCollection = new TrackFileCollection(episodeFile, { seriesId : this.model.get('seriesId') }); - this.listenTo(episodeFile, 'destroy', this._episodeFileDeleted); - - this._showTable(); - } - - else { - this.activity.show(new LoadingView()); - - var self = this; - var newEpisodeFile = new TrackFileModel({ id : episodeFileId }); - this.episodeFileCollection = new TrackFileCollection(newEpisodeFile, { seriesId : this.model.get('seriesId') }); - var promise = newEpisodeFile.fetch(); - this.listenTo(newEpisodeFile, 'destroy', this._trackFileDeleted); - - promise.done(function() { - self._showTable(); - }); - } - - this.listenTo(this.episodeFileCollection, 'add remove', this._collectionChanged); - } - - else { - this._showNoFileView(); - } - }, - - _showTable : function() { - this.activity.show(new Backgrid.Grid({ - collection : this.trackFileCollection, - columns : this.columns, - className : 'table table-bordered', - emptyText : 'Nothing to see here!' - })); - }, - - _showNoFileView : function() { - this.activity.show(new NoFileView()); - }, - - _collectionChanged : function() { - if (!this.trackFileCollection.any()) { - this._showNoFileView(); - } - - else { - this._showTable(); - } - }, - - _trackFileDeleted : function() { - this.model.set({ - trackFileId : 0, - hasFile : false - }); - } -}); \ No newline at end of file diff --git a/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs b/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs deleted file mode 100644 index b64441561..000000000 --- a/src/UI/Album/Summary/AlbumSummaryLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="album-info"> - {{profile profileId}} - <span class="label label-info">{{label}}</span> - <span class="label label-info">{{RelativeDate releaseDate}}</span> -</div> - -<div class="album-overview"> - {{overview}} -</div> - -<div class="album-file-info"></div> diff --git a/src/UI/Album/Summary/NoFileView.js b/src/UI/Album/Summary/NoFileView.js deleted file mode 100644 index facf379d0..000000000 --- a/src/UI/Album/Summary/NoFileView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Album/Summary/NoFileViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Album/Summary/NoFileViewTemplate.hbs b/src/UI/Album/Summary/NoFileViewTemplate.hbs deleted file mode 100644 index 6224d7fa0..000000000 --- a/src/UI/Album/Summary/NoFileViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No file(s) available for this album. -</p> \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioCollectionView.js b/src/UI/AlbumStudio/AlbumStudioCollectionView.js deleted file mode 100644 index b844743c2..000000000 --- a/src/UI/AlbumStudio/AlbumStudioCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var SingleAlbumCell = require('./SingleAlbumCell'); -var AsSortedCollectionView = require('../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : SingleAlbumCell, - - initialize : function(options) { - this.albumCollection = options.collection; - this.artist = options.artist; - }, - - itemViewOptions : function() { - return { - albumCollection : this.albumCollection, - artist : this.artist - }; - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioFooterView.js b/src/UI/AlbumStudio/AlbumStudioFooterView.js deleted file mode 100644 index 2726b0803..000000000 --- a/src/UI/AlbumStudio/AlbumStudioFooterView.js +++ /dev/null @@ -1,116 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Marionette = require('marionette'); -var vent = require('vent'); -var RootFolders = require('../AddArtist/RootFolders/RootFolderCollection'); - -module.exports = Marionette.ItemView.extend({ - template : 'AlbumStudio/AlbumStudioFooterViewTemplate', - - ui : { - artistMonitored : '.x-artist-monitored', - monitor : '.x-monitor', - selectedCount : '.x-selected-count', - container : '.artist-editor-footer', - actions : '.x-action', - indicator : '.x-indicator', - indicatorIcon : '.x-indicator-icon' - }, - - events : { - 'click .x-update' : '_update' - }, - - initialize : function(options) { - this.artistCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.artistCollection, 'backgrid:selected', this._updateInfo); - }, - - onRender : function() { - this._updateInfo(); - }, - - _update : function() { - var self = this; - var selected = this.editorGrid.getSelectedModels(); - var artistMonitored = this.ui.artistMonitored.val(); - var monitoringOptions; - - _.each(selected, function(model) { - if (artistMonitored === 'true') { - model.set('monitored', true); - } else if (artistMonitored === 'false') { - model.set('monitored', false); - } - - monitoringOptions = self._getMonitoringOptions(model); - model.set('addOptions', monitoringOptions); - }); - - var promise = $.ajax({ - url : window.NzbDrone.ApiRoot + '/albumstudio', - type : 'POST', - data : JSON.stringify({ - artist : _.map(selected, function (model) { - return model.toJSON(); - }), - monitoringOptions : monitoringOptions - }) - }); - - this.ui.indicator.show(); - - promise.always(function () { - self.ui.indicator.hide(); - }); - - promise.done(function () { - self.artistCollection.trigger('albumstudio:saved'); - }); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} artists selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _getMonitoringOptions : function(model) { - var monitor = this.ui.monitor.val(); - - if (monitor === 'noChange') { - return null; - } - - model.setAlbumPass(0); - - var options = { - ignoreTracksWithFiles : false, - ignoreTracksWithoutFiles : false, - monitored : true - }; - - if (monitor === 'all') { - return options; - } - - else if (monitor === 'none') { - options.monitored = false; - } - - return options; - } -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs b/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs deleted file mode 100644 index 35987aa7f..000000000 --- a/src/UI/AlbumStudio/AlbumStudioFooterViewTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ -<div class="artist-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitor artist</label> - - <select class="form-control x-action x-artist-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Monitor albums</label> - - <select class="form-control x-action x-monitor"> - <option value="noChange">No change</option> - <option value="all">All</option> - <option value="none">None</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 artists selected</label> - <div> - <button class="btn btn-primary x-action x-update">Update Selected Artist</button> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - </div> - </div> - </div> -</div> diff --git a/src/UI/AlbumStudio/AlbumStudioLayout.js b/src/UI/AlbumStudio/AlbumStudioLayout.js deleted file mode 100644 index aba4256c2..000000000 --- a/src/UI/AlbumStudio/AlbumStudioLayout.js +++ /dev/null @@ -1,147 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -var EmptyView = require('../Artist/Index/EmptyView'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./AlbumStudioFooterView'); -var SelectAllCell = require('../Cells/SelectAllCell'); -var ArtistStatusCell = require('../Cells/ArtistStatusCell'); -var ArtistTitleCell = require('../Cells/ArtistTitleCell'); -var ArtistMonitoredCell = require('../Cells/ArtistMonitoredCell'); -var AlbumsCell = require('./AlbumsCell'); -require('../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/AlbumStudioLayoutTemplate', - - regions : { - toolbar : '#x-toolbar', - artist : '#x-artist' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'monitored', - label : 'Artist', - cell : ArtistMonitoredCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle artist monitored status', - sortable : false - }, - { - name : 'albums', - label : 'Albums', - cell : AlbumsCell, - cellValue : 'this' - } - ], - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', this.render); - this.listenTo(this.artistCollection, 'albumstudio:saved', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'albumstudio.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showTable(); - this._showToolbar(); - this._showFooter(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - right : [this.filteringOptions], - context : this - })); - }, - - _showTable : function() { - if (this.artistCollection.shadowCollection.length === 0) { - this.artist.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.artistCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.artist.show(this.editorGrid); - this._showFooter(); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.artistCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs b/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs deleted file mode 100644 index 7f552b226..000000000 --- a/src/UI/AlbumStudio/AlbumStudioLayoutTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<h1>Album Studio</h1> -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div class="alert alert-info">Album Studio allows you to quickly change the monitored status of albums for all your artists in one place</div> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumsCell.js b/src/UI/AlbumStudio/AlbumsCell.js deleted file mode 100644 index 0c7d7efc0..000000000 --- a/src/UI/AlbumStudio/AlbumsCell.js +++ /dev/null @@ -1,59 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var TemplatedCell = require('../Cells/TemplatedCell'); -var AlbumCollection = require('../Artist/AlbumCollection'); -var LoadingView = require('../Shared/LoadingView'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var AlbumCollectionView = require('./AlbumStudioCollectionView'); -//require('../Handlebars/Helpers/Numbers'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/AlbumsCellTemplate', - tagName : 'td', - - regions : { - albums : '#albums' - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._artistRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - }, - - onRender : function(){ - this._showAlbums(); - }, - - _showAlbums : function() { - var self = this; - - this.albums.show(new LoadingView()); - - this.albumCollection = new AlbumCollection({ artistId : this.model.id }).bindSignalR(); - - $.when(this.albumCollection.fetch()).done(function() { - var albumCollectionView = new AlbumCollectionView({ - collection : self.albumCollection, - artist : self.model - }); - - if (!self.isClosed) { - self.albums.show(albumCollectionView); - } - }); - }, - - -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/AlbumsCellTemplate.hbs b/src/UI/AlbumStudio/AlbumsCellTemplate.hbs deleted file mode 100644 index b05bd454b..000000000 --- a/src/UI/AlbumStudio/AlbumsCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="albums" class="artist-albums"></div> \ No newline at end of file diff --git a/src/UI/AlbumStudio/SingleAlbumCell.js b/src/UI/AlbumStudio/SingleAlbumCell.js deleted file mode 100644 index 2553056fd..000000000 --- a/src/UI/AlbumStudio/SingleAlbumCell.js +++ /dev/null @@ -1,64 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../Cells/TrackMonitoredCell'); -var CommandController = require('../Commands/CommandController'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'AlbumStudio/SingleAlbumCellTemplate', - - ui : { - albumMonitored : '.x-album-monitored' - }, - - events : { - 'click .x-album-monitored' : '_albumMonitored' - }, - - - initialize : function(options) { - this.artist = options.artist; - this.listenTo(this.model, 'sync', this._afterAlbumMonitored); - - }, - - onRender : function() { - this._setAlbumMonitoredState(); - }, - - _albumMonitored : function() { - if (!this.artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.albumMonitored.spinForPromise(savePromise); - }, - - _afterAlbumMonitored : function() { - this.render(); - }, - - _setAlbumMonitoredState : function() { - this.ui.albumMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.albumMonitored.addClass('icon-lidarr-monitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.albumMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-monitored'); - } - } - -}); \ No newline at end of file diff --git a/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs b/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs deleted file mode 100644 index 5296d3b86..000000000 --- a/src/UI/AlbumStudio/SingleAlbumCellTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{#if_eq statistics.totalTrackCount compare=0}} -<span class="single-album album-unaired"> -{{else}} -{{#if_eq statistics.percentOfTracks compare=100}} -<span class="single-album album-all"> -{{else}} -<span class="single-album album-partial"> -{{/if_eq}} -{{/if_eq}} - <span class="label"> - <span class="x-album-monitored album-monitored" title="Toggle album monitored status" data-album-number="{{seasonNumber}}"> - - </span> - <span class="album-number">{{Pad2 title}}</span> - </span><span class="label"> - {{#with statistics}} - {{#if_eq trackCount compare=0}} - <span class="album-status" title="No aired tracks"> </span> - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - <span class="album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}}/{{trackCount}}</span> - {{else}} - <span class="album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}}/{{trackCount}}</span> - {{/if_eq}} - {{/if_eq}} - {{else}} - <span class="album-status" title="No aired tracks"> </span> - {{/with}} - </span> -</span> \ No newline at end of file diff --git a/src/UI/AlbumStudio/albumstudio.less b/src/UI/AlbumStudio/albumstudio.less deleted file mode 100644 index efa157d55..000000000 --- a/src/UI/AlbumStudio/albumstudio.less +++ /dev/null @@ -1,61 +0,0 @@ -@import "../Content/badges.less"; -@import "../Shared/Styles/clickable.less"; - -.artist-albums { - div { - display : inline-block; - padding : 4px; - } -} - -.single-album { - display : inline-block; - margin-bottom : 4px; - - .label { - .badge-inverse(); - - display : inline-block; - padding : 4px; - - font-size : 14px; - height : 25px; - } - - .label:first-child { - border-right : 0px; - border-top-right-radius : 0.0em; - border-bottom-right-radius : 0.0em; - color : #777; - background-color : #eee; - } - - .label:last-child { - border-left : 0px; - border-top-left-radius : 0.0em; - border-bottom-left-radius : 0.0em; - color : #999; - background-color : #f7f7f7; - } - - &.album-all .label:last-child { - background-color : #e0ffe0; - } - - .album-monitored { - width : 16px; - - i { - .clickable(); - } - } - - .album-number { - font-size : 12px; - } - - .album-status { - display : inline-block; - vertical-align : baseline !important; - } -} diff --git a/src/UI/AppLayout.js b/src/UI/AppLayout.js deleted file mode 100644 index 862961423..000000000 --- a/src/UI/AppLayout.js +++ /dev/null @@ -1,20 +0,0 @@ -var Marionette = require('marionette'); -var ModalRegion = require('./Shared/Modal/ModalRegion'); -var ModalRegion2 = require('./Shared/Modal/ModalRegion2'); -var ControlPanelRegion = require('./Shared/ControlPanel/ControlPanelRegion'); - -var Layout = Marionette.Layout.extend({ - regions : { - navbarRegion : '#nav-region', - mainRegion : '#main-region' - }, - - initialize : function() { - this.addRegions({ - modalRegion : ModalRegion, - modalRegion2 : ModalRegion2, - controlPanelRegion : ControlPanelRegion - }); - } -}); -module.exports = new Layout({ el : 'body' }); \ No newline at end of file diff --git a/src/UI/Artist/AlbumCollection.js b/src/UI/Artist/AlbumCollection.js deleted file mode 100644 index ac89d58cf..000000000 --- a/src/UI/Artist/AlbumCollection.js +++ /dev/null @@ -1,43 +0,0 @@ -var Backbone = require('backbone'); -var AlbumModel = require('./AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/album', - model : AlbumModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - this.models = []; - }, - - comparator : function(model1, model2) { - var album1 = model1.get('releaseDate'); - var album2 = model2.get('releaseDate'); - - if (album1 > album2) { - return -1; - } - - if (album1 < album2) { - return 1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/AlbumModel.js b/src/UI/Artist/AlbumModel.js deleted file mode 100644 index ecdda2ce4..000000000 --- a/src/UI/Artist/AlbumModel.js +++ /dev/null @@ -1,8 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - - defaults : { - artistId : 0 - }, -}); \ No newline at end of file diff --git a/src/UI/Artist/ArtistCollection.js b/src/UI/Artist/ArtistCollection.js deleted file mode 100644 index fe4b7d52b..000000000 --- a/src/UI/Artist/ArtistCollection.js +++ /dev/null @@ -1,145 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var ArtistModel = require('./ArtistModel'); -var ApiData = require('../Shared/ApiData'); -var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); -var moment = require('moment'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/artist', - model : ArtistModel, - tableName : 'artist', - - state : { - sortKey : 'sortName', - order : -1, - pageSize : 100000, - secondarySortKey : 'sortName', - secondarySortOrder : -1 - }, - - mode : 'client', - - importFromList : function(models) { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/import', - - toJSON : function() { - return models; - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true}); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - save : function() { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/editor', - - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'continuing' : [ - 'status', - 'continuing' - ], - 'ended' : [ - 'status', - 'ended' - ], - 'monitored' : [ - 'monitored', - true - ], - 'missing' : [ - null, - null, - function(model) { return model.get('trackCount') !== model.get('trackFileCount'); } - ] - }, - - sortMappings : { - title : { - sortKey : 'sortName' - }, - - artistName: { - sortKey : 'name' - }, - - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (order === 1) { - return 0; - } - - return Number.MAX_VALUE; - } - }, - - percentOfTracks : { - sortValue : function(model, attr) { - var percentOfTracks = model.get(attr); - var trackCount = model.get('trackCount'); - - return percentOfTracks + trackCount / 1000000; - } - }, - - path : { - sortValue : function(model) { - var path = model.get('path'); - - return path.toLowerCase(); - } - } - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -var data = ApiData.get('artist'); // TOOD: Build backend for artist - -module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Artist/ArtistController.js b/src/UI/Artist/ArtistController.js deleted file mode 100644 index 2f54bc2cf..000000000 --- a/src/UI/Artist/ArtistController.js +++ /dev/null @@ -1,36 +0,0 @@ -var NzbDroneController = require('../Shared/NzbDroneController'); -var AppLayout = require('../AppLayout'); -var ArtistCollection = require('./ArtistCollection'); -var ArtistIndexLayout = require('./Index/ArtistIndexLayout'); -var ArtistDetailsLayout = require('./Details/ArtistDetailsLayout'); - -module.exports = NzbDroneController.extend({ - _originalInit : NzbDroneController.prototype.initialize, - - initialize : function() { - this.route('', this.artist); - this.route('artist', this.artist); - this.route('artist/:query', this.artistDetails); - - this._originalInit.apply(this, arguments); - }, - - artist : function() { - this.setTitle('Lidarr'); - this.showMainRegion(new ArtistIndexLayout()); - }, - - artistDetails : function(query) { - var artists = ArtistCollection.where({ nameSlug : query }); - console.log('artistDetails, artists: ', artists); - if (artists.length !== 0) { - var targetArtist = artists[0]; - console.log("[ArtistController] targetArtist: ", targetArtist); - this.setTitle(targetArtist.get('name')); // TODO: Update NzbDroneController - //this.setArtistName(targetSeries.get('artistName')); - this.showMainRegion(new ArtistDetailsLayout({ model : targetArtist })); - } else { - this.showNotFound(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/ArtistModel.js b/src/UI/Artist/ArtistModel.js deleted file mode 100644 index 8f6a225b8..000000000 --- a/src/UI/Artist/ArtistModel.js +++ /dev/null @@ -1,31 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/artist', - - defaults : { - trackFileCount : 0, - trackCount : 0, - isExisting : false, - status : 0 - }, - - setAlbumsMonitored : function(albumId) { - _.each(this.get('albums'), function(album) { - if (album.albumId === albumId) { - album.monitored = !album.monitored; - } - }); - }, - - setAlbumPass : function(monitored) { - _.each(this.get('albums'), function(album) { - if (monitored === 0) { - album.monitored = true; - } else { - album.monitored = false; - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Delete/DeleteArtistTemplate.hbs b/src/UI/Artist/Delete/DeleteArtistTemplate.hbs deleted file mode 100644 index 815140776..000000000 --- a/src/UI/Artist/Delete/DeleteArtistTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete {{name}}</h3> - </div> - <div class="modal-body delete-artist-modal"> - - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - <h3 class="path">{{path}}</h3> - - <div class="form-group"> - <label class="col-sm-4 control-label">Delete all files</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-delete-files"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to delete all files from disk?"/> - <i class="icon-lidarr-form-warning" title="This option is irreversible, use with extreme caution"/> - </span> - </div> - </div> - </div> - <div class="col-md-offset-1 col-md-5 delete-files-info x-delete-files-info"> - {{trackFileCount}} track files will be deleted - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Artist/Delete/DeleteArtistView.js b/src/UI/Artist/Delete/DeleteArtistView.js deleted file mode 100644 index f71c1cfa8..000000000 --- a/src/UI/Artist/Delete/DeleteArtistView.js +++ /dev/null @@ -1,41 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Delete/DeleteArtistTemplate', - - events : { - 'click .x-confirm-delete' : 'removeSeries', - 'change .x-delete-files' : 'changeDeletedFiles' - }, - - ui : { - deleteFiles : '.x-delete-files', - deleteFilesInfo : '.x-delete-files-info', - indicator : '.x-indicator' - }, - - removeSeries : function() { - var self = this; - var deleteFiles = this.ui.deleteFiles.prop('checked'); - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'deleteFiles' : deleteFiles }, - wait : true - }).done(function() { - vent.trigger(vent.Events.SeriesDeleted, { series : self.model }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - }, - - changeDeletedFiles : function() { - var deleteFiles = this.ui.deleteFiles.prop('checked'); - - if (deleteFiles) { - this.ui.deleteFilesInfo.show(); - } else { - this.ui.deleteFilesInfo.hide(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumCollectionView.js b/src/UI/Artist/Details/AlbumCollectionView.js deleted file mode 100644 index ad6b4b6ec..000000000 --- a/src/UI/Artist/Details/AlbumCollectionView.js +++ /dev/null @@ -1,46 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var AlbumLayout = require('./AlbumLayout'); -var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : AlbumLayout, - - initialize : function(options) { - if (!options.trackCollection) { - throw 'trackCollection is needed'; - } - - this.albumCollection = options.collection; - this.trackCollection = options.trackCollection; - this.artist = options.artist; - }, - - itemViewOptions : function() { - return { - albumCollection : this.albumCollection, - trackCollection : this.trackCollection, - artist : this.artist - }; - }, - - onTrackGrabbed : function(message) { - if (message.track.artist.id !== this.trackCollection.artistId) { - return; - } - - var self = this; - - _.each(message.track.tracks, function(track) { - var ep = self.TrackCollection.get(track.id); - ep.set('downloading', true); - }); - - this.render(); - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumInfoView.js b/src/UI/Artist/Details/AlbumInfoView.js deleted file mode 100644 index 0ecbe5d29..000000000 --- a/src/UI/Artist/Details/AlbumInfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Details/AlbumInfoViewTemplate', - - initialize : function(options) { - - this.listenTo(this.model, 'change', this.render); - }, - - templateHelpers : function() { - return { - durationMin : FormatHelpers.timeMinSec(this.model.get('duration')) - }; - } - -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs b/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs deleted file mode 100644 index 2f8aebd67..000000000 --- a/src/UI/Artist/Details/AlbumInfoViewTemplate.hbs +++ /dev/null @@ -1,42 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if label}} - <span class="label label-info">{{label}}</span> - {{/if}} - - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{durationMin}} minutes</span> - </div> - <div class="col-md-9"> - <span class="album-info-links"> - <a href="{{MBAlbumUrl}}" class="label label-info">MusicBrainz</a> - - {{#if tadbId}} - <a href="{{TADBAlbumUrl}}" class="label label-info">The AudioDB</a> - {{/if}} - - {{#if discogsId}} - <a href="{{discogsAlbumUrl}}" class="label label-info">Discogs</a> - {{/if}} - - {{#if amId}} - <a href="{{allMusicAlbumUrl}}" class="label label-info">AllMusic</a> - {{/if}} - </span> - </div> -</div> - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Artist/Details/AlbumLayout.js b/src/UI/Artist/Details/AlbumLayout.js deleted file mode 100644 index 11686631b..000000000 --- a/src/UI/Artist/Details/AlbumLayout.js +++ /dev/null @@ -1,348 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../../Cells/TrackMonitoredCell'); -var TrackTitleCell = require('../../Cells/TrackTitleCell'); -var TrackExplicitCell = require('../../Cells/TrackExplicitCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var TrackStatusCell = require('../../Cells/TrackStatusCell'); -var TrackActionsCell = require('../../Cells/TrackActionsCell'); -var TrackNumberCell = require('./TrackNumberCell'); -var TrackWarningCell = require('./TrackWarningCell'); -var TrackRatingCell = require('./TrackRatingCell'); -var TrackDurationCell = require('../../Cells/TrackDurationCell'); -var AlbumInfoView = require('./AlbumInfoView'); -var CommandController = require('../../Commands/CommandController'); -//var TrackFileEditorLayout = require('../../TrackFile/Editor/TrackFileEditorLayout'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Details/AlbumLayoutTemplate', - - ui : { - albumSearch : '.x-album-search', - albumMonitored : '.x-album-monitored', - albumRename : '.x-album-rename', - albumDetails : '.x-album-details', - cover : '.x-album-cover' - }, - - events : { - 'click .x-track-file-editor' : '_openTrackFileEditor', - 'click .x-album-monitored' : '_albumMonitored', - 'click .x-album-search' : '_albumSearch', - 'click .x-album-rename' : '_albumRename', - 'click .x-album-details' : '_albumDetails', - 'click .x-show-hide-tracks' : '_showHideTracks', - 'dblclick .artist-album h2' : '_showHideTracks' - }, - - regions : { - trackGrid : '.x-track-grid', - albumInfo : '#album-info' - }, - - columns : [ - { - name : 'monitored', - label : '', - cell : ToggleCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle monitored status', - sortable : false - }, - { - name : 'trackNumber', - label : '#', - cell : TrackNumberCell - }, - { - name : 'this', - label : '', - cell : TrackWarningCell, - sortable : false, - className : 'track-warning-cell' - }, - { - name : 'this', - label : 'Title', - hideArtistLink : true, - cell : TrackTitleCell, - sortable : false - }, - { - name : 'this', - label : 'Rating', - cell : TrackRatingCell - }, - { - name : 'this', - label : 'Content', - cell : TrackExplicitCell - }, - //{ - // name : 'airDateUtc', - // label : 'Air Date', - // cell : RelativeDateCell - //}, - { - name : 'duration', - label : 'Duration', - cell : TrackDurationCell, - sortable : false - }, - { - name : 'status', - label : 'Status', - cell : TrackStatusCell, - sortable : false - } - //{ - // name : 'this', - // label : '', - // cell : TrackActionsCell, - // sortable : false - //} - ], - - templateHelpers : function() { - var trackCount = this.trackCollection.filter(function(track) { - return track.get('hasFile') || track.get('monitored'); - }).length; - - var trackFileCount = this.trackCollection.where({ hasFile : true }).length; - var percentOfTracks = 100; - - if (trackCount > 0) { - percentOfTracks = trackFileCount / trackCount * 100; - } - - return { - showingTracks : this.showingTracks, - trackCount : trackCount, - trackFileCount : trackFileCount, - percentOfTracks : percentOfTracks - }; - }, - - initialize : function(options) { - if (!options.trackCollection) { - throw 'trackCollection is required'; - } - - this.artist = options.artist; - this.fullTrackCollection = options.trackCollection; - - this.trackCollection = this.fullTrackCollection.byAlbum(this.model.get('id')); - this._updateTrackCollection(); - - this.showingTracks = this._shouldShowTracks(); - - this.listenTo(this.model, 'sync', this._afterAlbumMonitored); - this.listenTo(this.trackCollection, 'sync', this.render); - this.listenTo(this.fullTrackCollection, 'sync', this._refreshTracks); - this.listenTo(this.model, 'change:images', this._updateImages); - }, - - onRender : function() { - if (this.showingTracks) { - this._showTracks(); - } - - this._showAlbumInfo(); - - this._setAlbumMonitoredState(); - - CommandController.bindToCommand({ - element : this.ui.albumSearch, - command : { - name : 'albumSearch', - artistId : this.artist.id, - albumIds : [this.model.get('id')] - } - }); - - CommandController.bindToCommand({ - element : this.ui.albumRename, - command : { - name : 'renameFiles', - artistId : this.artist.id, - albumId : this.model.get('id') - } - }); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _albumSearch : function() { - CommandController.Execute('albumSearch', { - name : 'albumSearch', - artistId : this.artist.id, - albumIds : [this.model.get('id')] - }); - }, - - _albumRename : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { - artist : this.artist, - albumId : this.model.get('id') - }); - }, - - _albumDetails : function() { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : this.model }); - }, - - _albumMonitored : function() { - if (!this.artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - //var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.albumMonitored.spinForPromise(savePromise); - }, - - _afterAlbumMonitored : function() { - - this.render(); - }, - - _setAlbumMonitoredState : function() { - this.ui.albumMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.albumMonitored.addClass('icon-lidarr-monitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.albumMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.albumMonitored.removeClass('icon-lidarr-monitored'); - } - }, - - _showTracks : function() { - this.trackGrid.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.trackCollection, - className : 'table table-hover track-grid' - })); - }, - - _showAlbumInfo : function() { - this.albumInfo.show(new AlbumInfoView({ - model : this.model - })); - }, - - _shouldShowTracks : function() { - var startDate = moment().add('month', -1); - var endDate = moment().add('year', 1); - return true; - //return this.trackCollection.some(function(track) { - // var airDate = track.get('releasedDate'); - - // if (airDate) { - // var airDateMoment = moment(airDate); - - // if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { - // return true; - // } - // } - - // return false; - //}); - }, - - _showHideTracks : function() { - if (this.showingTracks) { - this.showingTracks = false; - this.trackGrid.close(); - } else { - this.showingTracks = true; - this._showTracks(); - } - - this.templateHelpers.showingTracks = this.showingTracks; - this.render(); - }, - - _trackMonitoredToggled : function(options) { - var model = options.model; - var shiftKey = options.shiftKey; - - if (!this.trackCollection.get(model.get('id'))) { - return; - } - - if (!shiftKey) { - return; - } - - var lastToggled = this.trackCollection.lastToggled; - - if (!lastToggled) { - return; - } - - var currentIndex = this.trackCollection.indexOf(model); - var lastIndex = this.trackCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - this.trackCollection.lastToggled = model; - }, - - _updateTrackCollection : function() { - var self = this; - - this.trackCollection.add(this.fullTrackCollection.byAlbum(this.model.get('albumId')).models, { merge : true }); - - this.trackCollection.each(function(model) { - model.trackCollection = self.trackCollection; - }); - }, - - _updateImages : function () { - var cover = this._getImage('cover'); - - if (cover) { - this.ui.poster.attr('src', cover); - } - }, - - _refreshTracks : function() { - this._updateTrackCollection(); - this.trackCollection.fullCollection.sort(); - this.render(); - }, - - _openTrackFileEditor : function() { - //var view = new TrackFileEditorLayout({ - // model : this.model, - // artist : this.artist, - // trackCollection : this.trackCollection - //}); - - //vent.trigger(vent.Commands.OpenModalCommand, view); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/AlbumLayoutTemplate.hbs b/src/UI/Artist/Details/AlbumLayoutTemplate.hbs deleted file mode 100644 index 6cd18d5d6..000000000 --- a/src/UI/Artist/Details/AlbumLayoutTemplate.hbs +++ /dev/null @@ -1,69 +0,0 @@ -<div class="artist-album" id="album-{{title}}"> - <div class="visible-lg col-lg-2 poster"> - {{cover}} - </div> - <div class="col-md-12 col-lg-10"> - <h2 class="header-text"> - <i class="x-album-monitored album-monitored clickable" title="Toggle album monitored status"/> - - {{#if title}} - <div class="x-album-details album-details"> - {{title}} ({{albumYear}}) - </div> - {{else}} - Specials - {{/if}} - - - {{#if_eq trackCount compare=0}} - {{#if monitored}} - <span class="badge badge-primary album-status" title="No aired tracks"> </span> - {{else}} - <span class="badge badge-warning album-status" title="Album is not monitored"> </span> - {{/if}} - {{else}} - {{#if_eq percentOfTracks compare=100}} - <span class="badge badge-success album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span> - {{else}} - <span class="badge badge-danger album-status" title="{{trackFileCount}}/{{trackCount}} tracks downloaded">{{trackFileCount}} / {{trackCount}} Tracks</span> - {{/if_eq}} - {{/if_eq}} - - <span class="album-actions pull-right"> - <div class="x-track-file-editor"> - <i class="icon-lidarr-track-file" title="Modify track files for album"/> - </div> - <div class="x-album-rename"> - <i class="icon-lidarr-rename" title="Preview rename for album {{albumId}}"/> - </div> - <div class="x-album-search"> - <i class="icon-lidarr-search" title="Search for monitored tracks in album {{albumId}}"/> - </div> - </span> - - </h2> - - <div class="album-detail-release"> - Release Date: {{albumReleaseDate}} - </div> - <div class="album-detail-type"> - Type: {{albumType}} - </div> - - <div id="album-info" class="album-info"></div> - </div> - - <div class="show-hide-tracks x-show-hide-tracks"> - <h4> - {{#if showingTracks}} - <i class="icon-lidarr-panel-hide"/> - Hide Tracks - {{else}} - <i class="icon-lidarr-panel-show"/> - Show Tracks - {{/if}} - </h4> - </div> - - <div class="x-track-grid table-responsive"></div> -</div> diff --git a/src/UI/Artist/Details/ArtistDetailsLayout.js b/src/UI/Artist/Details/ArtistDetailsLayout.js deleted file mode 100644 index 1bd338a22..000000000 --- a/src/UI/Artist/Details/ArtistDetailsLayout.js +++ /dev/null @@ -1,246 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var ArtistCollection = require('../ArtistCollection'); -var TrackCollection = require('../TrackCollection'); -var TrackFileCollection = require('../TrackFileCollection'); -var AlbumCollection = require('../AlbumCollection'); -var AlbumCollectionView = require('./AlbumCollectionView'); -var InfoView = require('./InfoView'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var TrackFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -require('backstrech'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - itemViewContainer : '.x-artist-albums', - template : 'Artist/Details/ArtistDetailsTemplate', - - regions : { - albums : '#albums', - info : '#info' - }, - - ui : { - header : '.x-header', - monitored : '.x-monitored', - edit : '.x-edit', - refresh : '.x-refresh', - rename : '.x-rename', - search : '.x-search', - poster : '.x-artist-poster' - }, - - events : { - 'click .x-track-file-editor' : '_openTrackFileEditor', - 'click .x-monitored' : '_toggleMonitored', - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist', - 'click .x-rename' : '_renameArtist', - 'click .x-search' : '_artistSearch' - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._artistRemoved); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - - this.listenTo(this.model, 'change:images', this._updateImages); - }, - - onShow : function() { - this._showBackdrop(); - this._showAlbums(); - this._setMonitoredState(); - this._showInfo(); - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist' - } - }); - CommandController.bindToCommand({ - element : this.ui.search, - command : { - name : 'artistSearch' - } - }); - - CommandController.bindToCommand({ - element : this.ui.rename, - command : { - name : 'renameFiles', - artistId : this.model.id, - albumId : -1 - } - }); - }, - - onClose : function() { - if (this._backstrech) { - this._backstrech.destroy(); - delete this._backstrech; - } - - $('body').removeClass('backdrop'); - reqres.removeHandler(reqres.Requests.GetEpisodeFileById); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _toggleMonitored : function() { - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.monitored.spinForPromise(savePromise); - }, - - _setMonitoredState : function() { - var monitored = this.model.get('monitored'); - - this.ui.monitored.removeAttr('data-idle-icon'); - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (monitored) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - this.$el.removeClass('artist-not-monitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - this.$el.addClass('artist-not-monitored'); - } - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - }, - - _artistRemoved : function() { - Backbone.history.navigate('/', { trigger : true }); - }, - - _renameArtist : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { artist : this.model }); - }, - - _artistSearch : function() { - console.log('_artistSearch:', this.model); - CommandController.Execute('artistSearch', { - name : 'artistSearch', - artistId : this.model.id - }); - }, - - _showAlbums : function() { - var self = this; - - this.albums.show(new LoadingView()); - - this.albumCollection = new AlbumCollection({ artistId : this.model.id }).bindSignalR(); - - this.trackCollection = new TrackCollection({ artistId : this.model.id }).bindSignalR(); - this.trackFileCollection = new TrackFileCollection({ artistId : this.model.id }).bindSignalR(); - - reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(trackFileId) { - return self.trackFileCollection.get(trackFileId); - }); - - - $.when(this.albumCollection.fetch(), this.trackCollection.fetch(), this.trackFileCollection.fetch()).done(function() { - var albumCollectionView = new AlbumCollectionView({ - collection : self.albumCollection, - trackCollection : self.trackCollection, - artist : self.model - }); - - if (!self.isClosed) { - self.albums.show(albumCollectionView); - } - }); - }, - - _showInfo : function() { - this.info.show(new InfoView({ - model : this.model, - trackFileCollection : this.trackFileCollection - })); - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'renamefiles') { - if (options.command.get('artistId') === this.model.get('id')) { - this._refresh(); - } - } - }, - - _refresh : function() { - this.albumCollection.fetch(); - this.trackCollection.fetch(); - this.trackFileCollection.fetch(); - - this._setMonitoredState(); - this._showInfo(); - }, - - _openTrackFileEditor : function() { - var view = new TrackFileEditorLayout({ - artist : this.model, - trackCollection : this.trackCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - }, - - _updateImages : function () { - var poster = this._getImage('poster'); - - if (poster) { - this.ui.poster.attr('src', poster); - } - - this._showBackdrop(); - }, - - _showBackdrop : function () { - $('body').addClass('backdrop'); - var fanArt = this._getImage('fanart'); - - if (fanArt) { - this._backstrech = $.backstretch(fanArt); - } else { - $('body').removeClass('backdrop'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs b/src/UI/Artist/Details/ArtistDetailsTemplate.hbs deleted file mode 100644 index 2e72f1127..000000000 --- a/src/UI/Artist/Details/ArtistDetailsTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="row artist-page-header"> - <div class="visible-lg col-lg-2 poster"> - {{poster}} - </div> - <div class="col-md-12 col-lg-10"> - <div> - <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire artist"/> - {{name}} - <div class="artist-actions pull-right"> - <div class="x-track-file-editor"> - <i class="icon-lidarr-track-file" title="Modify track files for artist"/> - </div> - <div class="x-refresh"> - <i class="icon-lidarr-refresh icon-can-spin" title="Update artist info and scan disk"/> - </div> - <div class="x-rename"> - <i class="icon-lidarr-rename" title="Preview rename for all albums"/> - </div> - <div class="x-search"> - <i class="icon-lidarr-search" title="Search for monitored albums in this artist"/> - </div> - <div class="x-edit"> - <i class="icon-lidarr-edit" title="Edit artist"/> - </div> - </div> - </h1> - </div> - <div class="artist-detail-overview"> - {{overview}} - </div> - <div id="info" class="artist-info"></div> - </div> -</div> -<div id="albums"></div> diff --git a/src/UI/Artist/Details/InfoView.js b/src/UI/Artist/Details/InfoView.js deleted file mode 100644 index 74ddb2bf4..000000000 --- a/src/UI/Artist/Details/InfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Details/InfoViewTemplate', - - initialize : function(options) { - this.trackFileCollection = options.trackFileCollection; - - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.trackFileCollection, 'sync', this.render); - }, - - templateHelpers : function() { - return { - fileCount : this.trackFileCollection.length - }; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/InfoViewTemplate.hbs b/src/UI/Artist/Details/InfoViewTemplate.hbs deleted file mode 100644 index 2075d3d8f..000000000 --- a/src/UI/Artist/Details/InfoViewTemplate.hbs +++ /dev/null @@ -1,70 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if network}} - <span class="label label-info">{{network}}</span> - {{/if}} - - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{Bytes sizeOnDisk}}</span> - - {{#if_eq fileCount compare="1"}} - <span class="label label-info"> 1 file</span> - {{else}} - <span class="label label-info"> {{fileCount}} files</span> - {{/if_eq}} - - {{#if_eq status compare="continuing"}} - <span class="label label-info">Continuing</span> - {{else}} - <span class="label label-default">Ended</span> - {{/if_eq}} - </div> - <div class="col-md-3"> - <span class="artist-info-links"> - <a href="{{MBUrl}}" class="label label-info">MusicBrainz</a> - - {{#if tadbId}} - <a href="{{TADBUrl}}" class="label label-info">The AudioDB</a> - {{/if}} - - {{#if discogsId}} - <a href="{{discogsUrl}}" class="label label-info">Discogs</a> - {{/if}} - - {{#if allMusicId}} - <a href="{{allMusicUrl}}" class="label label-info">AllMusic</a> - {{/if}} - </span> - </div> -</div> - -{{#if alternateTitles}} -<div class="row"> - <div class="col-md-12"> - {{#each alternateTitles}} - {{#if_eq seasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - - {{#if_eq sceneSeasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - {{/each}} - </div> -</div> -{{/if}} - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Artist/Details/TrackNumberCell.js b/src/UI/Artist/Details/TrackNumberCell.js deleted file mode 100644 index c085e1d15..000000000 --- a/src/UI/Artist/Details/TrackNumberCell.js +++ /dev/null @@ -1,43 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var reqres = require('../../reqres'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-number-cell', - template : 'Artist/Details/TrackNumberCellTemplate', - - render : function() { - this.$el.empty(); - this.$el.html(this.model.get('trackNumber')); - - var artist = ArtistCollection.get(this.model.get('artistId')); - - var alternateTitles = []; - - if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { - alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); - } - - if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var json = this.model.toJSON(); - json.alternateTitles = alternateTitles; - - var html = this.templateFunction(json); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Scene Information', - placement : 'right', - container : this.$el - }); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackNumberCellTemplate.hbs b/src/UI/Artist/Details/TrackNumberCellTemplate.hbs deleted file mode 100644 index a9028a423..000000000 --- a/src/UI/Artist/Details/TrackNumberCellTemplate.hbs +++ /dev/null @@ -1,39 +0,0 @@ -<div class="scene-info"> - {{#if sceneSeasonNumber}} - <div class="row"> - <div class="key">Season</div> - <div class="value">{{sceneSeasonNumber}}</div> - </div> - {{/if}} - - {{#if sceneEpisodeNumber}} - <div class="row"> - <div class="key">Episode</div> - <div class="value">{{sceneEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if sceneAbsoluteEpisodeNumber}} - <div class="row"> - <div class="key">Absolute</div> - <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if alternateTitles}} - <div class="row"> - {{#if_gt alternateTitles.length compare="1"}} - <div class="key">Titles</div> - {{else}} - <div class="key">Title</div> - {{/if_gt}} - <div class="value"> - <ul> - {{#each alternateTitles}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - </div> - {{/if}} -</div> \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackRatingCell.js b/src/UI/Artist/Details/TrackRatingCell.js deleted file mode 100644 index 934ac30fa..000000000 --- a/src/UI/Artist/Details/TrackRatingCell.js +++ /dev/null @@ -1,19 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-rating-cell', - - - render : function() { - this.$el.empty(); - var ratings = this.model.get('ratings'); - - if (ratings) { - this.$el.html(ratings.value + ' (' + ratings.votes + ' votes)'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Details/TrackWarningCell.js b/src/UI/Artist/Details/TrackWarningCell.js deleted file mode 100644 index 22098d1a8..000000000 --- a/src/UI/Artist/Details/TrackWarningCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var ArtistCollection = require('../ArtistCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'track-warning-cell', - - render : function() { - this.$el.empty(); - - if (this.model.get('unverifiedSceneNumbering')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); - } - - else if (ArtistCollection.get(this.model.get('artistId')).get('artistType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Track does not have an absolute track number"></i>'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Edit/EditArtistView.js b/src/UI/Artist/Edit/EditArtistView.js deleted file mode 100644 index dc0f8732a..000000000 --- a/src/UI/Artist/Edit/EditArtistView.js +++ /dev/null @@ -1,54 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Profiles = require('../../Profile/ProfileCollection'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); -require('../../Mixins/TagInput'); -require('../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Artist/Edit/EditArtistViewTemplate', - - ui : { - profile : '.x-profile', - path : '.x-path', - tags : '.x-tags' - }, - - events : { - 'click .x-remove' : '_removeArtist' - }, - - initialize : function() { - this.model.set('profiles', Profiles); - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onBeforeSave : function() { - var profileId = this.ui.profile.val(); - this.model.set({ profileId : profileId }); - }, - - _onAfterSave : function() { - this.trigger('saved'); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _removeArtist : function() { - vent.trigger(vent.Commands.DeleteArtistCommand, { artist : this.model }); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Artist/Edit/EditArtistViewTemplate.hbs b/src/UI/Artist/Edit/EditArtistViewTemplate.hbs deleted file mode 100644 index 666e9b673..000000000 --- a/src/UI/Artist/Edit/EditArtistViewTemplate.hbs +++ /dev/null @@ -1,104 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{name}}</h3> - </div> - <div class="modal-body edit-artist-modal"> - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - - <div class="form-group"> - <label class="col-sm-4 control-label">Monitored</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="monitored"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr download albums for this artist?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Use Album Folder</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="albumFolder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should downloaded albums be stored in album folders?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Profile</label> - - <div class="col-sm-4"> - <select class="form-control x-profile" id="inputProfile" name="profileId"> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Artist Type</label> - <div class="col-sm-4"> - {{> ArtistTypeSelectionPartial}} - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Path</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-path" placeholder="Path" name="path"> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Tags</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger pull-left x-remove">Delete</button> - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Artist/Editor/ArtistEditorFooterView.js b/src/UI/Artist/Editor/ArtistEditorFooterView.js deleted file mode 100644 index b32fdc7cc..000000000 --- a/src/UI/Artist/Editor/ArtistEditorFooterView.js +++ /dev/null @@ -1,126 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var vent = require('vent'); -var Profiles = require('../../Profile/ProfileCollection'); -var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection'); -var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout'); -var UpdateFilesArtistView = require('./Organize/OrganizeFilesView'); -var Config = require('../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Editor/ArtistEditorFooterViewTemplate', - - ui : { - monitored : '.x-monitored', - profile : '.x-profiles', - albumFolder : '.x-album-folder', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count', - container : '.artist-editor-footer', - actions : '.x-action' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged', - 'click .x-organize-files' : '_organizeFiles' - }, - - templateHelpers : function() { - return { - profiles : Profiles, - rootFolders : RootFolders.toJSON() - }; - }, - - initialize : function(options) { - this.artistCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.artistCollection, 'backgrid:selected', this._updateInfo); - this.listenTo(RootFolders, 'all', this.render); - }, - - onRender : function() { - this._updateInfo(); - }, - - _updateAndSave : function() { - var selected = this.editorGrid.getSelectedModels(); - - var monitored = this.ui.monitored.val(); - var profile = this.ui.profile.val(); - var albumFolder = this.ui.albumFolder.val(); - var rootFolder = this.ui.rootFolder.val(); - - _.each(selected, function(model) { - if (monitored === 'true') { - model.set('monitored', true); - } else if (monitored === 'false') { - model.set('monitored', false); - } - - if (profile !== 'noChange') { - model.set('profileId', parseInt(profile, 10)); - } - - if (albumFolder === 'true') { - model.set('albumFolder', true); - } else if (albumFolder === 'false') { - model.set('albumFolder', false); - } - - if (rootFolder !== 'noChange') { - var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); - - model.set('rootFolderPath', rootFolderPath.get('path')); - } - - model.edited = true; - }); - - this.artistCollection.save(); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} artist selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _organizeFiles : function() { - var selected = this.editorGrid.getSelectedModels(); - var updateFilesArtistView = new UpdateFilesArtistView({ artist : selected }); - this.listenToOnce(updateFilesArtistView, 'updatingFiles', this._afterSave); - - vent.trigger(vent.Commands.OpenModalCommand, updateFilesArtistView); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs b/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs deleted file mode 100644 index 6f92ca1a4..000000000 --- a/src/UI/Artist/Editor/ArtistEditorFooterViewTemplate.hbs +++ /dev/null @@ -1,54 +0,0 @@ -<div class="artist-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitored</label> - - <select class="form-control x-action x-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - - <select class="form-control x-action x-profiles"> - <option value="noChange">No change</option> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - </div> - - <div class="form-group col-md-2"> - <label>Album Folder</label> - - <select class="form-control x-action x-album-folder"> - <option value="noChange">No change</option> - <option value="true">Yes</option> - <option value="false">No</option> - </select> - </div> - - <div class="form-group col-md-3"> - <label>Root Folder</label> - - <select class="form-control x-action x-root-folder" validation-name="RootFolderPath"> - <option value="noChange">No change</option> - {{#each rootFolders}} - <option value="{{id}}">{{path}}</option> - {{/each}} - <option value="addNew">Add a different path</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 artist selected</label> - <div> - <button class="btn btn-primary x-action x-save">Save</button> - <button class="btn btn-danger x-action x-organize-files" title="Organize and rename track files">Organize</button> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Editor/ArtistEditorLayout.js b/src/UI/Artist/Editor/ArtistEditorLayout.js deleted file mode 100644 index 7325e82a4..000000000 --- a/src/UI/Artist/Editor/ArtistEditorLayout.js +++ /dev/null @@ -1,185 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EmptyView = require('../Index/EmptyView'); -var ArtistCollection = require('../ArtistCollection'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var ArtistStatusCell = require('../../Cells/ArtistStatusCell'); -var AlbumFolderCell = require('../../Cells/AlbumFolderCell'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./ArtistEditorFooterView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Editor/ArtistEditorLayoutTemplate', - - regions : { - artistRegion : '#x-artist-editor', - toolbar : '#x-toolbar' - }, - - ui : { - monitored : '.x-monitored', - profiles : '.x-profiles', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'name', - label : 'Artist', - cell : ArtistTitleCell, - cellValue : 'this' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'albumFolder', - label : 'Album Folder', - cell : AlbumFolderCell - }, - { - name : 'path', - label : 'Path', - cell : 'string' - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - items : [ - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshartist', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'save', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'artisteditor.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showToolbar(); - this._showTable(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showTable : function() { - if (this.artistCollection.shadowCollection.length === 0) { - this.artistRegion.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.artistCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.artistRegion.show(this.editorGrid); - this._showFooter(); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [ - this.leftSideButtons - ], - right : [ - this.filteringOptions - ], - context : this - })); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.artistCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs b/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs deleted file mode 100644 index 17746236f..000000000 --- a/src/UI/Artist/Editor/ArtistEditorLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist-editor" class="table-responsive"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Artist/Editor/Organize/OrganizeFilesView.js b/src/UI/Artist/Editor/Organize/OrganizeFilesView.js deleted file mode 100644 index bdd478c4c..000000000 --- a/src/UI/Artist/Editor/Organize/OrganizeFilesView.js +++ /dev/null @@ -1,33 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Artist/Editor/Organize/OrganizeFilesViewTemplate', - - events : { - 'click .x-confirm-organize' : '_organize' - }, - - initialize : function(options) { - this.artist = options.artist; - this.templateHelpers = { - numberOfArtists : this.artist.length, - artist : new Backbone.Collection(this.artist).toJSON() - }; - }, - - _organize : function() { - var artistIds = _.pluck(this.artist, 'id'); - - CommandController.Execute('renameArtist', { - name : 'renameArtist', - artistIds : artistIds - }); - - this.trigger('organizingFiles'); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs b/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs deleted file mode 100644 index 07d548748..000000000 --- a/src/UI/Artist/Editor/Organize/OrganizeFilesViewTemplate.hbs +++ /dev/null @@ -1,25 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Organize of Selected Artist(s)</h3> - </div> - <div class="modal-body update-files-artist-modal"> - <div class="alert alert-info"> - <button type="button" class="close" data-dismiss="alert">×</button> - Tip: To preview a rename... select "Cancel" then any artist title and use the <i data-original-title="" class="icon-lidarr-rename" title=""></i> - </div> - - Are you sure you want to update all files in the {{numberOfArtists}} selected artist(s)? - - {{debug}} - <ul class="selected-artist"> - {{#each artist}} - <li>{{name}}</li> - {{/each}} - </ul> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-organize">Organize</button> - </div> -</div> diff --git a/src/UI/Artist/Index/ArtistIndexItemView.js b/src/UI/Artist/Index/ArtistIndexItemView.js deleted file mode 100644 index f3cf4faee..000000000 --- a/src/UI/Artist/Index/ArtistIndexItemView.js +++ /dev/null @@ -1,35 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist' - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist', - artistId : this.model.get('id') - } - }); - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/ArtistIndexLayout.js b/src/UI/Artist/Index/ArtistIndexLayout.js deleted file mode 100644 index 19769e564..000000000 --- a/src/UI/Artist/Index/ArtistIndexLayout.js +++ /dev/null @@ -1,360 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var PosterCollectionView = require('./Posters/ArtistPostersCollectionView'); -var ListCollectionView = require('./Overview/ArtistOverviewCollectionView'); -var EmptyView = require('./EmptyView'); -var ArtistCollection = require('../ArtistCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var TrackProgressCell = require('../../Cells/TrackProgressCell'); -var ArtistActionsCell = require('../../Cells/ArtistActionsCell'); -var ArtistStatusCell = require('../../Cells/ArtistStatusCell'); -var FooterView = require('./FooterView'); -var FooterModel = require('./FooterModel'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Artist/Index/ArtistIndexLayoutTemplate', - - - regions : { - artistRegion : '#x-artist', - toolbar : '#x-toolbar', - toolbar2 : '#x-toolbar2', - footer : '#x-artist-footer' - }, - - columns : [ - { - name : 'statusWeight', - label : '', - cell : ArtistStatusCell - }, - { - name : 'title', - label : 'Title', - cell : ArtistTitleCell, - cellValue : 'this', - sortValue : 'sortName' - }, - { - name : 'albumCount', - label : 'Albums', - cell : 'integer' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'network', - label : 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell - }, - { - name : 'percentOfTracks', - label : 'Tracks', - cell : TrackProgressCell, - className : 'track-progress-cell' - }, - { - name : 'this', - label : '', - sortable : false, - cell : ArtistActionsCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Add Artist', - icon : 'icon-lidarr-add', - route : 'addartist' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Artist Editor', - icon : 'icon-lidarr-edit', - route : 'artisteditor' - }, - { - title : 'RSS Sync', - icon : 'icon-lidarr-rss', - command : 'rsssync', - errorMessage : 'RSS Sync Failed!' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshartist', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'add', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'remove', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.sortingOptions = { - type : 'sorting', - storeState : false, - viewCollection : this.artistCollection, - items : [ - { - title : 'Title', - name : 'title' - }, - { - title : 'Albums', - name : 'albumCount' - }, - { - title : 'Quality', - name : 'profileId' - }, - { - title : 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring' - }, - { - title : 'Tracks', - name : 'percentOfTracks' - } - ] - }; - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'series.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-artist-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-artist-ended', - callback : this._setFilter - }, - { - key : 'missing', - title : '', - tooltip : 'Missing', - icon : 'icon-lidarr-missing', - callback : this._setFilter - } - ] - }; - - this.viewButtons = { - type : 'radio', - storeState : true, - menuKey : 'seriesViewMode', - defaultAction : 'listView', - items : [ - { - key : 'posterView', - title : '', - tooltip : 'Posters', - icon : 'icon-lidarr-view-poster', - callback : this._showPosters - }, - { - key : 'listView', - title : '', - tooltip : 'Overview List', - icon : 'icon-lidarr-view-list', - callback : this._showList - }, - { - key : 'tableView', - title : '', - tooltip : 'Table', - icon : 'icon-lidarr-view-table', - callback : this._showTable - } - ] - }; - }, - - onShow : function() { - this._showToolbar(); - this._fetchCollection(); - }, - - _showTable : function() { - this.currentView = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this._renderView(); - }, - - _showList : function() { - this.currentView = new ListCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _showPosters : function() { - this.currentView = new PosterCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _renderView : function() { - // Problem is this is calling before artistCollection has updated. Where are the promises with backbone? - if (this.artistCollection.length === 0) { - this.artistRegion.show(new EmptyView()); - - this.toolbar.close(); - this.toolbar2.close(); - } else { - this.artistRegion.show(this.currentView); - - this._showToolbar(); - this._showFooter(); - } - }, - - _fetchCollection : function() { - this.artistCollection.fetch(); - console.log('index page, collection: ', this.artistCollection); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - }, - - _showToolbar : function() { - if (this.toolbar.currentView) { - return; - } - - this.toolbar2.show(new ToolbarLayout({ - right : [ - this.filteringOptions - ], - context : this - })); - - this.toolbar.show(new ToolbarLayout({ - right : [ - this.sortingOptions, - this.viewButtons - ], - left : [ - this.leftSideButtons - ], - context : this - })); - }, - - _showFooter : function() { - var footerModel = new FooterModel(); - var artist = this.artistCollection.models.length; - var albums = 0; - var tracks = 0; - var trackFiles = 0; - var ended = 0; - var continuing = 0; - var monitored = 0; - - _.each(this.artistCollection.models, function(model) { - albums += model.get('albumCount'); - tracks += model.get('trackCount'); // TODO: Refactor to Seasons and Tracks - trackFiles += model.get('trackFileCount'); - - /*if (model.get('status').toLowerCase() === 'ended') { - ended++; - } else { - continuing++; - }*/ - - if (model.get('monitored')) { - monitored++; - } - }); - - footerModel.set({ - artist : artist, - ended : ended, - continuing : continuing, - monitored : monitored, - unmonitored : artist - monitored, - albums : albums, - tracks : tracks, - trackFiles : trackFiles - }); - - this.footer.show(new FooterView({ model : footerModel })); - } -}); diff --git a/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs b/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs deleted file mode 100644 index ac13a764c..000000000 --- a/src/UI/Artist/Index/ArtistIndexLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="toolbars"> - <div id="x-toolbar"></div> - <div id="x-toolbar2"></div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-artist" class="table-responsive"></div> - </div> -</div> - -<div id="x-artist-footer"></div> \ No newline at end of file diff --git a/src/UI/Artist/Index/EmptyTemplate.hbs b/src/UI/Artist/Index/EmptyTemplate.hbs deleted file mode 100644 index ebb59426b..000000000 --- a/src/UI/Artist/Index/EmptyTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<div class="no-artist"> - <div class="row"> - <div class="well col-md-12"> - <i class="icon-lidarr-comment"/> - You must be new around here. You should add some music. - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'> - <i class='icon-lidarr-add'></i> - Add Music - </a> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/EmptyView.js b/src/UI/Artist/Index/EmptyView.js deleted file mode 100644 index 4ad54762c..000000000 --- a/src/UI/Artist/Index/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Artist/Index/EmptyTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterModel.js b/src/UI/Artist/Index/FooterModel.js deleted file mode 100644 index 235552061..000000000 --- a/src/UI/Artist/Index/FooterModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterView.js b/src/UI/Artist/Index/FooterView.js deleted file mode 100644 index a84d6d01b..000000000 --- a/src/UI/Artist/Index/FooterView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Artist/Index/FooterViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/FooterViewTemplate.hbs b/src/UI/Artist/Index/FooterViewTemplate.hbs deleted file mode 100644 index f61baa356..000000000 --- a/src/UI/Artist/Index/FooterViewTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<div class="row"> - <div class="artist-legend legend col-xs-6 col-sm-4"> - <ul class='legend-labels'> - <li><span class="progress-bar"></span>Continuing (All tracks downloaded)</li> - <li><span class="progress-bar-success"></span>Ended (All tracks downloaded)</li> - <li><span class="progress-bar-danger"></span>Missing Tracks (Artist monitored)</li> - <li><span class="progress-bar-warning"></span>Missing Tracks (Artist not monitored)</li> - </ul> - </div> - <div class="col-xs-5 col-sm-7"> - <div class="row"> - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Artists</dt> - <dd>{{artist}}</dd> - - <dt>Ended</dt> - <dd>{{ended}}</dd> - - <dt>Continuing</dt> - <dd>{{continuing}}</dd> - </dl> - </div> - - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Monitored</dt> - <dd>{{monitored}}</dd> - - <dt>Unmonitored</dt> - <dd>{{unmonitored}}</dd> - </dl> - </div> - - <div class="artist-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Albums</dt> - <dd>{{albums}}</dd> - - <dt>Tracks</dt> - <dd>{{tracks}}</dd> - - <dt>Files</dt> - <dd>{{trackFiles}}</dd> - </dl> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js b/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js deleted file mode 100644 index 0b879824d..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var ListItemView = require('./ArtistOverviewItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ListItemView, - itemViewContainer : '#x-artist-list', - template : 'Artist/Index/Overview/ArtistOverviewCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs b/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs deleted file mode 100644 index 04687a280..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="x-artist-list"/> diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js b/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js deleted file mode 100644 index 82b9485da..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewItemView.js +++ /dev/null @@ -1,7 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ArtistIndexItemView = require('../ArtistIndexItemView'); - -module.exports = ArtistIndexItemView.extend({ - template : 'Artist/Index/Overview/ArtistOverviewItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs b/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs deleted file mode 100644 index 0f2babb96..000000000 --- a/src/UI/Artist/Index/Overview/ArtistOverviewItemViewTemplate.hbs +++ /dev/null @@ -1,59 +0,0 @@ -<div class="artist-item"> - <div class="row"> - <div class="col-md-2 col-xs-3"> - <a href="{{route}}"> - {{poster}} - </a> - </div> - <div class="col-md-10 col-xs-9"> - <div class="row"> - <div class="col-md-10 col-xs-10"> - <a href="{{route}}" target="_blank"> - <h2>{{name}}</h2> - </a> - </div> - <div class="col-md-2 col-xs-2"> - <div class="pull-right artist-overview-list-actions"> - <i class="icon-lidarr-refresh x-refresh" title="Update artist info and scan disk"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-12"> - <div> - {{truncate overview 600}} - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> -   - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-8"> - <!--{{#if_eq status compare="ended"}} - <span class="label label-danger">Ended</span> - {{/if_eq}}--> - - <!-- - NOTE: We can show next drop date of album in future - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}}--> - - {{albumCountHelper}} - - {{profile profileId}} - </div> - <div class="col-md-2 col-xs-4"> - {{> TrackProgressPartial }} - </div> - <div class="col-md-8 col-xs-10"> - Path {{path}} - </div> - </div> - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js b/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js deleted file mode 100644 index cc712d9da..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var PosterItemView = require('./ArtistPostersItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : PosterItemView, - itemViewContainer : '#x-artist-posters', - template : 'Artist/Index/Posters/ArtistPostersCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs b/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs deleted file mode 100644 index 0ac6a892e..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<ul id="x-artist-posters" class="artist-posters"></ul> \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersItemView.js b/src/UI/Artist/Index/Posters/ArtistPostersItemView.js deleted file mode 100644 index 3dc34cb4f..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersItemView.js +++ /dev/null @@ -1,19 +0,0 @@ -var ArtistIndexItemView = require('../ArtistIndexItemView'); - -module.exports = ArtistIndexItemView.extend({ - tagName : 'li', - template : 'Artist/Index/Posters/ArtistPostersItemViewTemplate', - - initialize : function() { - this.events['mouseenter .x-artist-poster-container'] = 'posterHoverAction'; - this.events['mouseleave .x-artist-poster-container'] = 'posterHoverAction'; - - this.ui.controls = '.x-artist-controls'; - this.ui.title = '.x-title'; - }, - - posterHoverAction : function() { - this.ui.controls.slideToggle(); - this.ui.title.slideToggle(); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs b/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs deleted file mode 100644 index 62a0a0a10..000000000 --- a/src/UI/Artist/Index/Posters/ArtistPostersItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="artist-posters-item"> - <div class="center"> - <div class="artist-poster-container x-artist-poster-container"> - <div class="artist-controls x-artist-controls"> - <i class="icon-lidarr-refresh x-refresh" title="Refresh Artist"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - {{#unless_eq status compare="continuing"}} - <div class="ended-banner">Ended</div> - {{/unless_eq}} - <a href="{{route}}"> - {{poster}} - <div class="center title">{{title}}</div> - </a> - <div class="hidden-title x-title"> - {{title}} - </div> - </div> - </div> - - <div class="center"> - <div class="labels"> - {{> EpisodeProgressPartial }} - - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Artist/Index/TrackProgressPartial.hbs b/src/UI/Artist/Index/TrackProgressPartial.hbs deleted file mode 100644 index a9cec28f7..000000000 --- a/src/UI/Artist/Index/TrackProgressPartial.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="progress track-progress"> - <span class="progressbar-back-text">{{trackFileCount}} / {{trackCount}}</span> - <div class="progress-bar {{TrackProgressClass}} track-progress" style="width:{{percentOfTracks}}%"><span class="progressbar-front-text">{{trackFileCount}} / {{trackCount}}</span></div> -</div> \ No newline at end of file diff --git a/src/UI/Artist/TrackCollection.js b/src/UI/Artist/TrackCollection.js deleted file mode 100644 index 502827325..000000000 --- a/src/UI/Artist/TrackCollection.js +++ /dev/null @@ -1,62 +0,0 @@ -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var TrackModel = require('./TrackModel'); -require('./TrackCollection'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/track', - model : TrackModel, - - state : { - sortKey : 'trackNumber', - order : -1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - }, - - byAlbum : function(album) { - var filtered = this.filter(function(track) { - return track.get('albumId') === album; - }); - - var TrackCollection = require('./TrackCollection'); - - return new TrackCollection(filtered); - }, - - comparator : function(model1, model2) { - var track1 = model1.get('trackNumber'); - var track2 = model2.get('trackNumber'); - - if (track1 < track2) { - return -1; - } - - if (track1 > track2) { - return 1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/TrackFileCollection.js b/src/UI/Artist/TrackFileCollection.js deleted file mode 100644 index 9b9909ce6..000000000 --- a/src/UI/Artist/TrackFileCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var TrackFileModel = require('./TrackFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/trackfile', - model : TrackFileModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.artistId = options.artistId; - this.models = []; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { artistId : this.artistId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/TrackFileModel.js b/src/UI/Artist/TrackFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Artist/TrackFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Artist/TrackModel.js b/src/UI/Artist/TrackModel.js deleted file mode 100644 index 30a2702d7..000000000 --- a/src/UI/Artist/TrackModel.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - albumId : 0, - status : 0 - }, - - methodUrls : { - 'update' : window.NzbDrone.ApiRoot + '/track' - }, - - sync : function(method, model, options) { - if (model.methodUrls && model.methodUrls[method.toLowerCase()]) { - options = options || {}; - options.url = model.methodUrls[method.toLowerCase()]; - } - return Backbone.sync(method, model, options); - } -}); \ No newline at end of file diff --git a/src/UI/Artist/artist.less b/src/UI/Artist/artist.less deleted file mode 100644 index 55769827e..000000000 --- a/src/UI/Artist/artist.less +++ /dev/null @@ -1,534 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/prefixer"; - -.artist-poster { - min-width: 56px; - max-width: 100%; -} - -.album-cover { - min-width: 56px; - max-width: 100%; - -} - -.album-details { - display: inline; - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } -} - -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.edit-artist-modal, .delete-artist-modal { - overflow : visible; - - .artist-poster { - padding-left : 20px; - width : 168px; - } - - .album-cover { - padding-left : 20px; - width : 168px; - } - - .form-horizontal { - margin-top : 10px; - } - - .twitter-typeahead { - .form-control[disabled] { - background-color: #ffffff; - } - } -} - -.delete-artist-modal { - .path { - margin-left : 30px; - } - - .delete-files-info { - margin-top : 10px; - display : none; - } -} - -.artist-item { - padding-bottom : 30px; - - :hover { - text-decoration : none; - } - - h2 { - margin-top : 0px; - } - - a { - color : #000000; - } -} - -.artist-page-header { - .card(black); - .opacity(0.9); - background : #000000; - color : #ffffff; - padding : 30px 15px; - margin : 50px 10px; - - .poster { - margin-top : 4px; - } - - .header-text { - margin-top : 0px; - } -} - -.artist-album { - .card; - .opacity(0.9); - margin : 30px 10px; - padding : 30px 25px; - - .show-hide-tracks { - .clickable(); - text-align : center; - - i { - .clickable(); - } - } - .header-text { - margin-top : 0px; - } -} - -.artist-posters { - list-style-type: none; - - @media (max-width: @screen-xs-max) { - padding : 0px; - } - - li { - display : inline-block; - vertical-align : top; - } - - .artist-posters-item { - - .card; - .clickable; - margin-bottom : 20px; - height : 315px; - - .center { - display : block; - margin-left : auto; - margin-right : auto; - text-align : center; - - .progress { - text-align : left; - margin-top : 5px; - left : 0px; - width : 170px; - - .progressbar-front-text, .progressbar-back-text { - width : 170px; - } - } - } - - .labels { - display : inline-block; - .opacity(0.75); - width : 170px; - - :hover { - cursor : default; - } - - .label { - margin-top : 3px; - display : block; - } - - .tooltip { - .opacity(1); - } - } - - @media (max-width: @screen-xs-max) { - height : 235px; - margin : 5px; - padding : 6px 5px; - - .center { - .progress { - width : 125px; - - .progressbar-front-text, .progressbar-back-text { - width : 125px - } - } - } - - .labels { - width: 125px; - } - } - } - - .artist-poster-container { - position : relative; - overflow : hidden; - display : inline-block; - - .placeholder-image ~ .title { - opacity: 1.0; - } - - .title { - position : absolute; - top : 25px; - color : #f5f5f5; - width : 100%; - font-size : 22px; - line-height: 24px; - opacity : 0.0; - font-weight: 100; - } - - .ended-banner { - color : #eeeeee; - background-color : #b94a48; - .box-shadow(2px 2px 20px #888888); - -moz-transform-origin : 50% 50%; - -webkit-transform-origin : 50% 50%; - position : absolute; - width : 320px; - top : 200px; - left : -122px; - text-align : center; - .opacity(0.9); - - .transform(rotate(45deg)); - } - - .artist-controls { - position : absolute;; - top : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : right; - padding-right : 10px; - display : none; - .opacity(0.8); - - i { - .clickable(); - } - } - - .hidden-title { - position : absolute;; - bottom : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : center; - .opacity(0.8); - display : none; - } - - .artist-poster { - width : 168px; - height : 247px; - display : block; - font-size : 34px; - line-height : 34px; - } - - @media (max-width: @screen-xs-max) { - .artist-poster { - width : 120px; - height : 176px; - } - - .ended-banner { - top : 145px; - left : -137px; - } - } - } -} - -.artist-detail-overview { - margin-bottom : 50px; -} - -.artist-album { - - .track-number-cell { - width : 40px; - white-space: nowrap; - } - .track-air-date-cell { - width : 150px; - } - - .track-status-cell { - width : 100px; - } - .track-rating-cell { - width : 150px; - } - .track-explicit-cell { - width : 100px; - } - .track-duration-cell { - width : 100px; - } - - .track-title-cell { - cursor : pointer; - } -} - -.track-detail-modal { - - .track-info { - margin-bottom : 10px; - } - - .track-overview { - font-style : italic; - } - - .track-file-info { - margin-top : 30px; - font-size : 12px; - } - - .track-history-details-cell .popover { - max-width: 800px; - } - - .hidden-artist-title { - display : none; - } -} - -.track-grid { - .toggle-cell { - width : 28px; - text-align : center; - padding-left : 0px; - padding-right : 0px; - } - - .toggle-cell { - i { - .clickable; - } - } -} - -.album-actions { - width: 100px; -} - -.album-actions, .artist-actions { - - div { - display : inline-block - } - - text-transform : none; - - i { - .clickable(); - font-size : 24px; - margin-left : 5px; - } -} - -.artist-stats { - font-size : 11px; -} - -.artist-legend { - padding-top : 5px; -} - -.albumpass-artist { - .card; - margin : 20px 0px; - - .title { - font-weight : 300; - font-size : 24px; - line-height : 30px; - margin-left : 5px; - } - - .album-select { - margin-bottom : 0px; - } - - .expander { - .clickable; - line-height : 30px; - margin-left : 8px; - width : 16px; - } - - .album-grid { - margin-top : 10px; - } - - .album-pass-button { - display : inline-block; - } - - .artist-monitor-toggle { - font-size : 24px; - margin-top : 3px; - } - - .help-inline { - margin-top : 7px; - display : inline-block; - } -} - -.album-status { - font-size : 11px; - vertical-align : middle !important; -} - -//Overview List -.artist-overview-list-actions { - min-width: 56px; - max-width: 56px; - - i { - .clickable(); - } -} - -//Editor - -.artist-editor-footer { - max-width: 1160px; - color: #f5f5f5; - margin-left: auto; - margin-right: auto; - - .form-group { - padding-top: 0px; - } -} - -.update-files-artist-modal { - .selected-artist { - margin-top: 15px; - } -} - -//artist Details - -.artist-not-monitored { - .album-monitored, .track-monitored { - color: #888888; - cursor: not-allowed; - - i { - cursor: not-allowed; - } - } -} - -.artist-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .artist-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.album-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .album-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.scene-info { - .key, .value { - display : inline-block; - } - - .key { - width : 80px; - margin-left : 10px; - vertical-align : top; - } - - .value { - margin-right : 10px; - max-width : 170px; - } - - ul { - padding-left : 0px; - list-style-type : none; - } -} diff --git a/src/UI/Calendar/CalendarCollection.js b/src/UI/Calendar/CalendarCollection.js deleted file mode 100644 index a7a0214c5..000000000 --- a/src/UI/Calendar/CalendarCollection.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backbone = require('backbone'); -var AlbumModel = require('../Artist/AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/calendar', - model : AlbumModel, - tableName : 'calendar', - - comparator : function(model) { - var date = new Date(model.get('releaseDate')); - var time = date.getTime(); - return time; - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarFeedView.js b/src/UI/Calendar/CalendarFeedView.js deleted file mode 100644 index 5ccbc82e8..000000000 --- a/src/UI/Calendar/CalendarFeedView.js +++ /dev/null @@ -1,60 +0,0 @@ -var Marionette = require('marionette'); -var StatusModel = require('../System/StatusModel'); -require('../Mixins/CopyToClipboard'); -require('../Mixins/TagInput'); - -module.exports = Marionette.Layout.extend({ - template : 'Calendar/CalendarFeedViewTemplate', - - ui : { - includeUnmonitored : '.x-includeUnmonitored', - premiersOnly : '.x-premiersOnly', - asAllDay : '.x-asAllDay', - tags : '.x-tags', - icalUrl : '.x-ical-url', - icalCopy : '.x-ical-copy', - icalWebCal : '.x-ical-webcal' - }, - - events : { - 'click .x-includeUnmonitored' : '_updateUrl', - 'click .x-premiersOnly' : '_updateUrl', - 'click .x-asAllDay' : '_updateUrl', - 'itemAdded .x-tags' : '_updateUrl', - 'itemRemoved .x-tags' : '_updateUrl' - }, - - onShow : function() { - this._updateUrl(); - this.ui.icalCopy.copyToClipboard(this.ui.icalUrl); - this.ui.tags.tagInput({ allowNew: false }); - }, - - _updateUrl : function() { - var icalUrl = window.location.host + StatusModel.get('urlBase') + '/feed/calendar/Lidarr.ics?'; - - if (this.ui.includeUnmonitored.prop('checked')) { - icalUrl += 'unmonitored=true&'; - } - - if (this.ui.premiersOnly.prop('checked')) { - icalUrl += 'premiersOnly=true&'; - } - - if (this.ui.asAllDay.prop('checked')) { - icalUrl += 'asAllDay=true&'; - } - - if (this.ui.tags.val()) { - icalUrl += 'tags=' + this.ui.tags.val() + '&'; - } - - icalUrl += 'apikey=' + window.NzbDrone.ApiKey; - - var icalHttpUrl = window.location.protocol + '//' + icalUrl; - var icalWebCalUrl = 'webcal://' + icalUrl; - - this.ui.icalUrl.attr('value', icalHttpUrl); - this.ui.icalWebCal.attr('href', icalWebCalUrl); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarFeedViewTemplate.hbs b/src/UI/Calendar/CalendarFeedViewTemplate.hbs deleted file mode 100644 index 27411e29f..000000000 --- a/src/UI/Calendar/CalendarFeedViewTemplate.hbs +++ /dev/null @@ -1,57 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Lidarr Calendar feed</h3> - </div> - <div class="modal-body edit-series-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Include Unmonitored</label> - - <div class="col-sm-4"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeUnmonitored" class="form-control x-includeUnmonitored"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="One or more tags only show matching series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">iCal feed</label> - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Copy this url into your clients subscription form or use the subscribe button if your browser support webcal" /> - </div> - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group ical-url"> - <input type="text" class="form-control x-ical-url" readonly="readonly" /> - <div class="input-group-btn"> - <button class="btn btn-icon-only x-ical-copy"><i class="icon-lidarr-copy"></i></button> - <button class="btn btn-icon-only no-router x-ical-webcal" title="Subscribe" target="_blank"><i class="icon-lidarr-calendar-o"></i></button> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Calendar/CalendarLayout.js b/src/UI/Calendar/CalendarLayout.js deleted file mode 100644 index 3cd289d85..000000000 --- a/src/UI/Calendar/CalendarLayout.js +++ /dev/null @@ -1,96 +0,0 @@ -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var UpcomingCollectionView = require('./UpcomingCollectionView'); -var CalendarView = require('./CalendarView'); -var CalendarFeedView = require('./CalendarFeedView'); -var ToolbarLayout = require('../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Calendar/CalendarLayoutTemplate', - - regions : { - upcoming : '#x-upcoming', - calendar : '#x-calendar', - toolbar : '#x-toolbar' - }, - - onShow : function() { - this._showUpcoming(); - this._showCalendar(); - this._showToolbar(); - }, - - _showUpcoming : function() { - this.upcomingView = new UpcomingCollectionView(); - this.upcoming.show(this.upcomingView); - }, - - _showCalendar : function() { - this.calendarView = new CalendarView(); - this.calendar.show(this.calendarView); - }, - - _showiCal : function() { - var view = new CalendarFeedView(); - AppLayout.modalRegion.show(view); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Get iCal Link', - icon : 'icon-lidarr-calendar-o', - callback : this._showiCal, - ownerContext : this - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : true, - menuKey : 'calendar.show', - defaultAction : 'monitored', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setCalendarFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setCalendarFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterOptions], - context : this, - floatOnMobile : true - })); - }, - - _setCalendarFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - if (mode === 'all') { - this.calendarView.setShowUnmonitored(true); - this.upcomingView.setShowUnmonitored(true); - } - - else { - this.calendarView.setShowUnmonitored(false); - this.upcomingView.setShowUnmonitored(false); - } - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/CalendarLayoutTemplate.hbs b/src/UI/Calendar/CalendarLayoutTemplate.hbs deleted file mode 100644 index 508545565..000000000 --- a/src/UI/Calendar/CalendarLayoutTemplate.hbs +++ /dev/null @@ -1,21 +0,0 @@ -<div class="row"> - <div class="col-md-3 hidden-xs"> - <div class="pull-left"> - <h4>Upcoming</h4> - </div> - <div id="x-upcoming"/> - </div> - <div class="col-md-9 col-xs-12"> - <div id="x-toolbar" class="calendar-toolbar"/> - <div id="x-calendar" class="calendar"/> - <div class="legend calendar"> - <ul class='legend-labels'> - <li class="legend-label"><span class="primary" title="Album hasn't aired yet"></span>Unreleased</li> - <li class="legend-label"><span class="purple" title="Album is currently downloading"></span>Downloading</li> - <li class="legend-label"><span class="danger" title="Album file has not been found"></span>Missing</li> - <li class="legend-label"><span class="success" title="Album was downloaded and sorted"></span>Downloaded</li> - <li class="legend-label"><span class="unmonitored" title="Album is unmonitored"></span>Unmonitored</li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Calendar/CalendarView.js b/src/UI/Calendar/CalendarView.js deleted file mode 100644 index 058a19e61..000000000 --- a/src/UI/Calendar/CalendarView.js +++ /dev/null @@ -1,283 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var moment = require('moment'); -var CalendarCollection = require('./CalendarCollection'); -var UiSettings = require('../Shared/UiSettingsModel'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var Config = require('../Config'); - -require('../Mixins/backbone.signalr.mixin'); -require('fullcalendar'); -require('jquery.easypiechart'); - -module.exports = Marionette.ItemView.extend({ - storageKey : 'calendar.view', - - initialize : function() { - this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; - this.collection = new CalendarCollection().bindSignalR({ updateOnly : true }); - this.listenTo(this.collection, 'change', this._reloadCalendarEvents); - this.listenTo(QueueCollection, 'sync', this._reloadCalendarEvents); - }, - - render : function() { - this.$el.empty().fullCalendar(this._getOptions()); - }, - - onShow : function() { - this.$('.fc-today-button').click(); - }, - - setShowUnmonitored : function (showUnmonitored) { - if (this.showUnmonitored !== showUnmonitored) { - this.showUnmonitored = showUnmonitored; - this._getEvents(this.$el.fullCalendar('getView')); - } - }, - - _viewRender : function(view, element) { - if (Config.getValue(this.storageKey) !== view.name) { - Config.setValue(this.storageKey, view.name); - } - - this._getEvents(view); - element.find('.fc-day-grid-container').css('height', ''); - }, - - _eventRender : function(event, element) { - element.addClass(event.statusLevel); - element.children('.fc-content').addClass(event.statusLevel); - - if (event.downloading) { - var progress = 100 - event.downloading.get('sizeleft') / event.downloading.get('size') * 100; - var releaseTitle = event.downloading.get('title'); - var estimatedCompletionTime = moment(event.downloading.get('estimatedCompletionTime')).fromNow(); - var status = event.downloading.get('status').toLocaleLowerCase(); - var errorMessage = event.downloading.get('errorMessage'); - - if (status === 'pending') { - this._addStatusIcon(element, 'icon-lidarr-pending', 'Release will be processed {0}'.format(estimatedCompletionTime)); - } - - else if (errorMessage) { - if (status === 'completed') { - this._addStatusIcon(element, 'icon-lidarr-import-failed', 'Import failed: {0}'.format(errorMessage)); - } else { - this._addStatusIcon(element, 'icon-lidarr-download-failed', 'Download failed: {0}'.format(errorMessage)); - } - } - - else if (status === 'failed') { - this._addStatusIcon(element, 'icon-lidarr-download-failed', 'Download failed: check download client for more details'); - } - - else if (status === 'warning') { - this._addStatusIcon(element, 'icon-lidarr-download-warning', 'Download warning: check download client for more details'); - } - - else { - element.find('.fc-time').after('<span class="chart pull-right" data-percent="{0}"></span>'.format(progress)); - - element.find('.chart').easyPieChart({ - barColor : '#ffffff', - trackColor : false, - scaleColor : false, - lineWidth : 2, - size : 14, - animate : false - }); - - element.find('.chart').tooltip({ - title : 'Album is downloading - {0}% {1}'.format(progress.toFixed(1), releaseTitle), - container : '.fc' - }); - } - } - - else if (event.model.get('unverifiedSceneNumbering')) { - this._addStatusIcon(element, 'icon-lidarr-form-warning', 'Scene number hasn\'t been verified yet.'); - } - - }, - - _eventAfterAllRender : function () { - if ($(window).width() < 768) { - this.$('.fc-center').show(); - this.$('.calendar-title').remove(); - - var title = this.$('.fc-center').html(); - var titleDiv = '<div class="calendar-title">{0}</div>'.format(title); - - this.$('.fc-toolbar').before(titleDiv); - this.$('.fc-center').hide(); - } - - this._clearScrollBar(); - }, - - _windowResize : function () { - this._clearScrollBar(); - }, - - _getEvents : function(view) { - var start = moment(view.start.toISOString()).toISOString(); - var end = moment(view.end.toISOString()).toISOString(); - - this.$el.fullCalendar('removeEvents'); - - this.collection.fetch({ - data : { - start : start, - end : end, - unmonitored : this.showUnmonitored - }, - success : this._setEventData.bind(this) - }); - }, - - _setEventData : function(collection) { - if (collection.length === 0) { - return; - } - - var events = []; - var self = this; - - collection.each(function(model) { - var albumTitle = model.get('title'); - var artistName = model.get('artist').name; - var start = model.get('releaseDate'); - var runtime = '30'; - var end = moment(start).add('minutes', runtime).toISOString(); - - var event = { - title : artistName + " - " + albumTitle, - start : moment(start), - end : moment(end), - allDay : true, - statusLevel : self._getStatusLevel(model, end), - downloading : QueueCollection.findEpisode(model.get('id')), - model : model, - sortOrder : 0 - }; - - events.push(event); - }); - - this.$el.fullCalendar('addEventSource', events); - }, - - _getStatusLevel : function(element, endTime) { - var hasFile = element.get('hasFile'); - var downloading = QueueCollection.findEpisode(element.get('id')) || element.get('grabbed'); - var currentTime = moment(); - var start = moment(element.get('releaseDate')); - var end = moment(endTime); - var monitored = element.get('artist').monitored && element.get('monitored'); - - var statusLevel = 'primary'; - - if (hasFile) { - statusLevel = 'success'; - } - - else if (downloading) { - statusLevel = 'purple'; - } - - else if (!monitored) { - statusLevel = 'unmonitored'; - } - - else if (currentTime.isAfter(start) && currentTime.isBefore(end)) { - statusLevel = 'warning'; - } - - else if (start.isBefore(currentTime) && !hasFile) { - statusLevel = 'danger'; - } - - else if (element.get('episodeNumber') === 1) { - statusLevel = 'premiere'; - } - - if (end.isBefore(currentTime.startOf('day'))) { - statusLevel += ' past'; - } - - return statusLevel; - }, - - _reloadCalendarEvents : function() { - this.$el.fullCalendar('removeEvents'); - this._setEventData(this.collection); - }, - - _getOptions : function() { - var options = { - allDayDefault : true, - weekMode : 'variable', - firstDay : UiSettings.get('firstDayOfWeek'), - timeFormat : 'h(:mm)t', - viewRender : this._viewRender.bind(this), - eventRender : this._eventRender.bind(this), - eventAfterAllRender : this._eventAfterAllRender.bind(this), - windowResize : this._windowResize.bind(this), - eventClick : function(event) { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : event.model }); - } - }; - - if ($(window).width() < 768) { - options.defaultView = Config.getValue(this.storageKey, 'basicDay'); - - options.header = { - left : 'prev,next today', - center : 'title', - right : 'basicWeek,listYear' - }; - } - - else { - options.defaultView = Config.getValue(this.storageKey, 'basicWeek'); - - options.header = { - left : 'prev,next today', - center : 'title', - right : 'month,basicWeek,listYear' - }; - } - - options.views = { - month: { - titleFormat: 'MMMM YYYY', - columnFormat: 'ddd' - }, - week: { - titleFormat: UiSettings.get('shortDateFormat'), - columnFormat: UiSettings.get('calendarWeekColumnHeader') - } - - - }; - - options.timeFormat = UiSettings.get('timeFormat'); - - return options; - }, - - _addStatusIcon : function(element, icon, tooltip) { - element.find('.fc-time').after('<span class="status pull-right"><i class="{0}"></i></span>'.format(icon)); - element.find('.status').tooltip({ - title : tooltip, - container : '.fc' - }); - }, - - _clearScrollBar : function () { - // Remove height from calendar so we don't have another scroll bar - this.$('.fc-day-grid-container').css('height', ''); - this.$('.fc-row.fc-widget-header').attr('style', ''); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingCollection.js b/src/UI/Calendar/UpcomingCollection.js deleted file mode 100644 index 7409459a4..000000000 --- a/src/UI/Calendar/UpcomingCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var moment = require('moment'); -var AlbumModel = require('../Artist/AlbumModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/calendar', - model : AlbumModel, - - comparator : function(model1, model2) { - var airDate1 = model1.get('releaseDate'); - var date1 = moment(airDate1); - var time1 = date1.unix(); - - var airDate2 = model2.get('releaseDate'); - var date2 = moment(airDate2); - var time2 = date2.unix(); - - if (time1 < time2) { - return -1; - } - - if (time1 > time2) { - return 1; - } - - return 0; - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingCollectionView.js b/src/UI/Calendar/UpcomingCollectionView.js deleted file mode 100644 index c4b40e989..000000000 --- a/src/UI/Calendar/UpcomingCollectionView.js +++ /dev/null @@ -1,36 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var UpcomingCollection = require('./UpcomingCollection'); -var UpcomingItemView = require('./UpcomingItemView'); -var Config = require('../Config'); -require('../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.CollectionView.extend({ - itemView : UpcomingItemView, - - initialize : function() { - - this.showUnmonitored = Config.getValue('calendar.show', 'monitored') === 'all'; - - this.collection = new UpcomingCollection().bindSignalR({ updateOnly : true }); - this._fetchCollection(); - - this._fetchCollection = _.bind(this._fetchCollection, this); - this.timer = window.setInterval(this._fetchCollection, 60 * 60 * 1000); - }, - - onClose : function() { - window.clearInterval(this.timer); - }, - - setShowUnmonitored : function (showUnmonitored) { - if (this.showUnmonitored !== showUnmonitored) { - this.showUnmonitored = showUnmonitored; - this._fetchCollection(); - } - }, - - _fetchCollection : function() { - this.collection.fetch({ data: { unmonitored : this.showUnmonitored }}); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingItemView.js b/src/UI/Calendar/UpcomingItemView.js deleted file mode 100644 index 9a47a26dc..000000000 --- a/src/UI/Calendar/UpcomingItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var moment = require('moment'); - -module.exports = Marionette.ItemView.extend({ - template : 'Calendar/UpcomingItemViewTemplate', - tagName : 'div', - - events : { - 'click .x-album-title' : '_showAlbumDetails' - }, - - initialize : function() { - var start = this.model.get('releaseDate'); - var runtime = '30'; - var end = moment(start).add('minutes', runtime); - - this.model.set({ - end : end.toISOString() - }); - - this.listenTo(this.model, 'change', this.render); - }, - - _showAlbumDetails : function() { - vent.trigger(vent.Commands.ShowAlbumDetails, { album : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/Calendar/UpcomingItemViewTemplate.hbs b/src/UI/Calendar/UpcomingItemViewTemplate.hbs deleted file mode 100644 index 6c42e0bba..000000000 --- a/src/UI/Calendar/UpcomingItemViewTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="event"> - <div class="date {{StatusLevel}}"> - <h1>{{Day releaseDate}}</h1> - <h4>{{Month releaseDate}}</h4> - </div> - {{#with artist}} - <a href="{{route}}"> - <h4>{{name}}</h4> - </a> - {{/with}} - <p>{{#unless_today releaseDate}}{{ShortDate releaseDate}}{{/unless_today}}</p> - <p> - <span class="album-title x-album-title"> - {{title}} - </span> - <span class="pull-right">x</span> - </p> -</div> diff --git a/src/UI/Calendar/calendar.less b/src/UI/Calendar/calendar.less deleted file mode 100644 index 6214b4617..000000000 --- a/src/UI/Calendar/calendar.less +++ /dev/null @@ -1,258 +0,0 @@ -@import "../Content/Bootstrap/mixins"; -@import "../Content/Bootstrap/variables"; -@import "../Content/Bootstrap/buttons"; -@import "../Shared/Styles/clickable"; -@import "../Content/variables"; -@import "../Content/mixins"; -@import "../Content/Overrides/bootstrap"; - -.calendar { - width: 100%; - - th, td { - border-color : #eeeeee; - } - - .fc-event-skin { - background-color : #007ccd; - border : 1px solid #007ccd; - border-radius : 4px; - text-align : center; - } - - .fc-event { - .clickable; - - .status { - margin-right : 4px; - } - } - - th { - background-color : #eeeeee; - } - - h2 { - font-size : 17.5px; - } - - .fc-state-highlight { - background : #dbdbdb; - } - - .past { - opacity : 0.8; - } - - .fc-title { - white-space: normal; - } -} - -.event { - display : inline-block; - width : 100%; - margin-bottom : 10px; - border-top : 1px solid #eeeeee; - padding-top : 10px; - - h4 { - font-weight : 500; - color : #008dcd; - margin : 5px 0px; - } - - p { - color : #999999; - margin : 0px; - } - - .date { - text-align : center; - display : inline-block; - border-left : 4px solid #eeeeee; - padding-left : 16px; - float : left; - margin-right : 20px; - - h4 { - line-height : 1em; - color : #555555; - font-weight : 300; - text-transform : uppercase; - } - - h1 { - font-weight : 500; - line-height : 0.8em; - } - } - - .primary { - border-color : @btn-primary-bg; - } - - .info { - border-color : @btn-info-bg; - } - - .inverse { - border-color : @btn-link-disabled-color; - } - - .warning { - border-color : @btn-warning-bg; - } - - .danger { - border-color : @btn-danger-bg; - } - - .success { - border-color : @btn-success-bg; - } - - .purple { - border-color : @nzbdronePurple; - } - - .pink { - border-color : @nzbdronePink; - } - - .premiere { - border-color : @droneTeal; - } - - .unmonitored { - border-color : grey; - } - - .album-title { - .btn-link; - .text-overflow; - color : @link-color; - margin-top : 1px; - display : inline-block; - - @media (max-width: @screen-xs-min) { - width : 140px; - } - - @media (min-width: @screen-md-min) { - width : 135px; - } - } -} - -.calendar { - -// background-position : -160px -128px; - - .primary { - border-color : @btn-primary-bg; - background-color : @btn-primary-bg; - - .color-impaired-background-gradient(90deg, @btn-primary-bg); - } - - .info { - border-color : @btn-info-bg; - background-color : @btn-info-bg; - } - - .inverse { - border-color : @btn-link-disabled-color; - background-color : @btn-link-disabled-color; - } - - .warning { - border-color : @btn-warning-bg; - background-color : @btn-warning-bg; - - .color-impaired-background-gradient(90deg, @btn-warning-bg); - } - - .danger { - border-color : @btn-danger-bg; - background-color : @btn-danger-bg; - - .color-impaired-background-gradient(90deg, @btn-danger-bg); - } - - .success { - border-color : @btn-success-bg; - background-color : @btn-success-bg; - } - - .purple { - border-color : @nzbdronePurple; - background-color : @nzbdronePurple; - } - - .pink { - border-color : @nzbdronePink; - background-color : @nzbdronePink; - } - - .premiere { - border-color : @droneTeal; - background-color : @droneTeal; - - .color-impaired-background-gradient(90deg, @droneTeal); - } - - .unmonitored { - border-color : grey; - background-color : grey; - - .color-impaired-background-gradient(45deg, grey); - } - - .chart { - margin-top : 2px; - margin-right : 2px; - line-height : 12px; - } - - .legend-labels { - max-width : 100%; - width : 500px; - - @media (max-width: @screen-xs-min) { - width : 100%; - } - } - - .legend-label { - display : inline-block; - width : 150px; - } -} - -.ical { - color: @btn-link-disabled-color; - cursor: pointer; -} - -.ical-url { - - input, input[readonly] { - cursor : text; - } -} - -.calendar-title { - text-align : center; - - h2 { - margin-top : 0px; - margin-bottom : 5px; - } -} - -.calendar-toolbar { - .page-toolbar { - margin-bottom : 10px; - } -} diff --git a/src/UI/Cells/AlbumFolderCell.js b/src/UI/Cells/AlbumFolderCell.js deleted file mode 100644 index bb5173a5c..000000000 --- a/src/UI/Cells/AlbumFolderCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'artist-folder-cell', - - render : function() { - this.$el.empty(); - var albumFolder = this.model.get(this.column.get('name')); - this.$el.html(albumFolder.toString()); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/AlbumTitleCell.js b/src/UI/Cells/AlbumTitleCell.js deleted file mode 100644 index ca3d1cc48..000000000 --- a/src/UI/Cells/AlbumTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'album-title-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideArtistLink = this.column.get('hideArtistLink'); - vent.trigger(vent.Commands.ShowAlbumDetails, { - album : this.cellValue, - hideArtistLink : hideArtistLink - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ApprovalStatusCell.js b/src/UI/Cells/ApprovalStatusCell.js deleted file mode 100644 index 96e2a45a4..000000000 --- a/src/UI/Cells/ApprovalStatusCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -require('bootstrap'); - -module.exports = Backgrid.Cell.extend({ - className : 'approval-status-cell', - template : 'Cells/ApprovalStatusCellTemplate', - - render : function() { - - var rejections = this.model.get(this.column.get('name')); - - if (rejections.length === 0) { - return this; - } - - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var html = this.templateFunction(rejections); - this.$el.html('<i class="icon-lidarr-form-danger"/>'); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : this.column.get('title'), - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ApprovalStatusCellTemplate.hbs b/src/UI/Cells/ApprovalStatusCellTemplate.hbs deleted file mode 100644 index 87f28cbcb..000000000 --- a/src/UI/Cells/ApprovalStatusCellTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<ul> - {{#each this}} - <li> - {{#if reason}} - {{reason}} - {{else}} - {{this}} - {{/if}} - </li> - {{/each}} -</ul> diff --git a/src/UI/Cells/ArtistActionsCell.js b/src/UI/Cells/ArtistActionsCell.js deleted file mode 100644 index b8332ce1a..000000000 --- a/src/UI/Cells/ArtistActionsCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-actions-cell', - - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editArtist', - 'click .x-refresh' : '_refreshArtist' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-refresh x-refresh hidden-xs" title="" data-original-title="Update artist info and scan disk"></i> ' + - '<i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Artist"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-refresh'), - command : { - name : 'refreshArtist', - seriesId : this.model.get('id') - } - }); - - this.delegateEvents(); - return this; - }, - - _editArtist : function() { - vent.trigger(vent.Commands.EditArtistCommand, { artist : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - artistId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistMonitoredCell.js b/src/UI/Cells/ArtistMonitoredCell.js deleted file mode 100644 index c45af0b85..000000000 --- a/src/UI/Cells/ArtistMonitoredCell.js +++ /dev/null @@ -1,39 +0,0 @@ -var ToggleCell = require('./ToggleCell'); -var Handlebars = require('handlebars'); - -module.exports = ToggleCell.extend({ - className : 'artist-monitored-cell', - - events : { - 'click i' : '_onClick' - }, - - render : function() { - - this.$el.empty(); - this.$el.html('<i /><a href=""><span class="artist-monitored-name"></span></a>'); - - var name = this.column.get('name'); - - if (this.model.get(name)) { - this.$('i').addClass(this.column.get('trueClass')); - } else { - this.$('i').addClass(this.column.get('falseClass')); - } - - var link = '/artist/' + this.model.get('nameSlug'); - var artistName = this.model.get('name'); - - this.$('a').attr('href', link ); - this.$('span').html(artistName); - - var tooltip = this.column.get('tooltip'); - - if (tooltip) { - this.$('i').attr('title', tooltip); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistStatusCell.js b/src/UI/Cells/ArtistStatusCell.js deleted file mode 100644 index aa53482f1..000000000 --- a/src/UI/Cells/ArtistStatusCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'artist-status-cell', - - render : function() { - this.$el.empty(); - var monitored = this.model.get('monitored'); - var status = this.model.get('status'); - - if (status === 'ended') { - this.$el.html('<i class="icon-lidarr-artist-ended grid-icon" title="Ended"></i>'); - this._setStatusWeight(3); - } - - else if (!monitored) { - this.$el.html('<i class="icon-lidarr-artist-unmonitored grid-icon" title="Not Monitored"></i>'); - this._setStatusWeight(2); - } - - else { - this.$el.html('<i class="icon-lidarr-artist-continuing grid-icon" title="Continuing"></i>'); - this._setStatusWeight(1); - } - - return this; - }, - - _setStatusWeight : function(weight) { - this.model.set('statusWeight', weight, { silent : true }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistTitleCell.js b/src/UI/Cells/ArtistTitleCell.js deleted file mode 100644 index 5dcb62104..000000000 --- a/src/UI/Cells/ArtistTitleCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'artist-title-cell', - template : 'Cells/ArtistTitleTemplate' -}); \ No newline at end of file diff --git a/src/UI/Cells/ArtistTitleTemplate.hbs b/src/UI/Cells/ArtistTitleTemplate.hbs deleted file mode 100644 index cb3146db8..000000000 --- a/src/UI/Cells/ArtistTitleTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{route}}">{{name}}</a> diff --git a/src/UI/Cells/DeleteEpisodeFileCell.js b/src/UI/Cells/DeleteEpisodeFileCell.js deleted file mode 100644 index 11aa9c4f6..000000000 --- a/src/UI/Cells/DeleteEpisodeFileCell.js +++ /dev/null @@ -1,27 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'delete-episode-file-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-delete" title="Delete episode file from disk"></i>'); - - return this; - }, - - _onClick : function() { - var self = this; - - if (window.confirm('Are you sure you want to delete \'{0}\' from disk?'.format(this.model.get('path')))) { - this.model.destroy().done(function() { - vent.trigger(vent.Events.EpisodeFileDeleted, { episodeFile : self.model }); - }); - } - } -}); \ No newline at end of file diff --git a/src/UI/Cells/Edit/QualityCellEditor.js b/src/UI/Cells/Edit/QualityCellEditor.js deleted file mode 100644 index 00e469d83..000000000 --- a/src/UI/Cells/Edit/QualityCellEditor.js +++ /dev/null @@ -1,74 +0,0 @@ -var _ = require('underscore'); -var Backgrid = require('backgrid'); -var Marionette = require('marionette'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); - -module.exports = Backgrid.CellEditor.extend({ - className : 'quality-cell-editor', - template : 'Cells/Edit/QualityCellEditorTemplate', - tagName : 'select', - - events : { - 'change' : 'save', - 'blur' : 'close', - 'keydown' : 'close' - }, - - render : function() { - var self = this; - - var profileSchemaCollection = new ProfileSchemaCollection(); - var promise = profileSchemaCollection.fetch(); - - promise.done(function() { - var templateName = self.template; - self.schema = profileSchemaCollection.first(); - - var selected = _.find(self.schema.get('items'), function(model) { - return model.quality.id === self.model.get(self.column.get('name')).quality.id; - }); - - if (selected) { - selected.quality.selected = true; - } - - self.templateFunction = Marionette.TemplateCache.get(templateName); - var data = self.schema.toJSON(); - var html = self.templateFunction(data); - self.$el.html(html); - }); - - return this; - }, - - save : function(e) { - var model = this.model; - var column = this.column; - var selected = parseInt(this.$el.val(), 10); - - var profileItem = _.find(this.schema.get('items'), function(model) { - return model.quality.id === selected; - }); - - var newQuality = { - quality : profileItem.quality, - revision : { - version : 1, - real : 0 - } - }; - - model.set(column.get('name'), newQuality); - model.save(); - - model.trigger('backgrid:edited', model, column, new Backgrid.Command(e)); - }, - - close : function(e) { - var model = this.model; - var column = this.column; - var command = new Backgrid.Command(e); - - model.trigger('backgrid:edited', model, column, command); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs b/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs deleted file mode 100644 index b7039dd44..000000000 --- a/src/UI/Cells/Edit/QualityCellEditorTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#eachReverse items}} - {{#with quality}} - {{#if selected}} - <option value="{{id}}" selected="selected">{{name}}</option> - {{else}} - <option value="{{id}}">{{name}}</option> - {{/if}} - {{/with}} -{{/eachReverse}} \ No newline at end of file diff --git a/src/UI/Cells/EpisodeActionsCell.js b/src/UI/Cells/EpisodeActionsCell.js deleted file mode 100644 index 969c7c9a8..000000000 --- a/src/UI/Cells/EpisodeActionsCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-actions-cell', - - events : { - 'click .x-automatic-search' : '_automaticSearch', - 'click .x-manual-search' : '_manualSearch' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-search x-automatic-search" title="Automatic Search"></i>' + '<i class="icon-lidarr-search-manual x-manual-search" title="Manual Search"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-automatic-search'), - command : { - name : 'episodeSearch', - episodeIds : [this.model.get('id')] - } - }); - - this.delegateEvents(); - return this; - }, - - _automaticSearch : function() { - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : [this.model.get('id')] - }); - }, - - _manualSearch : function() { - vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.cellValue, - hideSeriesLink : true, - openingTab : 'search' - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeFilePathCell.js b/src/UI/Cells/EpisodeFilePathCell.js deleted file mode 100644 index 5f3916ead..000000000 --- a/src/UI/Cells/EpisodeFilePathCell.js +++ /dev/null @@ -1,19 +0,0 @@ -var reqres = require('../reqres'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-file-path-cell', - - render : function() { - this.$el.empty(); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); - - this.$el.html(episodeFile.get('relativePath')); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeMonitoredCell.js b/src/UI/Cells/EpisodeMonitoredCell.js deleted file mode 100644 index b615d2909..000000000 --- a/src/UI/Cells/EpisodeMonitoredCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var _ = require('underscore'); -var ToggleCell = require('./ToggleCell'); -var SeriesCollection = require('../Series/SeriesCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = ToggleCell.extend({ - className : 'toggle-cell episode-monitored', - - _originalOnClick : ToggleCell.prototype._onClick, - - _onClick : function(e) { - - var series = SeriesCollection.get(this.model.get('seriesId')); - - if (!series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - if (e.shiftKey && this.model.episodeCollection.lastToggled) { - this._selectRange(); - - return; - } - - this._originalOnClick.apply(this, arguments); - this.model.episodeCollection.lastToggled = this.model; - }, - - _selectRange : function() { - var episodeCollection = this.model.episodeCollection; - var lastToggled = episodeCollection.lastToggled; - - var currentIndex = episodeCollection.indexOf(this.model); - var lastIndex = episodeCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = episodeCollection.at(index); - - model.set('monitored', lastToggled.get('monitored')); - model.save(); - }); - - this.model.set('monitored', lastToggled.get('monitored')); - this.model.save(); - this.model.episodeCollection.lastToggled = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeNumberCell.js b/src/UI/Cells/EpisodeNumberCell.js deleted file mode 100644 index 6d4d804b2..000000000 --- a/src/UI/Cells/EpisodeNumberCell.js +++ /dev/null @@ -1,71 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var _ = require('underscore'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-number-cell', - - render : function() { - - this.$el.empty(); - - var airDateField = this.column.get('airDateUtc') || 'airDateUtc'; - var seasonField = this.column.get('seasonNumber') || 'seasonNumber'; - var episodeField = this.column.get('episodes') || 'episodeNumber'; - var absoluteEpisodeField = 'absoluteEpisodeNumber'; - - if (this.model) { - var result = 'Unknown'; - - var airDate = this.model.get(airDateField); - var seasonNumber = this.model.get(seasonField); - var episodes = this.model.get(episodeField); - var absoluteEpisodeNumber = this.model.get(absoluteEpisodeField); - - if (this.cellValue) { - if (!seasonNumber) { - seasonNumber = this.cellValue.get(seasonField); - } - - if (!episodes) { - episodes = this.cellValue.get(episodeField); - } - - if (absoluteEpisodeNumber === undefined) { - absoluteEpisodeNumber = this.cellValue.get(absoluteEpisodeField); - } - - if (!airDate) { - this.model.get(airDateField); - } - } - - if (episodes) { - - var paddedEpisodes; - var paddedAbsoluteEpisode; - - if (episodes.constructor === Array) { - paddedEpisodes = _.map(episodes, function(episodeNumber) { - return FormatHelpers.pad(episodeNumber, 2); - }).join(); - } else { - paddedEpisodes = FormatHelpers.pad(episodes, 2); - paddedAbsoluteEpisode = FormatHelpers.pad(absoluteEpisodeNumber, 2); - } - - result = '{0}x{1}'.format(seasonNumber, paddedEpisodes); - - if (absoluteEpisodeNumber !== undefined && paddedAbsoluteEpisode) { - result += ' ({0})'.format(paddedAbsoluteEpisode); - } - } else if (airDate) { - result = new Date(airDate).toLocaleDateString(); - } - - this.$el.html(result); - } - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeStatusCell.js b/src/UI/Cells/EpisodeStatusCell.js deleted file mode 100644 index 50de17206..000000000 --- a/src/UI/Cells/EpisodeStatusCell.js +++ /dev/null @@ -1,127 +0,0 @@ -var reqres = require('../reqres'); -var Backbone = require('backbone'); -var NzbDroneCell = require('./NzbDroneCell'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-status-cell', - - render : function() { - this.listenTo(QueueCollection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - - if (this.episodeFile) { - this.stopListening(this.episodeFile, 'change', this._refresh); - } - - this.$el.empty(); - - if (this.model) { - - var icon; - var tooltip; - - var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment()); - this.episodeFile = this._getFile(); - - if (this.episodeFile) { - this.listenTo(this.episodeFile, 'change', this._refresh); - - var quality = this.episodeFile.get('quality'); - var revision = quality.revision; - var size = FormatHelpers.bytes(this.episodeFile.get('size')); - var title = 'Episode downloaded'; - - if (revision.real && revision.real > 0) { - title += '[REAL]'; - } - - if (revision.version && revision.version > 1) { - title += ' [PROPER]'; - } - - if (size !== '') { - title += ' - {0}'.format(size); - } - - if (this.episodeFile.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return; - } - - else { - var model = this.model; - var downloading = QueueCollection.findEpisode(model.get('id')); - - if (downloading) { - var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); - - if (progress === 0) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Episode is downloading'; - } - - else { - this.$el.html('<div class="progress" title="Episode is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - return; - } - } - - else if (this.model.get('grabbed')) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Episode is downloading'; - } - - else if (!this.model.get('airDateUtc')) { - icon = 'icon-lidarr-tba'; - tooltip = 'TBA'; - } - - else if (hasAired) { - icon = 'icon-lidarr-missing'; - tooltip = 'Episode missing from disk'; - } else { - icon = 'icon-lidarr-not-aired'; - tooltip = 'Episode has not aired'; - } - } - - this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip)); - } - }, - - _getFile : function() { - var hasFile = this.model.get('hasFile'); - - if (hasFile) { - var episodeFile; - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, this.model.get('episodeFileId')); - } - - else if (this.model.has('episodeFile')) { - episodeFile = new Backbone.Model(this.model.get('episodeFile')); - } - - if (episodeFile) { - return episodeFile; - } - } - - return undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EpisodeTitleCell.js b/src/UI/Cells/EpisodeTitleCell.js deleted file mode 100644 index 7dce10ede..000000000 --- a/src/UI/Cells/EpisodeTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-title-cell', - - events : { - 'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideSeriesLink = this.column.get('hideSeriesLink'); - vent.trigger(vent.Commands.ShowEpisodeDetails, { - episode : this.cellValue, - hideSeriesLink : hideSeriesLink - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/EventTypeCell.js b/src/UI/Cells/EventTypeCell.js deleted file mode 100644 index d9c643795..000000000 --- a/src/UI/Cells/EventTypeCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'history-event-type-cell', - - render : function() { - this.$el.empty(); - - if (this.cellValue) { - var icon; - var toolTip; - - switch (this.cellValue.get('eventType')) { - case 'grabbed': - icon = 'icon-lidarr-downloading'; - toolTip = 'Episode grabbed from {0} and sent to download client'.format(this.cellValue.get('data').indexer); - break; - case 'seriesFolderImported': - icon = 'icon-lidarr-hdd'; - toolTip = 'Existing episode file added to library'; - break; - case 'downloadFolderImported': - icon = 'icon-lidarr-imported'; - toolTip = 'Episode downloaded successfully and picked up from download client'; - break; - case 'downloadFailed': - icon = 'icon-lidarr-download-failed'; - toolTip = 'Episode download failed'; - break; - case 'episodeFileDeleted': - icon = 'icon-lidarr-deleted'; - toolTip = 'Episode file deleted'; - break; - default: - icon = 'icon-lidarr-unknown'; - toolTip = 'unknown event'; - } - - this.$el.html('<i class="{0}" title="{1}" data-placement="right"/>'.format(icon, toolTip)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/FileSizeCell.js b/src/UI/Cells/FileSizeCell.js deleted file mode 100644 index 586d5f35c..000000000 --- a/src/UI/Cells/FileSizeCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'file-size-cell', - - render : function() { - var size = this.model.get(this.column.get('name')); - this.$el.html(FormatHelpers.bytes(size)); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/IndexerCell.js b/src/UI/Cells/IndexerCell.js deleted file mode 100644 index bbd2e90df..000000000 --- a/src/UI/Cells/IndexerCell.js +++ /dev/null @@ -1,11 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'indexer-cell', - - render : function() { - var indexer = this.model.get(this.column.get('name')); - this.$el.html(indexer); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/NzbDroneCell.js b/src/UI/Cells/NzbDroneCell.js deleted file mode 100644 index 7bd6125f3..000000000 --- a/src/UI/Cells/NzbDroneCell.js +++ /dev/null @@ -1,61 +0,0 @@ -var Backgrid = require('backgrid'); -var Backbone = require('backbone'); - -module.exports = Backgrid.Cell.extend({ - - _originalInit : Backgrid.Cell.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - this.cellValue = this._getValue(); - - this.listenTo(this.model, 'change', this._refresh); - - if (this._onEdit) { - this.listenTo(this.model, 'backgrid:edit', function(model, column, cell, editor) { - if (column.get('name') === this.column.get('name')) { - this._onEdit(model, column, cell, editor); - } - }); - } - }, - - _refresh : function() { - this.cellValue = this._getValue(); - this.render(); - }, - - _getValue : function() { - - var cellValue = this.column.get('cellValue'); - - if (cellValue) { - if (cellValue === 'this') { - return this.model; - } - - else { - return this.model.get(cellValue); - } - } - - var name = this.column.get('name'); - - if (name === 'this') { - return this.model; - } - - var value = this.model.get(name); - - if (!value) { - return undefined; - } - - //if not a model - if (!value.get && typeof value === 'object') { - value = new Backbone.Model(value); - } - - return value; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ProfileCell.js b/src/UI/Cells/ProfileCell.js deleted file mode 100644 index d87ee1af9..000000000 --- a/src/UI/Cells/ProfileCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var Backgrid = require('backgrid'); -var ProfileCollection = require('../Profile/ProfileCollection'); -var _ = require('underscore'); - -module.exports = Backgrid.Cell.extend({ - className : 'profile-cell', - - _originalInit : Backgrid.Cell.prototype.initialize, - - initialize : function () { - this._originalInit.apply(this, arguments); - - this.listenTo(ProfileCollection, 'sync', this.render); - }, - - render : function() { - - this.$el.empty(); - var profileId = this.model.get(this.column.get('name')); - - var profile = _.findWhere(ProfileCollection.models, { id : profileId }); - - if (profile) { - this.$el.html(profile.get('name')); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/QualityCell.js b/src/UI/Cells/QualityCell.js deleted file mode 100644 index 962bd2ab4..000000000 --- a/src/UI/Cells/QualityCell.js +++ /dev/null @@ -1,8 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); -var QualityCellEditor = require('./Edit/QualityCellEditor'); - -module.exports = TemplatedCell.extend({ - className : 'quality-cell', - template : 'Cells/QualityCellTemplate', - editor : QualityCellEditor -}); \ No newline at end of file diff --git a/src/UI/Cells/QualityCellTemplate.hbs b/src/UI/Cells/QualityCellTemplate.hbs deleted file mode 100644 index 6625ade9b..000000000 --- a/src/UI/Cells/QualityCellTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if_gt proper compare="1"}} - <span class="badge badge-info" title="PROPER">{{quality.name}}</span> -{{else}} - <span class="badge">{{quality.name}}</span> -{{/if_gt}} \ No newline at end of file diff --git a/src/UI/Cells/RelativeDateCell.js b/src/UI/Cells/RelativeDateCell.js deleted file mode 100644 index eb69fc855..000000000 --- a/src/UI/Cells/RelativeDateCell.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var UiSettings = require('../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'relative-date-cell', - - render : function() { - - var dateStr = this.model.get(this.column.get('name')); - - if (dateStr) { - var date = moment(dateStr); - var diff = date.diff(moment().zone(date.zone()).startOf('day'), 'days', true); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (diff > 0 && diff < 1) { - text = date.format(UiSettings.time(true, false)); - } else { - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(dateStr); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - } - - this.$el.html(result.format(tooltip, text)); - } - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/RelativeTimeCell.js b/src/UI/Cells/RelativeTimeCell.js deleted file mode 100644 index b0d552bfd..000000000 --- a/src/UI/Cells/RelativeTimeCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); -var UiSettings = require('../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'relative-time-cell', - - render : function() { - - var dateStr = this.model.get(this.column.get('name')); - - if (dateStr) { - var date = moment(dateStr); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = date.fromNow(); - } else { - text = date.format(UiSettings.shortDateTime()); - } - - this.$el.html(result.format(tooltip, text)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ReleaseTitleCell.js b/src/UI/Cells/ReleaseTitleCell.js deleted file mode 100644 index 7d3551e41..000000000 --- a/src/UI/Cells/ReleaseTitleCell.js +++ /dev/null @@ -1,20 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'release-title-cell', - - render : function() { - this.$el.empty(); - - var title = this.model.get('title'); - var infoUrl = this.model.get('infoUrl'); - - if (infoUrl) { - this.$el.html('<a href="{0}">{1}</a>'.format(infoUrl, title)); - } else { - this.$el.html(title); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeasonFolderCell.js b/src/UI/Cells/SeasonFolderCell.js deleted file mode 100644 index 7a9385b84..000000000 --- a/src/UI/Cells/SeasonFolderCell.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'season-folder-cell', - - render : function() { - this.$el.empty(); - - var seasonFolder = this.model.get(this.column.get('name')); - this.$el.html(seasonFolder.toString()); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SelectAllCell.js b/src/UI/Cells/SelectAllCell.js deleted file mode 100644 index e89289f40..000000000 --- a/src/UI/Cells/SelectAllCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var BackgridSelectAll = require('backgrid.selectall'); - -module.exports = BackgridSelectAll.extend({ - enterEditMode : function(e) { - var collection = this.column.get('sortedCollection') || this.model.collection; - - if (e.shiftKey && collection.lastToggled) { - this._selectRange(collection); - } - - var checked = $(e.target).prop('checked'); - - collection.lastToggled = this.model; - collection.checked = checked; - }, - - onChange : function(e) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger('backgrid:selected', this.model, checked); - }, - - _selectRange : function(collection) { - var lastToggled = collection.lastToggled; - var checked = collection.checked; - - var currentIndex = collection.indexOf(this.model); - var lastIndex = collection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = collection.at(index); - - model.trigger('backgrid:select', model, checked); - }); - - collection.lastToggled = undefined; - collection.checked = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesActionsCell.js b/src/UI/Cells/SeriesActionsCell.js deleted file mode 100644 index 71f7b22a8..000000000 --- a/src/UI/Cells/SeriesActionsCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'series-actions-cell', - - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshSeries' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-refresh x-refresh hidden-xs" title="" data-original-title="Update series info and scan disk"></i> ' + - '<i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Series"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-refresh'), - command : { - name : 'refreshSeries', - seriesId : this.model.get('id') - } - }); - - this.delegateEvents(); - return this; - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshSeries : function() { - CommandController.Execute('refreshSeries', { - name : 'refreshSeries', - seriesId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesStatusCell.js b/src/UI/Cells/SeriesStatusCell.js deleted file mode 100644 index 7e99d8587..000000000 --- a/src/UI/Cells/SeriesStatusCell.js +++ /dev/null @@ -1,32 +0,0 @@ -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'series-status-cell', - - render : function() { - this.$el.empty(); - var monitored = this.model.get('monitored'); - var status = this.model.get('status'); - - if (status === 'ended') { - this.$el.html('<i class="icon-lidarr-series-ended grid-icon" title="Ended"></i>'); - this._setStatusWeight(3); - } - - else if (!monitored) { - this.$el.html('<i class="icon-lidarr-series-unmonitored grid-icon" title="Not Monitored"></i>'); - this._setStatusWeight(2); - } - - else { - this.$el.html('<i class="icon-lidarr-series-continuing grid-icon" title="Continuing"></i>'); - this._setStatusWeight(1); - } - - return this; - }, - - _setStatusWeight : function(weight) { - this.model.set('statusWeight', weight, { silent : true }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesTitleCell.js b/src/UI/Cells/SeriesTitleCell.js deleted file mode 100644 index a516b4e09..000000000 --- a/src/UI/Cells/SeriesTitleCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('./TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'series-title-cell', - template : 'Cells/SeriesTitleTemplate' -}); \ No newline at end of file diff --git a/src/UI/Cells/SeriesTitleTemplate.hbs b/src/UI/Cells/SeriesTitleTemplate.hbs deleted file mode 100644 index 99205b00a..000000000 --- a/src/UI/Cells/SeriesTitleTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{route}}">{{title}}</a> diff --git a/src/UI/Cells/TemplatedCell.js b/src/UI/Cells/TemplatedCell.js deleted file mode 100644 index 1299d4e36..000000000 --- a/src/UI/Cells/TemplatedCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - render : function() { - - var templateName = this.column.get('template') || this.template; - - this.templateFunction = Marionette.TemplateCache.get(templateName); - this.$el.empty(); - - if (this.cellValue) { - var data = this.cellValue.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/ToggleCell.js b/src/UI/Cells/ToggleCell.js deleted file mode 100644 index 0de0762fd..000000000 --- a/src/UI/Cells/ToggleCell.js +++ /dev/null @@ -1,48 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'toggle-cell', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - - var self = this; - - this.$el.tooltip('hide'); - - var name = this.column.get('name'); - this.model.set(name, !this.model.get(name)); - - var promise = this.model.save(); - - this.$('i').spinForPromise(promise); - - promise.always(function() { - self.render(); - }); - }, - - render : function() { - this.$el.empty(); - this.$el.html('<i />'); - - var name = this.column.get('name'); - - if (this.model.get(name)) { - this.$('i').addClass(this.column.get('trueClass')); - } else { - this.$('i').addClass(this.column.get('falseClass')); - } - - var tooltip = this.column.get('tooltip'); - - if (tooltip) { - this.$('i').attr('title', tooltip); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackActionsCell.js b/src/UI/Cells/TrackActionsCell.js deleted file mode 100644 index 1bf8baaf4..000000000 --- a/src/UI/Cells/TrackActionsCell.js +++ /dev/null @@ -1,44 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); -var CommandController = require('../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'track-actions-cell', - - events : { - 'click .x-automatic-search' : '_automaticSearch', - 'click .x-manual-search' : '_manualSearch' - }, - - render : function() { - this.$el.empty(); - - this.$el.html('<i class="icon-lidarr-search x-automatic-search" title="Automatic Search"></i>' + '<i class="icon-lidarr-search-manual x-manual-search" title="Manual Search"></i>'); - - CommandController.bindToCommand({ - element : this.$el.find('.x-automatic-search'), - command : { - name : 'trackSearch', - trackIds : [this.model.get('id')] - } - }); - - this.delegateEvents(); - return this; - }, - - _automaticSearch : function() { - CommandController.Execute('trackSearch', { - name : 'trackSearch', - trackIds : [this.model.get('id')] - }); - }, - - _manualSearch : function() { - vent.trigger(vent.Commands.ShowTrackDetails, { - track : this.cellValue, - hideSeriesLink : true, - openingTab : 'search' - }); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackDurationCell.js b/src/UI/Cells/TrackDurationCell.js deleted file mode 100644 index d3a9d42ec..000000000 --- a/src/UI/Cells/TrackDurationCell.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'track-duration-cell', - - render : function() { - var duration = this.model.get(this.column.get('name')); - this.$el.html(FormatHelpers.timeMinSec(duration,'ms')); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackExplicitCell.js b/src/UI/Cells/TrackExplicitCell.js deleted file mode 100644 index 19eb89ea5..000000000 --- a/src/UI/Cells/TrackExplicitCell.js +++ /dev/null @@ -1,20 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-explicit-cell', - template : 'Cells/TrackExplicitCellTemplate', - - render : function() { - var explicit = this.cellValue.get('explicit'); - var print = ''; - - if (explicit === true) { - print = 'Explicit'; - } - - this.$el.html(print); - return this; - } - -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackMonitoredCell.js b/src/UI/Cells/TrackMonitoredCell.js deleted file mode 100644 index aa3d987ae..000000000 --- a/src/UI/Cells/TrackMonitoredCell.js +++ /dev/null @@ -1,57 +0,0 @@ -var _ = require('underscore'); -var ToggleCell = require('./ToggleCell'); -var ArtistCollection = require('../Artist/ArtistCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = ToggleCell.extend({ - className : 'toggle-cell track-monitored', - - _originalOnClick : ToggleCell.prototype._onClick, - - _onClick : function(e) { - - var artist = ArtistCollection.get(this.model.get('artistId')); - - if (!artist.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when artist is not monitored', - type : 'error' - }); - - return; - } - - if (e.shiftKey && this.model.trackCollection.lastToggled) { - this._selectRange(); - - return; - } - - this._originalOnClick.apply(this, arguments); - this.model.trackCollection.lastToggled = this.model; - }, - - _selectRange : function() { - var trackCollection = this.model.trackCollection; - var lastToggled = trackCollection.lastToggled; - - var currentIndex = trackCollection.indexOf(this.model); - var lastIndex = trackCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - _.each(range, function(index) { - var model = trackCollection.at(index); - - model.set('monitored', lastToggled.get('monitored')); - model.save(); - }); - - this.model.set('monitored', lastToggled.get('monitored')); - this.model.save(); - this.model.trackCollection.lastToggled = undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackProgressCell.js b/src/UI/Cells/TrackProgressCell.js deleted file mode 100644 index 1b9f93599..000000000 --- a/src/UI/Cells/TrackProgressCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-progress-cell', - template : 'Cells/TrackProgressCellTemplate', - - render : function() { - - var trackCount = this.model.get('trackCount'); - var trackFileCount = this.model.get('trackFileCount'); - - var percent = 100; - - if (trackCount > 0) { - percent = trackFileCount / trackCount * 100; - } - - this.model.set('percentOfTracks', percent); - - this.templateFunction = Marionette.TemplateCache.get(this.template); - var data = this.model.toJSON(); - var html = this.templateFunction(data); - this.$el.html(html); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackProgressCellTemplate.hbs b/src/UI/Cells/TrackProgressCellTemplate.hbs deleted file mode 100644 index b4899728f..000000000 --- a/src/UI/Cells/TrackProgressCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -{{> TrackProgressPartial }} \ No newline at end of file diff --git a/src/UI/Cells/TrackStatusCell.js b/src/UI/Cells/TrackStatusCell.js deleted file mode 100644 index c7e9e6362..000000000 --- a/src/UI/Cells/TrackStatusCell.js +++ /dev/null @@ -1,127 +0,0 @@ -var reqres = require('../reqres'); -var Backbone = require('backbone'); -var NzbDroneCell = require('./NzbDroneCell'); -var QueueCollection = require('../Activity/Queue/QueueCollection'); -var moment = require('moment'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = NzbDroneCell.extend({ - className : 'track-status-cell', - - render : function() { - this.listenTo(QueueCollection, 'sync', this._renderCell); - - this._renderCell(); - - return this; - }, - - _renderCell : function() { - - if (this.trackFile) { - this.stopListening(this.trackFile, 'change', this._refresh); - } - - this.$el.empty(); - - if (this.model) { - - var icon; - var tooltip; - - var hasAired = moment(this.model.get('airDateUtc')).isBefore(moment()); - this.trackFile = this._getFile(); - - if (this.trackFile) { - this.listenTo(this.trackFile, 'change', this._refresh); - - var quality = this.trackFile.get('quality'); - var revision = quality.revision; - var size = FormatHelpers.bytes(this.trackFile.get('size')); - var title = 'Track downloaded'; - - if (revision.real && revision.real > 0) { - title += '[REAL]'; - } - - if (revision.version && revision.version > 1) { - title += ' [PROPER]'; - } - - if (size !== '') { - title += ' - {0}'.format(size); - } - - if (this.trackFile.get('qualityCutoffNotMet')) { - this.$el.html('<span class="badge badge-inverse" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } else { - this.$el.html('<span class="badge" title="{0}">{1}</span>'.format(title, quality.quality.name)); - } - - return; - } - - else { - var model = this.model; - var downloading = false; //TODO Fix this by adding to QueueCollection - - if (downloading) { - var progress = 100 - (downloading.get('sizeleft') / downloading.get('size') * 100); - - if (progress === 0) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Track is downloading'; - } - - else { - this.$el.html('<div class="progress" title="Track is downloading - {0}% {1}">'.format(progress.toFixed(1), downloading.get('title')) + - '<div class="progress-bar progress-bar-purple" style="width: {0}%;"></div></div>'.format(progress)); - return; - } - } - - else if (this.model.get('grabbed')) { - icon = 'icon-lidarr-downloading'; - tooltip = 'Track is downloading'; - } - - else if (!this.model.get('airDateUtc')) { - icon = 'icon-lidarr-tba'; - tooltip = 'TBA'; - } - - else if (hasAired) { - icon = 'icon-lidarr-missing'; - tooltip = 'Track missing from disk'; - } else { - icon = 'icon-lidarr-not-aired'; - tooltip = 'Track has not aired'; - } - } - - this.$el.html('<i class="{0}" title="{1}"/>'.format(icon, tooltip)); - } - }, - - _getFile : function() { - var hasFile = this.model.get('hasFile'); - - if (hasFile) { - var trackFile; - - if (reqres.hasHandler(reqres.Requests.GetTrackFileById)) { - trackFile = reqres.request(reqres.Requests.GetTrackFileById, this.model.get('trackFileId')); - } - - else if (this.model.has('trackFile')) { - trackFile = new Backbone.Model(this.model.get('trackFile')); - } - - if (trackFile) { - return trackFile; - } - } - - return undefined; - } -}); \ No newline at end of file diff --git a/src/UI/Cells/TrackTitleCell.js b/src/UI/Cells/TrackTitleCell.js deleted file mode 100644 index ab2777e52..000000000 --- a/src/UI/Cells/TrackTitleCell.js +++ /dev/null @@ -1,29 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('./NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'track-title-cell', - - events : { - //'click' : '_showDetails' - }, - - render : function() { - var title = this.cellValue.get('title'); - - if (!title || title === '') { - title = 'TBA'; - } - - this.$el.html(title); - return this; - }, - - _showDetails : function() { - var hideArtistLink = this.column.get('hideArtistLink'); - //vent.trigger(vent.Commands.ShowTrackDetails, { //TODO Impelement Track search and screen as well as album? - // track : this.cellValue, - // hideArtistLink : hideArtistLink - //}); - } -}); \ No newline at end of file diff --git a/src/UI/Cells/cells.less b/src/UI/Cells/cells.less deleted file mode 100644 index f873017c9..000000000 --- a/src/UI/Cells/cells.less +++ /dev/null @@ -1,286 +0,0 @@ -@import "../Content/Bootstrap/mixins"; -@import "../Content/Bootstrap/variables"; -@import "../Content/Bootstrap/buttons"; -@import "../Shared/Styles/clickable"; -@import "../Content/mixins"; -@import "../Content/variables"; - -.artist-title-cell { - .text-overflow(); - - max-width: 450px; - - @media @sm { - max-width: 250px - } -} - -.artist-monitored-cell { - .text-overflow(); - - .artist-monitored-name { - padding-left: 10px; - } -} - -.track-title-cell { - .text-overflow(); - - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } - - @media @lg { - max-width: 350px; - } - - @media @md { - max-width: 250px; - } - - @media @sm { - max-width: 200px; - } -} - -.album-title-cell { - .text-overflow(); - - color: #428bca; - text-decoration: none; - - &:focus, &:hover { - color: #2a6496; - text-decoration: underline; - cursor: pointer; - } - - @media @lg { - max-width: 350px; - } - - @media @md { - max-width: 250px; - } - - @media @sm { - max-width: 200px; - } -} - -.air-date-cell { - width : 120px; - cursor: default; - .text-overflow(); -} - -.relative-date-cell, .relative-time-cell { - .text-overflow(); - cursor : default; -} - -.relative-date-cell { - width : 150px; -} - -.history-event-type-cell { - width : 10px; -} - -.download-report-cell { - .clickable(); - - width : 32px; - - i { - .clickable(); - } -} - -.toggle-cell{ - .clickable(); - .not-selectable; -} - -.approval-status-cell { - - .popover { - max-width : 400px; - - ul { - margin-left: -25px; - } - } - - i { - color : @brand-danger; - } -} - -td.track-status-cell, td.quality-cell, td.history-quality-cell, td.progress-cell { - text-align: center; - width: 80px; - - .badge { - font-size: 10px; - } - - .progress { - height : 10px; - margin-top : 5px; - margin-bottom : 0px; - } -} - -.history-details-cell { - .clickable(); - width: 10px; - - i { - .clickable(); - } -} - -.release-title-cell { - max-width: 400px; - word-wrap: break-word; -} - -.track-actions-cell { - width: 55px; - - i { - .clickable(); - margin-left : 8px; - - &:first-of-type { - margin-left : 0px; - } - } -} - -.track-history-details-cell { - width : 18px; -} - -.track-detail-modal { - .track-actions-cell { - width : 18px; - } -} - -.artist-actions-cell { - width : 56px; - min-width : 56px; -} - -.timeleft-cell { - cursor : default; - width : 80px; - text-align : center; -} - -.queue-status-cell { - width : 20px; - text-align : center !important; -} - -.queue-actions-cell { - min-width : 65px; - width : 65px; - text-align : right !important; - - i { - .clickable(); - margin-left : 1px; - margin-right : 1px; - } -} - -.download-log-cell { - width : 80px; -} - -td.delete-track-file-cell { - .clickable(); - - text-align : center; - width : 20px; - - i { - .clickable(); - } -} - -.artist-status-cell { - width: 16px; -} - -.track-number-cell { - cursor : default; -} - -.backup-type-cell { - width : 20px; -} - -.table>tbody>tr>td, .table>thead>tr>th { - - &.track-warning-cell { - width : 1px; - padding-left : 0px; - padding-right : 0px; - } -} - -.log-message-cell { - word-break: break-all; - word-wrap: break-word; -} - -.execute-task-cell { - width : 28px; - - i { - .clickable(); - } -} - -.task-interval-cell, .next-execution-cell { - cursor : default; -} - -.task-interval-cell { - width : 150px; -} - -.next-execution-cell { - width : 200px; -} - -.tasks { - .relative-time-cell { - width : 200px; - } -} - -.age-cell { - cursor : default; -} - -.blacklist-actions-cell { - min-width : 55px; - width : 55px; - text-align : right !important; - - i { - .clickable(); - margin-left : 2px; - margin-right : 2px; - } -} diff --git a/src/UI/Commands/CommandCollection.js b/src/UI/Commands/CommandCollection.js deleted file mode 100644 index b8eaae543..000000000 --- a/src/UI/Commands/CommandCollection.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); -var CommandModel = require('./CommandModel'); -require('../Mixins/backbone.signalr.mixin'); - -var CommandCollection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/command', - model : CommandModel, - - findCommand : function(command) { - return this.find(function(model) { - return model.isSameCommand(command); - }); - } -}); - -var collection = new CommandCollection().bindSignalR(); - -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Commands/CommandController.js b/src/UI/Commands/CommandController.js deleted file mode 100644 index 2232d45ae..000000000 --- a/src/UI/Commands/CommandController.js +++ /dev/null @@ -1,94 +0,0 @@ -var vent = require('vent'); -var CommandModel = require('./CommandModel'); -var CommandCollection = require('./CommandCollection'); -var CommandMessengerCollectionView = require('./CommandMessengerCollectionView'); -var _ = require('underscore'); -var moment = require('moment'); -var Messenger = require('../Shared/Messenger'); -require('../jQuery/jquery.spin'); - -CommandMessengerCollectionView.render(); - -var singleton = function() { - - return { - - _lastCommand : {}, - - Execute : function(name, properties) { - - var attr = _.extend({ name : name.toLocaleLowerCase() }, properties); - var commandModel = new CommandModel(attr); - - if (this._lastCommand.command && this._lastCommand.command.isSameCommand(attr) && moment().add('seconds', -5).isBefore(this._lastCommand.time)) { - - Messenger.show({ - message : 'Please wait at least 5 seconds before running this command again', - hideAfter : 5, - type : 'error' - }); - - return this._lastCommand.promise; - } - - var promise = commandModel.save().success(function() { - CommandCollection.add(commandModel); - }); - - this._lastCommand = { - command : commandModel, - promise : promise, - time : moment() - }; - - return promise; - }, - - bindToCommand : function(options) { - - var self = this; - var existingCommand = CommandCollection.findCommand(options.command); - - if (existingCommand) { - this._bindToCommandModel.call(this, existingCommand, options); - } - - CommandCollection.bind('add', function(model) { - if (model.isSameCommand(options.command)) { - self._bindToCommandModel.call(self, model, options); - } - }); - - CommandCollection.bind('sync', function() { - var command = CommandCollection.findCommand(options.command); - if (command) { - self._bindToCommandModel.call(self, command, options); - } - }); - }, - - _bindToCommandModel : function bindToCommand (model, options) { - - if (!model.isActive()) { - options.element.stopSpin(); - return; - } - - model.bind('change:status', function(model) { - if (!model.isActive()) { - options.element.stopSpin(); - - if (model.isComplete()) { - vent.trigger(vent.Events.CommandComplete, { - command : model, - model : options.model - }); - } - } - }); - - options.element.startSpin(); - } - }; -}; -module.exports = singleton(); diff --git a/src/UI/Commands/CommandMessengerCollectionView.js b/src/UI/Commands/CommandMessengerCollectionView.js deleted file mode 100644 index 007760087..000000000 --- a/src/UI/Commands/CommandMessengerCollectionView.js +++ /dev/null @@ -1,11 +0,0 @@ -var Marionette = require('marionette'); -var commandCollection = require('./CommandCollection'); -var CommandMessengerItemView = require('./CommandMessengerItemView'); - -var CollectionView = Marionette.CollectionView.extend({ - itemView : CommandMessengerItemView -}); - -module.exports = new CollectionView({ - collection : commandCollection -}); diff --git a/src/UI/Commands/CommandMessengerItemView.js b/src/UI/Commands/CommandMessengerItemView.js deleted file mode 100644 index c7f419f31..000000000 --- a/src/UI/Commands/CommandMessengerItemView.js +++ /dev/null @@ -1,45 +0,0 @@ -var Marionette = require('marionette'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.ItemView.extend({ - initialize : function() { - this.listenTo(this.model, 'change', this.render); - }, - - render : function() { - if (!this.model.get('message') || !this.model.get('sendUpdatesToClient')) { - return; - } - - var message = { - type : 'info', - message : '[{0}] {1}'.format(this.model.get('name'), this.model.get('message')), - id : this.model.id, - hideAfter : 0 - }; - - var isManual = this.model.get('manual'); - - switch (this.model.get('state')) { - case 'completed': - message.hideAfter = 4; - break; - case 'failed': - message.hideAfter = isManual ? 10 : 4; - message.type = 'error'; - break; - default : - message.hideAfter = 0; - } - - if (this.messenger) { - this.messenger.update(message); - } - - else { - this.messenger = Messenger.show(message); - } - - console.log(message.message); - } -}); \ No newline at end of file diff --git a/src/UI/Commands/CommandModel.js b/src/UI/Commands/CommandModel.js deleted file mode 100644 index 674067b24..000000000 --- a/src/UI/Commands/CommandModel.js +++ /dev/null @@ -1,50 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/command', - - parse : function(response) { - response.name = response.name.toLocaleLowerCase(); - response.body.name = response.body.name.toLocaleLowerCase(); - - for (var key in response.body) { - response[key] = response.body[key]; - } - - delete response.body; - - return response; - }, - - isSameCommand : function(command) { - - if (command.name.toLocaleLowerCase() !== this.get('name').toLocaleLowerCase()) { - return false; - } - - for (var key in command) { - if (key !== 'name') { - if (Array.isArray(command[key])) { - if (_.difference(command[key], this.get(key)).length > 0) { - return false; - } - } - - else if (command[key] !== this.get(key)) { - return false; - } - } - } - - return true; - }, - - isActive : function() { - return this.get('status') !== 'completed' && this.get('status') !== 'failed'; - }, - - isComplete : function() { - return this.get('status') === 'completed'; - } -}); \ No newline at end of file diff --git a/src/UI/Config.js b/src/UI/Config.js deleted file mode 100644 index a080f1343..000000000 --- a/src/UI/Config.js +++ /dev/null @@ -1,69 +0,0 @@ -var $ = require('jquery'); -var vent = require('./vent'); - -module.exports = { - Events : { - ConfigUpdatedEvent : 'ConfigUpdatedEvent' - }, - - Keys : { - DefaultProfileId : 'DefaultProfileId', - DefaultRootFolderId : 'DefaultRootFolderId', - UseAlbumFolder : 'UseAlbumFolder', - DefaultArtistType : 'DefaultArtistType', - MonitorEpisodes : 'MonitorEpisodes', - AdvancedSettings : 'advancedSettings' - }, - - getValueJson : function (key, defaultValue) { - defaultValue = defaultValue || {}; - - var storeValue = window.localStorage.getItem(key); - - if (!storeValue) { - return defaultValue; - } - - return $.parseJSON(storeValue); - }, - - getValueBoolean : function(key, defaultValue) { - defaultValue = defaultValue || false; - - return this.getValue(key, defaultValue.toString()) === 'true'; - }, - - getValue : function(key, defaultValue) { - var storeValue = window.localStorage.getItem(key); - - if (!storeValue) { - return defaultValue; - } - - return storeValue.toString(); - }, - - setValueJson : function(key, value) { - return this.setValue(key, JSON.stringify(value)); - }, - - setValue : function(key, value) { - - console.log('Config: [{0}] => [{1}]'.format(key, value)); - - if (this.getValue(key) === value.toString()) { - return; - } - - try { - window.localStorage.setItem(key, value); - vent.trigger(this.Events.ConfigUpdatedEvent, { - key : key, - value : value - }); - } - catch (error) { - console.error('Unable to save config: [{0}] => [{1}]'.format(key, value)); - } - } -}; diff --git a/src/UI/Content/Backgrid/backgrid.less b/src/UI/Content/Backgrid/backgrid.less deleted file mode 100644 index ae1d46943..000000000 --- a/src/UI/Content/Backgrid/backgrid.less +++ /dev/null @@ -1,3 +0,0 @@ -@import "filter"; -@import "paginator"; -@import "selectall"; \ No newline at end of file diff --git a/src/UI/Content/Backgrid/filter.less b/src/UI/Content/Backgrid/filter.less deleted file mode 100644 index 84313310a..000000000 --- a/src/UI/Content/Backgrid/filter.less +++ /dev/null @@ -1,11 +0,0 @@ -.backgrid-filter .close { - display : inline-block; - float : none; - width : 20px; - height : 20px; - margin-top : -4px; - font-size : 20px; - line-height : 20px; - text-align : center; - vertical-align : text-top; -} diff --git a/src/UI/Content/Backgrid/paginator.less b/src/UI/Content/Backgrid/paginator.less deleted file mode 100644 index 61fced052..000000000 --- a/src/UI/Content/Backgrid/paginator.less +++ /dev/null @@ -1,66 +0,0 @@ -@import "../prefixer"; -@import "../../Shared/Styles/clickable.less"; - -.backgrid-paginator { - text-align : center; - box-sizing : border-box; - border-top : none; - .box-sizing(border-box); - .border-radius(0 0 4px 4px); - position: relative; - - .total-records { - display : inline-block; - height : 30px; - padding : 0; - line-height: 30px; - font-size : 13px; - position : absolute; - right : 0; - - .label { - margin-top: 5px; - } - } - - ul { - display : inline-block; - - li { - display : inline; - - i, span { - float : left; - width : 30px; - height : 30px; - padding : 0; - line-height : 30px; - text-decoration : none; - } - - select { - width: auto; - } - - .pager-btn { - .clickable; - } - } - .active { - span { - background-color : #f5f5f5; - color : #999999; - cursor : default; - width : inherit; - padding : 0px 2px; - } - } - - .disabled { - i, span { - color : #999999; - cursor : default; - } - } - } -} diff --git a/src/UI/Content/Backgrid/selectall.less b/src/UI/Content/Backgrid/selectall.less deleted file mode 100644 index 322853304..000000000 --- a/src/UI/Content/Backgrid/selectall.less +++ /dev/null @@ -1,12 +0,0 @@ -/* - backgrid-select-all - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ - -.select-row-cell, .select-all-header-cell { - text-align: center; - width: 16px; -} \ No newline at end of file diff --git a/src/UI/Content/Bootstrap/.csscomb.json b/src/UI/Content/Bootstrap/.csscomb.json deleted file mode 100644 index 40695a478..000000000 --- a/src/UI/Content/Bootstrap/.csscomb.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "always-semicolon": true, - "block-indent": 2, - "color-case": "lower", - "color-shorthand": true, - "element-case": "lower", - "eof-newline": true, - "leading-zero": false, - "remove-empty-rulesets": true, - "space-after-colon": 1, - "space-after-combinator": 1, - "space-before-selector-delimiter": 0, - "space-between-declarations": "\n", - "space-after-opening-brace": "\n", - "space-before-closing-brace": "\n", - "space-before-colon": 0, - "space-before-combinator": 1, - "space-before-opening-brace": 1, - "strip-spaces": true, - "unitless-zero": true, - "vendor-prefix-align": true, - "sort-order": [ - [ - "position", - "top", - "right", - "bottom", - "left", - "z-index", - "display", - "float", - "width", - "min-width", - "max-width", - "height", - "min-height", - "max-height", - "-webkit-box-sizing", - "-moz-box-sizing", - "box-sizing", - "-webkit-appearance", - "padding", - "padding-top", - "padding-right", - "padding-bottom", - "padding-left", - "margin", - "margin-top", - "margin-right", - "margin-bottom", - "margin-left", - "overflow", - "overflow-x", - "overflow-y", - "-webkit-overflow-scrolling", - "-ms-overflow-x", - "-ms-overflow-y", - "-ms-overflow-style", - "clip", - "clear", - "font", - "font-family", - "font-size", - "font-style", - "font-weight", - "font-variant", - "font-size-adjust", - "font-stretch", - "font-effect", - "font-emphasize", - "font-emphasize-position", - "font-emphasize-style", - "font-smooth", - "-webkit-hyphens", - "-moz-hyphens", - "hyphens", - "line-height", - "color", - "text-align", - "-webkit-text-align-last", - "-moz-text-align-last", - "-ms-text-align-last", - "text-align-last", - "text-emphasis", - "text-emphasis-color", - "text-emphasis-style", - "text-emphasis-position", - "text-decoration", - "text-indent", - "text-justify", - "text-outline", - "-ms-text-overflow", - "text-overflow", - "text-overflow-ellipsis", - "text-overflow-mode", - "text-shadow", - "text-transform", - "text-wrap", - "-webkit-text-size-adjust", - "-ms-text-size-adjust", - "letter-spacing", - "-ms-word-break", - "word-break", - "word-spacing", - "-ms-word-wrap", - "word-wrap", - "-moz-tab-size", - "-o-tab-size", - "tab-size", - "white-space", - "vertical-align", - "list-style", - "list-style-position", - "list-style-type", - "list-style-image", - "pointer-events", - "-ms-touch-action", - "touch-action", - "cursor", - "visibility", - "zoom", - "flex-direction", - "flex-order", - "flex-pack", - "flex-align", - "table-layout", - "empty-cells", - "caption-side", - "border-spacing", - "border-collapse", - "content", - "quotes", - "counter-reset", - "counter-increment", - "resize", - "-webkit-user-select", - "-moz-user-select", - "-ms-user-select", - "-o-user-select", - "user-select", - "nav-index", - "nav-up", - "nav-right", - "nav-down", - "nav-left", - "background", - "background-color", - "background-image", - "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", - "filter:progid:DXImageTransform.Microsoft.gradient", - "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", - "filter", - "background-repeat", - "background-attachment", - "background-position", - "background-position-x", - "background-position-y", - "-webkit-background-clip", - "-moz-background-clip", - "background-clip", - "background-origin", - "-webkit-background-size", - "-moz-background-size", - "-o-background-size", - "background-size", - "border", - "border-color", - "border-style", - "border-width", - "border-top", - "border-top-color", - "border-top-style", - "border-top-width", - "border-right", - "border-right-color", - "border-right-style", - "border-right-width", - "border-bottom", - "border-bottom-color", - "border-bottom-style", - "border-bottom-width", - "border-left", - "border-left-color", - "border-left-style", - "border-left-width", - "border-radius", - "border-top-left-radius", - "border-top-right-radius", - "border-bottom-right-radius", - "border-bottom-left-radius", - "-webkit-border-image", - "-moz-border-image", - "-o-border-image", - "border-image", - "-webkit-border-image-source", - "-moz-border-image-source", - "-o-border-image-source", - "border-image-source", - "-webkit-border-image-slice", - "-moz-border-image-slice", - "-o-border-image-slice", - "border-image-slice", - "-webkit-border-image-width", - "-moz-border-image-width", - "-o-border-image-width", - "border-image-width", - "-webkit-border-image-outset", - "-moz-border-image-outset", - "-o-border-image-outset", - "border-image-outset", - "-webkit-border-image-repeat", - "-moz-border-image-repeat", - "-o-border-image-repeat", - "border-image-repeat", - "outline", - "outline-width", - "outline-style", - "outline-color", - "outline-offset", - "-webkit-box-shadow", - "-moz-box-shadow", - "box-shadow", - "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", - "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", - "opacity", - "-ms-interpolation-mode", - "-webkit-transition", - "-moz-transition", - "-ms-transition", - "-o-transition", - "transition", - "-webkit-transition-delay", - "-moz-transition-delay", - "-ms-transition-delay", - "-o-transition-delay", - "transition-delay", - "-webkit-transition-timing-function", - "-moz-transition-timing-function", - "-ms-transition-timing-function", - "-o-transition-timing-function", - "transition-timing-function", - "-webkit-transition-duration", - "-moz-transition-duration", - "-ms-transition-duration", - "-o-transition-duration", - "transition-duration", - "-webkit-transition-property", - "-moz-transition-property", - "-ms-transition-property", - "-o-transition-property", - "transition-property", - "-webkit-transform", - "-moz-transform", - "-ms-transform", - "-o-transform", - "transform", - "-webkit-transform-origin", - "-moz-transform-origin", - "-ms-transform-origin", - "-o-transform-origin", - "transform-origin", - "-webkit-animation", - "-moz-animation", - "-ms-animation", - "-o-animation", - "animation", - "-webkit-animation-name", - "-moz-animation-name", - "-ms-animation-name", - "-o-animation-name", - "animation-name", - "-webkit-animation-duration", - "-moz-animation-duration", - "-ms-animation-duration", - "-o-animation-duration", - "animation-duration", - "-webkit-animation-play-state", - "-moz-animation-play-state", - "-ms-animation-play-state", - "-o-animation-play-state", - "animation-play-state", - "-webkit-animation-timing-function", - "-moz-animation-timing-function", - "-ms-animation-timing-function", - "-o-animation-timing-function", - "animation-timing-function", - "-webkit-animation-delay", - "-moz-animation-delay", - "-ms-animation-delay", - "-o-animation-delay", - "animation-delay", - "-webkit-animation-iteration-count", - "-moz-animation-iteration-count", - "-ms-animation-iteration-count", - "-o-animation-iteration-count", - "animation-iteration-count", - "-webkit-animation-direction", - "-moz-animation-direction", - "-ms-animation-direction", - "-o-animation-direction", - "animation-direction" - ] - ] -} diff --git a/src/UI/Content/Bootstrap/.csslintrc b/src/UI/Content/Bootstrap/.csslintrc deleted file mode 100644 index 005b86236..000000000 --- a/src/UI/Content/Bootstrap/.csslintrc +++ /dev/null @@ -1,19 +0,0 @@ -{ - "adjoining-classes": false, - "box-sizing": false, - "box-model": false, - "compatible-vendor-prefixes": false, - "floats": false, - "font-sizes": false, - "gradients": false, - "important": false, - "known-properties": false, - "outline-none": false, - "qualified-headings": false, - "regex-selectors": false, - "shorthand": false, - "text-indent": false, - "unique-headings": false, - "universal-selector": false, - "unqualified-attributes": false -} diff --git a/src/UI/Content/Bootstrap/alerts.less b/src/UI/Content/Bootstrap/alerts.less deleted file mode 100644 index c4199db92..000000000 --- a/src/UI/Content/Bootstrap/alerts.less +++ /dev/null @@ -1,73 +0,0 @@ -// -// Alerts -// -------------------------------------------------- - - -// Base styles -// ------------------------- - -.alert { - padding: @alert-padding; - margin-bottom: @line-height-computed; - border: 1px solid transparent; - border-radius: @alert-border-radius; - - // Headings for larger alerts - h4 { - margin-top: 0; - // Specified for the h4 to prevent conflicts of changing @headings-color - color: inherit; - } - - // Provide class for links that match alerts - .alert-link { - font-weight: @alert-link-font-weight; - } - - // Improve alignment and spacing of inner content - > p, - > ul { - margin-bottom: 0; - } - - > p + p { - margin-top: 5px; - } -} - -// Dismissible alerts -// -// Expand the right padding and account for the close button's positioning. - -.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0. -.alert-dismissible { - padding-right: (@alert-padding + 20); - - // Adjust close link position - .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; - } -} - -// Alternate styles -// -// Generate contextual modifier classes for colorizing the alert. - -.alert-success { - .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); -} - -.alert-info { - .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); -} - -.alert-warning { - .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); -} - -.alert-danger { - .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); -} diff --git a/src/UI/Content/Bootstrap/badges.less b/src/UI/Content/Bootstrap/badges.less deleted file mode 100644 index 6ee16dca4..000000000 --- a/src/UI/Content/Bootstrap/badges.less +++ /dev/null @@ -1,66 +0,0 @@ -// -// Badges -// -------------------------------------------------- - - -// Base class -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: @font-size-small; - font-weight: @badge-font-weight; - color: @badge-color; - line-height: @badge-line-height; - vertical-align: middle; - white-space: nowrap; - text-align: center; - background-color: @badge-bg; - border-radius: @badge-border-radius; - - // Empty badges collapse automatically (not available in IE8) - &:empty { - display: none; - } - - // Quick fix for badges in buttons - .btn & { - position: relative; - top: -1px; - } - - .btn-xs &, - .btn-group-xs > .btn & { - top: 0; - padding: 1px 5px; - } - - // Hover state, but only for links - a& { - &:hover, - &:focus { - color: @badge-link-hover-color; - text-decoration: none; - cursor: pointer; - } - } - - // Account for badges in navs - .list-group-item.active > &, - .nav-pills > .active > a > & { - color: @badge-active-color; - background-color: @badge-active-bg; - } - - .list-group-item > & { - float: right; - } - - .list-group-item > & + & { - margin-right: 5px; - } - - .nav-pills > li > a > & { - margin-left: 3px; - } -} diff --git a/src/UI/Content/Bootstrap/bootstrap.less b/src/UI/Content/Bootstrap/bootstrap.less deleted file mode 100644 index 4b9916e6c..000000000 --- a/src/UI/Content/Bootstrap/bootstrap.less +++ /dev/null @@ -1,56 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -// Core variables and mixins -@import "variables.less"; -@import "mixins.less"; - -// Reset and dependencies -@import "normalize.less"; -@import "print.less"; -@import "glyphicons.less"; - -// Core CSS -@import "scaffolding.less"; -@import "type.less"; -@import "code.less"; -@import "grid.less"; -@import "tables.less"; -@import "forms.less"; -@import "buttons.less"; - -// Components -@import "component-animations.less"; -@import "dropdowns.less"; -@import "button-groups.less"; -@import "input-groups.less"; -@import "navs.less"; -@import "navbar.less"; -@import "breadcrumbs.less"; -@import "pagination.less"; -@import "pager.less"; -@import "labels.less"; -@import "badges.less"; -@import "jumbotron.less"; -@import "thumbnails.less"; -@import "alerts.less"; -@import "progress-bars.less"; -@import "media.less"; -@import "list-group.less"; -@import "panels.less"; -@import "responsive-embed.less"; -@import "wells.less"; -@import "close.less"; - -// Components w/ JavaScript -@import "modals.less"; -@import "tooltip.less"; -@import "popovers.less"; -@import "carousel.less"; - -// Utility classes -@import "utilities.less"; -@import "responsive-utilities.less"; diff --git a/src/UI/Content/Bootstrap/breadcrumbs.less b/src/UI/Content/Bootstrap/breadcrumbs.less deleted file mode 100644 index cb01d503f..000000000 --- a/src/UI/Content/Bootstrap/breadcrumbs.less +++ /dev/null @@ -1,26 +0,0 @@ -// -// Breadcrumbs -// -------------------------------------------------- - - -.breadcrumb { - padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; - margin-bottom: @line-height-computed; - list-style: none; - background-color: @breadcrumb-bg; - border-radius: @border-radius-base; - - > li { - display: inline-block; - - + li:before { - content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space - padding: 0 5px; - color: @breadcrumb-color; - } - } - - > .active { - color: @breadcrumb-active-color; - } -} diff --git a/src/UI/Content/Bootstrap/button-groups.less b/src/UI/Content/Bootstrap/button-groups.less deleted file mode 100644 index 6a0c5a865..000000000 --- a/src/UI/Content/Bootstrap/button-groups.less +++ /dev/null @@ -1,244 +0,0 @@ -// -// Button groups -// -------------------------------------------------- - -// Make the div behave like a button -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; // match .btn alignment given font-size hack above - > .btn { - position: relative; - float: left; - // Bring the "active" button to the front - &:hover, - &:focus, - &:active, - &.active { - z-index: 2; - } - } -} - -// Prevent double borders when buttons are next to each other -.btn-group { - .btn + .btn, - .btn + .btn-group, - .btn-group + .btn, - .btn-group + .btn-group { - margin-left: -1px; - } -} - -// Optional: Group multiple button groups together for a toolbar -.btn-toolbar { - margin-left: -5px; // Offset the first child's margin - &:extend(.clearfix all); - - .btn, - .btn-group, - .input-group { - float: left; - } - > .btn, - > .btn-group, - > .input-group { - margin-left: 5px; - } -} - -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} - -// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match -.btn-group > .btn:first-child { - margin-left: 0; - &:not(:last-child):not(.dropdown-toggle) { - .border-right-radius(0); - } -} -// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - .border-left-radius(0); -} - -// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child:not(:last-child) { - > .btn:last-child, - > .dropdown-toggle { - .border-right-radius(0); - } -} -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - .border-left-radius(0); -} - -// On active and open, don't show outline -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} - - -// Sizing -// -// Remix the default button sizing classes into new ones for easier manipulation. - -.btn-group-xs > .btn { &:extend(.btn-xs); } -.btn-group-sm > .btn { &:extend(.btn-sm); } -.btn-group-lg > .btn { &:extend(.btn-lg); } - - -// Split button dropdowns -// ---------------------- - -// Give the line between buttons some depth -.btn-group > .btn + .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} - -// The clickable button for toggling the menu -// Remove the gradient and set the same inset shadow as the :active state -.btn-group.open .dropdown-toggle { - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - - // Show no shadow for `.btn-link` since it has no other button styles. - &.btn-link { - .box-shadow(none); - } -} - - -// Reposition the caret -.btn .caret { - margin-left: 0; -} -// Carets in other button sizes -.btn-lg .caret { - border-width: @caret-width-large @caret-width-large 0; - border-bottom-width: 0; -} -// Upside down carets for .dropup -.dropup .btn-lg .caret { - border-width: 0 @caret-width-large @caret-width-large; -} - - -// Vertical button groups -// ---------------------- - -.btn-group-vertical { - > .btn, - > .btn-group, - > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; - } - - // Clear floats so dropdown menus can be properly placed - > .btn-group { - &:extend(.clearfix all); - > .btn { - float: none; - } - } - - > .btn + .btn, - > .btn + .btn-group, - > .btn-group + .btn, - > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; - } -} - -.btn-group-vertical > .btn { - &:not(:first-child):not(:last-child) { - border-radius: 0; - } - &:first-child:not(:last-child) { - border-top-right-radius: @btn-border-radius-base; - .border-bottom-radius(0); - } - &:last-child:not(:first-child) { - border-bottom-left-radius: @btn-border-radius-base; - .border-top-radius(0); - } -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) { - > .btn:last-child, - > .dropdown-toggle { - .border-bottom-radius(0); - } -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - .border-top-radius(0); -} - - -// Justified button groups -// ---------------------- - -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; - > .btn, - > .btn-group { - float: none; - display: table-cell; - width: 1%; - } - > .btn-group .btn { - width: 100%; - } - - > .btn-group .dropdown-menu { - left: auto; - } -} - - -// Checkbox and radio options -// -// In order to support the browser's form validation feedback, powered by the -// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use -// `display: none;` or `visibility: hidden;` as that also hides the popover. -// Simply visually hiding the inputs via `opacity` would leave them clickable in -// certain cases which is prevented by using `clip` and `pointer-events`. -// This way, we ensure a DOM element is visible to position the popover from. -// -// See https://github.com/twbs/bootstrap/pull/12794 and -// https://github.com/twbs/bootstrap/pull/14559 for more information. - -[data-toggle="buttons"] { - > .btn, - > .btn-group > .btn { - input[type="radio"], - input[type="checkbox"] { - position: absolute; - clip: rect(0,0,0,0); - pointer-events: none; - } - } -} diff --git a/src/UI/Content/Bootstrap/buttons.less b/src/UI/Content/Bootstrap/buttons.less deleted file mode 100644 index 9cbb8f416..000000000 --- a/src/UI/Content/Bootstrap/buttons.less +++ /dev/null @@ -1,166 +0,0 @@ -// -// Buttons -// -------------------------------------------------- - - -// Base styles -// -------------------------------------------------- - -.btn { - display: inline-block; - margin-bottom: 0; // For input.btn - font-weight: @btn-font-weight; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - cursor: pointer; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid transparent; - white-space: nowrap; - .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base); - .user-select(none); - - &, - &:active, - &.active { - &:focus, - &.focus { - .tab-focus(); - } - } - - &:hover, - &:focus, - &.focus { - color: @btn-default-color; - text-decoration: none; - } - - &:active, - &.active { - outline: 0; - background-image: none; - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - cursor: @cursor-disabled; - .opacity(.65); - .box-shadow(none); - } - - a& { - &.disabled, - fieldset[disabled] & { - pointer-events: none; // Future-proof disabling of clicks on `<a>` elements - } - } -} - - -// Alternate buttons -// -------------------------------------------------- - -.btn-default { - .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); -} -.btn-primary { - .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); -} -// Success appears as green -.btn-success { - .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); -} -// Info appears as blue-green -.btn-info { - .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); -} -// Warning appears as orange -.btn-warning { - .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); -} -// Danger and error appear as red -.btn-danger { - .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); -} - - -// Link buttons -// ------------------------- - -// Make a button look and behave like a link -.btn-link { - color: @link-color; - font-weight: normal; - border-radius: 0; - - &, - &:active, - &.active, - &[disabled], - fieldset[disabled] & { - background-color: transparent; - .box-shadow(none); - } - &, - &:hover, - &:focus, - &:active { - border-color: transparent; - } - &:hover, - &:focus { - color: @link-hover-color; - text-decoration: @link-hover-decoration; - background-color: transparent; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @btn-link-disabled-color; - text-decoration: none; - } - } -} - - -// Button Sizes -// -------------------------------------------------- - -.btn-lg { - // line-height: ensure even-numbered height of button next to large input - .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @btn-border-radius-large); -} -.btn-sm { - // line-height: ensure proper height of button next to small input - .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); -} -.btn-xs { - .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @btn-border-radius-small); -} - - -// Block button -// -------------------------------------------------- - -.btn-block { - display: block; - width: 100%; -} - -// Vertically space out multiple block buttons -.btn-block + .btn-block { - margin-top: 5px; -} - -// Specificity overrides -input[type="submit"], -input[type="reset"], -input[type="button"] { - &.btn-block { - width: 100%; - } -} diff --git a/src/UI/Content/Bootstrap/carousel.less b/src/UI/Content/Bootstrap/carousel.less deleted file mode 100644 index 87ed6961d..000000000 --- a/src/UI/Content/Bootstrap/carousel.less +++ /dev/null @@ -1,269 +0,0 @@ -// -// Carousel -// -------------------------------------------------- - - -// Wrapper for the slide container and indicators -.carousel { - position: relative; -} - -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; - - > .item { - display: none; - position: relative; - .transition(.6s ease-in-out left); - - // Account for jankitude on images - > img, - > a > img { - &:extend(.img-responsive); - line-height: 1; - } - - // WebKit CSS3 transforms for supported devices - @media all and (transform-3d), (-webkit-transform-3d) { - .transition-transform(~'0.6s ease-in-out'); - .backface-visibility(~'hidden'); - .perspective(1000px); - - &.next, - &.active.right { - .translate3d(100%, 0, 0); - left: 0; - } - &.prev, - &.active.left { - .translate3d(-100%, 0, 0); - left: 0; - } - &.next.left, - &.prev.right, - &.active { - .translate3d(0, 0, 0); - left: 0; - } - } - } - - > .active, - > .next, - > .prev { - display: block; - } - - > .active { - left: 0; - } - - > .next, - > .prev { - position: absolute; - top: 0; - width: 100%; - } - - > .next { - left: 100%; - } - > .prev { - left: -100%; - } - > .next.left, - > .prev.right { - left: 0; - } - - > .active.left { - left: -100%; - } - > .active.right { - left: 100%; - } - -} - -// Left/right controls for nav -// --------------------------- - -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: @carousel-control-width; - .opacity(@carousel-control-opacity); - font-size: @carousel-control-font-size; - color: @carousel-control-color; - text-align: center; - text-shadow: @carousel-text-shadow; - // We can't have this transition here because WebKit cancels the carousel - // animation if you trip this while in the middle of another animation. - - // Set gradients for backgrounds - &.left { - #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); - } - &.right { - left: auto; - right: 0; - #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); - } - - // Hover/focus state - &:hover, - &:focus { - outline: 0; - color: @carousel-control-color; - text-decoration: none; - .opacity(.9); - } - - // Toggles - .icon-prev, - .icon-next, - .glyphicon-chevron-left, - .glyphicon-chevron-right { - position: absolute; - top: 50%; - margin-top: -10px; - z-index: 5; - display: inline-block; - } - .icon-prev, - .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; - } - .icon-next, - .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; - } - .icon-prev, - .icon-next { - width: 20px; - height: 20px; - line-height: 1; - font-family: serif; - } - - - .icon-prev { - &:before { - content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) - } - } - .icon-next { - &:before { - content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) - } - } -} - -// Optional indicator pips -// -// Add an unordered list with the following class and add a list item for each -// slide your carousel holds. - -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; - - li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid @carousel-indicator-border-color; - border-radius: 10px; - cursor: pointer; - - // IE8-9 hack for event handling - // - // Internet Explorer 8-9 does not support clicks on elements without a set - // `background-color`. We cannot use `filter` since that's not viewed as a - // background color by the browser. Thus, a hack is needed. - // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer - // - // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we - // set alpha transparency for the best results possible. - background-color: #000 \9; // IE8 - background-color: rgba(0,0,0,0); // IE9 - } - .active { - margin: 0; - width: 12px; - height: 12px; - background-color: @carousel-indicator-active-bg; - } -} - -// Optional captions -// ----------------------------- -// Hidden by default for smaller viewports -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: @carousel-caption-color; - text-align: center; - text-shadow: @carousel-text-shadow; - & .btn { - text-shadow: none; // No shadow for button elements in carousel-caption - } -} - - -// Scale up controls for tablets and up -@media screen and (min-width: @screen-sm-min) { - - // Scale up the controls a smidge - .carousel-control { - .glyphicon-chevron-left, - .glyphicon-chevron-right, - .icon-prev, - .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .glyphicon-chevron-left, - .icon-prev { - margin-left: -15px; - } - .glyphicon-chevron-right, - .icon-next { - margin-right: -15px; - } - } - - // Show and left align the captions - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - - // Move up the indicators - .carousel-indicators { - bottom: 20px; - } -} diff --git a/src/UI/Content/Bootstrap/close.less b/src/UI/Content/Bootstrap/close.less deleted file mode 100644 index 6d5bfe087..000000000 --- a/src/UI/Content/Bootstrap/close.less +++ /dev/null @@ -1,34 +0,0 @@ -// -// Close icons -// -------------------------------------------------- - - -.close { - float: right; - font-size: (@font-size-base * 1.5); - font-weight: @close-font-weight; - line-height: 1; - color: @close-color; - text-shadow: @close-text-shadow; - .opacity(.2); - - &:hover, - &:focus { - color: @close-color; - text-decoration: none; - cursor: pointer; - .opacity(.5); - } - - // Additional properties for button version - // iOS requires the button element instead of an anchor tag. - // If you want the anchor version, it requires `href="#"`. - // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - button& { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; - } -} diff --git a/src/UI/Content/Bootstrap/code.less b/src/UI/Content/Bootstrap/code.less deleted file mode 100644 index a08b4d48c..000000000 --- a/src/UI/Content/Bootstrap/code.less +++ /dev/null @@ -1,69 +0,0 @@ -// -// Code (inline and block) -// -------------------------------------------------- - - -// Inline and block code styles -code, -kbd, -pre, -samp { - font-family: @font-family-monospace; -} - -// Inline code -code { - padding: 2px 4px; - font-size: 90%; - color: @code-color; - background-color: @code-bg; - border-radius: @border-radius-base; -} - -// User input typically entered via keyboard -kbd { - padding: 2px 4px; - font-size: 90%; - color: @kbd-color; - background-color: @kbd-bg; - border-radius: @border-radius-small; - box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); - - kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - box-shadow: none; - } -} - -// Blocks of code -pre { - display: block; - padding: ((@line-height-computed - 1) / 2); - margin: 0 0 (@line-height-computed / 2); - font-size: (@font-size-base - 1); // 14px to 13px - line-height: @line-height-base; - word-break: break-all; - word-wrap: break-word; - color: @pre-color; - background-color: @pre-bg; - border: 1px solid @pre-border-color; - border-radius: @border-radius-base; - - // Account for some code outputs that place code tags in pre tags - code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; - } -} - -// Enable scrollable blocks of code -.pre-scrollable { - max-height: @pre-scrollable-max-height; - overflow-y: scroll; -} diff --git a/src/UI/Content/Bootstrap/component-animations.less b/src/UI/Content/Bootstrap/component-animations.less deleted file mode 100644 index 0bcee910a..000000000 --- a/src/UI/Content/Bootstrap/component-animations.less +++ /dev/null @@ -1,33 +0,0 @@ -// -// Component animations -// -------------------------------------------------- - -// Heads up! -// -// We don't use the `.opacity()` mixin here since it causes a bug with text -// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. - -.fade { - opacity: 0; - .transition(opacity .15s linear); - &.in { - opacity: 1; - } -} - -.collapse { - display: none; - - &.in { display: block; } - tr&.in { display: table-row; } - tbody&.in { display: table-row-group; } -} - -.collapsing { - position: relative; - height: 0; - overflow: hidden; - .transition-property(~"height, visibility"); - .transition-duration(.35s); - .transition-timing-function(ease); -} diff --git a/src/UI/Content/Bootstrap/dropdowns.less b/src/UI/Content/Bootstrap/dropdowns.less deleted file mode 100644 index f6876c1a9..000000000 --- a/src/UI/Content/Bootstrap/dropdowns.less +++ /dev/null @@ -1,216 +0,0 @@ -// -// Dropdown menus -// -------------------------------------------------- - - -// Dropdown arrow/caret -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: @caret-width-base dashed; - border-top: @caret-width-base solid ~"\9"; // IE8 - border-right: @caret-width-base solid transparent; - border-left: @caret-width-base solid transparent; -} - -// The dropdown wrapper (div) -.dropup, -.dropdown { - position: relative; -} - -// Prevent the focus on the dropdown toggle when closing dropdowns -.dropdown-toggle:focus { - outline: 0; -} - -// The dropdown menu (ul) -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: @zindex-dropdown; - display: none; // none by default, but block on "open" of the menu - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; // override default ul - list-style: none; - font-size: @font-size-base; - text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer) - background-color: @dropdown-bg; - border: 1px solid @dropdown-fallback-border; // IE8 fallback - border: 1px solid @dropdown-border; - border-radius: @border-radius-base; - .box-shadow(0 6px 12px rgba(0,0,0,.175)); - background-clip: padding-box; - - // Aligns the dropdown menu to right - // - // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` - &.pull-right { - right: 0; - left: auto; - } - - // Dividers (basically an hr) within the dropdown - .divider { - .nav-divider(@dropdown-divider-bg); - } - - // Links within the dropdown menu - > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: @line-height-base; - color: @dropdown-link-color; - white-space: nowrap; // prevent links from randomly breaking onto new lines - } -} - -// Hover/Focus state -.dropdown-menu > li > a { - &:hover, - &:focus { - text-decoration: none; - color: @dropdown-link-hover-color; - background-color: @dropdown-link-hover-bg; - } -} - -// Active state -.dropdown-menu > .active > a { - &, - &:hover, - &:focus { - color: @dropdown-link-active-color; - text-decoration: none; - outline: 0; - background-color: @dropdown-link-active-bg; - } -} - -// Disabled state -// -// Gray out text and ensure the hover/focus state remains gray - -.dropdown-menu > .disabled > a { - &, - &:hover, - &:focus { - color: @dropdown-link-disabled-color; - } - - // Nuke hover/focus effects - &:hover, - &:focus { - text-decoration: none; - background-color: transparent; - background-image: none; // Remove CSS gradient - .reset-filter(); - cursor: @cursor-disabled; - } -} - -// Open state for the dropdown -.open { - // Show the menu - > .dropdown-menu { - display: block; - } - - // Remove the outline when :focus is triggered - > a { - outline: 0; - } -} - -// Menu positioning -// -// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown -// menu with the parent. -.dropdown-menu-right { - left: auto; // Reset the default from `.dropdown-menu` - right: 0; -} -// With v3, we enabled auto-flipping if you have a dropdown within a right -// aligned nav component. To enable the undoing of that, we provide an override -// to restore the default dropdown menu alignment. -// -// This is only for left-aligning a dropdown menu within a `.navbar-right` or -// `.pull-right` nav component. -.dropdown-menu-left { - left: 0; - right: auto; -} - -// Dropdown section headers -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: @font-size-small; - line-height: @line-height-base; - color: @dropdown-header-color; - white-space: nowrap; // as with > li > a -} - -// Backdrop to catch body clicks on mobile, etc. -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: (@zindex-dropdown - 10); -} - -// Right aligned dropdowns -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} - -// Allow for dropdowns to go bottom up (aka, dropup-menu) -// -// Just add .dropup after the standard .dropdown class and you're set, bro. -// TODO: abstract this so that the navbar fixed styles are not placed here? - -.dropup, -.navbar-fixed-bottom .dropdown { - // Reverse the caret - .caret { - border-top: 0; - border-bottom: @caret-width-base dashed; - border-bottom: @caret-width-base solid ~"\9"; // IE8 - content: ""; - } - // Different positioning for bottom up menu - .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; - } -} - - -// Component alignment -// -// Reiterate per navbar.less and the modified component alignment there. - -@media (min-width: @grid-float-breakpoint) { - .navbar-right { - .dropdown-menu { - .dropdown-menu-right(); - } - // Necessary for overrides of the default right aligned menu. - // Will remove come v4 in all likelihood. - .dropdown-menu-left { - .dropdown-menu-left(); - } - } -} diff --git a/src/UI/Content/Bootstrap/forms.less b/src/UI/Content/Bootstrap/forms.less deleted file mode 100644 index b064ede46..000000000 --- a/src/UI/Content/Bootstrap/forms.less +++ /dev/null @@ -1,607 +0,0 @@ -// -// Forms -// -------------------------------------------------- - - -// Normalize non-controls -// -// Restyle and baseline non-control form elements. - -fieldset { - padding: 0; - margin: 0; - border: 0; - // Chrome and Firefox set a `min-width: min-content;` on fieldsets, - // so we reset that to ensure it behaves more like a standard block element. - // See https://github.com/twbs/bootstrap/issues/12359. - min-width: 0; -} - -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: @line-height-computed; - font-size: (@font-size-base * 1.5); - line-height: inherit; - color: @legend-color; - border: 0; - border-bottom: 1px solid @legend-border-color; -} - -label { - display: inline-block; - max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141) - margin-bottom: 5px; - font-weight: bold; -} - - -// Normalize form controls -// -// While most of our form styles require extra classes, some basic normalization -// is required to ensure optimum display with or without those classes to better -// address browser inconsistencies. - -// Override content-box in Normalize (* isn't specific enough) -input[type="search"] { - .box-sizing(border-box); -} - -// Position radios and checkboxes better -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; // IE8-9 - line-height: normal; -} - -input[type="file"] { - display: block; -} - -// Make range inputs behave like textual form controls -input[type="range"] { - display: block; - width: 100%; -} - -// Make multiple select elements height not fixed -select[multiple], -select[size] { - height: auto; -} - -// Focus for file, radio, and checkbox -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - .tab-focus(); -} - -// Adjust output element -output { - display: block; - padding-top: (@padding-base-vertical + 1); - font-size: @font-size-base; - line-height: @line-height-base; - color: @input-color; -} - - -// Common form controls -// -// Shared size and type resets for form controls. Apply `.form-control` to any -// of the following form controls: -// -// select -// textarea -// input[type="text"] -// input[type="password"] -// input[type="datetime"] -// input[type="datetime-local"] -// input[type="date"] -// input[type="month"] -// input[type="time"] -// input[type="week"] -// input[type="number"] -// input[type="email"] -// input[type="url"] -// input[type="search"] -// input[type="tel"] -// input[type="color"] - -.form-control { - display: block; - width: 100%; - height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border) - padding: @padding-base-vertical @padding-base-horizontal; - font-size: @font-size-base; - line-height: @line-height-base; - color: @input-color; - background-color: @input-bg; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid @input-border; - border-radius: @input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS. - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); - .transition(~"border-color ease-in-out .15s, box-shadow ease-in-out .15s"); - - // Customize the `:focus` state to imitate native WebKit styles. - .form-control-focus(); - - // Placeholder - .placeholder(); - - // Disabled and read-only inputs - // - // HTML5 says that controls under a fieldset > legend:first-child won't be - // disabled if the fieldset is disabled. Due to implementation difficulty, we - // don't honor that edge case; we style them as disabled anyway. - &[disabled], - &[readonly], - fieldset[disabled] & { - background-color: @input-bg-disabled; - opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655 - } - - &[disabled], - fieldset[disabled] & { - cursor: @cursor-disabled; - } - - // Reset height for `textarea`s - textarea& { - height: auto; - } -} - - -// Search inputs in iOS -// -// This overrides the extra rounded corners on search inputs in iOS so that our -// `.form-control` class can properly style them. Note that this cannot simply -// be added to `.form-control` as it's not specific enough. For details, see -// https://github.com/twbs/bootstrap/issues/11586. - -input[type="search"] { - -webkit-appearance: none; -} - - -// Special styles for iOS temporal inputs -// -// In Mobile Safari, setting `display: block` on temporal inputs causes the -// text within the input to become vertically misaligned. As a workaround, we -// set a pixel line-height that matches the given height of the input, but only -// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848 -// -// Note that as of 8.3, iOS doesn't support `datetime` or `week`. - -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"], - input[type="time"], - input[type="datetime-local"], - input[type="month"] { - &.form-control { - line-height: @input-height-base; - } - - &.input-sm, - .input-group-sm & { - line-height: @input-height-small; - } - - &.input-lg, - .input-group-lg & { - line-height: @input-height-large; - } - } -} - - -// Form groups -// -// Designed to help with the organization and spacing of vertical forms. For -// horizontal forms, use the predefined grid classes. - -.form-group { - margin-bottom: @form-group-margin-bottom; -} - - -// Checkboxes and radios -// -// Indent the labels to position radios/checkboxes as hanging controls. - -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; - - label { - min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; - } -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} - -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing -} - -// Radios and checkboxes on same line -.radio-inline, -.checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - vertical-align: middle; - font-weight: normal; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; // space out consecutive inline controls -} - -// Apply same disabled cursor tweak as for inputs -// Some special care is needed because <label>s don't inherit their parent's `cursor`. -// -// Note: Neither radios nor checkboxes can be readonly. -input[type="radio"], -input[type="checkbox"] { - &[disabled], - &.disabled, - fieldset[disabled] & { - cursor: @cursor-disabled; - } -} -// These classes are used directly on <label>s -.radio-inline, -.checkbox-inline { - &.disabled, - fieldset[disabled] & { - cursor: @cursor-disabled; - } -} -// These classes are used on elements with <label> descendants -.radio, -.checkbox { - &.disabled, - fieldset[disabled] & { - label { - cursor: @cursor-disabled; - } - } -} - - -// Static form control text -// -// Apply class to a `p` element to make any string of text align with labels in -// a horizontal form layout. - -.form-control-static { - // Size it appropriately next to real form controls - padding-top: (@padding-base-vertical + 1); - padding-bottom: (@padding-base-vertical + 1); - // Remove default margin from `p` - margin-bottom: 0; - min-height: (@line-height-computed + @font-size-base); - - &.input-lg, - &.input-sm { - padding-left: 0; - padding-right: 0; - } -} - - -// Form control sizing -// -// Build on `.form-control` with modifier classes to decrease or increase the -// height and font-size of form controls. -// -// The `.form-group-* form-control` variations are sadly duplicated to avoid the -// issue documented in https://github.com/twbs/bootstrap/issues/15074. - -.input-sm { - .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @input-border-radius-small); -} -.form-group-sm { - .form-control { - height: @input-height-small; - padding: @padding-small-vertical @padding-small-horizontal; - font-size: @font-size-small; - line-height: @line-height-small; - border-radius: @input-border-radius-small; - } - select.form-control { - height: @input-height-small; - line-height: @input-height-small; - } - textarea.form-control, - select[multiple].form-control { - height: auto; - } - .form-control-static { - height: @input-height-small; - min-height: (@line-height-computed + @font-size-small); - padding: (@padding-small-vertical + 1) @padding-small-horizontal; - font-size: @font-size-small; - line-height: @line-height-small; - } -} - -.input-lg { - .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @input-border-radius-large); -} -.form-group-lg { - .form-control { - height: @input-height-large; - padding: @padding-large-vertical @padding-large-horizontal; - font-size: @font-size-large; - line-height: @line-height-large; - border-radius: @input-border-radius-large; - } - select.form-control { - height: @input-height-large; - line-height: @input-height-large; - } - textarea.form-control, - select[multiple].form-control { - height: auto; - } - .form-control-static { - height: @input-height-large; - min-height: (@line-height-computed + @font-size-large); - padding: (@padding-large-vertical + 1) @padding-large-horizontal; - font-size: @font-size-large; - line-height: @line-height-large; - } -} - - -// Form control feedback states -// -// Apply contextual and semantic states to individual form controls. - -.has-feedback { - // Enable absolute positioning - position: relative; - - // Ensure icons don't overlap text - .form-control { - padding-right: (@input-height-base * 1.25); - } -} -// Feedback icon (requires .glyphicon classes) -.form-control-feedback { - position: absolute; - top: 0; - right: 0; - z-index: 2; // Ensure icon is above input groups - display: block; - width: @input-height-base; - height: @input-height-base; - line-height: @input-height-base; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: @input-height-large; - height: @input-height-large; - line-height: @input-height-large; -} -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: @input-height-small; - height: @input-height-small; - line-height: @input-height-small; -} - -// Feedback states -.has-success { - .form-control-validation(@state-success-text; @state-success-text; @state-success-bg); -} -.has-warning { - .form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg); -} -.has-error { - .form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg); -} - -// Reposition feedback icon if input has visible label above -.has-feedback label { - - & ~ .form-control-feedback { - top: (@line-height-computed + 5); // Height of the `label` and its margin - } - &.sr-only ~ .form-control-feedback { - top: 0; - } -} - - -// Help text -// -// Apply to any element you wish to create light text for placement immediately -// below a form control. Use for general help, formatting, or instructional text. - -.help-block { - display: block; // account for any element using help-block - margin-top: 5px; - margin-bottom: 10px; - color: lighten(@text-color, 25%); // lighten the text some for contrast -} - - -// Inline forms -// -// Make forms appear inline(-block) by adding the `.form-inline` class. Inline -// forms begin stacked on extra small (mobile) devices and then go inline when -// viewports reach <768px. -// -// Requires wrapping inputs and labels with `.form-group` for proper display of -// default HTML form controls and our custom form controls (e.g., input groups). -// -// Heads up! This is mixin-ed into `.navbar-form` in navbars.less. - -.form-inline { - - // Kick in the inline - @media (min-width: @screen-sm-min) { - // Inline-block all the things for "inline" - .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - - // In navbar-form, allow folks to *not* use `.form-group` - .form-control { - display: inline-block; - width: auto; // Prevent labels from stacking above inputs in `.form-group` - vertical-align: middle; - } - - // Make static controls behave like regular ones - .form-control-static { - display: inline-block; - } - - .input-group { - display: inline-table; - vertical-align: middle; - - .input-group-addon, - .input-group-btn, - .form-control { - width: auto; - } - } - - // Input groups need that 100% width though - .input-group > .form-control { - width: 100%; - } - - .control-label { - margin-bottom: 0; - vertical-align: middle; - } - - // Remove default margin on radios/checkboxes that were used for stacking, and - // then undo the floating of radios and checkboxes to match. - .radio, - .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - - label { - padding-left: 0; - } - } - .radio input[type="radio"], - .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - - // Re-override the feedback icon. - .has-feedback .form-control-feedback { - top: 0; - } - } -} - - -// Horizontal forms -// -// Horizontal forms are built on grid classes and allow you to create forms with -// labels on the left and inputs on the right. - -.form-horizontal { - - // Consistent vertical alignment of radios and checkboxes - // - // Labels also get some reset styles, but that is scoped to a media query below. - .radio, - .checkbox, - .radio-inline, - .checkbox-inline { - margin-top: 0; - margin-bottom: 0; - padding-top: (@padding-base-vertical + 1); // Default padding plus a border - } - // Account for padding we're adding to ensure the alignment and of help text - // and other content below items - .radio, - .checkbox { - min-height: (@line-height-computed + (@padding-base-vertical + 1)); - } - - // Make form groups behave like rows - .form-group { - .make-row(); - } - - // Reset spacing and right align labels, but scope to media queries so that - // labels on narrow viewports stack the same as a default form example. - @media (min-width: @screen-sm-min) { - .control-label { - text-align: right; - margin-bottom: 0; - padding-top: (@padding-base-vertical + 1); // Default padding plus a border - } - } - - // Validation states - // - // Reposition the icon because it's now within a grid column and columns have - // `position: relative;` on them. Also accounts for the grid gutter padding. - .has-feedback .form-control-feedback { - right: floor((@grid-gutter-width / 2)); - } - - // Form group sizes - // - // Quick utility class for applying `.input-lg` and `.input-sm` styles to the - // inputs and labels within a `.form-group`. - .form-group-lg { - @media (min-width: @screen-sm-min) { - .control-label { - padding-top: ((@padding-large-vertical * @line-height-large) + 1); - font-size: @font-size-large; - } - } - } - .form-group-sm { - @media (min-width: @screen-sm-min) { - .control-label { - padding-top: (@padding-small-vertical + 1); - font-size: @font-size-small; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/glyphicons.less b/src/UI/Content/Bootstrap/glyphicons.less deleted file mode 100644 index 335d80aa6..000000000 --- a/src/UI/Content/Bootstrap/glyphicons.less +++ /dev/null @@ -1,305 +0,0 @@ -// -// Glyphicons for Bootstrap -// -// Since icons are fonts, they can be placed anywhere text is placed and are -// thus automatically sized to match the surrounding child. To use, create an -// inline element with the appropriate classes, like so: -// -// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a> - -// Import the fonts -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('@{icon-font-path}@{icon-font-name}.eot'); - src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'), - url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'), - url('@{icon-font-path}@{icon-font-name}.woff') format('woff'), - url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'), - url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg'); -} - -// Catchall baseclass -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -// Individual icons -.glyphicon-asterisk { &:before { content: "\2a"; } } -.glyphicon-plus { &:before { content: "\2b"; } } -.glyphicon-euro, -.glyphicon-eur { &:before { content: "\20ac"; } } -.glyphicon-minus { &:before { content: "\2212"; } } -.glyphicon-cloud { &:before { content: "\2601"; } } -.glyphicon-envelope { &:before { content: "\2709"; } } -.glyphicon-pencil { &:before { content: "\270f"; } } -.glyphicon-glass { &:before { content: "\e001"; } } -.glyphicon-music { &:before { content: "\e002"; } } -.glyphicon-search { &:before { content: "\e003"; } } -.glyphicon-heart { &:before { content: "\e005"; } } -.glyphicon-star { &:before { content: "\e006"; } } -.glyphicon-star-empty { &:before { content: "\e007"; } } -.glyphicon-user { &:before { content: "\e008"; } } -.glyphicon-film { &:before { content: "\e009"; } } -.glyphicon-th-large { &:before { content: "\e010"; } } -.glyphicon-th { &:before { content: "\e011"; } } -.glyphicon-th-list { &:before { content: "\e012"; } } -.glyphicon-ok { &:before { content: "\e013"; } } -.glyphicon-remove { &:before { content: "\e014"; } } -.glyphicon-zoom-in { &:before { content: "\e015"; } } -.glyphicon-zoom-out { &:before { content: "\e016"; } } -.glyphicon-off { &:before { content: "\e017"; } } -.glyphicon-signal { &:before { content: "\e018"; } } -.glyphicon-cog { &:before { content: "\e019"; } } -.glyphicon-trash { &:before { content: "\e020"; } } -.glyphicon-home { &:before { content: "\e021"; } } -.glyphicon-file { &:before { content: "\e022"; } } -.glyphicon-time { &:before { content: "\e023"; } } -.glyphicon-road { &:before { content: "\e024"; } } -.glyphicon-download-alt { &:before { content: "\e025"; } } -.glyphicon-download { &:before { content: "\e026"; } } -.glyphicon-upload { &:before { content: "\e027"; } } -.glyphicon-inbox { &:before { content: "\e028"; } } -.glyphicon-play-circle { &:before { content: "\e029"; } } -.glyphicon-repeat { &:before { content: "\e030"; } } -.glyphicon-refresh { &:before { content: "\e031"; } } -.glyphicon-list-alt { &:before { content: "\e032"; } } -.glyphicon-lock { &:before { content: "\e033"; } } -.glyphicon-flag { &:before { content: "\e034"; } } -.glyphicon-headphones { &:before { content: "\e035"; } } -.glyphicon-volume-off { &:before { content: "\e036"; } } -.glyphicon-volume-down { &:before { content: "\e037"; } } -.glyphicon-volume-up { &:before { content: "\e038"; } } -.glyphicon-qrcode { &:before { content: "\e039"; } } -.glyphicon-barcode { &:before { content: "\e040"; } } -.glyphicon-tag { &:before { content: "\e041"; } } -.glyphicon-tags { &:before { content: "\e042"; } } -.glyphicon-book { &:before { content: "\e043"; } } -.glyphicon-bookmark { &:before { content: "\e044"; } } -.glyphicon-print { &:before { content: "\e045"; } } -.glyphicon-camera { &:before { content: "\e046"; } } -.glyphicon-font { &:before { content: "\e047"; } } -.glyphicon-bold { &:before { content: "\e048"; } } -.glyphicon-italic { &:before { content: "\e049"; } } -.glyphicon-text-height { &:before { content: "\e050"; } } -.glyphicon-text-width { &:before { content: "\e051"; } } -.glyphicon-align-left { &:before { content: "\e052"; } } -.glyphicon-align-center { &:before { content: "\e053"; } } -.glyphicon-align-right { &:before { content: "\e054"; } } -.glyphicon-align-justify { &:before { content: "\e055"; } } -.glyphicon-list { &:before { content: "\e056"; } } -.glyphicon-indent-left { &:before { content: "\e057"; } } -.glyphicon-indent-right { &:before { content: "\e058"; } } -.glyphicon-facetime-video { &:before { content: "\e059"; } } -.glyphicon-picture { &:before { content: "\e060"; } } -.glyphicon-map-marker { &:before { content: "\e062"; } } -.glyphicon-adjust { &:before { content: "\e063"; } } -.glyphicon-tint { &:before { content: "\e064"; } } -.glyphicon-edit { &:before { content: "\e065"; } } -.glyphicon-share { &:before { content: "\e066"; } } -.glyphicon-check { &:before { content: "\e067"; } } -.glyphicon-move { &:before { content: "\e068"; } } -.glyphicon-step-backward { &:before { content: "\e069"; } } -.glyphicon-fast-backward { &:before { content: "\e070"; } } -.glyphicon-backward { &:before { content: "\e071"; } } -.glyphicon-play { &:before { content: "\e072"; } } -.glyphicon-pause { &:before { content: "\e073"; } } -.glyphicon-stop { &:before { content: "\e074"; } } -.glyphicon-forward { &:before { content: "\e075"; } } -.glyphicon-fast-forward { &:before { content: "\e076"; } } -.glyphicon-step-forward { &:before { content: "\e077"; } } -.glyphicon-eject { &:before { content: "\e078"; } } -.glyphicon-chevron-left { &:before { content: "\e079"; } } -.glyphicon-chevron-right { &:before { content: "\e080"; } } -.glyphicon-plus-sign { &:before { content: "\e081"; } } -.glyphicon-minus-sign { &:before { content: "\e082"; } } -.glyphicon-remove-sign { &:before { content: "\e083"; } } -.glyphicon-ok-sign { &:before { content: "\e084"; } } -.glyphicon-question-sign { &:before { content: "\e085"; } } -.glyphicon-info-sign { &:before { content: "\e086"; } } -.glyphicon-screenshot { &:before { content: "\e087"; } } -.glyphicon-remove-circle { &:before { content: "\e088"; } } -.glyphicon-ok-circle { &:before { content: "\e089"; } } -.glyphicon-ban-circle { &:before { content: "\e090"; } } -.glyphicon-arrow-left { &:before { content: "\e091"; } } -.glyphicon-arrow-right { &:before { content: "\e092"; } } -.glyphicon-arrow-up { &:before { content: "\e093"; } } -.glyphicon-arrow-down { &:before { content: "\e094"; } } -.glyphicon-share-alt { &:before { content: "\e095"; } } -.glyphicon-resize-full { &:before { content: "\e096"; } } -.glyphicon-resize-small { &:before { content: "\e097"; } } -.glyphicon-exclamation-sign { &:before { content: "\e101"; } } -.glyphicon-gift { &:before { content: "\e102"; } } -.glyphicon-leaf { &:before { content: "\e103"; } } -.glyphicon-fire { &:before { content: "\e104"; } } -.glyphicon-eye-open { &:before { content: "\e105"; } } -.glyphicon-eye-close { &:before { content: "\e106"; } } -.glyphicon-warning-sign { &:before { content: "\e107"; } } -.glyphicon-plane { &:before { content: "\e108"; } } -.glyphicon-calendar { &:before { content: "\e109"; } } -.glyphicon-random { &:before { content: "\e110"; } } -.glyphicon-comment { &:before { content: "\e111"; } } -.glyphicon-magnet { &:before { content: "\e112"; } } -.glyphicon-chevron-up { &:before { content: "\e113"; } } -.glyphicon-chevron-down { &:before { content: "\e114"; } } -.glyphicon-retweet { &:before { content: "\e115"; } } -.glyphicon-shopping-cart { &:before { content: "\e116"; } } -.glyphicon-folder-close { &:before { content: "\e117"; } } -.glyphicon-folder-open { &:before { content: "\e118"; } } -.glyphicon-resize-vertical { &:before { content: "\e119"; } } -.glyphicon-resize-horizontal { &:before { content: "\e120"; } } -.glyphicon-hdd { &:before { content: "\e121"; } } -.glyphicon-bullhorn { &:before { content: "\e122"; } } -.glyphicon-bell { &:before { content: "\e123"; } } -.glyphicon-certificate { &:before { content: "\e124"; } } -.glyphicon-thumbs-up { &:before { content: "\e125"; } } -.glyphicon-thumbs-down { &:before { content: "\e126"; } } -.glyphicon-hand-right { &:before { content: "\e127"; } } -.glyphicon-hand-left { &:before { content: "\e128"; } } -.glyphicon-hand-up { &:before { content: "\e129"; } } -.glyphicon-hand-down { &:before { content: "\e130"; } } -.glyphicon-circle-arrow-right { &:before { content: "\e131"; } } -.glyphicon-circle-arrow-left { &:before { content: "\e132"; } } -.glyphicon-circle-arrow-up { &:before { content: "\e133"; } } -.glyphicon-circle-arrow-down { &:before { content: "\e134"; } } -.glyphicon-globe { &:before { content: "\e135"; } } -.glyphicon-wrench { &:before { content: "\e136"; } } -.glyphicon-tasks { &:before { content: "\e137"; } } -.glyphicon-filter { &:before { content: "\e138"; } } -.glyphicon-briefcase { &:before { content: "\e139"; } } -.glyphicon-fullscreen { &:before { content: "\e140"; } } -.glyphicon-dashboard { &:before { content: "\e141"; } } -.glyphicon-paperclip { &:before { content: "\e142"; } } -.glyphicon-heart-empty { &:before { content: "\e143"; } } -.glyphicon-link { &:before { content: "\e144"; } } -.glyphicon-phone { &:before { content: "\e145"; } } -.glyphicon-pushpin { &:before { content: "\e146"; } } -.glyphicon-usd { &:before { content: "\e148"; } } -.glyphicon-gbp { &:before { content: "\e149"; } } -.glyphicon-sort { &:before { content: "\e150"; } } -.glyphicon-sort-by-alphabet { &:before { content: "\e151"; } } -.glyphicon-sort-by-alphabet-alt { &:before { content: "\e152"; } } -.glyphicon-sort-by-order { &:before { content: "\e153"; } } -.glyphicon-sort-by-order-alt { &:before { content: "\e154"; } } -.glyphicon-sort-by-attributes { &:before { content: "\e155"; } } -.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } } -.glyphicon-unchecked { &:before { content: "\e157"; } } -.glyphicon-expand { &:before { content: "\e158"; } } -.glyphicon-collapse-down { &:before { content: "\e159"; } } -.glyphicon-collapse-up { &:before { content: "\e160"; } } -.glyphicon-log-in { &:before { content: "\e161"; } } -.glyphicon-flash { &:before { content: "\e162"; } } -.glyphicon-log-out { &:before { content: "\e163"; } } -.glyphicon-new-window { &:before { content: "\e164"; } } -.glyphicon-record { &:before { content: "\e165"; } } -.glyphicon-save { &:before { content: "\e166"; } } -.glyphicon-open { &:before { content: "\e167"; } } -.glyphicon-saved { &:before { content: "\e168"; } } -.glyphicon-import { &:before { content: "\e169"; } } -.glyphicon-export { &:before { content: "\e170"; } } -.glyphicon-send { &:before { content: "\e171"; } } -.glyphicon-floppy-disk { &:before { content: "\e172"; } } -.glyphicon-floppy-saved { &:before { content: "\e173"; } } -.glyphicon-floppy-remove { &:before { content: "\e174"; } } -.glyphicon-floppy-save { &:before { content: "\e175"; } } -.glyphicon-floppy-open { &:before { content: "\e176"; } } -.glyphicon-credit-card { &:before { content: "\e177"; } } -.glyphicon-transfer { &:before { content: "\e178"; } } -.glyphicon-cutlery { &:before { content: "\e179"; } } -.glyphicon-header { &:before { content: "\e180"; } } -.glyphicon-compressed { &:before { content: "\e181"; } } -.glyphicon-earphone { &:before { content: "\e182"; } } -.glyphicon-phone-alt { &:before { content: "\e183"; } } -.glyphicon-tower { &:before { content: "\e184"; } } -.glyphicon-stats { &:before { content: "\e185"; } } -.glyphicon-sd-video { &:before { content: "\e186"; } } -.glyphicon-hd-video { &:before { content: "\e187"; } } -.glyphicon-subtitles { &:before { content: "\e188"; } } -.glyphicon-sound-stereo { &:before { content: "\e189"; } } -.glyphicon-sound-dolby { &:before { content: "\e190"; } } -.glyphicon-sound-5-1 { &:before { content: "\e191"; } } -.glyphicon-sound-6-1 { &:before { content: "\e192"; } } -.glyphicon-sound-7-1 { &:before { content: "\e193"; } } -.glyphicon-copyright-mark { &:before { content: "\e194"; } } -.glyphicon-registration-mark { &:before { content: "\e195"; } } -.glyphicon-cloud-download { &:before { content: "\e197"; } } -.glyphicon-cloud-upload { &:before { content: "\e198"; } } -.glyphicon-tree-conifer { &:before { content: "\e199"; } } -.glyphicon-tree-deciduous { &:before { content: "\e200"; } } -.glyphicon-cd { &:before { content: "\e201"; } } -.glyphicon-save-file { &:before { content: "\e202"; } } -.glyphicon-open-file { &:before { content: "\e203"; } } -.glyphicon-level-up { &:before { content: "\e204"; } } -.glyphicon-copy { &:before { content: "\e205"; } } -.glyphicon-paste { &:before { content: "\e206"; } } -// The following 2 Glyphicons are omitted for the time being because -// they currently use Unicode codepoints that are outside the -// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle -// non-BMP codepoints in CSS string escapes, and thus can't display these two icons. -// Notably, the bug affects some older versions of the Android Browser. -// More info: https://github.com/twbs/bootstrap/issues/10106 -// .glyphicon-door { &:before { content: "\1f6aa"; } } -// .glyphicon-key { &:before { content: "\1f511"; } } -.glyphicon-alert { &:before { content: "\e209"; } } -.glyphicon-equalizer { &:before { content: "\e210"; } } -.glyphicon-king { &:before { content: "\e211"; } } -.glyphicon-queen { &:before { content: "\e212"; } } -.glyphicon-pawn { &:before { content: "\e213"; } } -.glyphicon-bishop { &:before { content: "\e214"; } } -.glyphicon-knight { &:before { content: "\e215"; } } -.glyphicon-baby-formula { &:before { content: "\e216"; } } -.glyphicon-tent { &:before { content: "\26fa"; } } -.glyphicon-blackboard { &:before { content: "\e218"; } } -.glyphicon-bed { &:before { content: "\e219"; } } -.glyphicon-apple { &:before { content: "\f8ff"; } } -.glyphicon-erase { &:before { content: "\e221"; } } -.glyphicon-hourglass { &:before { content: "\231b"; } } -.glyphicon-lamp { &:before { content: "\e223"; } } -.glyphicon-duplicate { &:before { content: "\e224"; } } -.glyphicon-piggy-bank { &:before { content: "\e225"; } } -.glyphicon-scissors { &:before { content: "\e226"; } } -.glyphicon-bitcoin { &:before { content: "\e227"; } } -.glyphicon-btc { &:before { content: "\e227"; } } -.glyphicon-xbt { &:before { content: "\e227"; } } -.glyphicon-yen { &:before { content: "\00a5"; } } -.glyphicon-jpy { &:before { content: "\00a5"; } } -.glyphicon-ruble { &:before { content: "\20bd"; } } -.glyphicon-rub { &:before { content: "\20bd"; } } -.glyphicon-scale { &:before { content: "\e230"; } } -.glyphicon-ice-lolly { &:before { content: "\e231"; } } -.glyphicon-ice-lolly-tasted { &:before { content: "\e232"; } } -.glyphicon-education { &:before { content: "\e233"; } } -.glyphicon-option-horizontal { &:before { content: "\e234"; } } -.glyphicon-option-vertical { &:before { content: "\e235"; } } -.glyphicon-menu-hamburger { &:before { content: "\e236"; } } -.glyphicon-modal-window { &:before { content: "\e237"; } } -.glyphicon-oil { &:before { content: "\e238"; } } -.glyphicon-grain { &:before { content: "\e239"; } } -.glyphicon-sunglasses { &:before { content: "\e240"; } } -.glyphicon-text-size { &:before { content: "\e241"; } } -.glyphicon-text-color { &:before { content: "\e242"; } } -.glyphicon-text-background { &:before { content: "\e243"; } } -.glyphicon-object-align-top { &:before { content: "\e244"; } } -.glyphicon-object-align-bottom { &:before { content: "\e245"; } } -.glyphicon-object-align-horizontal{ &:before { content: "\e246"; } } -.glyphicon-object-align-left { &:before { content: "\e247"; } } -.glyphicon-object-align-vertical { &:before { content: "\e248"; } } -.glyphicon-object-align-right { &:before { content: "\e249"; } } -.glyphicon-triangle-right { &:before { content: "\e250"; } } -.glyphicon-triangle-left { &:before { content: "\e251"; } } -.glyphicon-triangle-bottom { &:before { content: "\e252"; } } -.glyphicon-triangle-top { &:before { content: "\e253"; } } -.glyphicon-console { &:before { content: "\e254"; } } -.glyphicon-superscript { &:before { content: "\e255"; } } -.glyphicon-subscript { &:before { content: "\e256"; } } -.glyphicon-menu-left { &:before { content: "\e257"; } } -.glyphicon-menu-right { &:before { content: "\e258"; } } -.glyphicon-menu-down { &:before { content: "\e259"; } } -.glyphicon-menu-up { &:before { content: "\e260"; } } diff --git a/src/UI/Content/Bootstrap/grid.less b/src/UI/Content/Bootstrap/grid.less deleted file mode 100644 index e100655b7..000000000 --- a/src/UI/Content/Bootstrap/grid.less +++ /dev/null @@ -1,84 +0,0 @@ -// -// Grid system -// -------------------------------------------------- - - -// Container widths -// -// Set the container width, and override it for fixed navbars in media queries. - -.container { - .container-fixed(); - - @media (min-width: @screen-sm-min) { - width: @container-sm; - } - @media (min-width: @screen-md-min) { - width: @container-md; - } - @media (min-width: @screen-lg-min) { - width: @container-lg; - } -} - - -// Fluid container -// -// Utilizes the mixin meant for fixed width containers, but without any defined -// width for fluid, full width layouts. - -.container-fluid { - .container-fixed(); -} - - -// Row -// -// Rows contain and clear the floats of your columns. - -.row { - .make-row(); -} - - -// Columns -// -// Common styles for small and large grid columns - -.make-grid-columns(); - - -// Extra small grid -// -// Columns, offsets, pushes, and pulls for extra small devices like -// smartphones. - -.make-grid(xs); - - -// Small grid -// -// Columns, offsets, pushes, and pulls for the small device range, from phones -// to tablets. - -@media (min-width: @screen-sm-min) { - .make-grid(sm); -} - - -// Medium grid -// -// Columns, offsets, pushes, and pulls for the desktop device range. - -@media (min-width: @screen-md-min) { - .make-grid(md); -} - - -// Large grid -// -// Columns, offsets, pushes, and pulls for the large desktop device range. - -@media (min-width: @screen-lg-min) { - .make-grid(lg); -} diff --git a/src/UI/Content/Bootstrap/input-groups.less b/src/UI/Content/Bootstrap/input-groups.less deleted file mode 100644 index 457ea60ba..000000000 --- a/src/UI/Content/Bootstrap/input-groups.less +++ /dev/null @@ -1,167 +0,0 @@ -// -// Input groups -// -------------------------------------------------- - -// Base styles -// ------------------------- -.input-group { - position: relative; // For dropdowns - display: table; - border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table - - // Undo padding and float of grid classes - &[class*="col-"] { - float: none; - padding-left: 0; - padding-right: 0; - } - - .form-control { - // Ensure that the input is always above the *appended* addon button for - // proper border colors. - position: relative; - z-index: 2; - - // IE9 fubars the placeholder attribute in text inputs and the arrows on - // select elements in input groups. To fix it, we float the input. Details: - // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 - float: left; - - width: 100%; - margin-bottom: 0; - } -} - -// Sizing options -// -// Remix the default form control sizing classes into new ones for easier -// manipulation. - -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - .input-lg(); -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - .input-sm(); -} - - -// Display as table-cell -// ------------------------- -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; - - &:not(:first-child):not(:last-child) { - border-radius: 0; - } -} -// Addon and addon wrapper for buttons -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; // Match the inputs -} - -// Text input groups -// ------------------------- -.input-group-addon { - padding: @padding-base-vertical @padding-base-horizontal; - font-size: @font-size-base; - font-weight: normal; - line-height: 1; - color: @input-color; - text-align: center; - background-color: @input-group-addon-bg; - border: 1px solid @input-group-addon-border-color; - border-radius: @border-radius-base; - - // Sizing - &.input-sm { - padding: @padding-small-vertical @padding-small-horizontal; - font-size: @font-size-small; - border-radius: @border-radius-small; - } - &.input-lg { - padding: @padding-large-vertical @padding-large-horizontal; - font-size: @font-size-large; - border-radius: @border-radius-large; - } - - // Nuke default margins from checkboxes and radios to vertically center within. - input[type="radio"], - input[type="checkbox"] { - margin-top: 0; - } -} - -// Reset rounded corners -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - .border-right-radius(0); -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - .border-left-radius(0); -} -.input-group-addon:last-child { - border-left: 0; -} - -// Button input groups -// ------------------------- -.input-group-btn { - position: relative; - // Jankily prevent input button groups from wrapping with `white-space` and - // `font-size` in combination with `inline-block` on buttons. - font-size: 0; - white-space: nowrap; - - // Negative margin for spacing, position for bringing hovered/focused/actived - // element above the siblings. - > .btn { - position: relative; - + .btn { - margin-left: -1px; - } - // Bring the "active" button to the front - &:hover, - &:focus, - &:active { - z-index: 2; - } - } - - // Negative margin to only have a 1px border between the two - &:first-child { - > .btn, - > .btn-group { - margin-right: -1px; - } - } - &:last-child { - > .btn, - > .btn-group { - z-index: 2; - margin-left: -1px; - } - } -} diff --git a/src/UI/Content/Bootstrap/jumbotron.less b/src/UI/Content/Bootstrap/jumbotron.less deleted file mode 100644 index fa80a38c6..000000000 --- a/src/UI/Content/Bootstrap/jumbotron.less +++ /dev/null @@ -1,52 +0,0 @@ -// -// Jumbotron -// -------------------------------------------------- - - -.jumbotron { - padding-top: @jumbotron-padding; - padding-bottom: @jumbotron-padding; - margin-bottom: @jumbotron-padding; - color: @jumbotron-color; - background-color: @jumbotron-bg; - - h1, - .h1 { - color: @jumbotron-heading-color; - } - - p { - margin-bottom: (@jumbotron-padding / 2); - font-size: @jumbotron-font-size; - font-weight: 200; - } - - > hr { - border-top-color: darken(@jumbotron-bg, 10%); - } - - .container &, - .container-fluid & { - border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container - } - - .container { - max-width: 100%; - } - - @media screen and (min-width: @screen-sm-min) { - padding-top: (@jumbotron-padding * 1.6); - padding-bottom: (@jumbotron-padding * 1.6); - - .container &, - .container-fluid & { - padding-left: (@jumbotron-padding * 2); - padding-right: (@jumbotron-padding * 2); - } - - h1, - .h1 { - font-size: @jumbotron-heading-font-size; - } - } -} diff --git a/src/UI/Content/Bootstrap/labels.less b/src/UI/Content/Bootstrap/labels.less deleted file mode 100644 index 9a5a27006..000000000 --- a/src/UI/Content/Bootstrap/labels.less +++ /dev/null @@ -1,64 +0,0 @@ -// -// Labels -// -------------------------------------------------- - -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: @label-color; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; - - // Add hover effects, but only for links - a& { - &:hover, - &:focus { - color: @label-link-hover-color; - text-decoration: none; - cursor: pointer; - } - } - - // Empty labels collapse automatically (not available in IE8) - &:empty { - display: none; - } - - // Quick fix for labels in buttons - .btn & { - position: relative; - top: -1px; - } -} - -// Colors -// Contextual variations (linked labels get darker on :hover) - -.label-default { - .label-variant(@label-default-bg); -} - -.label-primary { - .label-variant(@label-primary-bg); -} - -.label-success { - .label-variant(@label-success-bg); -} - -.label-info { - .label-variant(@label-info-bg); -} - -.label-warning { - .label-variant(@label-warning-bg); -} - -.label-danger { - .label-variant(@label-danger-bg); -} diff --git a/src/UI/Content/Bootstrap/list-group.less b/src/UI/Content/Bootstrap/list-group.less deleted file mode 100644 index 216b91230..000000000 --- a/src/UI/Content/Bootstrap/list-group.less +++ /dev/null @@ -1,130 +0,0 @@ -// -// List groups -// -------------------------------------------------- - - -// Base class -// -// Easily usable on <ul>, <ol>, or <div>. - -.list-group { - // No need to set list-style: none; since .list-group-item is block level - margin-bottom: 20px; - padding-left: 0; // reset padding because ul and ol -} - - -// Individual list items -// -// Use on `li`s or `div`s within the `.list-group` parent. - -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - // Place the border on the list items and negative margin up for better styling - margin-bottom: -1px; - background-color: @list-group-bg; - border: 1px solid @list-group-border; - - // Round the first and last items - &:first-child { - .border-top-radius(@list-group-border-radius); - } - &:last-child { - margin-bottom: 0; - .border-bottom-radius(@list-group-border-radius); - } -} - - -// Interactive list items -// -// Use anchor or button elements instead of `li`s or `div`s to create interactive items. -// Includes an extra `.active` modifier class for showing selected items. - -a.list-group-item, -button.list-group-item { - color: @list-group-link-color; - - .list-group-item-heading { - color: @list-group-link-heading-color; - } - - // Hover state - &:hover, - &:focus { - text-decoration: none; - color: @list-group-link-hover-color; - background-color: @list-group-hover-bg; - } -} - -button.list-group-item { - width: 100%; - text-align: left; -} - -.list-group-item { - // Disabled state - &.disabled, - &.disabled:hover, - &.disabled:focus { - background-color: @list-group-disabled-bg; - color: @list-group-disabled-color; - cursor: @cursor-disabled; - - // Force color to inherit for custom content - .list-group-item-heading { - color: inherit; - } - .list-group-item-text { - color: @list-group-disabled-text-color; - } - } - - // Active class on item itself, not parent - &.active, - &.active:hover, - &.active:focus { - z-index: 2; // Place active items above their siblings for proper border styling - color: @list-group-active-color; - background-color: @list-group-active-bg; - border-color: @list-group-active-border; - - // Force color to inherit for custom content - .list-group-item-heading, - .list-group-item-heading > small, - .list-group-item-heading > .small { - color: inherit; - } - .list-group-item-text { - color: @list-group-active-text-color; - } - } -} - - -// Contextual variants -// -// Add modifier classes to change text and background color on individual items. -// Organizationally, this must come after the `:hover` states. - -.list-group-item-variant(success; @state-success-bg; @state-success-text); -.list-group-item-variant(info; @state-info-bg; @state-info-text); -.list-group-item-variant(warning; @state-warning-bg; @state-warning-text); -.list-group-item-variant(danger; @state-danger-bg; @state-danger-text); - - -// Custom content options -// -// Extra classes for creating well-formatted content within `.list-group-item`s. - -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} diff --git a/src/UI/Content/Bootstrap/media.less b/src/UI/Content/Bootstrap/media.less deleted file mode 100644 index 8c835e861..000000000 --- a/src/UI/Content/Bootstrap/media.less +++ /dev/null @@ -1,66 +0,0 @@ -.media { - // Proper spacing between instances of .media - margin-top: 15px; - - &:first-child { - margin-top: 0; - } -} - -.media, -.media-body { - zoom: 1; - overflow: hidden; -} - -.media-body { - width: 10000px; -} - -.media-object { - display: block; - - // Fix collapse in webkit from max-width: 100% and display: table-cell. - &.img-thumbnail { - max-width: none; - } -} - -.media-right, -.media > .pull-right { - padding-left: 10px; -} - -.media-left, -.media > .pull-left { - padding-right: 10px; -} - -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; -} - -.media-middle { - vertical-align: middle; -} - -.media-bottom { - vertical-align: bottom; -} - -// Reset margins on headings for tighter default spacing -.media-heading { - margin-top: 0; - margin-bottom: 5px; -} - -// Media list variation -// -// Undo default ul/ol styles -.media-list { - padding-left: 0; - list-style: none; -} diff --git a/src/UI/Content/Bootstrap/mixins.less b/src/UI/Content/Bootstrap/mixins.less deleted file mode 100644 index e6f9fe684..000000000 --- a/src/UI/Content/Bootstrap/mixins.less +++ /dev/null @@ -1,40 +0,0 @@ -// Mixins -// -------------------------------------------------- - -// Utilities -@import "mixins/hide-text.less"; -@import "mixins/opacity.less"; -@import "mixins/image.less"; -@import "mixins/labels.less"; -@import "mixins/reset-filter.less"; -@import "mixins/resize.less"; -@import "mixins/responsive-visibility.less"; -@import "mixins/size.less"; -@import "mixins/tab-focus.less"; -@import "mixins/reset-text.less"; -@import "mixins/text-emphasis.less"; -@import "mixins/text-overflow.less"; -@import "mixins/vendor-prefixes.less"; - -// Components -@import "mixins/alerts.less"; -@import "mixins/buttons.less"; -@import "mixins/panels.less"; -@import "mixins/pagination.less"; -@import "mixins/list-group.less"; -@import "mixins/nav-divider.less"; -@import "mixins/forms.less"; -@import "mixins/progress-bar.less"; -@import "mixins/table-row.less"; - -// Skins -@import "mixins/background-variant.less"; -@import "mixins/border-radius.less"; -@import "mixins/gradients.less"; - -// Layout -@import "mixins/clearfix.less"; -@import "mixins/center-block.less"; -@import "mixins/nav-vertical-align.less"; -@import "mixins/grid-framework.less"; -@import "mixins/grid.less"; diff --git a/src/UI/Content/Bootstrap/mixins/alerts.less b/src/UI/Content/Bootstrap/mixins/alerts.less deleted file mode 100644 index 396196f43..000000000 --- a/src/UI/Content/Bootstrap/mixins/alerts.less +++ /dev/null @@ -1,14 +0,0 @@ -// Alerts - -.alert-variant(@background; @border; @text-color) { - background-color: @background; - border-color: @border; - color: @text-color; - - hr { - border-top-color: darken(@border, 5%); - } - .alert-link { - color: darken(@text-color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/background-variant.less b/src/UI/Content/Bootstrap/mixins/background-variant.less deleted file mode 100644 index a85c22b74..000000000 --- a/src/UI/Content/Bootstrap/mixins/background-variant.less +++ /dev/null @@ -1,9 +0,0 @@ -// Contextual backgrounds - -.bg-variant(@color) { - background-color: @color; - a&:hover, - a&:focus { - background-color: darken(@color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/border-radius.less b/src/UI/Content/Bootstrap/mixins/border-radius.less deleted file mode 100644 index ca05dbf45..000000000 --- a/src/UI/Content/Bootstrap/mixins/border-radius.less +++ /dev/null @@ -1,18 +0,0 @@ -// Single side border-radius - -.border-top-radius(@radius) { - border-top-right-radius: @radius; - border-top-left-radius: @radius; -} -.border-right-radius(@radius) { - border-bottom-right-radius: @radius; - border-top-right-radius: @radius; -} -.border-bottom-radius(@radius) { - border-bottom-right-radius: @radius; - border-bottom-left-radius: @radius; -} -.border-left-radius(@radius) { - border-bottom-left-radius: @radius; - border-top-left-radius: @radius; -} diff --git a/src/UI/Content/Bootstrap/mixins/buttons.less b/src/UI/Content/Bootstrap/mixins/buttons.less deleted file mode 100644 index 6875a97c8..000000000 --- a/src/UI/Content/Bootstrap/mixins/buttons.less +++ /dev/null @@ -1,68 +0,0 @@ -// Button variants -// -// Easily pump out default styles, as well as :hover, :focus, :active, -// and disabled options for all buttons - -.button-variant(@color; @background; @border) { - color: @color; - background-color: @background; - border-color: @border; - - &:focus, - &.focus { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 25%); - } - &:hover { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 12%); - } - &:active, - &.active, - .open > .dropdown-toggle& { - color: @color; - background-color: darken(@background, 10%); - border-color: darken(@border, 12%); - - &:hover, - &:focus, - &.focus { - color: @color; - background-color: darken(@background, 17%); - border-color: darken(@border, 25%); - } - } - &:active, - &.active, - .open > .dropdown-toggle& { - background-image: none; - } - &.disabled, - &[disabled], - fieldset[disabled] & { - &, - &:hover, - &:focus, - &.focus, - &:active, - &.active { - background-color: @background; - border-color: @border; - } - } - - .badge { - color: @background; - background-color: @color; - } -} - -// Button sizes -.button-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - border-radius: @border-radius; -} diff --git a/src/UI/Content/Bootstrap/mixins/center-block.less b/src/UI/Content/Bootstrap/mixins/center-block.less deleted file mode 100644 index d18d6de9e..000000000 --- a/src/UI/Content/Bootstrap/mixins/center-block.less +++ /dev/null @@ -1,7 +0,0 @@ -// Center-align a block level element - -.center-block() { - display: block; - margin-left: auto; - margin-right: auto; -} diff --git a/src/UI/Content/Bootstrap/mixins/clearfix.less b/src/UI/Content/Bootstrap/mixins/clearfix.less deleted file mode 100644 index 3f7a3820c..000000000 --- a/src/UI/Content/Bootstrap/mixins/clearfix.less +++ /dev/null @@ -1,22 +0,0 @@ -// Clearfix -// -// For modern browsers -// 1. The space content is one way to avoid an Opera bug when the -// contenteditable attribute is included anywhere else in the document. -// Otherwise it causes space to appear at the top and bottom of elements -// that are clearfixed. -// 2. The use of `table` rather than `block` is only necessary if using -// `:before` to contain the top-margins of child elements. -// -// Source: http://nicolasgallagher.com/micro-clearfix-hack/ - -.clearfix() { - &:before, - &:after { - content: " "; // 1 - display: table; // 2 - } - &:after { - clear: both; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/forms.less b/src/UI/Content/Bootstrap/mixins/forms.less deleted file mode 100644 index 6f55ed967..000000000 --- a/src/UI/Content/Bootstrap/mixins/forms.less +++ /dev/null @@ -1,85 +0,0 @@ -// Form validation states -// -// Used in forms.less to generate the form validation CSS for warnings, errors, -// and successes. - -.form-control-validation(@text-color: #555; @border-color: #ccc; @background-color: #f5f5f5) { - // Color the label and help text - .help-block, - .control-label, - .radio, - .checkbox, - .radio-inline, - .checkbox-inline, - &.radio label, - &.checkbox label, - &.radio-inline label, - &.checkbox-inline label { - color: @text-color; - } - // Set the border and box shadow on specific inputs to match - .form-control { - border-color: @border-color; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@border-color, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); - .box-shadow(@shadow); - } - } - // Set validation states also for addons - .input-group-addon { - color: @text-color; - border-color: @border-color; - background-color: @background-color; - } - // Optional feedback icon - .form-control-feedback { - color: @text-color; - } -} - - -// Form control focus state -// -// Generate a customized focus state and for any input with the specified color, -// which defaults to the `@input-border-focus` variable. -// -// We highly encourage you to not customize the default value, but instead use -// this to tweak colors on an as-needed basis. This aesthetic change is based on -// WebKit's default styles, but applicable to a wider range of browsers. Its -// usability and accessibility should be taken into account with any change. -// -// Example usage: change the default blue border and shadow to white for better -// contrast against a dark gray background. -.form-control-focus(@color: @input-border-focus) { - @color-rgba: rgba(red(@color), green(@color), blue(@color), .6); - &:focus { - border-color: @color; - outline: 0; - .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px @{color-rgba}"); - } -} - -// Form control sizing -// -// Relative text size, padding, and border-radii changes for form controls. For -// horizontal sizing, wrap controls in the predefined grid classes. `<select>` -// element gets special love because it's special, and that's a fact! -.input-size(@input-height; @padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - height: @input-height; - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - border-radius: @border-radius; - - select& { - height: @input-height; - line-height: @input-height; - } - - textarea&, - select[multiple]& { - height: auto; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/gradients.less b/src/UI/Content/Bootstrap/mixins/gradients.less deleted file mode 100644 index 0b88a89cc..000000000 --- a/src/UI/Content/Bootstrap/mixins/gradients.less +++ /dev/null @@ -1,59 +0,0 @@ -// Gradients - -#gradient { - - // Horizontal gradient, from left to right - // - // Creates two color stops, start and end, by specifying a color and position for each color stop. - // Color stops are not available in IE9 and below. - .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { - background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12 - background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down - } - - // Vertical gradient, from top to bottom - // - // Creates two color stops, start and end, by specifying a color and position for each color stop. - // Color stops are not available in IE9 and below. - .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) { - background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12 - background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - background-repeat: repeat-x; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down - } - - .directional(@start-color: #555; @end-color: #333; @deg: 45deg) { - background-repeat: repeat-x; - background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+ - background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12 - background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+ - } - .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { - background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); - background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color); - background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback - } - .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) { - background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color); - background-repeat: no-repeat; - filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback - } - .radial(@inner-color: #555; @outer-color: #333) { - background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color); - background-image: radial-gradient(circle, @inner-color, @outer-color); - background-repeat: no-repeat; - } - .striped(@color: rgba(255,255,255,.15); @angle: 45deg) { - background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/grid-framework.less b/src/UI/Content/Bootstrap/mixins/grid-framework.less deleted file mode 100644 index 8c23eed24..000000000 --- a/src/UI/Content/Bootstrap/mixins/grid-framework.less +++ /dev/null @@ -1,91 +0,0 @@ -// Framework grid generation -// -// Used only by Bootstrap to generate the correct number of grid classes given -// any value of `@grid-columns`. - -.make-grid-columns() { - // Common styles for all sizes of grid columns, widths 1-12 - .col(@index) { // initial - @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; - .col((@index + 1), @item); - } - .col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo - @item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}"; - .col((@index + 1), ~"@{list}, @{item}"); - } - .col(@index, @list) when (@index > @grid-columns) { // terminal - @{list} { - position: relative; - // Prevent columns from collapsing when empty - min-height: 1px; - // Inner gutter via padding - padding-left: ceil((@grid-gutter-width / 2)); - padding-right: floor((@grid-gutter-width / 2)); - } - } - .col(1); // kickstart it -} - -.float-grid-columns(@class) { - .col(@index) { // initial - @item: ~".col-@{class}-@{index}"; - .col((@index + 1), @item); - } - .col(@index, @list) when (@index =< @grid-columns) { // general - @item: ~".col-@{class}-@{index}"; - .col((@index + 1), ~"@{list}, @{item}"); - } - .col(@index, @list) when (@index > @grid-columns) { // terminal - @{list} { - float: left; - } - } - .col(1); // kickstart it -} - -.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) { - .col-@{class}-@{index} { - width: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) { - .col-@{class}-push-@{index} { - left: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) { - .col-@{class}-push-0 { - left: auto; - } -} -.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) { - .col-@{class}-pull-@{index} { - right: percentage((@index / @grid-columns)); - } -} -.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) { - .col-@{class}-pull-0 { - right: auto; - } -} -.calc-grid-column(@index, @class, @type) when (@type = offset) { - .col-@{class}-offset-@{index} { - margin-left: percentage((@index / @grid-columns)); - } -} - -// Basic looping in LESS -.loop-grid-columns(@index, @class, @type) when (@index >= 0) { - .calc-grid-column(@index, @class, @type); - // next iteration - .loop-grid-columns((@index - 1), @class, @type); -} - -// Create grid for specific class -.make-grid(@class) { - .float-grid-columns(@class); - .loop-grid-columns(@grid-columns, @class, width); - .loop-grid-columns(@grid-columns, @class, pull); - .loop-grid-columns(@grid-columns, @class, push); - .loop-grid-columns(@grid-columns, @class, offset); -} diff --git a/src/UI/Content/Bootstrap/mixins/grid.less b/src/UI/Content/Bootstrap/mixins/grid.less deleted file mode 100644 index f144c15f4..000000000 --- a/src/UI/Content/Bootstrap/mixins/grid.less +++ /dev/null @@ -1,122 +0,0 @@ -// Grid system -// -// Generate semantic grid columns with these mixins. - -// Centered container element -.container-fixed(@gutter: @grid-gutter-width) { - margin-right: auto; - margin-left: auto; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - &:extend(.clearfix all); -} - -// Creates a wrapper for a series of columns -.make-row(@gutter: @grid-gutter-width) { - margin-left: ceil((@gutter / -2)); - margin-right: floor((@gutter / -2)); - &:extend(.clearfix all); -} - -// Generate the extra small columns -.make-xs-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - float: left; - width: percentage((@columns / @grid-columns)); - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); -} -.make-xs-column-offset(@columns) { - margin-left: percentage((@columns / @grid-columns)); -} -.make-xs-column-push(@columns) { - left: percentage((@columns / @grid-columns)); -} -.make-xs-column-pull(@columns) { - right: percentage((@columns / @grid-columns)); -} - -// Generate the small columns -.make-sm-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-sm-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-offset(@columns) { - @media (min-width: @screen-sm-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-push(@columns) { - @media (min-width: @screen-sm-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-sm-column-pull(@columns) { - @media (min-width: @screen-sm-min) { - right: percentage((@columns / @grid-columns)); - } -} - -// Generate the medium columns -.make-md-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-md-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-md-column-offset(@columns) { - @media (min-width: @screen-md-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-md-column-push(@columns) { - @media (min-width: @screen-md-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-md-column-pull(@columns) { - @media (min-width: @screen-md-min) { - right: percentage((@columns / @grid-columns)); - } -} - -// Generate the large columns -.make-lg-column(@columns; @gutter: @grid-gutter-width) { - position: relative; - min-height: 1px; - padding-left: (@gutter / 2); - padding-right: (@gutter / 2); - - @media (min-width: @screen-lg-min) { - float: left; - width: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-offset(@columns) { - @media (min-width: @screen-lg-min) { - margin-left: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-push(@columns) { - @media (min-width: @screen-lg-min) { - left: percentage((@columns / @grid-columns)); - } -} -.make-lg-column-pull(@columns) { - @media (min-width: @screen-lg-min) { - right: percentage((@columns / @grid-columns)); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/hide-text.less b/src/UI/Content/Bootstrap/mixins/hide-text.less deleted file mode 100644 index bc7011850..000000000 --- a/src/UI/Content/Bootstrap/mixins/hide-text.less +++ /dev/null @@ -1,21 +0,0 @@ -// CSS image replacement -// -// Heads up! v3 launched with only `.hide-text()`, but per our pattern for -// mixins being reused as classes with the same name, this doesn't hold up. As -// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. -// -// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 - -// Deprecated as of v3.0.1 (will be removed in v4) -.hide-text() { - font: ~"0/0" a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} - -// New mixin to use as of v3.0.1 -.text-hide() { - .hide-text(); -} diff --git a/src/UI/Content/Bootstrap/mixins/image.less b/src/UI/Content/Bootstrap/mixins/image.less deleted file mode 100644 index f233cb3e1..000000000 --- a/src/UI/Content/Bootstrap/mixins/image.less +++ /dev/null @@ -1,33 +0,0 @@ -// Image Mixins -// - Responsive image -// - Retina image - - -// Responsive image -// -// Keep images from scaling beyond the width of their parents. -.img-responsive(@display: block) { - display: @display; - max-width: 100%; // Part 1: Set a maximum relative to the parent - height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching -} - - -// Retina image -// -// Short retina mixin for setting background-image and -size. Note that the -// spelling of `min--moz-device-pixel-ratio` is intentional. -.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) { - background-image: url("@{file-1x}"); - - @media - only screen and (-webkit-min-device-pixel-ratio: 2), - only screen and ( min--moz-device-pixel-ratio: 2), - only screen and ( -o-min-device-pixel-ratio: 2/1), - only screen and ( min-device-pixel-ratio: 2), - only screen and ( min-resolution: 192dpi), - only screen and ( min-resolution: 2dppx) { - background-image: url("@{file-2x}"); - background-size: @width-1x @height-1x; - } -} diff --git a/src/UI/Content/Bootstrap/mixins/labels.less b/src/UI/Content/Bootstrap/mixins/labels.less deleted file mode 100644 index 9f7a67ee3..000000000 --- a/src/UI/Content/Bootstrap/mixins/labels.less +++ /dev/null @@ -1,12 +0,0 @@ -// Labels - -.label-variant(@color) { - background-color: @color; - - &[href] { - &:hover, - &:focus { - background-color: darken(@color, 10%); - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/list-group.less b/src/UI/Content/Bootstrap/mixins/list-group.less deleted file mode 100644 index 03aa19069..000000000 --- a/src/UI/Content/Bootstrap/mixins/list-group.less +++ /dev/null @@ -1,30 +0,0 @@ -// List Groups - -.list-group-item-variant(@state; @background; @color) { - .list-group-item-@{state} { - color: @color; - background-color: @background; - - a&, - button& { - color: @color; - - .list-group-item-heading { - color: inherit; - } - - &:hover, - &:focus { - color: @color; - background-color: darken(@background, 5%); - } - &.active, - &.active:hover, - &.active:focus { - color: #fff; - background-color: @color; - border-color: @color; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/nav-divider.less b/src/UI/Content/Bootstrap/mixins/nav-divider.less deleted file mode 100644 index feb1e9ed0..000000000 --- a/src/UI/Content/Bootstrap/mixins/nav-divider.less +++ /dev/null @@ -1,10 +0,0 @@ -// Horizontal dividers -// -// Dividers (basically an hr) within dropdowns and nav lists - -.nav-divider(@color: #e5e5e5) { - height: 1px; - margin: ((@line-height-computed / 2) - 1) 0; - overflow: hidden; - background-color: @color; -} diff --git a/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less b/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less deleted file mode 100644 index d458c7861..000000000 --- a/src/UI/Content/Bootstrap/mixins/nav-vertical-align.less +++ /dev/null @@ -1,9 +0,0 @@ -// Navbar vertical align -// -// Vertically center elements in the navbar. -// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. - -.navbar-vertical-align(@element-height) { - margin-top: ((@navbar-height - @element-height) / 2); - margin-bottom: ((@navbar-height - @element-height) / 2); -} diff --git a/src/UI/Content/Bootstrap/mixins/opacity.less b/src/UI/Content/Bootstrap/mixins/opacity.less deleted file mode 100644 index 33ed25ce6..000000000 --- a/src/UI/Content/Bootstrap/mixins/opacity.less +++ /dev/null @@ -1,8 +0,0 @@ -// Opacity - -.opacity(@opacity) { - opacity: @opacity; - // IE8 filter - @opacity-ie: (@opacity * 100); - filter: ~"alpha(opacity=@{opacity-ie})"; -} diff --git a/src/UI/Content/Bootstrap/mixins/pagination.less b/src/UI/Content/Bootstrap/mixins/pagination.less deleted file mode 100644 index 618804f2d..000000000 --- a/src/UI/Content/Bootstrap/mixins/pagination.less +++ /dev/null @@ -1,24 +0,0 @@ -// Pagination - -.pagination-size(@padding-vertical; @padding-horizontal; @font-size; @line-height; @border-radius) { - > li { - > a, - > span { - padding: @padding-vertical @padding-horizontal; - font-size: @font-size; - line-height: @line-height; - } - &:first-child { - > a, - > span { - .border-left-radius(@border-radius); - } - } - &:last-child { - > a, - > span { - .border-right-radius(@border-radius); - } - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/panels.less b/src/UI/Content/Bootstrap/mixins/panels.less deleted file mode 100644 index 49ee10d4a..000000000 --- a/src/UI/Content/Bootstrap/mixins/panels.less +++ /dev/null @@ -1,24 +0,0 @@ -// Panels - -.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) { - border-color: @border; - - & > .panel-heading { - color: @heading-text-color; - background-color: @heading-bg-color; - border-color: @heading-border; - - + .panel-collapse > .panel-body { - border-top-color: @border; - } - .badge { - color: @heading-bg-color; - background-color: @heading-text-color; - } - } - & > .panel-footer { - + .panel-collapse > .panel-body { - border-bottom-color: @border; - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/progress-bar.less b/src/UI/Content/Bootstrap/mixins/progress-bar.less deleted file mode 100644 index f07996a34..000000000 --- a/src/UI/Content/Bootstrap/mixins/progress-bar.less +++ /dev/null @@ -1,10 +0,0 @@ -// Progress bars - -.progress-bar-variant(@color) { - background-color: @color; - - // Deprecated parent class requirement as of v3.2.0 - .progress-striped & { - #gradient > .striped(); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/reset-filter.less b/src/UI/Content/Bootstrap/mixins/reset-filter.less deleted file mode 100644 index 68cdb5e18..000000000 --- a/src/UI/Content/Bootstrap/mixins/reset-filter.less +++ /dev/null @@ -1,8 +0,0 @@ -// Reset filters for IE -// -// When you need to remove a gradient background, do not forget to use this to reset -// the IE filter for IE9 and below. - -.reset-filter() { - filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)")); -} diff --git a/src/UI/Content/Bootstrap/mixins/reset-text.less b/src/UI/Content/Bootstrap/mixins/reset-text.less deleted file mode 100644 index 58dd4d19b..000000000 --- a/src/UI/Content/Bootstrap/mixins/reset-text.less +++ /dev/null @@ -1,18 +0,0 @@ -.reset-text() { - font-family: @font-family-base; - // We deliberately do NOT reset font-size. - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: @line-height-base; - text-align: left; // Fallback for where `start` is not supported - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; -} diff --git a/src/UI/Content/Bootstrap/mixins/resize.less b/src/UI/Content/Bootstrap/mixins/resize.less deleted file mode 100644 index 3acd3afdb..000000000 --- a/src/UI/Content/Bootstrap/mixins/resize.less +++ /dev/null @@ -1,6 +0,0 @@ -// Resize anything - -.resizable(@direction) { - resize: @direction; // Options: horizontal, vertical, both - overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` -} diff --git a/src/UI/Content/Bootstrap/mixins/responsive-visibility.less b/src/UI/Content/Bootstrap/mixins/responsive-visibility.less deleted file mode 100644 index ecf1e979f..000000000 --- a/src/UI/Content/Bootstrap/mixins/responsive-visibility.less +++ /dev/null @@ -1,15 +0,0 @@ -// Responsive utilities - -// -// More easily include all the states for responsive-utilities.less. -.responsive-visibility() { - display: block !important; - table& { display: table !important; } - tr& { display: table-row !important; } - th&, - td& { display: table-cell !important; } -} - -.responsive-invisibility() { - display: none !important; -} diff --git a/src/UI/Content/Bootstrap/mixins/size.less b/src/UI/Content/Bootstrap/mixins/size.less deleted file mode 100644 index a8be65089..000000000 --- a/src/UI/Content/Bootstrap/mixins/size.less +++ /dev/null @@ -1,10 +0,0 @@ -// Sizing shortcuts - -.size(@width; @height) { - width: @width; - height: @height; -} - -.square(@size) { - .size(@size; @size); -} diff --git a/src/UI/Content/Bootstrap/mixins/tab-focus.less b/src/UI/Content/Bootstrap/mixins/tab-focus.less deleted file mode 100644 index 1f1f05ab0..000000000 --- a/src/UI/Content/Bootstrap/mixins/tab-focus.less +++ /dev/null @@ -1,9 +0,0 @@ -// WebKit-style focus - -.tab-focus() { - // Default - outline: thin dotted; - // WebKit - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} diff --git a/src/UI/Content/Bootstrap/mixins/table-row.less b/src/UI/Content/Bootstrap/mixins/table-row.less deleted file mode 100644 index 0f287f1a8..000000000 --- a/src/UI/Content/Bootstrap/mixins/table-row.less +++ /dev/null @@ -1,28 +0,0 @@ -// Tables - -.table-row-variant(@state; @background) { - // Exact selectors below required to override `.table-striped` and prevent - // inheritance to nested tables. - .table > thead > tr, - .table > tbody > tr, - .table > tfoot > tr { - > td.@{state}, - > th.@{state}, - &.@{state} > td, - &.@{state} > th { - background-color: @background; - } - } - - // Hover states for `.table-hover` - // Note: this is not available for cells or rows within `thead` or `tfoot`. - .table-hover > tbody > tr { - > td.@{state}:hover, - > th.@{state}:hover, - &.@{state}:hover > td, - &:hover > .@{state}, - &.@{state}:hover > th { - background-color: darken(@background, 5%); - } - } -} diff --git a/src/UI/Content/Bootstrap/mixins/text-emphasis.less b/src/UI/Content/Bootstrap/mixins/text-emphasis.less deleted file mode 100644 index 9e8a77a69..000000000 --- a/src/UI/Content/Bootstrap/mixins/text-emphasis.less +++ /dev/null @@ -1,9 +0,0 @@ -// Typography - -.text-emphasis-variant(@color) { - color: @color; - a&:hover, - a&:focus { - color: darken(@color, 10%); - } -} diff --git a/src/UI/Content/Bootstrap/mixins/text-overflow.less b/src/UI/Content/Bootstrap/mixins/text-overflow.less deleted file mode 100644 index c11ad2fb7..000000000 --- a/src/UI/Content/Bootstrap/mixins/text-overflow.less +++ /dev/null @@ -1,8 +0,0 @@ -// Text overflow -// Requires inline-block or block for proper styling - -.text-overflow() { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less b/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less deleted file mode 100644 index afd3331c3..000000000 --- a/src/UI/Content/Bootstrap/mixins/vendor-prefixes.less +++ /dev/null @@ -1,227 +0,0 @@ -// Vendor Prefixes -// -// All vendor mixins are deprecated as of v3.2.0 due to the introduction of -// Autoprefixer in our Gruntfile. They will be removed in v4. - -// - Animations -// - Backface visibility -// - Box shadow -// - Box sizing -// - Content columns -// - Hyphens -// - Placeholder text -// - Transformations -// - Transitions -// - User Select - - -// Animations -.animation(@animation) { - -webkit-animation: @animation; - -o-animation: @animation; - animation: @animation; -} -.animation-name(@name) { - -webkit-animation-name: @name; - animation-name: @name; -} -.animation-duration(@duration) { - -webkit-animation-duration: @duration; - animation-duration: @duration; -} -.animation-timing-function(@timing-function) { - -webkit-animation-timing-function: @timing-function; - animation-timing-function: @timing-function; -} -.animation-delay(@delay) { - -webkit-animation-delay: @delay; - animation-delay: @delay; -} -.animation-iteration-count(@iteration-count) { - -webkit-animation-iteration-count: @iteration-count; - animation-iteration-count: @iteration-count; -} -.animation-direction(@direction) { - -webkit-animation-direction: @direction; - animation-direction: @direction; -} -.animation-fill-mode(@fill-mode) { - -webkit-animation-fill-mode: @fill-mode; - animation-fill-mode: @fill-mode; -} - -// Backface visibility -// Prevent browsers from flickering when using CSS 3D transforms. -// Default value is `visible`, but can be changed to `hidden` - -.backface-visibility(@visibility){ - -webkit-backface-visibility: @visibility; - -moz-backface-visibility: @visibility; - backface-visibility: @visibility; -} - -// Drop shadows -// -// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's -// supported browsers that have box shadow capabilities now support it. - -.box-shadow(@shadow) { - -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1 - box-shadow: @shadow; -} - -// Box sizing -.box-sizing(@boxmodel) { - -webkit-box-sizing: @boxmodel; - -moz-box-sizing: @boxmodel; - box-sizing: @boxmodel; -} - -// CSS3 Content Columns -.content-columns(@column-count; @column-gap: @grid-gutter-width) { - -webkit-column-count: @column-count; - -moz-column-count: @column-count; - column-count: @column-count; - -webkit-column-gap: @column-gap; - -moz-column-gap: @column-gap; - column-gap: @column-gap; -} - -// Optional hyphenation -.hyphens(@mode: auto) { - word-wrap: break-word; - -webkit-hyphens: @mode; - -moz-hyphens: @mode; - -ms-hyphens: @mode; // IE10+ - -o-hyphens: @mode; - hyphens: @mode; -} - -// Placeholder text -.placeholder(@color: @input-color-placeholder) { - // Firefox - &::-moz-placeholder { - color: @color; - opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526 - } - &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+ - &::-webkit-input-placeholder { color: @color; } // Safari and Chrome -} - -// Transformations -.scale(@ratio) { - -webkit-transform: scale(@ratio); - -ms-transform: scale(@ratio); // IE9 only - -o-transform: scale(@ratio); - transform: scale(@ratio); -} -.scale(@ratioX; @ratioY) { - -webkit-transform: scale(@ratioX, @ratioY); - -ms-transform: scale(@ratioX, @ratioY); // IE9 only - -o-transform: scale(@ratioX, @ratioY); - transform: scale(@ratioX, @ratioY); -} -.scaleX(@ratio) { - -webkit-transform: scaleX(@ratio); - -ms-transform: scaleX(@ratio); // IE9 only - -o-transform: scaleX(@ratio); - transform: scaleX(@ratio); -} -.scaleY(@ratio) { - -webkit-transform: scaleY(@ratio); - -ms-transform: scaleY(@ratio); // IE9 only - -o-transform: scaleY(@ratio); - transform: scaleY(@ratio); -} -.skew(@x; @y) { - -webkit-transform: skewX(@x) skewY(@y); - -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+ - -o-transform: skewX(@x) skewY(@y); - transform: skewX(@x) skewY(@y); -} -.translate(@x; @y) { - -webkit-transform: translate(@x, @y); - -ms-transform: translate(@x, @y); // IE9 only - -o-transform: translate(@x, @y); - transform: translate(@x, @y); -} -.translate3d(@x; @y; @z) { - -webkit-transform: translate3d(@x, @y, @z); - transform: translate3d(@x, @y, @z); -} -.rotate(@degrees) { - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); // IE9 only - -o-transform: rotate(@degrees); - transform: rotate(@degrees); -} -.rotateX(@degrees) { - -webkit-transform: rotateX(@degrees); - -ms-transform: rotateX(@degrees); // IE9 only - -o-transform: rotateX(@degrees); - transform: rotateX(@degrees); -} -.rotateY(@degrees) { - -webkit-transform: rotateY(@degrees); - -ms-transform: rotateY(@degrees); // IE9 only - -o-transform: rotateY(@degrees); - transform: rotateY(@degrees); -} -.perspective(@perspective) { - -webkit-perspective: @perspective; - -moz-perspective: @perspective; - perspective: @perspective; -} -.perspective-origin(@perspective) { - -webkit-perspective-origin: @perspective; - -moz-perspective-origin: @perspective; - perspective-origin: @perspective; -} -.transform-origin(@origin) { - -webkit-transform-origin: @origin; - -moz-transform-origin: @origin; - -ms-transform-origin: @origin; // IE9 only - transform-origin: @origin; -} - - -// Transitions - -.transition(@transition) { - -webkit-transition: @transition; - -o-transition: @transition; - transition: @transition; -} -.transition-property(@transition-property) { - -webkit-transition-property: @transition-property; - transition-property: @transition-property; -} -.transition-delay(@transition-delay) { - -webkit-transition-delay: @transition-delay; - transition-delay: @transition-delay; -} -.transition-duration(@transition-duration) { - -webkit-transition-duration: @transition-duration; - transition-duration: @transition-duration; -} -.transition-timing-function(@timing-function) { - -webkit-transition-timing-function: @timing-function; - transition-timing-function: @timing-function; -} -.transition-transform(@transition) { - -webkit-transition: -webkit-transform @transition; - -moz-transition: -moz-transform @transition; - -o-transition: -o-transform @transition; - transition: transform @transition; -} - - -// User select -// For selecting text on the page - -.user-select(@select) { - -webkit-user-select: @select; - -moz-user-select: @select; - -ms-user-select: @select; // IE10+ - user-select: @select; -} diff --git a/src/UI/Content/Bootstrap/modals.less b/src/UI/Content/Bootstrap/modals.less deleted file mode 100644 index 1de622050..000000000 --- a/src/UI/Content/Bootstrap/modals.less +++ /dev/null @@ -1,150 +0,0 @@ -// -// Modals -// -------------------------------------------------- - -// .modal-open - body class for killing the scroll -// .modal - container to scroll within -// .modal-dialog - positioning shell for the actual modal -// .modal-content - actual modal w/ bg and corners and shit - -// Kill the scroll on the body -.modal-open { - overflow: hidden; -} - -// Container that the modal scrolls within -.modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-modal; - -webkit-overflow-scrolling: touch; - - // Prevent Chrome on Windows from adding a focus outline. For details, see - // https://github.com/twbs/bootstrap/pull/10951. - outline: 0; - - // When fading in the modal, animate it to slide down - &.fade .modal-dialog { - .translate(0, -25%); - .transition-transform(~"0.3s ease-out"); - } - &.in .modal-dialog { .translate(0, 0) } -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} - -// Shell div to position the modal with bottom padding -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} - -// Actual modal -.modal-content { - position: relative; - background-color: @modal-content-bg; - border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc) - border: 1px solid @modal-content-border-color; - border-radius: @border-radius-large; - .box-shadow(0 3px 9px rgba(0,0,0,.5)); - background-clip: padding-box; - // Remove focus outline from opened modal - outline: 0; -} - -// Modal background -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: @zindex-modal-background; - background-color: @modal-backdrop-bg; - // Fade for backdrop - &.fade { .opacity(0); } - &.in { .opacity(@modal-backdrop-opacity); } -} - -// Modal header -// Top section of the modal w/ title and dismiss -.modal-header { - padding: @modal-title-padding; - border-bottom: 1px solid @modal-header-border-color; - min-height: (@modal-title-padding + @modal-title-line-height); -} -// Close icon -.modal-header .close { - margin-top: -2px; -} - -// Title text within header -.modal-title { - margin: 0; - line-height: @modal-title-line-height; -} - -// Modal body -// Where all modal content resides (sibling of .modal-header and .modal-footer) -.modal-body { - position: relative; - padding: @modal-inner-padding; -} - -// Footer (for actions) -.modal-footer { - padding: @modal-inner-padding; - text-align: right; // right align buttons - border-top: 1px solid @modal-footer-border-color; - &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons - - // Properly space out buttons - .btn + .btn { - margin-left: 5px; - margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs - } - // but override that for button groups - .btn-group .btn + .btn { - margin-left: -1px; - } - // and override it for block buttons as well - .btn-block + .btn-block { - margin-left: 0; - } -} - -// Measure scrollbar width for padding body during modal show/hide -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} - -// Scale up the modal -@media (min-width: @screen-sm-min) { - // Automatically set modal's width for larger viewports - .modal-dialog { - width: @modal-md; - margin: 30px auto; - } - .modal-content { - .box-shadow(0 5px 15px rgba(0,0,0,.5)); - } - - // Modal sizes - .modal-sm { width: @modal-sm; } -} - -@media (min-width: @screen-md-min) { - .modal-lg { width: @modal-lg; } -} diff --git a/src/UI/Content/Bootstrap/navbar.less b/src/UI/Content/Bootstrap/navbar.less deleted file mode 100644 index 6d751bb9c..000000000 --- a/src/UI/Content/Bootstrap/navbar.less +++ /dev/null @@ -1,660 +0,0 @@ -// -// Navbars -// -------------------------------------------------- - - -// Wrapper and base class -// -// Provide a static navbar from which we expand to create full-width, fixed, and -// other navbar variations. - -.navbar { - position: relative; - min-height: @navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode) - margin-bottom: @navbar-margin-bottom; - border: 1px solid transparent; - - // Prevent floats from breaking the navbar - &:extend(.clearfix all); - - @media (min-width: @grid-float-breakpoint) { - border-radius: @navbar-border-radius; - } -} - - -// Navbar heading -// -// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy -// styling of responsive aspects. - -.navbar-header { - &:extend(.clearfix all); - - @media (min-width: @grid-float-breakpoint) { - float: left; - } -} - - -// Navbar collapse (body) -// -// Group your navbar content into this for easy collapsing and expanding across -// various device sizes. By default, this content is collapsed when <768px, but -// will expand past that for a horizontal display. -// -// To start (on mobile devices) the navbar links, forms, and buttons are stacked -// vertically and include a `max-height` to overflow in case you have too much -// content for the user's viewport. - -.navbar-collapse { - overflow-x: visible; - padding-right: @navbar-padding-horizontal; - padding-left: @navbar-padding-horizontal; - border-top: 1px solid transparent; - box-shadow: inset 0 1px 0 rgba(255,255,255,.1); - &:extend(.clearfix all); - -webkit-overflow-scrolling: touch; - - &.in { - overflow-y: auto; - } - - @media (min-width: @grid-float-breakpoint) { - width: auto; - border-top: 0; - box-shadow: none; - - &.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; // Override default setting - overflow: visible !important; - } - - &.in { - overflow-y: visible; - } - - // Undo the collapse side padding for navbars with containers to ensure - // alignment of right-aligned contents. - .navbar-fixed-top &, - .navbar-static-top &, - .navbar-fixed-bottom & { - padding-left: 0; - padding-right: 0; - } - } -} - -.navbar-fixed-top, -.navbar-fixed-bottom { - .navbar-collapse { - max-height: @navbar-collapse-max-height; - - @media (max-device-width: @screen-xs-min) and (orientation: landscape) { - max-height: 200px; - } - } -} - - -// Both navbar header and collapse -// -// When a container is present, change the behavior of the header and collapse. - -.container, -.container-fluid { - > .navbar-header, - > .navbar-collapse { - margin-right: -@navbar-padding-horizontal; - margin-left: -@navbar-padding-horizontal; - - @media (min-width: @grid-float-breakpoint) { - margin-right: 0; - margin-left: 0; - } - } -} - - -// -// Navbar alignment options -// -// Display the navbar across the entirety of the page or fixed it to the top or -// bottom of the page. - -// Static top (unfixed, but 100% wide) navbar -.navbar-static-top { - z-index: @zindex-navbar; - border-width: 0 0 1px; - - @media (min-width: @grid-float-breakpoint) { - border-radius: 0; - } -} - -// Fix the top/bottom navbars when screen real estate supports it -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: @zindex-navbar-fixed; - - // Undo the rounded corners - @media (min-width: @grid-float-breakpoint) { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; // override .navbar defaults - border-width: 1px 0 0; -} - - -// Brand/project name - -.navbar-brand { - float: left; - padding: @navbar-padding-vertical @navbar-padding-horizontal; - font-size: @font-size-large; - line-height: @line-height-computed; - height: @navbar-height; - - &:hover, - &:focus { - text-decoration: none; - } - - > img { - display: block; - } - - @media (min-width: @grid-float-breakpoint) { - .navbar > .container &, - .navbar > .container-fluid & { - margin-left: -@navbar-padding-horizontal; - } - } -} - - -// Navbar toggle -// -// Custom button for toggling the `.navbar-collapse`, powered by the collapse -// JavaScript plugin. - -.navbar-toggle { - position: relative; - float: right; - margin-right: @navbar-padding-horizontal; - padding: 9px 10px; - .navbar-vertical-align(34px); - background-color: transparent; - background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 - border: 1px solid transparent; - border-radius: @border-radius-base; - - // We remove the `outline` here, but later compensate by attaching `:hover` - // styles to `:focus`. - &:focus { - outline: 0; - } - - // Bars - .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; - } - .icon-bar + .icon-bar { - margin-top: 4px; - } - - @media (min-width: @grid-float-breakpoint) { - display: none; - } -} - - -// Navbar nav links -// -// Builds on top of the `.nav` components with its own modifier class to make -// the nav the full height of the horizontal nav (above 768px). - -.navbar-nav { - margin: (@navbar-padding-vertical / 2) -@navbar-padding-horizontal; - - > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: @line-height-computed; - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display when collapsed - .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - box-shadow: none; - > li > a, - .dropdown-header { - padding: 5px 15px 5px 25px; - } - > li > a { - line-height: @line-height-computed; - &:hover, - &:focus { - background-image: none; - } - } - } - } - - // Uncollapse the nav - @media (min-width: @grid-float-breakpoint) { - float: left; - margin: 0; - - > li { - float: left; - > a { - padding-top: @navbar-padding-vertical; - padding-bottom: @navbar-padding-vertical; - } - } - } -} - - -// Navbar form -// -// Extension of the `.form-inline` with some extra flavor for optimum display in -// our navbars. - -.navbar-form { - margin-left: -@navbar-padding-horizontal; - margin-right: -@navbar-padding-horizontal; - padding: 10px @navbar-padding-horizontal; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1); - .box-shadow(@shadow); - - // Mixin behavior for optimum display - .form-inline(); - - .form-group { - @media (max-width: @grid-float-breakpoint-max) { - margin-bottom: 5px; - - &:last-child { - margin-bottom: 0; - } - } - } - - // Vertically center in expanded, horizontal navbar - .navbar-vertical-align(@input-height-base); - - // Undo 100% width for pull classes - @media (min-width: @grid-float-breakpoint) { - width: auto; - border: 0; - margin-left: 0; - margin-right: 0; - padding-top: 0; - padding-bottom: 0; - .box-shadow(none); - } -} - - -// Dropdown menus - -// Menu position and menu carets -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - .border-top-radius(0); -} -// Menu position and menu caret support for dropups via extra dropup class -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - .border-top-radius(@navbar-border-radius); - .border-bottom-radius(0); -} - - -// Buttons in navbars -// -// Vertically center a button within a navbar (when *not* in a form). - -.navbar-btn { - .navbar-vertical-align(@input-height-base); - - &.btn-sm { - .navbar-vertical-align(@input-height-small); - } - &.btn-xs { - .navbar-vertical-align(22); - } -} - - -// Text in navbars -// -// Add a class to make any element properly align itself vertically within the navbars. - -.navbar-text { - .navbar-vertical-align(@line-height-computed); - - @media (min-width: @grid-float-breakpoint) { - float: left; - margin-left: @navbar-padding-horizontal; - margin-right: @navbar-padding-horizontal; - } -} - - -// Component alignment -// -// Repurpose the pull utilities as their own navbar utilities to avoid specificity -// issues with parents and chaining. Only do this when the navbar is uncollapsed -// though so that navbar contents properly stack and align in mobile. -// -// Declared after the navbar components to ensure more specificity on the margins. - -@media (min-width: @grid-float-breakpoint) { - .navbar-left { .pull-left(); } - .navbar-right { - .pull-right(); - margin-right: -@navbar-padding-horizontal; - - ~ .navbar-right { - margin-right: 0; - } - } -} - - -// Alternate navbars -// -------------------------------------------------- - -// Default navbar -.navbar-default { - background-color: @navbar-default-bg; - border-color: @navbar-default-border; - - .navbar-brand { - color: @navbar-default-brand-color; - &:hover, - &:focus { - color: @navbar-default-brand-hover-color; - background-color: @navbar-default-brand-hover-bg; - } - } - - .navbar-text { - color: @navbar-default-color; - } - - .navbar-nav { - > li > a { - color: @navbar-default-link-color; - - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - background-color: @navbar-default-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-active-color; - background-color: @navbar-default-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - background-color: @navbar-default-link-disabled-bg; - } - } - } - - .navbar-toggle { - border-color: @navbar-default-toggle-border-color; - &:hover, - &:focus { - background-color: @navbar-default-toggle-hover-bg; - } - .icon-bar { - background-color: @navbar-default-toggle-icon-bar-bg; - } - } - - .navbar-collapse, - .navbar-form { - border-color: @navbar-default-border; - } - - // Dropdown menu items - .navbar-nav { - // Remove background color from open dropdown - > .open > a { - &, - &:hover, - &:focus { - background-color: @navbar-default-link-active-bg; - color: @navbar-default-link-active-color; - } - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display when collapsed - .open .dropdown-menu { - > li > a { - color: @navbar-default-link-color; - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - background-color: @navbar-default-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-active-color; - background-color: @navbar-default-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - background-color: @navbar-default-link-disabled-bg; - } - } - } - } - } - - - // Links in navbars - // - // Add a class to ensure links outside the navbar nav are colored correctly. - - .navbar-link { - color: @navbar-default-link-color; - &:hover { - color: @navbar-default-link-hover-color; - } - } - - .btn-link { - color: @navbar-default-link-color; - &:hover, - &:focus { - color: @navbar-default-link-hover-color; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @navbar-default-link-disabled-color; - } - } - } -} - -// Inverse navbar - -.navbar-inverse { - background-color: @navbar-inverse-bg; - border-color: @navbar-inverse-border; - - .navbar-brand { - color: @navbar-inverse-brand-color; - &:hover, - &:focus { - color: @navbar-inverse-brand-hover-color; - background-color: @navbar-inverse-brand-hover-bg; - } - } - - .navbar-text { - color: @navbar-inverse-color; - } - - .navbar-nav { - > li > a { - color: @navbar-inverse-link-color; - - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - background-color: @navbar-inverse-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-active-color; - background-color: @navbar-inverse-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - background-color: @navbar-inverse-link-disabled-bg; - } - } - } - - // Darken the responsive nav toggle - .navbar-toggle { - border-color: @navbar-inverse-toggle-border-color; - &:hover, - &:focus { - background-color: @navbar-inverse-toggle-hover-bg; - } - .icon-bar { - background-color: @navbar-inverse-toggle-icon-bar-bg; - } - } - - .navbar-collapse, - .navbar-form { - border-color: darken(@navbar-inverse-bg, 7%); - } - - // Dropdowns - .navbar-nav { - > .open > a { - &, - &:hover, - &:focus { - background-color: @navbar-inverse-link-active-bg; - color: @navbar-inverse-link-active-color; - } - } - - @media (max-width: @grid-float-breakpoint-max) { - // Dropdowns get custom display - .open .dropdown-menu { - > .dropdown-header { - border-color: @navbar-inverse-border; - } - .divider { - background-color: @navbar-inverse-border; - } - > li > a { - color: @navbar-inverse-link-color; - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - background-color: @navbar-inverse-link-hover-bg; - } - } - > .active > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-active-color; - background-color: @navbar-inverse-link-active-bg; - } - } - > .disabled > a { - &, - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - background-color: @navbar-inverse-link-disabled-bg; - } - } - } - } - } - - .navbar-link { - color: @navbar-inverse-link-color; - &:hover { - color: @navbar-inverse-link-hover-color; - } - } - - .btn-link { - color: @navbar-inverse-link-color; - &:hover, - &:focus { - color: @navbar-inverse-link-hover-color; - } - &[disabled], - fieldset[disabled] & { - &:hover, - &:focus { - color: @navbar-inverse-link-disabled-color; - } - } - } -} diff --git a/src/UI/Content/Bootstrap/navs.less b/src/UI/Content/Bootstrap/navs.less deleted file mode 100644 index a3d11b136..000000000 --- a/src/UI/Content/Bootstrap/navs.less +++ /dev/null @@ -1,242 +0,0 @@ -// -// Navs -// -------------------------------------------------- - - -// Base class -// -------------------------------------------------- - -.nav { - margin-bottom: 0; - padding-left: 0; // Override default ul/ol - list-style: none; - &:extend(.clearfix all); - - > li { - position: relative; - display: block; - - > a { - position: relative; - display: block; - padding: @nav-link-padding; - &:hover, - &:focus { - text-decoration: none; - background-color: @nav-link-hover-bg; - } - } - - // Disabled state sets text to gray and nukes hover/tab effects - &.disabled > a { - color: @nav-disabled-link-color; - - &:hover, - &:focus { - color: @nav-disabled-link-hover-color; - text-decoration: none; - background-color: transparent; - cursor: @cursor-disabled; - } - } - } - - // Open dropdowns - .open > a { - &, - &:hover, - &:focus { - background-color: @nav-link-hover-bg; - border-color: @link-color; - } - } - - // Nav dividers (deprecated with v3.0.1) - // - // This should have been removed in v3 with the dropping of `.nav-list`, but - // we missed it. We don't currently support this anywhere, but in the interest - // of maintaining backward compatibility in case you use it, it's deprecated. - .nav-divider { - .nav-divider(); - } - - // Prevent IE8 from misplacing imgs - // - // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989 - > li > a > img { - max-width: none; - } -} - - -// Tabs -// ------------------------- - -// Give the tabs something to sit on -.nav-tabs { - border-bottom: 1px solid @nav-tabs-border-color; - > li { - float: left; - // Make the list-items overlay the bottom border - margin-bottom: -1px; - - // Actual tabs (as links) - > a { - margin-right: 2px; - line-height: @line-height-base; - border: 1px solid transparent; - border-radius: @border-radius-base @border-radius-base 0 0; - &:hover { - border-color: @nav-tabs-link-hover-border-color @nav-tabs-link-hover-border-color @nav-tabs-border-color; - } - } - - // Active state, and its :hover to override normal :hover - &.active > a { - &, - &:hover, - &:focus { - color: @nav-tabs-active-link-hover-color; - background-color: @nav-tabs-active-link-hover-bg; - border: 1px solid @nav-tabs-active-link-hover-border-color; - border-bottom-color: transparent; - cursor: default; - } - } - } - // pulling this in mainly for less shorthand - &.nav-justified { - .nav-justified(); - .nav-tabs-justified(); - } -} - - -// Pills -// ------------------------- -.nav-pills { - > li { - float: left; - - // Links rendered as pills - > a { - border-radius: @nav-pills-border-radius; - } - + li { - margin-left: 2px; - } - - // Active state - &.active > a { - &, - &:hover, - &:focus { - color: @nav-pills-active-link-hover-color; - background-color: @nav-pills-active-link-hover-bg; - } - } - } -} - - -// Stacked pills -.nav-stacked { - > li { - float: none; - + li { - margin-top: 2px; - margin-left: 0; // no need for this gap between nav items - } - } -} - - -// Nav variations -// -------------------------------------------------- - -// Justified nav links -// ------------------------- - -.nav-justified { - width: 100%; - - > li { - float: none; - > a { - text-align: center; - margin-bottom: 5px; - } - } - - > .dropdown .dropdown-menu { - top: auto; - left: auto; - } - - @media (min-width: @screen-sm-min) { - > li { - display: table-cell; - width: 1%; - > a { - margin-bottom: 0; - } - } - } -} - -// Move borders to anchors instead of bottom of list -// -// Mixin for adding on top the shared `.nav-justified` styles for our tabs -.nav-tabs-justified { - border-bottom: 0; - - > li > a { - // Override margin from .nav-tabs - margin-right: 0; - border-radius: @border-radius-base; - } - - > .active > a, - > .active > a:hover, - > .active > a:focus { - border: 1px solid @nav-tabs-justified-link-border-color; - } - - @media (min-width: @screen-sm-min) { - > li > a { - border-bottom: 1px solid @nav-tabs-justified-link-border-color; - border-radius: @border-radius-base @border-radius-base 0 0; - } - > .active > a, - > .active > a:hover, - > .active > a:focus { - border-bottom-color: @nav-tabs-justified-active-link-border-color; - } - } -} - - -// Tabbable tabs -// ------------------------- - -// Hide tabbable panes to start, show them when `.active` -.tab-content { - > .tab-pane { - display: none; - } - > .active { - display: block; - } -} - - -// Dropdowns -// ------------------------- - -// Specific dropdowns -.nav-tabs .dropdown-menu { - // make dropdown border overlap tab border - margin-top: -1px; - // Remove the top rounded corners here since there is a hard edge above the menu - .border-top-radius(0); -} diff --git a/src/UI/Content/Bootstrap/normalize.less b/src/UI/Content/Bootstrap/normalize.less deleted file mode 100644 index 9dddf73ad..000000000 --- a/src/UI/Content/Bootstrap/normalize.less +++ /dev/null @@ -1,424 +0,0 @@ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ - -// -// 1. Set default font family to sans-serif. -// 2. Prevent iOS and IE text size adjust after device orientation change, -// without disabling user zoom. -// - -html { - font-family: sans-serif; // 1 - -ms-text-size-adjust: 100%; // 2 - -webkit-text-size-adjust: 100%; // 2 -} - -// -// Remove default margin. -// - -body { - margin: 0; -} - -// HTML5 display definitions -// ========================================================================== - -// -// Correct `block` display not defined for any HTML5 element in IE 8/9. -// Correct `block` display not defined for `details` or `summary` in IE 10/11 -// and Firefox. -// Correct `block` display not defined for `main` in IE 11. -// - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} - -// -// 1. Correct `inline-block` display not defined in IE 8/9. -// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. -// - -audio, -canvas, -progress, -video { - display: inline-block; // 1 - vertical-align: baseline; // 2 -} - -// -// Prevent modern browsers from displaying `audio` without controls. -// Remove excess height in iOS 5 devices. -// - -audio:not([controls]) { - display: none; - height: 0; -} - -// -// Address `[hidden]` styling not present in IE 8/9/10. -// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. -// - -[hidden], -template { - display: none; -} - -// Links -// ========================================================================== - -// -// Remove the gray background color from active links in IE 10. -// - -a { - background-color: transparent; -} - -// -// Improve readability of focused elements when they are also in an -// active/hover state. -// - -a:active, -a:hover { - outline: 0; -} - -// Text-level semantics -// ========================================================================== - -// -// Address styling not present in IE 8/9/10/11, Safari, and Chrome. -// - -abbr[title] { - border-bottom: 1px dotted; -} - -// -// Address style set to `bolder` in Firefox 4+, Safari, and Chrome. -// - -b, -strong { - font-weight: bold; -} - -// -// Address styling not present in Safari and Chrome. -// - -dfn { - font-style: italic; -} - -// -// Address variable `h1` font-size and margin within `section` and `article` -// contexts in Firefox 4+, Safari, and Chrome. -// - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -// -// Address styling not present in IE 8/9. -// - -mark { - background: #ff0; - color: #000; -} - -// -// Address inconsistent and variable font size in all browsers. -// - -small { - font-size: 80%; -} - -// -// Prevent `sub` and `sup` affecting `line-height` in all browsers. -// - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -// Embedded content -// ========================================================================== - -// -// Remove border when inside `a` element in IE 8/9/10. -// - -img { - border: 0; -} - -// -// Correct overflow not hidden in IE 9/10/11. -// - -svg:not(:root) { - overflow: hidden; -} - -// Grouping content -// ========================================================================== - -// -// Address margin not present in IE 8/9 and Safari. -// - -figure { - margin: 1em 40px; -} - -// -// Address differences between Firefox and other browsers. -// - -hr { - box-sizing: content-box; - height: 0; -} - -// -// Contain overflow in all browsers. -// - -pre { - overflow: auto; -} - -// -// Address odd `em`-unit font size rendering in all browsers. -// - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} - -// Forms -// ========================================================================== - -// -// Known limitation: by default, Chrome and Safari on OS X allow very limited -// styling of `select`, unless a `border` property is set. -// - -// -// 1. Correct color not being inherited. -// Known issue: affects color of disabled elements. -// 2. Correct font properties not being inherited. -// 3. Address margins set differently in Firefox 4+, Safari, and Chrome. -// - -button, -input, -optgroup, -select, -textarea { - color: inherit; // 1 - font: inherit; // 2 - margin: 0; // 3 -} - -// -// Address `overflow` set to `hidden` in IE 8/9/10/11. -// - -button { - overflow: visible; -} - -// -// Address inconsistent `text-transform` inheritance for `button` and `select`. -// All other form control elements do not inherit `text-transform` values. -// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. -// Correct `select` style inheritance in Firefox. -// - -button, -select { - text-transform: none; -} - -// -// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` -// and `video` controls. -// 2. Correct inability to style clickable `input` types in iOS. -// 3. Improve usability and consistency of cursor style between image-type -// `input` and others. -// - -button, -html input[type="button"], // 1 -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; // 2 - cursor: pointer; // 3 -} - -// -// Re-set default cursor for disabled elements. -// - -button[disabled], -html input[disabled] { - cursor: default; -} - -// -// Remove inner padding and border in Firefox 4+. -// - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -// -// Address Firefox 4+ setting `line-height` on `input` using `!important` in -// the UA stylesheet. -// - -input { - line-height: normal; -} - -// -// It's recommended that you don't attempt to style these elements. -// Firefox's implementation doesn't respect box-sizing, padding, or width. -// -// 1. Address box sizing set to `content-box` in IE 8/9/10. -// 2. Remove excess padding in IE 8/9/10. -// - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; // 1 - padding: 0; // 2 -} - -// -// Fix the cursor style for Chrome's increment/decrement buttons. For certain -// `font-size` values of the `input`, it causes the cursor style of the -// decrement button to change from `default` to `text`. -// - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -// -// 1. Address `appearance` set to `searchfield` in Safari and Chrome. -// 2. Address `box-sizing` set to `border-box` in Safari and Chrome. -// - -input[type="search"] { - -webkit-appearance: textfield; // 1 - box-sizing: content-box; //2 -} - -// -// Remove inner padding and search cancel button in Safari and Chrome on OS X. -// Safari (but not Chrome) clips the cancel button when the search input has -// padding (and `textfield` appearance). -// - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -// -// Define consistent border, margin, and padding. -// - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -// -// 1. Correct `color` not being inherited in IE 8/9/10/11. -// 2. Remove padding so people aren't caught out if they zero out fieldsets. -// - -legend { - border: 0; // 1 - padding: 0; // 2 -} - -// -// Remove default vertical scrollbar in IE 8/9/10/11. -// - -textarea { - overflow: auto; -} - -// -// Don't inherit the `font-weight` (applied by a rule above). -// NOTE: the default cannot safely be changed in Chrome and Safari on OS X. -// - -optgroup { - font-weight: bold; -} - -// Tables -// ========================================================================== - -// -// Remove most spacing between table cells. -// - -table { - border-collapse: collapse; - border-spacing: 0; -} - -td, -th { - padding: 0; -} diff --git a/src/UI/Content/Bootstrap/pager.less b/src/UI/Content/Bootstrap/pager.less deleted file mode 100644 index 41abaaadc..000000000 --- a/src/UI/Content/Bootstrap/pager.less +++ /dev/null @@ -1,54 +0,0 @@ -// -// Pager pagination -// -------------------------------------------------- - - -.pager { - padding-left: 0; - margin: @line-height-computed 0; - list-style: none; - text-align: center; - &:extend(.clearfix all); - li { - display: inline; - > a, - > span { - display: inline-block; - padding: 5px 14px; - background-color: @pager-bg; - border: 1px solid @pager-border; - border-radius: @pager-border-radius; - } - - > a:hover, - > a:focus { - text-decoration: none; - background-color: @pager-hover-bg; - } - } - - .next { - > a, - > span { - float: right; - } - } - - .previous { - > a, - > span { - float: left; - } - } - - .disabled { - > a, - > a:hover, - > a:focus, - > span { - color: @pager-disabled-color; - background-color: @pager-bg; - cursor: @cursor-disabled; - } - } -} diff --git a/src/UI/Content/Bootstrap/pagination.less b/src/UI/Content/Bootstrap/pagination.less deleted file mode 100644 index 31a23bf79..000000000 --- a/src/UI/Content/Bootstrap/pagination.less +++ /dev/null @@ -1,89 +0,0 @@ -// -// Pagination (multiple pages) -// -------------------------------------------------- -.pagination { - display: inline-block; - padding-left: 0; - margin: @line-height-computed 0; - border-radius: @border-radius-base; - - > li { - display: inline; // Remove list-style and block-level defaults - > a, - > span { - position: relative; - float: left; // Collapse white-space - padding: @padding-base-vertical @padding-base-horizontal; - line-height: @line-height-base; - text-decoration: none; - color: @pagination-color; - background-color: @pagination-bg; - border: 1px solid @pagination-border; - margin-left: -1px; - } - &:first-child { - > a, - > span { - margin-left: 0; - .border-left-radius(@border-radius-base); - } - } - &:last-child { - > a, - > span { - .border-right-radius(@border-radius-base); - } - } - } - - > li > a, - > li > span { - &:hover, - &:focus { - z-index: 3; - color: @pagination-hover-color; - background-color: @pagination-hover-bg; - border-color: @pagination-hover-border; - } - } - - > .active > a, - > .active > span { - &, - &:hover, - &:focus { - z-index: 2; - color: @pagination-active-color; - background-color: @pagination-active-bg; - border-color: @pagination-active-border; - cursor: default; - } - } - - > .disabled { - > span, - > span:hover, - > span:focus, - > a, - > a:hover, - > a:focus { - color: @pagination-disabled-color; - background-color: @pagination-disabled-bg; - border-color: @pagination-disabled-border; - cursor: @cursor-disabled; - } - } -} - -// Sizing -// -------------------------------------------------- - -// Large -.pagination-lg { - .pagination-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); -} - -// Small -.pagination-sm { - .pagination-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); -} diff --git a/src/UI/Content/Bootstrap/panels.less b/src/UI/Content/Bootstrap/panels.less deleted file mode 100644 index 425eb5e64..000000000 --- a/src/UI/Content/Bootstrap/panels.less +++ /dev/null @@ -1,271 +0,0 @@ -// -// Panels -// -------------------------------------------------- - - -// Base class -.panel { - margin-bottom: @line-height-computed; - background-color: @panel-bg; - border: 1px solid transparent; - border-radius: @panel-border-radius; - .box-shadow(0 1px 1px rgba(0,0,0,.05)); -} - -// Panel contents -.panel-body { - padding: @panel-body-padding; - &:extend(.clearfix all); -} - -// Optional heading -.panel-heading { - padding: @panel-heading-padding; - border-bottom: 1px solid transparent; - .border-top-radius((@panel-border-radius - 1)); - - > .dropdown .dropdown-toggle { - color: inherit; - } -} - -// Within heading, strip any `h*` tag of its default margins for spacing. -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: ceil((@font-size-base * 1.125)); - color: inherit; - - > a, - > small, - > .small, - > small > a, - > .small > a { - color: inherit; - } -} - -// Optional footer (stays gray in every modifier class) -.panel-footer { - padding: @panel-footer-padding; - background-color: @panel-footer-bg; - border-top: 1px solid @panel-inner-border; - .border-bottom-radius((@panel-border-radius - 1)); -} - - -// List groups in panels -// -// By default, space out list group content from panel headings to account for -// any kind of custom content between the two. - -.panel { - > .list-group, - > .panel-collapse > .list-group { - margin-bottom: 0; - - .list-group-item { - border-width: 1px 0; - border-radius: 0; - } - - // Add border top radius for first one - &:first-child { - .list-group-item:first-child { - border-top: 0; - .border-top-radius((@panel-border-radius - 1)); - } - } - - // Add border bottom radius for last one - &:last-child { - .list-group-item:last-child { - border-bottom: 0; - .border-bottom-radius((@panel-border-radius - 1)); - } - } - } - > .panel-heading + .panel-collapse > .list-group { - .list-group-item:first-child { - .border-top-radius(0); - } - } -} -// Collapse space between when there's no additional content. -.panel-heading + .list-group { - .list-group-item:first-child { - border-top-width: 0; - } -} -.list-group + .panel-footer { - border-top-width: 0; -} - -// Tables in panels -// -// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and -// watch it go full width. - -.panel { - > .table, - > .table-responsive > .table, - > .panel-collapse > .table { - margin-bottom: 0; - - caption { - padding-left: @panel-body-padding; - padding-right: @panel-body-padding; - } - } - // Add border top radius for first one - > .table:first-child, - > .table-responsive:first-child > .table:first-child { - .border-top-radius((@panel-border-radius - 1)); - - > thead:first-child, - > tbody:first-child { - > tr:first-child { - border-top-left-radius: (@panel-border-radius - 1); - border-top-right-radius: (@panel-border-radius - 1); - - td:first-child, - th:first-child { - border-top-left-radius: (@panel-border-radius - 1); - } - td:last-child, - th:last-child { - border-top-right-radius: (@panel-border-radius - 1); - } - } - } - } - // Add border bottom radius for last one - > .table:last-child, - > .table-responsive:last-child > .table:last-child { - .border-bottom-radius((@panel-border-radius - 1)); - - > tbody:last-child, - > tfoot:last-child { - > tr:last-child { - border-bottom-left-radius: (@panel-border-radius - 1); - border-bottom-right-radius: (@panel-border-radius - 1); - - td:first-child, - th:first-child { - border-bottom-left-radius: (@panel-border-radius - 1); - } - td:last-child, - th:last-child { - border-bottom-right-radius: (@panel-border-radius - 1); - } - } - } - } - > .panel-body + .table, - > .panel-body + .table-responsive, - > .table + .panel-body, - > .table-responsive + .panel-body { - border-top: 1px solid @table-border-color; - } - > .table > tbody:first-child > tr:first-child th, - > .table > tbody:first-child > tr:first-child td { - border-top: 0; - } - > .table-bordered, - > .table-responsive > .table-bordered { - border: 0; - > thead, - > tbody, - > tfoot { - > tr { - > th:first-child, - > td:first-child { - border-left: 0; - } - > th:last-child, - > td:last-child { - border-right: 0; - } - } - } - > thead, - > tbody { - > tr:first-child { - > td, - > th { - border-bottom: 0; - } - } - } - > tbody, - > tfoot { - > tr:last-child { - > td, - > th { - border-bottom: 0; - } - } - } - } - > .table-responsive { - border: 0; - margin-bottom: 0; - } -} - - -// Collapsable panels (aka, accordion) -// -// Wrap a series of panels in `.panel-group` to turn them into an accordion with -// the help of our collapse JavaScript plugin. - -.panel-group { - margin-bottom: @line-height-computed; - - // Tighten up margin so it's only between panels - .panel { - margin-bottom: 0; - border-radius: @panel-border-radius; - - + .panel { - margin-top: 5px; - } - } - - .panel-heading { - border-bottom: 0; - - + .panel-collapse > .panel-body, - + .panel-collapse > .list-group { - border-top: 1px solid @panel-inner-border; - } - } - - .panel-footer { - border-top: 0; - + .panel-collapse .panel-body { - border-bottom: 1px solid @panel-inner-border; - } - } -} - - -// Contextual variations -.panel-default { - .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border); -} -.panel-primary { - .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border); -} -.panel-success { - .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border); -} -.panel-info { - .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border); -} -.panel-warning { - .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border); -} -.panel-danger { - .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border); -} diff --git a/src/UI/Content/Bootstrap/popovers.less b/src/UI/Content/Bootstrap/popovers.less deleted file mode 100644 index 3a62a6455..000000000 --- a/src/UI/Content/Bootstrap/popovers.less +++ /dev/null @@ -1,131 +0,0 @@ -// -// Popovers -// -------------------------------------------------- - - -.popover { - position: absolute; - top: 0; - left: 0; - z-index: @zindex-popover; - display: none; - max-width: @popover-max-width; - padding: 1px; - // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - .reset-text(); - font-size: @font-size-base; - - background-color: @popover-bg; - background-clip: padding-box; - border: 1px solid @popover-fallback-border-color; - border: 1px solid @popover-border-color; - border-radius: @border-radius-large; - .box-shadow(0 5px 10px rgba(0,0,0,.2)); - - // Offset the popover to account for the popover arrow - &.top { margin-top: -@popover-arrow-width; } - &.right { margin-left: @popover-arrow-width; } - &.bottom { margin-top: @popover-arrow-width; } - &.left { margin-left: -@popover-arrow-width; } -} - -.popover-title { - margin: 0; // reset heading margin - padding: 8px 14px; - font-size: @font-size-base; - background-color: @popover-title-bg; - border-bottom: 1px solid darken(@popover-title-bg, 5%); - border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0; -} - -.popover-content { - padding: 9px 14px; -} - -// Arrows -// -// .arrow is outer, .arrow:after is inner - -.popover > .arrow { - &, - &:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - } -} -.popover > .arrow { - border-width: @popover-arrow-outer-width; -} -.popover > .arrow:after { - border-width: @popover-arrow-width; - content: ""; -} - -.popover { - &.top > .arrow { - left: 50%; - margin-left: -@popover-arrow-outer-width; - border-bottom-width: 0; - border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-top-color: @popover-arrow-outer-color; - bottom: -@popover-arrow-outer-width; - &:after { - content: " "; - bottom: 1px; - margin-left: -@popover-arrow-width; - border-bottom-width: 0; - border-top-color: @popover-arrow-color; - } - } - &.right > .arrow { - top: 50%; - left: -@popover-arrow-outer-width; - margin-top: -@popover-arrow-outer-width; - border-left-width: 0; - border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-right-color: @popover-arrow-outer-color; - &:after { - content: " "; - left: 1px; - bottom: -@popover-arrow-width; - border-left-width: 0; - border-right-color: @popover-arrow-color; - } - } - &.bottom > .arrow { - left: 50%; - margin-left: -@popover-arrow-outer-width; - border-top-width: 0; - border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-bottom-color: @popover-arrow-outer-color; - top: -@popover-arrow-outer-width; - &:after { - content: " "; - top: 1px; - margin-left: -@popover-arrow-width; - border-top-width: 0; - border-bottom-color: @popover-arrow-color; - } - } - - &.left > .arrow { - top: 50%; - right: -@popover-arrow-outer-width; - margin-top: -@popover-arrow-outer-width; - border-right-width: 0; - border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback - border-left-color: @popover-arrow-outer-color; - &:after { - content: " "; - right: 1px; - border-right-width: 0; - border-left-color: @popover-arrow-color; - bottom: -@popover-arrow-width; - } - } -} diff --git a/src/UI/Content/Bootstrap/print.less b/src/UI/Content/Bootstrap/print.less deleted file mode 100644 index 66e54ab48..000000000 --- a/src/UI/Content/Bootstrap/print.less +++ /dev/null @@ -1,101 +0,0 @@ -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ - -// ========================================================================== -// Print styles. -// Inlined to avoid the additional HTTP request: h5bp.com/r -// ========================================================================== - -@media print { - *, - *:before, - *:after { - background: transparent !important; - color: #000 !important; // Black prints faster: h5bp.com/s - box-shadow: none !important; - text-shadow: none !important; - } - - a, - a:visited { - text-decoration: underline; - } - - a[href]:after { - content: " (" attr(href) ")"; - } - - abbr[title]:after { - content: " (" attr(title) ")"; - } - - // Don't show links that are fragment identifiers, - // or use the `javascript:` pseudo protocol - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - - thead { - display: table-header-group; // h5bp.com/t - } - - tr, - img { - page-break-inside: avoid; - } - - img { - max-width: 100% !important; - } - - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - - h2, - h3 { - page-break-after: avoid; - } - - // Bootstrap specific changes start - - // Bootstrap components - .navbar { - display: none; - } - .btn, - .dropup > .btn { - > .caret { - border-top-color: #000 !important; - } - } - .label { - border: 1px solid #000; - } - - .table { - border-collapse: collapse !important; - - td, - th { - background-color: #fff !important; - } - } - .table-bordered { - th, - td { - border: 1px solid #ddd !important; - } - } - - // Bootstrap specific changes end -} diff --git a/src/UI/Content/Bootstrap/progress-bars.less b/src/UI/Content/Bootstrap/progress-bars.less deleted file mode 100644 index 8868a1fee..000000000 --- a/src/UI/Content/Bootstrap/progress-bars.less +++ /dev/null @@ -1,87 +0,0 @@ -// -// Progress bars -// -------------------------------------------------- - - -// Bar animations -// ------------------------- - -// WebKit -@-webkit-keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - -// Spec and IE10+ -@keyframes progress-bar-stripes { - from { background-position: 40px 0; } - to { background-position: 0 0; } -} - - -// Bar itself -// ------------------------- - -// Outer container -.progress { - overflow: hidden; - height: @line-height-computed; - margin-bottom: @line-height-computed; - background-color: @progress-bg; - border-radius: @progress-border-radius; - .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); -} - -// Bar of progress -.progress-bar { - float: left; - width: 0%; - height: 100%; - font-size: @font-size-small; - line-height: @line-height-computed; - color: @progress-bar-color; - text-align: center; - background-color: @progress-bar-bg; - .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); - .transition(width .6s ease); -} - -// Striped bars -// -// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the -// `.progress-bar-striped` class, which you just add to an existing -// `.progress-bar`. -.progress-striped .progress-bar, -.progress-bar-striped { - #gradient > .striped(); - background-size: 40px 40px; -} - -// Call animation for the active one -// -// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the -// `.progress-bar.active` approach. -.progress.active .progress-bar, -.progress-bar.active { - .animation(progress-bar-stripes 2s linear infinite); -} - - -// Variations -// ------------------------- - -.progress-bar-success { - .progress-bar-variant(@progress-bar-success-bg); -} - -.progress-bar-info { - .progress-bar-variant(@progress-bar-info-bg); -} - -.progress-bar-warning { - .progress-bar-variant(@progress-bar-warning-bg); -} - -.progress-bar-danger { - .progress-bar-variant(@progress-bar-danger-bg); -} diff --git a/src/UI/Content/Bootstrap/responsive-embed.less b/src/UI/Content/Bootstrap/responsive-embed.less deleted file mode 100644 index 080a5118f..000000000 --- a/src/UI/Content/Bootstrap/responsive-embed.less +++ /dev/null @@ -1,35 +0,0 @@ -// Embeds responsive -// -// Credit: Nicolas Gallagher and SUIT CSS. - -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; - - .embed-responsive-item, - iframe, - embed, - object, - video { - position: absolute; - top: 0; - left: 0; - bottom: 0; - height: 100%; - width: 100%; - border: 0; - } -} - -// Modifier class for 16:9 aspect ratio -.embed-responsive-16by9 { - padding-bottom: 56.25%; -} - -// Modifier class for 4:3 aspect ratio -.embed-responsive-4by3 { - padding-bottom: 75%; -} diff --git a/src/UI/Content/Bootstrap/responsive-utilities.less b/src/UI/Content/Bootstrap/responsive-utilities.less deleted file mode 100644 index b1db31d7b..000000000 --- a/src/UI/Content/Bootstrap/responsive-utilities.less +++ /dev/null @@ -1,194 +0,0 @@ -// -// Responsive: Utility classes -// -------------------------------------------------- - - -// IE10 in Windows (Phone) 8 -// -// Support for responsive views via media queries is kind of borked in IE10, for -// Surface/desktop in split view and for Windows Phone 8. This particular fix -// must be accompanied by a snippet of JavaScript to sniff the user agent and -// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at -// our Getting Started page for more information on this bug. -// -// For more information, see the following: -// -// Issue: https://github.com/twbs/bootstrap/issues/10497 -// Docs: http://getbootstrap.com/getting-started/#support-ie10-width -// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/ -// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/ - -@-ms-viewport { - width: device-width; -} - - -// Visibility utilities -// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0 -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - .responsive-invisibility(); -} - -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} - -.visible-xs { - @media (max-width: @screen-xs-max) { - .responsive-visibility(); - } -} -.visible-xs-block { - @media (max-width: @screen-xs-max) { - display: block !important; - } -} -.visible-xs-inline { - @media (max-width: @screen-xs-max) { - display: inline !important; - } -} -.visible-xs-inline-block { - @media (max-width: @screen-xs-max) { - display: inline-block !important; - } -} - -.visible-sm { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - .responsive-visibility(); - } -} -.visible-sm-block { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: block !important; - } -} -.visible-sm-inline { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: inline !important; - } -} -.visible-sm-inline-block { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - display: inline-block !important; - } -} - -.visible-md { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - .responsive-visibility(); - } -} -.visible-md-block { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: block !important; - } -} -.visible-md-inline { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: inline !important; - } -} -.visible-md-inline-block { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - display: inline-block !important; - } -} - -.visible-lg { - @media (min-width: @screen-lg-min) { - .responsive-visibility(); - } -} -.visible-lg-block { - @media (min-width: @screen-lg-min) { - display: block !important; - } -} -.visible-lg-inline { - @media (min-width: @screen-lg-min) { - display: inline !important; - } -} -.visible-lg-inline-block { - @media (min-width: @screen-lg-min) { - display: inline-block !important; - } -} - -.hidden-xs { - @media (max-width: @screen-xs-max) { - .responsive-invisibility(); - } -} -.hidden-sm { - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - .responsive-invisibility(); - } -} -.hidden-md { - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - .responsive-invisibility(); - } -} -.hidden-lg { - @media (min-width: @screen-lg-min) { - .responsive-invisibility(); - } -} - - -// Print utilities -// -// Media queries are placed on the inside to be mixin-friendly. - -// Note: Deprecated .visible-print as of v3.2.0 -.visible-print { - .responsive-invisibility(); - - @media print { - .responsive-visibility(); - } -} -.visible-print-block { - display: none !important; - - @media print { - display: block !important; - } -} -.visible-print-inline { - display: none !important; - - @media print { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; - - @media print { - display: inline-block !important; - } -} - -.hidden-print { - @media print { - .responsive-invisibility(); - } -} diff --git a/src/UI/Content/Bootstrap/scaffolding.less b/src/UI/Content/Bootstrap/scaffolding.less deleted file mode 100644 index 1929bfc5c..000000000 --- a/src/UI/Content/Bootstrap/scaffolding.less +++ /dev/null @@ -1,161 +0,0 @@ -// -// Scaffolding -// -------------------------------------------------- - - -// Reset the box-sizing -// -// Heads up! This reset may cause conflicts with some third-party widgets. -// For recommendations on resolving such conflicts, see -// http://getbootstrap.com/getting-started/#third-box-sizing -* { - .box-sizing(border-box); -} -*:before, -*:after { - .box-sizing(border-box); -} - - -// Body reset - -html { - font-size: 10px; - -webkit-tap-highlight-color: rgba(0,0,0,0); -} - -body { - font-family: @font-family-base; - font-size: @font-size-base; - line-height: @line-height-base; - color: @text-color; - background-color: @body-bg; -} - -// Reset fonts for relevant elements -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} - - -// Links - -a { - color: @link-color; - text-decoration: none; - - &:hover, - &:focus { - color: @link-hover-color; - text-decoration: @link-hover-decoration; - } - - &:focus { - .tab-focus(); - } -} - - -// Figures -// -// We reset this here because previously Normalize had no `figure` margins. This -// ensures we don't break anyone's use of the element. - -figure { - margin: 0; -} - - -// Images - -img { - vertical-align: middle; -} - -// Responsive images (ensure images don't scale beyond their parents) -.img-responsive { - .img-responsive(); -} - -// Rounded corners -.img-rounded { - border-radius: @border-radius-large; -} - -// Image thumbnails -// -// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`. -.img-thumbnail { - padding: @thumbnail-padding; - line-height: @line-height-base; - background-color: @thumbnail-bg; - border: 1px solid @thumbnail-border; - border-radius: @thumbnail-border-radius; - .transition(all .2s ease-in-out); - - // Keep them at most 100% wide - .img-responsive(inline-block); -} - -// Perfect circle -.img-circle { - border-radius: 50%; // set radius in percents -} - - -// Horizontal rules - -hr { - margin-top: @line-height-computed; - margin-bottom: @line-height-computed; - border: 0; - border-top: 1px solid @hr-border; -} - - -// Only display content to screen readers -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// Credit: HTML5 Boilerplate - -.sr-only-focusable { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} - - -// iOS "clickable elements" fix for role="button" -// -// Fixes "clickability" issue (and more generally, the firing of events such as focus as well) -// for traditionally non-focusable elements with role="button" -// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile - -[role="button"] { - cursor: pointer; -} diff --git a/src/UI/Content/Bootstrap/tables.less b/src/UI/Content/Bootstrap/tables.less deleted file mode 100644 index 2242c0368..000000000 --- a/src/UI/Content/Bootstrap/tables.less +++ /dev/null @@ -1,234 +0,0 @@ -// -// Tables -// -------------------------------------------------- - - -table { - background-color: @table-bg; -} -caption { - padding-top: @table-cell-padding; - padding-bottom: @table-cell-padding; - color: @text-muted; - text-align: left; -} -th { - text-align: left; -} - - -// Baseline styles - -.table { - width: 100%; - max-width: 100%; - margin-bottom: @line-height-computed; - // Cells - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - padding: @table-cell-padding; - line-height: @line-height-base; - vertical-align: top; - border-top: 1px solid @table-border-color; - } - } - } - // Bottom align for column headings - > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid @table-border-color; - } - // Remove top border from thead by default - > caption + thead, - > colgroup + thead, - > thead:first-child { - > tr:first-child { - > th, - > td { - border-top: 0; - } - } - } - // Account for multiple tbody instances - > tbody + tbody { - border-top: 2px solid @table-border-color; - } - - // Nesting - .table { - background-color: @body-bg; - } -} - - -// Condensed table w/ half padding - -.table-condensed { - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - padding: @table-condensed-cell-padding; - } - } - } -} - - -// Bordered version -// -// Add borders all around the table and between all the columns. - -.table-bordered { - border: 1px solid @table-border-color; - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - border: 1px solid @table-border-color; - } - } - } - > thead > tr { - > th, - > td { - border-bottom-width: 2px; - } - } -} - - -// Zebra-striping -// -// Default zebra-stripe styles (alternating gray and transparent backgrounds) - -.table-striped { - > tbody > tr:nth-of-type(odd) { - background-color: @table-bg-accent; - } -} - - -// Hover effect -// -// Placed here since it has to come after the potential zebra striping - -.table-hover { - > tbody > tr:hover { - background-color: @table-bg-hover; - } -} - - -// Table cell sizing -// -// Reset default table behavior - -table col[class*="col-"] { - position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) - float: none; - display: table-column; -} -table { - td, - th { - &[class*="col-"] { - position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623) - float: none; - display: table-cell; - } - } -} - - -// Table backgrounds -// -// Exact selectors below required to override `.table-striped` and prevent -// inheritance to nested tables. - -// Generate the contextual variants -.table-row-variant(active; @table-bg-active); -.table-row-variant(success; @state-success-bg); -.table-row-variant(info; @state-info-bg); -.table-row-variant(warning; @state-warning-bg); -.table-row-variant(danger; @state-danger-bg); - - -// Responsive tables -// -// Wrap your tables in `.table-responsive` and we'll make them mobile friendly -// by enabling horizontal scrolling. Only applies <768px. Everything above that -// will display normally. - -.table-responsive { - overflow-x: auto; - min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837) - - @media screen and (max-width: @screen-xs-max) { - width: 100%; - margin-bottom: (@line-height-computed * 0.75); - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid @table-border-color; - - // Tighten up spacing - > .table { - margin-bottom: 0; - - // Ensure the content doesn't wrap - > thead, - > tbody, - > tfoot { - > tr { - > th, - > td { - white-space: nowrap; - } - } - } - } - - // Special overrides for the bordered tables - > .table-bordered { - border: 0; - - // Nuke the appropriate borders so that the parent can handle them - > thead, - > tbody, - > tfoot { - > tr { - > th:first-child, - > td:first-child { - border-left: 0; - } - > th:last-child, - > td:last-child { - border-right: 0; - } - } - } - - // Only nuke the last row's bottom-border in `tbody` and `tfoot` since - // chances are there will be only one `tr` in a `thead` and that would - // remove the border altogether. - > tbody, - > tfoot { - > tr:last-child { - > th, - > td { - border-bottom: 0; - } - } - } - - } - } -} diff --git a/src/UI/Content/Bootstrap/theme.less b/src/UI/Content/Bootstrap/theme.less deleted file mode 100644 index 8371872b0..000000000 --- a/src/UI/Content/Bootstrap/theme.less +++ /dev/null @@ -1,291 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -// -// Load core variables and mixins -// -------------------------------------------------- - -@import "variables.less"; -@import "mixins.less"; - - -// -// Buttons -// -------------------------------------------------- - -// Common styles -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - text-shadow: 0 -1px 0 rgba(0,0,0,.2); - @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075); - .box-shadow(@shadow); - - // Reset the shadow - &:active, - &.active { - .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - .box-shadow(none); - } - - .badge { - text-shadow: none; - } -} - -// Mixin for generating new styles -.btn-styles(@btn-color: #555) { - #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%)); - .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620 - background-repeat: repeat-x; - border-color: darken(@btn-color, 14%); - - &:hover, - &:focus { - background-color: darken(@btn-color, 12%); - background-position: 0 -15px; - } - - &:active, - &.active { - background-color: darken(@btn-color, 12%); - border-color: darken(@btn-color, 14%); - } - - &.disabled, - &[disabled], - fieldset[disabled] & { - &, - &:hover, - &:focus, - &.focus, - &:active, - &.active { - background-color: darken(@btn-color, 12%); - background-image: none; - } - } -} - -// Common styles -.btn { - // Remove the gradient for the pressed/active state - &:active, - &.active { - background-image: none; - } -} - -// Apply the mixin to the buttons -.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; } -.btn-primary { .btn-styles(@btn-primary-bg); } -.btn-success { .btn-styles(@btn-success-bg); } -.btn-info { .btn-styles(@btn-info-bg); } -.btn-warning { .btn-styles(@btn-warning-bg); } -.btn-danger { .btn-styles(@btn-danger-bg); } - - -// -// Images -// -------------------------------------------------- - -.thumbnail, -.img-thumbnail { - .box-shadow(0 1px 2px rgba(0,0,0,.075)); -} - - -// -// Dropdowns -// -------------------------------------------------- - -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%)); - background-color: darken(@dropdown-link-hover-bg, 5%); -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); - background-color: darken(@dropdown-link-active-bg, 5%); -} - - -// -// Navbar -// -------------------------------------------------- - -// Default navbar -.navbar-default { - #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg); - .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered - border-radius: @navbar-border-radius; - @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075); - .box-shadow(@shadow); - - .navbar-nav > .open > a, - .navbar-nav > .active > a { - #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%)); - .box-shadow(inset 0 3px 9px rgba(0,0,0,.075)); - } -} -.navbar-brand, -.navbar-nav > li > a { - text-shadow: 0 1px 0 rgba(255,255,255,.25); -} - -// Inverted navbar -.navbar-inverse { - #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg); - .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257 - border-radius: @navbar-border-radius; - .navbar-nav > .open > a, - .navbar-nav > .active > a { - #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%)); - .box-shadow(inset 0 3px 9px rgba(0,0,0,.25)); - } - - .navbar-brand, - .navbar-nav > li > a { - text-shadow: 0 -1px 0 rgba(0,0,0,.25); - } -} - -// Undo rounded corners in static and fixed navbars -.navbar-static-top, -.navbar-fixed-top, -.navbar-fixed-bottom { - border-radius: 0; -} - -// Fix active state of dropdown items in collapsed mode -@media (max-width: @grid-float-breakpoint-max) { - .navbar .navbar-nav .open .dropdown-menu > .active > a { - &, - &:hover, - &:focus { - color: #fff; - #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%)); - } - } -} - - -// -// Alerts -// -------------------------------------------------- - -// Common styles -.alert { - text-shadow: 0 1px 0 rgba(255,255,255,.2); - @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05); - .box-shadow(@shadow); -} - -// Mixin for generating new styles -.alert-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%)); - border-color: darken(@color, 15%); -} - -// Apply the mixin to the alerts -.alert-success { .alert-styles(@alert-success-bg); } -.alert-info { .alert-styles(@alert-info-bg); } -.alert-warning { .alert-styles(@alert-warning-bg); } -.alert-danger { .alert-styles(@alert-danger-bg); } - - -// -// Progress bars -// -------------------------------------------------- - -// Give the progress background some depth -.progress { - #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg) -} - -// Mixin for generating new styles -.progress-bar-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%)); -} - -// Apply the mixin to the progress bars -.progress-bar { .progress-bar-styles(@progress-bar-bg); } -.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); } -.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); } -.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); } -.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); } - -// Reset the striped class because our mixins don't do multiple gradients and -// the above custom styles override the new `.progress-bar-striped` in v3.2.0. -.progress-bar-striped { - #gradient > .striped(); -} - - -// -// List groups -// -------------------------------------------------- - -.list-group { - border-radius: @border-radius-base; - .box-shadow(0 1px 2px rgba(0,0,0,.075)); -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%); - #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%)); - border-color: darken(@list-group-active-border, 7.5%); - - .badge { - text-shadow: none; - } -} - - -// -// Panels -// -------------------------------------------------- - -// Common styles -.panel { - .box-shadow(0 1px 2px rgba(0,0,0,.05)); -} - -// Mixin for generating new styles -.panel-heading-styles(@color) { - #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%)); -} - -// Apply the mixin to the panel headings only -.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); } -.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); } -.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); } -.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); } -.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); } -.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); } - - -// -// Wells -// -------------------------------------------------- - -.well { - #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg); - border-color: darken(@well-bg, 10%); - @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1); - .box-shadow(@shadow); -} diff --git a/src/UI/Content/Bootstrap/thumbnails.less b/src/UI/Content/Bootstrap/thumbnails.less deleted file mode 100644 index 0713e67d0..000000000 --- a/src/UI/Content/Bootstrap/thumbnails.less +++ /dev/null @@ -1,36 +0,0 @@ -// -// Thumbnails -// -------------------------------------------------- - - -// Mixin and adjust the regular image class -.thumbnail { - display: block; - padding: @thumbnail-padding; - margin-bottom: @line-height-computed; - line-height: @line-height-base; - background-color: @thumbnail-bg; - border: 1px solid @thumbnail-border; - border-radius: @thumbnail-border-radius; - .transition(border .2s ease-in-out); - - > img, - a > img { - &:extend(.img-responsive); - margin-left: auto; - margin-right: auto; - } - - // Add a hover state for linked versions only - a&:hover, - a&:focus, - a&.active { - border-color: @link-color; - } - - // Image captions - .caption { - padding: @thumbnail-caption-padding; - color: @thumbnail-caption-color; - } -} diff --git a/src/UI/Content/Bootstrap/tooltip.less b/src/UI/Content/Bootstrap/tooltip.less deleted file mode 100644 index b48d63e07..000000000 --- a/src/UI/Content/Bootstrap/tooltip.less +++ /dev/null @@ -1,101 +0,0 @@ -// -// Tooltips -// -------------------------------------------------- - - -// Base class -.tooltip { - position: absolute; - z-index: @zindex-tooltip; - display: block; - // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element. - // So reset our font and text properties to avoid inheriting weird values. - .reset-text(); - font-size: @font-size-small; - - .opacity(0); - - &.in { .opacity(@tooltip-opacity); } - &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; } - &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; } - &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; } - &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; } -} - -// Wrapper for the tooltip content -.tooltip-inner { - max-width: @tooltip-max-width; - padding: 3px 8px; - color: @tooltip-color; - text-align: center; - background-color: @tooltip-bg; - border-radius: @border-radius-base; -} - -// Arrows -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1 -.tooltip { - &.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.top-left .tooltip-arrow { - bottom: 0; - right: @tooltip-arrow-width; - margin-bottom: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.top-right .tooltip-arrow { - bottom: 0; - left: @tooltip-arrow-width; - margin-bottom: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width 0; - border-top-color: @tooltip-arrow-color; - } - &.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0; - border-right-color: @tooltip-arrow-color; - } - &.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -@tooltip-arrow-width; - border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width; - border-left-color: @tooltip-arrow-color; - } - &.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } - &.bottom-left .tooltip-arrow { - top: 0; - right: @tooltip-arrow-width; - margin-top: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } - &.bottom-right .tooltip-arrow { - top: 0; - left: @tooltip-arrow-width; - margin-top: -@tooltip-arrow-width; - border-width: 0 @tooltip-arrow-width @tooltip-arrow-width; - border-bottom-color: @tooltip-arrow-color; - } -} diff --git a/src/UI/Content/Bootstrap/type.less b/src/UI/Content/Bootstrap/type.less deleted file mode 100644 index 68ba6017b..000000000 --- a/src/UI/Content/Bootstrap/type.less +++ /dev/null @@ -1,302 +0,0 @@ -// -// Typography -// -------------------------------------------------- - - -// Headings -// ------------------------- - -h1, h2, h3, h4, h5, h6, -.h1, .h2, .h3, .h4, .h5, .h6 { - font-family: @headings-font-family; - font-weight: @headings-font-weight; - line-height: @headings-line-height; - color: @headings-color; - - small, - .small { - font-weight: normal; - line-height: 1; - color: @headings-small-color; - } -} - -h1, .h1, -h2, .h2, -h3, .h3 { - margin-top: @line-height-computed; - margin-bottom: (@line-height-computed / 2); - - small, - .small { - font-size: 65%; - } -} -h4, .h4, -h5, .h5, -h6, .h6 { - margin-top: (@line-height-computed / 2); - margin-bottom: (@line-height-computed / 2); - - small, - .small { - font-size: 75%; - } -} - -h1, .h1 { font-size: @font-size-h1; } -h2, .h2 { font-size: @font-size-h2; } -h3, .h3 { font-size: @font-size-h3; } -h4, .h4 { font-size: @font-size-h4; } -h5, .h5 { font-size: @font-size-h5; } -h6, .h6 { font-size: @font-size-h6; } - - -// Body text -// ------------------------- - -p { - margin: 0 0 (@line-height-computed / 2); -} - -.lead { - margin-bottom: @line-height-computed; - font-size: floor((@font-size-base * 1.15)); - font-weight: 300; - line-height: 1.4; - - @media (min-width: @screen-sm-min) { - font-size: (@font-size-base * 1.5); - } -} - - -// Emphasis & misc -// ------------------------- - -// Ex: (12px small font / 14px base font) * 100% = about 85% -small, -.small { - font-size: floor((100% * @font-size-small / @font-size-base)); -} - -mark, -.mark { - background-color: @state-warning-bg; - padding: .2em; -} - -// Alignment -.text-left { text-align: left; } -.text-right { text-align: right; } -.text-center { text-align: center; } -.text-justify { text-align: justify; } -.text-nowrap { white-space: nowrap; } - -// Transformation -.text-lowercase { text-transform: lowercase; } -.text-uppercase { text-transform: uppercase; } -.text-capitalize { text-transform: capitalize; } - -// Contextual colors -.text-muted { - color: @text-muted; -} -.text-primary { - .text-emphasis-variant(@brand-primary); -} -.text-success { - .text-emphasis-variant(@state-success-text); -} -.text-info { - .text-emphasis-variant(@state-info-text); -} -.text-warning { - .text-emphasis-variant(@state-warning-text); -} -.text-danger { - .text-emphasis-variant(@state-danger-text); -} - -// Contextual backgrounds -// For now we'll leave these alongside the text classes until v4 when we can -// safely shift things around (per SemVer rules). -.bg-primary { - // Given the contrast here, this is the only class to have its color inverted - // automatically. - color: #fff; - .bg-variant(@brand-primary); -} -.bg-success { - .bg-variant(@state-success-bg); -} -.bg-info { - .bg-variant(@state-info-bg); -} -.bg-warning { - .bg-variant(@state-warning-bg); -} -.bg-danger { - .bg-variant(@state-danger-bg); -} - - -// Page header -// ------------------------- - -.page-header { - padding-bottom: ((@line-height-computed / 2) - 1); - margin: (@line-height-computed * 2) 0 @line-height-computed; - border-bottom: 1px solid @page-header-border-color; -} - - -// Lists -// ------------------------- - -// Unordered and Ordered lists -ul, -ol { - margin-top: 0; - margin-bottom: (@line-height-computed / 2); - ul, - ol { - margin-bottom: 0; - } -} - -// List options - -// Unstyled keeps list items block level, just removes default browser padding and list-style -.list-unstyled { - padding-left: 0; - list-style: none; -} - -// Inline turns list items into inline-block -.list-inline { - .list-unstyled(); - margin-left: -5px; - - > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; - } -} - -// Description Lists -dl { - margin-top: 0; // Remove browser default - margin-bottom: @line-height-computed; -} -dt, -dd { - line-height: @line-height-base; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; // Undo browser default -} - -// Horizontal description lists -// -// Defaults to being stacked without any of the below styles applied, until the -// grid breakpoint is reached (default of ~768px). - -.dl-horizontal { - dd { - &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present - } - - @media (min-width: @grid-float-breakpoint) { - dt { - float: left; - width: (@dl-horizontal-offset - 20); - clear: left; - text-align: right; - .text-overflow(); - } - dd { - margin-left: @dl-horizontal-offset; - } - } -} - - -// Misc -// ------------------------- - -// Abbreviations and acronyms -abbr[title], -// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257 -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted @abbr-border-color; -} -.initialism { - font-size: 90%; - .text-uppercase(); -} - -// Blockquotes -blockquote { - padding: (@line-height-computed / 2) @line-height-computed; - margin: 0 0 @line-height-computed; - font-size: @blockquote-font-size; - border-left: 5px solid @blockquote-border-color; - - p, - ul, - ol { - &:last-child { - margin-bottom: 0; - } - } - - // Note: Deprecated small and .small as of v3.1.0 - // Context: https://github.com/twbs/bootstrap/issues/11660 - footer, - small, - .small { - display: block; - font-size: 80%; // back to default font-size - line-height: @line-height-base; - color: @blockquote-small-color; - - &:before { - content: '\2014 \00A0'; // em dash, nbsp - } - } -} - -// Opposite alignment of blockquote -// -// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0. -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - border-right: 5px solid @blockquote-border-color; - border-left: 0; - text-align: right; - - // Account for citation - footer, - small, - .small { - &:before { content: ''; } - &:after { - content: '\00A0 \2014'; // nbsp, em dash - } - } -} - -// Addresses -address { - margin-bottom: @line-height-computed; - font-style: normal; - line-height: @line-height-base; -} diff --git a/src/UI/Content/Bootstrap/utilities.less b/src/UI/Content/Bootstrap/utilities.less deleted file mode 100644 index 7a8ca27a8..000000000 --- a/src/UI/Content/Bootstrap/utilities.less +++ /dev/null @@ -1,55 +0,0 @@ -// -// Utility classes -// -------------------------------------------------- - - -// Floats -// ------------------------- - -.clearfix { - .clearfix(); -} -.center-block { - .center-block(); -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} - - -// Toggling content -// ------------------------- - -// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - .text-hide(); -} - - -// Hide from screenreaders and browsers -// -// Credit: HTML5 Boilerplate - -.hidden { - display: none !important; -} - - -// For Affix plugin -// ------------------------- - -.affix { - position: fixed; -} diff --git a/src/UI/Content/Bootstrap/variables.less b/src/UI/Content/Bootstrap/variables.less deleted file mode 100644 index c1861a8e0..000000000 --- a/src/UI/Content/Bootstrap/variables.less +++ /dev/null @@ -1,867 +0,0 @@ -// -// Variables -// -------------------------------------------------- - - -//== Colors -// -//## Gray and brand colors for use across Bootstrap. - -@gray-base: #000; -@gray-darker: lighten(@gray-base, 13.5%); // #222 -@gray-dark: lighten(@gray-base, 20%); // #333 -@gray: lighten(@gray-base, 33.5%); // #555 -@gray-light: lighten(@gray-base, 46.7%); // #777 -@gray-lighter: lighten(@gray-base, 93.5%); // #eee - -@brand-primary: darken(#428bca, 6.5%); // #337ab7 -@brand-success: #5cb85c; -@brand-info: #5bc0de; -@brand-warning: #f0ad4e; -@brand-danger: #d9534f; - - -//== Scaffolding -// -//## Settings for some of the most global styles. - -//** Background color for `<body>`. -@body-bg: #fff; -//** Global text color on `<body>`. -@text-color: @gray-dark; - -//** Global textual link color. -@link-color: @brand-primary; -//** Link hover color set via `darken()` function. -@link-hover-color: darken(@link-color, 15%); -//** Link hover decoration. -@link-hover-decoration: underline; - - -//== Typography -// -//## Font, line-height, and color for body text, headings, and more. - -@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; -@font-family-serif: Georgia, "Times New Roman", Times, serif; -//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`. -@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; -@font-family-base: @font-family-sans-serif; - -@font-size-base: 14px; -@font-size-large: ceil((@font-size-base * 1.25)); // ~18px -@font-size-small: ceil((@font-size-base * 0.85)); // ~12px - -@font-size-h1: floor((@font-size-base * 2.6)); // ~36px -@font-size-h2: floor((@font-size-base * 2.15)); // ~30px -@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px -@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px -@font-size-h5: @font-size-base; -@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px - -//** Unit-less `line-height` for use in components like buttons. -@line-height-base: 1.428571429; // 20/14 -//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc. -@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px - -//** By default, this inherits from the `<body>`. -@headings-font-family: inherit; -@headings-font-weight: 500; -@headings-line-height: 1.1; -@headings-color: inherit; - - -//== Iconography -// -//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower. - -//** Load fonts from this directory. -@icon-font-path: "../fonts/"; -//** File name for all font files. -@icon-font-name: "glyphicons-halflings-regular"; -//** Element ID within SVG icon file. -@icon-font-svg-id: "glyphicons_halflingsregular"; - - -//== Components -// -//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start). - -@padding-base-vertical: 6px; -@padding-base-horizontal: 12px; - -@padding-large-vertical: 10px; -@padding-large-horizontal: 16px; - -@padding-small-vertical: 5px; -@padding-small-horizontal: 10px; - -@padding-xs-vertical: 1px; -@padding-xs-horizontal: 5px; - -@line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome -@line-height-small: 1.5; - -@border-radius-base: 4px; -@border-radius-large: 6px; -@border-radius-small: 3px; - -//** Global color for active items (e.g., navs or dropdowns). -@component-active-color: #fff; -//** Global background color for active items (e.g., navs or dropdowns). -@component-active-bg: @brand-primary; - -//** Width of the `border` for generating carets that indicator dropdowns. -@caret-width-base: 4px; -//** Carets increase slightly in size for larger components. -@caret-width-large: 5px; - - -//== Tables -// -//## Customizes the `.table` component with basic values, each used across all table variations. - -//** Padding for `<th>`s and `<td>`s. -@table-cell-padding: 8px; -//** Padding for cells in `.table-condensed`. -@table-condensed-cell-padding: 5px; - -//** Default background color used for all tables. -@table-bg: transparent; -//** Background color used for `.table-striped`. -@table-bg-accent: #f9f9f9; -//** Background color used for `.table-hover`. -@table-bg-hover: #f5f5f5; -@table-bg-active: @table-bg-hover; - -//** Border color for table and cell borders. -@table-border-color: #ddd; - - -//== Buttons -// -//## For each of Bootstrap's buttons, define text, background and border color. - -@btn-font-weight: normal; - -@btn-default-color: #333; -@btn-default-bg: #fff; -@btn-default-border: #ccc; - -@btn-primary-color: #fff; -@btn-primary-bg: @brand-primary; -@btn-primary-border: darken(@btn-primary-bg, 5%); - -@btn-success-color: #fff; -@btn-success-bg: @brand-success; -@btn-success-border: darken(@btn-success-bg, 5%); - -@btn-info-color: #fff; -@btn-info-bg: @brand-info; -@btn-info-border: darken(@btn-info-bg, 5%); - -@btn-warning-color: #fff; -@btn-warning-bg: @brand-warning; -@btn-warning-border: darken(@btn-warning-bg, 5%); - -@btn-danger-color: #fff; -@btn-danger-bg: @brand-danger; -@btn-danger-border: darken(@btn-danger-bg, 5%); - -@btn-link-disabled-color: @gray-light; - -// Allows for customizing button radius independently from global border radius -@btn-border-radius-base: @border-radius-base; -@btn-border-radius-large: @border-radius-large; -@btn-border-radius-small: @border-radius-small; - - -//== Forms -// -//## - -//** `<input>` background color -@input-bg: #fff; -//** `<input disabled>` background color -@input-bg-disabled: @gray-lighter; - -//** Text color for `<input>`s -@input-color: @gray; -//** `<input>` border color -@input-border: #ccc; - -// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4 -//** Default `.form-control` border radius -// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS. -@input-border-radius: @border-radius-base; -//** Large `.form-control` border radius -@input-border-radius-large: @border-radius-large; -//** Small `.form-control` border radius -@input-border-radius-small: @border-radius-small; - -//** Border color for inputs on focus -@input-border-focus: #66afe9; - -//** Placeholder text color -@input-color-placeholder: #999; - -//** Default `.form-control` height -@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); -//** Large `.form-control` height -@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); -//** Small `.form-control` height -@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); - -//** `.form-group` margin -@form-group-margin-bottom: 15px; - -@legend-color: @gray-dark; -@legend-border-color: #e5e5e5; - -//** Background color for textual input addons -@input-group-addon-bg: @gray-lighter; -//** Border color for textual input addons -@input-group-addon-border-color: @input-border; - -//** Disabled cursor for form controls and buttons. -@cursor-disabled: not-allowed; - - -//== Dropdowns -// -//## Dropdown menu container and contents. - -//** Background for the dropdown menu. -@dropdown-bg: #fff; -//** Dropdown menu `border-color`. -@dropdown-border: rgba(0,0,0,.15); -//** Dropdown menu `border-color` **for IE8**. -@dropdown-fallback-border: #ccc; -//** Divider color for between dropdown items. -@dropdown-divider-bg: #e5e5e5; - -//** Dropdown link text color. -@dropdown-link-color: @gray-dark; -//** Hover color for dropdown links. -@dropdown-link-hover-color: darken(@gray-dark, 5%); -//** Hover background for dropdown links. -@dropdown-link-hover-bg: #f5f5f5; - -//** Active dropdown menu item text color. -@dropdown-link-active-color: @component-active-color; -//** Active dropdown menu item background color. -@dropdown-link-active-bg: @component-active-bg; - -//** Disabled dropdown menu item background color. -@dropdown-link-disabled-color: @gray-light; - -//** Text color for headers within dropdown menus. -@dropdown-header-color: @gray-light; - -//** Deprecated `@dropdown-caret-color` as of v3.1.0 -@dropdown-caret-color: #000; - - -//-- Z-index master list -// -// Warning: Avoid customizing these values. They're used for a bird's eye view -// of components dependent on the z-axis and are designed to all work together. -// -// Note: These variables are not generated into the Customizer. - -@zindex-navbar: 1000; -@zindex-dropdown: 1000; -@zindex-popover: 1060; -@zindex-tooltip: 1070; -@zindex-navbar-fixed: 1030; -@zindex-modal-background: 1040; -@zindex-modal: 1050; - - -//== Media queries breakpoints -// -//## Define the breakpoints at which your layout will change, adapting to different screen sizes. - -// Extra small screen / phone -//** Deprecated `@screen-xs` as of v3.0.1 -@screen-xs: 480px; -//** Deprecated `@screen-xs-min` as of v3.2.0 -@screen-xs-min: @screen-xs; -//** Deprecated `@screen-phone` as of v3.0.1 -@screen-phone: @screen-xs-min; - -// Small screen / tablet -//** Deprecated `@screen-sm` as of v3.0.1 -@screen-sm: 768px; -@screen-sm-min: @screen-sm; -//** Deprecated `@screen-tablet` as of v3.0.1 -@screen-tablet: @screen-sm-min; - -// Medium screen / desktop -//** Deprecated `@screen-md` as of v3.0.1 -@screen-md: 992px; -@screen-md-min: @screen-md; -//** Deprecated `@screen-desktop` as of v3.0.1 -@screen-desktop: @screen-md-min; - -// Large screen / wide desktop -//** Deprecated `@screen-lg` as of v3.0.1 -@screen-lg: 1200px; -@screen-lg-min: @screen-lg; -//** Deprecated `@screen-lg-desktop` as of v3.0.1 -@screen-lg-desktop: @screen-lg-min; - -// So media queries don't overlap when required, provide a maximum -@screen-xs-max: (@screen-sm-min - 1); -@screen-sm-max: (@screen-md-min - 1); -@screen-md-max: (@screen-lg-min - 1); - - -//== Grid system -// -//## Define your custom responsive grid. - -//** Number of columns in the grid. -@grid-columns: 12; -//** Padding between columns. Gets divided in half for the left and right. -@grid-gutter-width: 30px; -// Navbar collapse -//** Point at which the navbar becomes uncollapsed. -@grid-float-breakpoint: @screen-sm-min; -//** Point at which the navbar begins collapsing. -@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); - - -//== Container sizes -// -//## Define the maximum width of `.container` for different screen sizes. - -// Small screen / tablet -@container-tablet: (720px + @grid-gutter-width); -//** For `@screen-sm-min` and up. -@container-sm: @container-tablet; - -// Medium screen / desktop -@container-desktop: (940px + @grid-gutter-width); -//** For `@screen-md-min` and up. -@container-md: @container-desktop; - -// Large screen / wide desktop -@container-large-desktop: (1140px + @grid-gutter-width); -//** For `@screen-lg-min` and up. -@container-lg: @container-large-desktop; - - -//== Navbar -// -//## - -// Basics of a navbar -@navbar-height: 50px; -@navbar-margin-bottom: @line-height-computed; -@navbar-border-radius: @border-radius-base; -@navbar-padding-horizontal: floor((@grid-gutter-width / 2)); -@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); -@navbar-collapse-max-height: 340px; - -@navbar-default-color: #777; -@navbar-default-bg: #f8f8f8; -@navbar-default-border: darken(@navbar-default-bg, 6.5%); - -// Navbar links -@navbar-default-link-color: #777; -@navbar-default-link-hover-color: #333; -@navbar-default-link-hover-bg: transparent; -@navbar-default-link-active-color: #555; -@navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%); -@navbar-default-link-disabled-color: #ccc; -@navbar-default-link-disabled-bg: transparent; - -// Navbar brand label -@navbar-default-brand-color: @navbar-default-link-color; -@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); -@navbar-default-brand-hover-bg: transparent; - -// Navbar toggle -@navbar-default-toggle-hover-bg: #ddd; -@navbar-default-toggle-icon-bar-bg: #888; -@navbar-default-toggle-border-color: #ddd; - - -//=== Inverted navbar -// Reset inverted navbar basics -@navbar-inverse-color: lighten(@gray-light, 15%); -@navbar-inverse-bg: #222; -@navbar-inverse-border: darken(@navbar-inverse-bg, 10%); - -// Inverted navbar links -@navbar-inverse-link-color: lighten(@gray-light, 15%); -@navbar-inverse-link-hover-color: #fff; -@navbar-inverse-link-hover-bg: transparent; -@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color; -@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%); -@navbar-inverse-link-disabled-color: #444; -@navbar-inverse-link-disabled-bg: transparent; - -// Inverted navbar brand label -@navbar-inverse-brand-color: @navbar-inverse-link-color; -@navbar-inverse-brand-hover-color: #fff; -@navbar-inverse-brand-hover-bg: transparent; - -// Inverted navbar toggle -@navbar-inverse-toggle-hover-bg: #333; -@navbar-inverse-toggle-icon-bar-bg: #fff; -@navbar-inverse-toggle-border-color: #333; - - -//== Navs -// -//## - -//=== Shared nav styles -@nav-link-padding: 10px 15px; -@nav-link-hover-bg: @gray-lighter; - -@nav-disabled-link-color: @gray-light; -@nav-disabled-link-hover-color: @gray-light; - -//== Tabs -@nav-tabs-border-color: #ddd; - -@nav-tabs-link-hover-border-color: @gray-lighter; - -@nav-tabs-active-link-hover-bg: @body-bg; -@nav-tabs-active-link-hover-color: @gray; -@nav-tabs-active-link-hover-border-color: #ddd; - -@nav-tabs-justified-link-border-color: #ddd; -@nav-tabs-justified-active-link-border-color: @body-bg; - -//== Pills -@nav-pills-border-radius: @border-radius-base; -@nav-pills-active-link-hover-bg: @component-active-bg; -@nav-pills-active-link-hover-color: @component-active-color; - - -//== Pagination -// -//## - -@pagination-color: @link-color; -@pagination-bg: #fff; -@pagination-border: #ddd; - -@pagination-hover-color: @link-hover-color; -@pagination-hover-bg: @gray-lighter; -@pagination-hover-border: #ddd; - -@pagination-active-color: #fff; -@pagination-active-bg: @brand-primary; -@pagination-active-border: @brand-primary; - -@pagination-disabled-color: @gray-light; -@pagination-disabled-bg: #fff; -@pagination-disabled-border: #ddd; - - -//== Pager -// -//## - -@pager-bg: @pagination-bg; -@pager-border: @pagination-border; -@pager-border-radius: 15px; - -@pager-hover-bg: @pagination-hover-bg; - -@pager-active-bg: @pagination-active-bg; -@pager-active-color: @pagination-active-color; - -@pager-disabled-color: @pagination-disabled-color; - - -//== Jumbotron -// -//## - -@jumbotron-padding: 30px; -@jumbotron-color: inherit; -@jumbotron-bg: @gray-lighter; -@jumbotron-heading-color: inherit; -@jumbotron-font-size: ceil((@font-size-base * 1.5)); -@jumbotron-heading-font-size: ceil((@font-size-base * 4.5)); - - -//== Form states and alerts -// -//## Define colors for form feedback states and, by default, alerts. - -@state-success-text: #3c763d; -@state-success-bg: #dff0d8; -@state-success-border: darken(spin(@state-success-bg, -10), 5%); - -@state-info-text: #31708f; -@state-info-bg: #d9edf7; -@state-info-border: darken(spin(@state-info-bg, -10), 7%); - -@state-warning-text: #8a6d3b; -@state-warning-bg: #fcf8e3; -@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); - -@state-danger-text: #a94442; -@state-danger-bg: #f2dede; -@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); - - -//== Tooltips -// -//## - -//** Tooltip max width -@tooltip-max-width: 200px; -//** Tooltip text color -@tooltip-color: #fff; -//** Tooltip background color -@tooltip-bg: #000; -@tooltip-opacity: .9; - -//** Tooltip arrow width -@tooltip-arrow-width: 5px; -//** Tooltip arrow color -@tooltip-arrow-color: @tooltip-bg; - - -//== Popovers -// -//## - -//** Popover body background color -@popover-bg: #fff; -//** Popover maximum width -@popover-max-width: 276px; -//** Popover border color -@popover-border-color: rgba(0,0,0,.2); -//** Popover fallback border color -@popover-fallback-border-color: #ccc; - -//** Popover title background color -@popover-title-bg: darken(@popover-bg, 3%); - -//** Popover arrow width -@popover-arrow-width: 10px; -//** Popover arrow color -@popover-arrow-color: @popover-bg; - -//** Popover outer arrow width -@popover-arrow-outer-width: (@popover-arrow-width + 1); -//** Popover outer arrow color -@popover-arrow-outer-color: fadein(@popover-border-color, 5%); -//** Popover outer arrow fallback color -@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%); - - -//== Labels -// -//## - -//** Default label background color -@label-default-bg: @gray-light; -//** Primary label background color -@label-primary-bg: @brand-primary; -//** Success label background color -@label-success-bg: @brand-success; -//** Info label background color -@label-info-bg: @brand-info; -//** Warning label background color -@label-warning-bg: @brand-warning; -//** Danger label background color -@label-danger-bg: @brand-danger; - -//** Default label text color -@label-color: #fff; -//** Default text color of a linked label -@label-link-hover-color: #fff; - - -//== Modals -// -//## - -//** Padding applied to the modal body -@modal-inner-padding: 15px; - -//** Padding applied to the modal title -@modal-title-padding: 15px; -//** Modal title line-height -@modal-title-line-height: @line-height-base; - -//** Background color of modal content area -@modal-content-bg: #fff; -//** Modal content border color -@modal-content-border-color: rgba(0,0,0,.2); -//** Modal content border color **for IE8** -@modal-content-fallback-border-color: #999; - -//** Modal backdrop background color -@modal-backdrop-bg: #000; -//** Modal backdrop opacity -@modal-backdrop-opacity: .5; -//** Modal header border color -@modal-header-border-color: #e5e5e5; -//** Modal footer border color -@modal-footer-border-color: @modal-header-border-color; - -@modal-lg: 900px; -@modal-md: 600px; -@modal-sm: 300px; - - -//== Alerts -// -//## Define alert colors, border radius, and padding. - -@alert-padding: 15px; -@alert-border-radius: @border-radius-base; -@alert-link-font-weight: bold; - -@alert-success-bg: @state-success-bg; -@alert-success-text: @state-success-text; -@alert-success-border: @state-success-border; - -@alert-info-bg: @state-info-bg; -@alert-info-text: @state-info-text; -@alert-info-border: @state-info-border; - -@alert-warning-bg: @state-warning-bg; -@alert-warning-text: @state-warning-text; -@alert-warning-border: @state-warning-border; - -@alert-danger-bg: @state-danger-bg; -@alert-danger-text: @state-danger-text; -@alert-danger-border: @state-danger-border; - - -//== Progress bars -// -//## - -//** Background color of the whole progress component -@progress-bg: #f5f5f5; -//** Progress bar text color -@progress-bar-color: #fff; -//** Variable for setting rounded corners on progress bar. -@progress-border-radius: @border-radius-base; - -//** Default progress bar color -@progress-bar-bg: @brand-primary; -//** Success progress bar color -@progress-bar-success-bg: @brand-success; -//** Warning progress bar color -@progress-bar-warning-bg: @brand-warning; -//** Danger progress bar color -@progress-bar-danger-bg: @brand-danger; -//** Info progress bar color -@progress-bar-info-bg: @brand-info; - - -//== List group -// -//## - -//** Background color on `.list-group-item` -@list-group-bg: #fff; -//** `.list-group-item` border color -@list-group-border: #ddd; -//** List group border radius -@list-group-border-radius: @border-radius-base; - -//** Background color of single list items on hover -@list-group-hover-bg: #f5f5f5; -//** Text color of active list items -@list-group-active-color: @component-active-color; -//** Background color of active list items -@list-group-active-bg: @component-active-bg; -//** Border color of active list elements -@list-group-active-border: @list-group-active-bg; -//** Text color for content within active list items -@list-group-active-text-color: lighten(@list-group-active-bg, 40%); - -//** Text color of disabled list items -@list-group-disabled-color: @gray-light; -//** Background color of disabled list items -@list-group-disabled-bg: @gray-lighter; -//** Text color for content within disabled list items -@list-group-disabled-text-color: @list-group-disabled-color; - -@list-group-link-color: #555; -@list-group-link-hover-color: @list-group-link-color; -@list-group-link-heading-color: #333; - - -//== Panels -// -//## - -@panel-bg: #fff; -@panel-body-padding: 15px; -@panel-heading-padding: 10px 15px; -@panel-footer-padding: @panel-heading-padding; -@panel-border-radius: @border-radius-base; - -//** Border color for elements within panels -@panel-inner-border: #ddd; -@panel-footer-bg: #f5f5f5; - -@panel-default-text: @gray-dark; -@panel-default-border: #ddd; -@panel-default-heading-bg: #f5f5f5; - -@panel-primary-text: #fff; -@panel-primary-border: @brand-primary; -@panel-primary-heading-bg: @brand-primary; - -@panel-success-text: @state-success-text; -@panel-success-border: @state-success-border; -@panel-success-heading-bg: @state-success-bg; - -@panel-info-text: @state-info-text; -@panel-info-border: @state-info-border; -@panel-info-heading-bg: @state-info-bg; - -@panel-warning-text: @state-warning-text; -@panel-warning-border: @state-warning-border; -@panel-warning-heading-bg: @state-warning-bg; - -@panel-danger-text: @state-danger-text; -@panel-danger-border: @state-danger-border; -@panel-danger-heading-bg: @state-danger-bg; - - -//== Thumbnails -// -//## - -//** Padding around the thumbnail image -@thumbnail-padding: 4px; -//** Thumbnail background color -@thumbnail-bg: @body-bg; -//** Thumbnail border color -@thumbnail-border: #ddd; -//** Thumbnail border radius -@thumbnail-border-radius: @border-radius-base; - -//** Custom text color for thumbnail captions -@thumbnail-caption-color: @text-color; -//** Padding around the thumbnail caption -@thumbnail-caption-padding: 9px; - - -//== Wells -// -//## - -@well-bg: #f5f5f5; -@well-border: darken(@well-bg, 7%); - - -//== Badges -// -//## - -@badge-color: #fff; -//** Linked badge text color on hover -@badge-link-hover-color: #fff; -@badge-bg: @gray-light; - -//** Badge text color in active nav link -@badge-active-color: @link-color; -//** Badge background color in active nav link -@badge-active-bg: #fff; - -@badge-font-weight: bold; -@badge-line-height: 1; -@badge-border-radius: 10px; - - -//== Breadcrumbs -// -//## - -@breadcrumb-padding-vertical: 8px; -@breadcrumb-padding-horizontal: 15px; -//** Breadcrumb background color -@breadcrumb-bg: #f5f5f5; -//** Breadcrumb text color -@breadcrumb-color: #ccc; -//** Text color of current page in the breadcrumb -@breadcrumb-active-color: @gray-light; -//** Textual separator for between breadcrumb elements -@breadcrumb-separator: "/"; - - -//== Carousel -// -//## - -@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6); - -@carousel-control-color: #fff; -@carousel-control-width: 15%; -@carousel-control-opacity: .5; -@carousel-control-font-size: 20px; - -@carousel-indicator-active-bg: #fff; -@carousel-indicator-border-color: #fff; - -@carousel-caption-color: #fff; - - -//== Close -// -//## - -@close-font-weight: bold; -@close-color: #000; -@close-text-shadow: 0 1px 0 #fff; - - -//== Code -// -//## - -@code-color: #c7254e; -@code-bg: #f9f2f4; - -@kbd-color: #fff; -@kbd-bg: #333; - -@pre-bg: #f5f5f5; -@pre-color: @gray-dark; -@pre-border-color: #ccc; -@pre-scrollable-max-height: 340px; - - -//== Type -// -//## - -//** Horizontal offset for forms and lists. -@component-offset-horizontal: 180px; -//** Text muted color -@text-muted: @gray-light; -//** Abbreviations and acronyms border color -@abbr-border-color: @gray-light; -//** Headings small color -@headings-small-color: @gray-light; -//** Blockquote small color -@blockquote-small-color: @gray-light; -//** Blockquote font size -@blockquote-font-size: (@font-size-base * 1.25); -//** Blockquote border color -@blockquote-border-color: @gray-lighter; -//** Page header border color -@page-header-border-color: @gray-lighter; -//** Width of horizontal description list titles -@dl-horizontal-offset: @component-offset-horizontal; -//** Horizontal line color. -@hr-border: @gray-lighter; diff --git a/src/UI/Content/Bootstrap/wells.less b/src/UI/Content/Bootstrap/wells.less deleted file mode 100644 index 15d072b0c..000000000 --- a/src/UI/Content/Bootstrap/wells.less +++ /dev/null @@ -1,29 +0,0 @@ -// -// Wells -// -------------------------------------------------- - - -// Base class -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: @well-bg; - border: 1px solid @well-border; - border-radius: @border-radius-base; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); - blockquote { - border-color: #ddd; - border-color: rgba(0,0,0,.15); - } -} - -// Sizes -.well-lg { - padding: 24px; - border-radius: @border-radius-large; -} -.well-sm { - padding: 9px; - border-radius: @border-radius-small; -} diff --git a/src/UI/Content/FontAwesome/FontAwesome.otf b/src/UI/Content/FontAwesome/FontAwesome.otf deleted file mode 100644 index f7936cc1e789eea5438d576d6b12de20191da09d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93888 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 z -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%0A;R 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 E0UHWiZ2$lO diff --git a/src/UI/Content/FontAwesome/animated.less b/src/UI/Content/FontAwesome/animated.less deleted file mode 100644 index 66ad52a5b..000000000 --- a/src/UI/Content/FontAwesome/animated.less +++ /dev/null @@ -1,34 +0,0 @@ -// Animated Icons -// -------------------------- - -.@{fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.@{fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/src/UI/Content/FontAwesome/bordered-pulled.less b/src/UI/Content/FontAwesome/bordered-pulled.less deleted file mode 100644 index 0c90eb567..000000000 --- a/src/UI/Content/FontAwesome/bordered-pulled.less +++ /dev/null @@ -1,16 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.@{fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em @fa-border-color; - border-radius: .1em; -} - -.pull-right { float: right; } -.pull-left { float: left; } - -.@{fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/src/UI/Content/FontAwesome/core.less b/src/UI/Content/FontAwesome/core.less deleted file mode 100644 index f814f1e17..000000000 --- a/src/UI/Content/FontAwesome/core.less +++ /dev/null @@ -1,13 +0,0 @@ -// Base Class Definition -// ------------------------- - -.@{fa-css-prefix} { - display: inline-block; - font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transform: translate(0, 0); // ensures no half-pixel rendering in firefox - -} diff --git a/src/UI/Content/FontAwesome/fixed-width.less b/src/UI/Content/FontAwesome/fixed-width.less deleted file mode 100644 index 110289f2f..000000000 --- a/src/UI/Content/FontAwesome/fixed-width.less +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.@{fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/src/UI/Content/FontAwesome/font-awesome.less b/src/UI/Content/FontAwesome/font-awesome.less deleted file mode 100644 index 1f45c63d1..000000000 --- a/src/UI/Content/FontAwesome/font-awesome.less +++ /dev/null @@ -1,17 +0,0 @@ -/*! - * Font Awesome 4.3.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables.less"; -@import "mixins.less"; -@import "path.less"; -@import "core.less"; -@import "larger.less"; -@import "fixed-width.less"; -@import "list.less"; -@import "bordered-pulled.less"; -@import "animated.less"; -@import "rotated-flipped.less"; -@import "stacked.less"; -@import "icons.less"; diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.eot b/src/UI/Content/FontAwesome/fontawesome-webfont.eot deleted file mode 100644 index 33b2bb80055cc480e797de704925acaba4ba7d7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60767 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�ly@k*2(3WLEleNarQ}K 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%0DKehr}RFC`2XTB_<sB5 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 J000000038FvZ??8 diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.svg b/src/UI/Content/FontAwesome/fontawesome-webfont.svg deleted file mode 100644 index 1ee89d436..000000000 --- a/src/UI/Content/FontAwesome/fontawesome-webfont.svg +++ /dev/null @@ -1,565 +0,0 @@ -<?xml version="1.0" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> -<metadata></metadata> -<defs> -<font id="fontawesomeregular" horiz-adv-x="1536" > -<font-face units-per-em="1792" ascent="1536" descent="-256" /> -<missing-glyph horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode=" " horiz-adv-x="448" /> -<glyph unicode="¨" horiz-adv-x="1792" /> -<glyph unicode="©" horiz-adv-x="1792" /> -<glyph unicode="®" horiz-adv-x="1792" /> -<glyph unicode="´" horiz-adv-x="1792" /> -<glyph unicode="Æ" horiz-adv-x="1792" /> -<glyph unicode="Ø" horiz-adv-x="1792" /> -<glyph unicode=" " horiz-adv-x="768" /> -<glyph unicode=" " horiz-adv-x="1537" /> -<glyph unicode=" " horiz-adv-x="768" /> -<glyph unicode=" " horiz-adv-x="1537" /> -<glyph unicode=" " horiz-adv-x="512" /> -<glyph unicode=" " horiz-adv-x="384" /> -<glyph unicode=" " horiz-adv-x="256" /> -<glyph unicode=" " horiz-adv-x="256" /> -<glyph unicode=" " horiz-adv-x="192" /> -<glyph unicode=" " horiz-adv-x="307" /> -<glyph unicode=" " horiz-adv-x="85" /> -<glyph unicode=" " horiz-adv-x="307" /> -<glyph unicode=" " horiz-adv-x="384" /> -<glyph unicode="™" horiz-adv-x="1792" /> -<glyph unicode="∞" horiz-adv-x="1792" /> -<glyph unicode="≠" horiz-adv-x="1792" /> -<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1699 1350q0 -35 -43 -78l-632 -632v-768h320q26 0 45 -19t19 -45t-19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45t45 19h320v768l-632 632q-43 43 -43 78q0 23 18 36.5t38 17.5t43 4h1408q23 0 43 -4t38 -17.5t18 -36.5z" /> -<glyph unicode="" d="M1536 1312v-1120q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v537l-768 -237v-709q0 -50 -34 -89t-86 -60.5t-103.5 -32t-96.5 -10.5t-96.5 10.5t-103.5 32t-86 60.5t-34 89 t34 89t86 60.5t103.5 32t96.5 10.5q105 0 192 -39v967q0 31 19 56.5t49 35.5l832 256q12 4 28 4q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -52 -38 -90t-90 -38q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5 t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 32v768q-32 -36 -69 -66q-268 -206 -426 -338q-51 -43 -83 -67t-86.5 -48.5t-102.5 -24.5h-1h-1q-48 0 -102.5 24.5t-86.5 48.5t-83 67q-158 132 -426 338q-37 30 -69 66v-768q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1664 1083v11v13.5t-0.5 13 t-3 12.5t-5.5 9t-9 7.5t-14 2.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5q0 -168 147 -284q193 -152 401 -317q6 -5 35 -29.5t46 -37.5t44.5 -31.5t50.5 -27.5t43 -9h1h1q20 0 43 9t50.5 27.5t44.5 31.5t46 37.5t35 29.5q208 165 401 317q54 43 100.5 115.5t46.5 131.5z M1792 1120v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 -128q-26 0 -44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124t127 -344q0 -221 -229 -450l-623 -600 q-18 -18 -44 -18z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -21 -10.5 -35.5t-30.5 -14.5q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455 l502 -73q56 -9 56 -46z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1137 532l306 297l-422 62l-189 382l-189 -382l-422 -62l306 -297l-73 -421l378 199l377 -199zM1664 889q0 -22 -26 -48l-363 -354l86 -500q1 -7 1 -20q0 -50 -41 -50q-19 0 -40 12l-449 236l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500 l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41t49 -41l225 -455l502 -73q56 -9 56 -46z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 131q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q9 0 42 -21.5t74.5 -48t108 -48t133.5 -21.5t133.5 21.5t108 48t74.5 48t42 21.5q61 0 111.5 -20t85.5 -53.5t62 -81 t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M384 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 320v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM384 704v128q0 26 -19 45t-45 19h-128 q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 -64v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM384 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45 t45 -19h128q26 0 45 19t19 45zM1792 -64v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1408 704v512q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-512q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1792 320v128 q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 704v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1792 1088v128q0 26 -19 45t-45 19h-128q-26 0 -45 -19 t-19 -45v-128q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1920 1248v-1344q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1344q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 512v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM768 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 512v-384q0 -52 -38 -90t-90 -38 h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90zM1664 1280v-384q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 288v-192q0 -40 -28 -68t-68 -28h-320 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1152 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M512 288v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM512 800v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 288v-192q0 -40 -28 -68t-68 -28h-960 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68zM512 1312v-192q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h320q40 0 68 -28t28 -68zM1792 800v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28 h960q40 0 68 -28t28 -68zM1792 1312v-192q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h960q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1671 970q0 -40 -28 -68l-724 -724l-136 -136q-28 -28 -68 -28t-68 28l-136 136l-362 362q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -295l656 657q28 28 68 28t68 -28l136 -136q28 -28 28 -68z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1298 214q0 -40 -28 -68l-136 -136q-28 -28 -68 -28t-68 28l-294 294l-294 -294q-28 -28 -68 -28t-68 28l-136 136q-28 28 -28 68t28 68l294 294l-294 294q-28 28 -28 68t28 68l136 136q28 28 68 28t68 -28l294 -294l294 294q28 28 68 28t68 -28l136 -136q28 -28 28 -68 t-28 -68l-294 -294l294 -294q28 -28 28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-224q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v224h-224q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h224v224q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5v-224h224 q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5 t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1024 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-576q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h576q13 0 22.5 -9.5t9.5 -22.5zM1152 704q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5z M1664 -128q0 -53 -37.5 -90.5t-90.5 -37.5q-54 0 -90 38l-343 342q-179 -124 -399 -124q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -220 -124 -399l343 -343q37 -37 37 -90z " /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61t-298 61t-245 164t-164 245t-61 298q0 182 80.5 343t226.5 270q43 32 95.5 25t83.5 -50q32 -42 24.5 -94.5t-49.5 -84.5q-98 -74 -151.5 -181t-53.5 -228q0 -104 40.5 -198.5t109.5 -163.5t163.5 -109.5 t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5q0 121 -53.5 228t-151.5 181q-42 32 -49.5 84.5t24.5 94.5q31 43 84 50t95 -25q146 -109 226.5 -270t80.5 -343zM896 1408v-640q0 -52 -38 -90t-90 -38t-90 38t-38 90v640q0 52 38 90t90 38t90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 96v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 224v-320q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 480v-576q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1408 864v-960q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1376v-1472q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1472q0 14 9 23t23 9h192q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1536 749v-222q0 -12 -8 -23t-20 -13l-185 -28q-19 -54 -39 -91q35 -50 107 -138q10 -12 10 -25t-9 -23q-27 -37 -99 -108t-94 -71q-12 0 -26 9l-138 108q-44 -23 -91 -38 q-16 -136 -29 -186q-7 -28 -36 -28h-222q-14 0 -24.5 8.5t-11.5 21.5l-28 184q-49 16 -90 37l-141 -107q-10 -9 -25 -9q-14 0 -25 11q-126 114 -165 168q-7 10 -7 23q0 12 8 23q15 21 51 66.5t54 70.5q-27 50 -41 99l-183 27q-13 2 -21 12.5t-8 23.5v222q0 12 8 23t19 13 l186 28q14 46 39 92q-40 57 -107 138q-10 12 -10 24q0 10 9 23q26 36 98.5 107.5t94.5 71.5q13 0 26 -10l138 -107q44 23 91 38q16 136 29 186q7 28 36 28h222q14 0 24.5 -8.5t11.5 -21.5l28 -184q49 -16 90 -37l142 107q9 9 24 9q13 0 25 -10q129 -119 165 -170q7 -8 7 -22 q0 -12 -8 -23q-15 -21 -51 -66.5t-54 -70.5q26 -50 41 -98l183 -28q13 -2 21 -12.5t8 -23.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM768 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1024 800v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1152 76v948h-896v-948q0 -22 7 -40.5t14.5 -27t10.5 -8.5h832q3 0 10.5 8.5t14.5 27t7 40.5zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832 q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 544v-480q0 -26 -19 -45t-45 -19h-384v384h-256v-384h-384q-26 0 -45 19t-19 45v480q0 1 0.5 3t0.5 3l575 474l575 -474q1 -2 1 -6zM1631 613l-62 -74q-8 -9 -21 -11h-3q-13 0 -21 7l-692 577l-692 -577q-12 -8 -24 -7q-13 2 -21 11l-62 74q-8 10 -7 23.5t11 21.5 l719 599q32 26 76 26t76 -26l244 -204v195q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-408l219 -182q10 -8 11 -21.5t-7 -23.5z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z " /> -<glyph unicode="" d="M896 992v-448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1111 540v4l-24 320q-1 13 -11 22.5t-23 9.5h-186q-13 0 -23 -9.5t-11 -22.5l-24 -320v-4q-1 -12 8 -20t21 -8h244q12 0 21 8t8 20zM1870 73q0 -73 -46 -73h-704q13 0 22 9.5t8 22.5l-20 256q-1 13 -11 22.5t-23 9.5h-272q-13 0 -23 -9.5t-11 -22.5l-20 -256 q-1 -13 8 -22.5t22 -9.5h-704q-46 0 -46 73q0 54 26 116l417 1044q8 19 26 33t38 14h339q-13 0 -23 -9.5t-11 -22.5l-15 -192q-1 -14 8 -23t22 -9h166q13 0 22 9t8 23l-15 192q-1 13 -11 22.5t-23 9.5h339q20 0 38 -14t26 -33l417 -1044q26 -62 26 -116z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1280 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 416v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h465l135 -136 q58 -56 136 -56t136 56l136 136h464q40 0 68 -28t28 -68zM1339 985q17 -41 -14 -70l-448 -448q-18 -19 -45 -19t-45 19l-448 448q-31 29 -14 70q17 39 59 39h256v448q0 26 19 45t45 19h256q26 0 45 -19t19 -45v-448h256q42 0 59 -39z" /> -<glyph unicode="" d="M1120 608q0 -12 -10 -24l-319 -319q-11 -9 -23 -9t-23 9l-320 320q-15 16 -7 35q8 20 30 20h192v352q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-352h192q14 0 23 -9t9 -23zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273 t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1118 660q-8 -20 -30 -20h-192v-352q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v352h-192q-14 0 -23 9t-9 23q0 12 10 24l319 319q11 9 23 9t23 -9l320 -320q15 -16 7 -35zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198 t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1023 576h316q-1 3 -2.5 8t-2.5 8l-212 496h-708l-212 -496q-1 -2 -2.5 -8t-2.5 -8h316l95 -192h320zM1536 546v-482q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v482q0 62 25 123l238 552q10 25 36.5 42t52.5 17h832q26 0 52.5 -17t36.5 -42l238 -552 q25 -61 25 -123z" /> -<glyph unicode="" d="M1184 640q0 -37 -32 -55l-544 -320q-15 -9 -32 -9q-16 0 -32 8q-32 19 -32 56v640q0 37 32 56q33 18 64 -1l544 -320q32 -18 32 -55zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l138 138q-148 137 -349 137q-104 0 -198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5q119 0 225 52t179 147q7 10 23 12q14 0 25 -9 l137 -138q9 -8 9.5 -20.5t-7.5 -22.5q-109 -132 -264 -204.5t-327 -72.5q-156 0 -298 61t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q147 0 284.5 -55.5t244.5 -156.5l130 129q29 31 70 14q39 -17 39 -59z" /> -<glyph unicode="" d="M1511 480q0 -5 -1 -7q-64 -268 -268 -434.5t-478 -166.5q-146 0 -282.5 55t-243.5 157l-129 -129q-19 -19 -45 -19t-45 19t-19 45v448q0 26 19 45t45 19h448q26 0 45 -19t19 -45t-19 -45l-137 -137q71 -66 161 -102t187 -36q134 0 250 65t186 179q11 17 53 117 q8 23 30 23h192q13 0 22.5 -9.5t9.5 -22.5zM1536 1280v-448q0 -26 -19 -45t-45 -19h-448q-26 0 -45 19t-19 45t19 45l138 138q-148 137 -349 137q-134 0 -250 -65t-186 -179q-11 -17 -53 -117q-8 -23 -30 -23h-199q-13 0 -22.5 9.5t-9.5 22.5v7q65 268 270 434.5t480 166.5 q146 0 284 -55.5t245 -156.5l130 129q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M384 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1536 352v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5z M1536 608v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5t9.5 -22.5zM1536 864v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h960q13 0 22.5 -9.5 t9.5 -22.5zM1664 160v832q0 13 -9.5 22.5t-22.5 9.5h-1472q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1472q13 0 22.5 9.5t9.5 22.5zM1792 1248v-1088q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1472q66 0 113 -47 t47 -113z" /> -<glyph unicode="" horiz-adv-x="1152" d="M320 768h512v192q0 106 -75 181t-181 75t-181 -75t-75 -181v-192zM1152 672v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v192q0 184 132 316t316 132t316 -132t132 -316v-192h32q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M320 1280q0 -72 -64 -110v-1266q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v1266q-64 38 -64 110q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -25 -12.5 -38.5t-39.5 -27.5q-215 -116 -369 -116q-61 0 -123.5 22t-108.5 48 t-115.5 48t-142.5 22q-192 0 -464 -146q-17 -9 -33 -9q-26 0 -45 19t-19 45v742q0 32 31 55q21 14 79 43q236 120 421 120q107 0 200 -29t219 -88q38 -19 88 -19q54 0 117.5 21t110 47t88 47t54.5 21q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 650q0 -166 -60 -314l-20 -49l-185 -33q-22 -83 -90.5 -136.5t-156.5 -53.5v-32q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-32q71 0 130 -35.5t93 -95.5l68 12q29 95 29 193q0 148 -88 279t-236.5 209t-315.5 78 t-315.5 -78t-236.5 -209t-88 -279q0 -98 29 -193l68 -12q34 60 93 95.5t130 35.5v32q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v32q-88 0 -156.5 53.5t-90.5 136.5l-185 33l-20 49q-60 148 -60 314q0 151 67 291t179 242.5 t266 163.5t320 61t320 -61t266 -163.5t179 -242.5t67 -291z" /> -<glyph unicode="" horiz-adv-x="768" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1152" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 1184v-1088q0 -26 -19 -45t-45 -19t-45 19l-333 333h-262q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h262l333 333q19 19 45 19t45 -19t19 -45zM1152 640q0 -76 -42.5 -141.5t-112.5 -93.5q-10 -5 -25 -5q-26 0 -45 18.5t-19 45.5q0 21 12 35.5t29 25t34 23t29 35.5 t12 57t-12 57t-29 35.5t-34 23t-29 25t-12 35.5q0 27 19 45.5t45 18.5q15 0 25 -5q70 -27 112.5 -93t42.5 -142zM1408 640q0 -153 -85 -282.5t-225 -188.5q-13 -5 -25 -5q-27 0 -46 19t-19 45q0 39 39 59q56 29 76 44q74 54 115.5 135.5t41.5 173.5t-41.5 173.5 t-115.5 135.5q-20 15 -76 44q-39 20 -39 59q0 26 19 45t45 19q13 0 26 -5q140 -59 225 -188.5t85 -282.5zM1664 640q0 -230 -127 -422.5t-338 -283.5q-13 -5 -26 -5q-26 0 -45 19t-19 45q0 36 39 59q7 4 22.5 10.5t22.5 10.5q46 25 82 51q123 91 192 227t69 289t-69 289 t-192 227q-36 26 -82 51q-7 4 -22.5 10.5t-22.5 10.5q-39 23 -39 59q0 26 19 45t45 19q13 0 26 -5q211 -91 338 -283.5t127 -422.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 384v-128h-128v128h128zM384 1152v-128h-128v128h128zM1152 1152v-128h-128v128h128zM128 129h384v383h-384v-383zM128 896h384v384h-384v-384zM896 896h384v384h-384v-384zM640 640v-640h-640v640h640zM1152 128v-128h-128v128h128zM1408 128v-128h-128v128h128z M1408 640v-384h-384v128h-128v-384h-128v640h384v-128h128v128h128zM640 1408v-640h-640v640h640zM1408 1408v-640h-640v640h640z" /> -<glyph unicode="" horiz-adv-x="1792" d="M63 0h-63v1408h63v-1408zM126 1h-32v1407h32v-1407zM220 1h-31v1407h31v-1407zM377 1h-31v1407h31v-1407zM534 1h-62v1407h62v-1407zM660 1h-31v1407h31v-1407zM723 1h-31v1407h31v-1407zM786 1h-31v1407h31v-1407zM943 1h-63v1407h63v-1407zM1100 1h-63v1407h63v-1407z M1226 1h-63v1407h63v-1407zM1352 1h-63v1407h63v-1407zM1446 1h-63v1407h63v-1407zM1635 1h-94v1407h94v-1407zM1698 1h-32v1407h32v-1407zM1792 0h-63v1408h63v-1408z" /> -<glyph unicode="" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1920" d="M448 1088q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1515 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-53 0 -90 37l-715 716q-38 37 -64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117 -26.5t102 -64.5 l715 -714q37 -39 37 -91zM1899 512q0 -53 -37 -90l-491 -492q-39 -37 -91 -37q-36 0 -59 14t-53 45l470 470q37 37 37 90q0 52 -37 91l-715 714q-38 38 -102 64.5t-117 26.5h224q53 0 117 -26.5t102 -64.5l715 -714q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1639 1058q40 -57 18 -129l-275 -906q-19 -64 -76.5 -107.5t-122.5 -43.5h-923q-77 0 -148.5 53.5t-99.5 131.5q-24 67 -2 127q0 4 3 27t4 37q1 8 -3 21.5t-3 19.5q2 11 8 21t16.5 23.5t16.5 23.5q23 38 45 91.5t30 91.5q3 10 0.5 30t-0.5 28q3 11 17 28t17 23 q21 36 42 92t25 90q1 9 -2.5 32t0.5 28q4 13 22 30.5t22 22.5q19 26 42.5 84.5t27.5 96.5q1 8 -3 25.5t-2 26.5q2 8 9 18t18 23t17 21q8 12 16.5 30.5t15 35t16 36t19.5 32t26.5 23.5t36 11.5t47.5 -5.5l-1 -3q38 9 51 9h761q74 0 114 -56t18 -130l-274 -906 q-36 -119 -71.5 -153.5t-128.5 -34.5h-869q-27 0 -38 -15q-11 -16 -1 -43q24 -70 144 -70h923q29 0 56 15.5t35 41.5l300 987q7 22 5 57q38 -15 59 -43zM575 1056q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5 t-16.5 -22.5zM492 800q-4 -13 2 -22.5t20 -9.5h608q13 0 25.5 9.5t16.5 22.5l21 64q4 13 -2 22.5t-20 9.5h-608q-13 0 -25.5 -9.5t-16.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> -<glyph unicode="" horiz-adv-x="1664" d="M384 0h896v256h-896v-256zM384 640h896v384h-160q-40 0 -68 28t-28 68v160h-640v-640zM1536 576q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 576v-416q0 -13 -9.5 -22.5t-22.5 -9.5h-224v-160q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68 v160h-224q-13 0 -22.5 9.5t-9.5 22.5v416q0 79 56.5 135.5t135.5 56.5h64v544q0 40 28 68t68 28h672q40 0 88 -20t76 -48l152 -152q28 -28 48 -76t20 -88v-256h64q79 0 135.5 -56.5t56.5 -135.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M960 864q119 0 203.5 -84.5t84.5 -203.5t-84.5 -203.5t-203.5 -84.5t-203.5 84.5t-84.5 203.5t84.5 203.5t203.5 84.5zM1664 1280q106 0 181 -75t75 -181v-896q0 -106 -75 -181t-181 -75h-1408q-106 0 -181 75t-75 181v896q0 106 75 181t181 75h224l51 136 q19 49 69.5 84.5t103.5 35.5h512q53 0 103.5 -35.5t69.5 -84.5l51 -136h224zM960 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M725 977l-170 -450q33 0 136.5 -2t160.5 -2q19 0 57 2q-87 253 -184 452zM0 -128l2 79q23 7 56 12.5t57 10.5t49.5 14.5t44.5 29t31 50.5l237 616l280 724h75h53q8 -14 11 -21l205 -480q33 -78 106 -257.5t114 -274.5q15 -34 58 -144.5t72 -168.5q20 -45 35 -57 q19 -15 88 -29.5t84 -20.5q6 -38 6 -57q0 -4 -0.5 -13t-0.5 -13q-63 0 -190 8t-191 8q-76 0 -215 -7t-178 -8q0 43 4 78l131 28q1 0 12.5 2.5t15.5 3.5t14.5 4.5t15 6.5t11 8t9 11t2.5 14q0 16 -31 96.5t-72 177.5t-42 100l-450 2q-26 -58 -76.5 -195.5t-50.5 -162.5 q0 -22 14 -37.5t43.5 -24.5t48.5 -13.5t57 -8.5t41 -4q1 -19 1 -58q0 -9 -2 -27q-58 0 -174.5 10t-174.5 10q-8 0 -26.5 -4t-21.5 -4q-80 -14 -188 -14z" /> -<glyph unicode="" horiz-adv-x="1408" d="M555 15q74 -32 140 -32q376 0 376 335q0 114 -41 180q-27 44 -61.5 74t-67.5 46.5t-80.5 25t-84 10.5t-94.5 2q-73 0 -101 -10q0 -53 -0.5 -159t-0.5 -158q0 -8 -1 -67.5t-0.5 -96.5t4.5 -83.5t12 -66.5zM541 761q42 -7 109 -7q82 0 143 13t110 44.5t74.5 89.5t25.5 142 q0 70 -29 122.5t-79 82t-108 43.5t-124 14q-50 0 -130 -13q0 -50 4 -151t4 -152q0 -27 -0.5 -80t-0.5 -79q0 -46 1 -69zM0 -128l2 94q15 4 85 16t106 27q7 12 12.5 27t8.5 33.5t5.5 32.5t3 37.5t0.5 34v35.5v30q0 982 -22 1025q-4 8 -22 14.5t-44.5 11t-49.5 7t-48.5 4.5 t-30.5 3l-4 83q98 2 340 11.5t373 9.5q23 0 68.5 -0.5t67.5 -0.5q70 0 136.5 -13t128.5 -42t108 -71t74 -104.5t28 -137.5q0 -52 -16.5 -95.5t-39 -72t-64.5 -57.5t-73 -45t-84 -40q154 -35 256.5 -134t102.5 -248q0 -100 -35 -179.5t-93.5 -130.5t-138 -85.5t-163.5 -48.5 t-176 -14q-44 0 -132 3t-132 3q-106 0 -307 -11t-231 -12z" /> -<glyph unicode="" horiz-adv-x="1024" d="M0 -126l17 85q6 2 81.5 21.5t111.5 37.5q28 35 41 101q1 7 62 289t114 543.5t52 296.5v25q-24 13 -54.5 18.5t-69.5 8t-58 5.5l19 103q33 -2 120 -6.5t149.5 -7t120.5 -2.5q48 0 98.5 2.5t121 7t98.5 6.5q-5 -39 -19 -89q-30 -10 -101.5 -28.5t-108.5 -33.5 q-8 -19 -14 -42.5t-9 -40t-7.5 -45.5t-6.5 -42q-27 -148 -87.5 -419.5t-77.5 -355.5q-2 -9 -13 -58t-20 -90t-16 -83.5t-6 -57.5l1 -18q17 -4 185 -31q-3 -44 -16 -99q-11 0 -32.5 -1.5t-32.5 -1.5q-29 0 -87 10t-86 10q-138 2 -206 2q-51 0 -143 -9t-121 -11z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1744 128q33 0 42 -18.5t-11 -44.5l-126 -162q-20 -26 -49 -26t-49 26l-126 162q-20 26 -11 44.5t42 18.5h80v1024h-80q-33 0 -42 18.5t11 44.5l126 162q20 26 49 26t49 -26l126 -162q20 -26 11 -44.5t-42 -18.5h-80v-1024h80zM81 1407l54 -27q12 -5 211 -5q44 0 132 2 t132 2q36 0 107.5 -0.5t107.5 -0.5h293q6 0 21 -0.5t20.5 0t16 3t17.5 9t15 17.5l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 48t-14.5 73.5t-7.5 35.5q-6 8 -12 12.5t-15.5 6t-13 2.5t-18 0.5t-16.5 -0.5 q-17 0 -66.5 0.5t-74.5 0.5t-64 -2t-71 -6q-9 -81 -8 -136q0 -94 2 -388t2 -455q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q19 42 19 383q0 101 -3 303t-3 303v117q0 2 0.5 15.5t0.5 25t-1 25.5t-3 24t-5 14q-11 12 -162 12q-33 0 -93 -12t-80 -26q-19 -13 -34 -72.5t-31.5 -111t-42.5 -53.5q-42 26 -56 44v383z" /> -<glyph unicode="" d="M81 1407l54 -27q12 -5 211 -5q44 0 132 2t132 2q70 0 246.5 1t304.5 0.5t247 -4.5q33 -1 56 31l42 1q4 0 14 -0.5t14 -0.5q2 -112 2 -336q0 -80 -5 -109q-39 -14 -68 -18q-25 44 -54 128q-3 9 -11 47.5t-15 73.5t-7 36q-10 13 -27 19q-5 2 -66 2q-30 0 -93 1t-103 1 t-94 -2t-96 -7q-9 -81 -8 -136l1 -152v52q0 -55 1 -154t1.5 -180t0.5 -153q0 -16 -2.5 -71.5t0 -91.5t12.5 -69q40 -21 124 -42.5t120 -37.5q5 -40 5 -50q0 -14 -3 -29l-34 -1q-76 -2 -218 8t-207 10q-50 0 -151 -9t-152 -9q-3 51 -3 52v9q17 27 61.5 43t98.5 29t78 27 q7 16 11.5 74t6 145.5t1.5 155t-0.5 153.5t-0.5 89q0 7 -2.5 21.5t-2.5 22.5q0 7 0.5 44t1 73t0 76.5t-3 67.5t-6.5 32q-11 12 -162 12q-41 0 -163 -13.5t-138 -24.5q-19 -12 -34 -71.5t-31.5 -111.5t-42.5 -54q-42 26 -56 44v383zM1310 125q12 0 42 -19.5t57.5 -41.5 t59.5 -49t36 -30q26 -21 26 -49t-26 -49q-4 -3 -36 -30t-59.5 -49t-57.5 -41.5t-42 -19.5q-13 0 -20.5 10.5t-10 28.5t-2.5 33.5t1.5 33t1.5 19.5h-1024q0 -2 1.5 -19.5t1.5 -33t-2.5 -33.5t-10 -28.5t-20.5 -10.5q-12 0 -42 19.5t-57.5 41.5t-59.5 49t-36 30q-26 21 -26 49 t26 49q4 3 36 30t59.5 49t57.5 41.5t42 19.5q13 0 20.5 -10.5t10 -28.5t2.5 -33.5t-1.5 -33t-1.5 -19.5h1024q0 2 -1.5 19.5t-1.5 33t2.5 33.5t10 28.5t20.5 10.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1408 576v-128q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h896q26 0 45 -19t19 -45zM1664 960v-128q0 -26 -19 -45t-45 -19 h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1280 1344v-128q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h640q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1280q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1536q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1536q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1152q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 192v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 576v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 960v-128q0 -26 -19 -45 t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-128q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM256 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5 t9.5 -22.5zM256 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344 q13 0 22.5 -9.5t9.5 -22.5zM256 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v192 q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 992v-576q0 -13 -9.5 -22.5t-22.5 -9.5q-14 0 -23 9l-288 288q-9 9 -9 23t9 23l288 288q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M352 704q0 -14 -9 -23l-288 -288q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v576q0 13 9.5 22.5t22.5 9.5q14 0 23 -9l288 -288q9 -9 9 -23zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5 t9.5 -22.5zM1792 608v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088q13 0 22.5 -9.5t9.5 -22.5zM1792 992v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1088q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1088 q13 0 22.5 -9.5t9.5 -22.5zM1792 1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1728q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1728q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 1184v-1088q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-403 403v-166q0 -119 -84.5 -203.5t-203.5 -84.5h-704q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h704q119 0 203.5 -84.5t84.5 -203.5v-165l403 402q18 19 45 19q12 0 25 -5 q39 -17 39 -59z" /> -<glyph unicode="" horiz-adv-x="1920" d="M640 960q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 576v-448h-1408v192l320 320l160 -160l512 512zM1760 1280h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-1216q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5v1216 q0 13 -9.5 22.5t-22.5 9.5zM1920 1248v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" d="M363 0l91 91l-235 235l-91 -91v-107h128v-128h107zM886 928q0 22 -22 22q-10 0 -17 -7l-542 -542q-7 -7 -7 -17q0 -22 22 -22q10 0 17 7l542 542q7 7 7 17zM832 1120l416 -416l-832 -832h-416v416zM1515 1024q0 -53 -37 -90l-166 -166l-416 416l166 165q36 38 90 38 q53 0 91 -38l235 -234q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1024" d="M768 896q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1024 896q0 -109 -33 -179l-364 -774q-16 -33 -47.5 -52t-67.5 -19t-67.5 19t-46.5 52l-365 774q-33 70 -33 179q0 212 150 362t362 150t362 -150t150 -362z" /> -<glyph unicode="" d="M768 96v1088q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M512 384q0 36 -20 69q-1 1 -15.5 22.5t-25.5 38t-25 44t-21 50.5q-4 16 -21 16t-21 -16q-7 -23 -21 -50.5t-25 -44t-25.5 -38t-15.5 -22.5q-20 -33 -20 -69q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 512q0 -212 -150 -362t-362 -150t-362 150t-150 362 q0 145 81 275q6 9 62.5 90.5t101 151t99.5 178t83 201.5q9 30 34 47t51 17t51.5 -17t33.5 -47q28 -93 83 -201.5t99.5 -178t101 -151t62.5 -90.5q81 -127 81 -275z" /> -<glyph unicode="" horiz-adv-x="1792" d="M888 352l116 116l-152 152l-116 -116v-56h96v-96h56zM1328 1072q-16 16 -33 -1l-350 -350q-17 -17 -1 -33t33 1l350 350q17 17 1 33zM1408 478v-190q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-14 -14 -32 -8q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v126q0 13 9 22l64 64q15 15 35 7t20 -29zM1312 1216l288 -288l-672 -672h-288v288zM1756 1084l-92 -92 l-288 288l92 92q28 28 68 28t68 -28l152 -152q28 -28 28 -68t-28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 547v-259q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h255v0q13 0 22.5 -9.5t9.5 -22.5q0 -27 -26 -32q-77 -26 -133 -60q-10 -4 -16 -4h-112q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832 q66 0 113 47t47 113v214q0 19 18 29q28 13 54 37q16 16 35 8q21 -9 21 -29zM1645 1043l-384 -384q-18 -19 -45 -19q-12 0 -25 5q-39 17 -39 59v192h-160q-323 0 -438 -131q-119 -137 -74 -473q3 -23 -20 -34q-8 -2 -12 -2q-16 0 -26 13q-10 14 -21 31t-39.5 68.5t-49.5 99.5 t-38.5 114t-17.5 122q0 49 3.5 91t14 90t28 88t47 81.5t68.5 74t94.5 61.5t124.5 48.5t159.5 30.5t196.5 11h160v192q0 42 39 59q13 5 25 5q26 0 45 -19l384 -384q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1408 606v-318q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q63 0 117 -25q15 -7 18 -23q3 -17 -9 -29l-49 -49q-10 -10 -23 -10q-3 0 -9 2q-23 6 -45 6h-832q-66 0 -113 -47t-47 -113v-832 q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v254q0 13 9 22l64 64q10 10 23 10q6 0 12 -3q20 -8 20 -29zM1639 1095l-814 -814q-24 -24 -57 -24t-57 24l-430 430q-24 24 -24 57t24 57l110 110q24 24 57 24t57 -24l263 -263l647 647q24 24 57 24t57 -24l110 -110 q24 -24 24 -57t-24 -57z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-384v-384h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v384h-384v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45 t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h384v384h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45t-19 -45t-45 -19h-128v-384h384v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M979 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1747 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-9 9 -13 19v-678q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-678q4 11 13 19l710 710 q19 19 32 13t13 -32v-710q4 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1619 1395q19 19 32 13t13 -32v-1472q0 -26 -13 -32t-32 13l-710 710q-8 9 -13 19v-710q0 -26 -13 -32t-32 13l-710 710q-19 19 -19 45t19 45l710 710q19 19 32 13t13 -32v-710q5 11 13 19z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1384 609l-1328 -738q-23 -13 -39.5 -3t-16.5 36v1472q0 26 16.5 36t39.5 -3l1328 -738q23 -13 23 -31t-23 -31z" /> -<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45zM640 1344v-1408q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h512q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q19 -19 19 -45t-19 -45l-710 -710q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1792" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v710q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19l-710 -710 q-19 -19 -32 -13t-13 32v710q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1024" d="M45 -115q-19 -19 -32 -13t-13 32v1472q0 26 13 32t32 -13l710 -710q8 -8 13 -19v678q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-1408q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v678q-5 -10 -13 -19z" /> -<glyph unicode="" horiz-adv-x="1538" d="M14 557l710 710q19 19 45 19t45 -19l710 -710q19 -19 13 -32t-32 -13h-1472q-26 0 -32 13t13 32zM1473 0h-1408q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1408q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1171 1235l-531 -531l531 -531q19 -19 19 -45t-19 -45l-166 -166q-19 -19 -45 -19t-45 19l-742 742q-19 19 -19 45t19 45l742 742q19 19 45 19t45 -19l166 -166q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1107 659l-742 -742q-19 -19 -45 -19t-45 19l-166 166q-19 19 -19 45t19 45l531 531l-531 531q-19 19 -19 45t19 45l166 166q19 19 45 19t45 -19l742 -742q19 -19 19 -45t-19 -45z" /> -<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-256v256q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-256h-256q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h256v-256q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v256h256q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5 t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1216 576v128q0 26 -19 45t-45 19h-768q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h768q26 0 45 19t19 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" /> -<glyph unicode="" d="M1149 414q0 26 -19 45l-181 181l181 181q19 19 19 45q0 27 -19 46l-90 90q-19 19 -46 19q-26 0 -45 -19l-181 -181l-181 181q-19 19 -45 19q-27 0 -46 -19l-90 -90q-19 -19 -19 -46q0 -26 19 -45l181 -181l-181 -181q-19 -19 -19 -45q0 -27 19 -46l90 -90q19 -19 46 -19 q26 0 45 19l181 181l181 -181q19 -19 45 -19q27 0 46 19l90 90q19 19 19 46zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 802q0 28 -18 46l-91 90q-19 19 -45 19t-45 -19l-408 -407l-226 226q-19 19 -45 19t-45 -19l-91 -90q-18 -18 -18 -46q0 -27 18 -45l362 -362q19 -19 45 -19q27 0 46 19l543 543q18 18 18 45zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M896 160v192q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h192q14 0 23 9t9 23zM1152 832q0 88 -55.5 163t-138.5 116t-170 41q-243 0 -371 -213q-15 -24 8 -42l132 -100q7 -6 19 -6q16 0 25 12q53 68 86 92q34 24 86 24q48 0 85.5 -26t37.5 -59 q0 -38 -20 -61t-68 -45q-63 -28 -115.5 -86.5t-52.5 -125.5v-36q0 -14 9 -23t23 -9h192q14 0 23 9t9 23q0 19 21.5 49.5t54.5 49.5q32 18 49 28.5t46 35t44.5 48t28 60.5t12.5 81zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1024 160v160q0 14 -9 23t-23 9h-96v512q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h96v-320h-96q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23t23 -9h448q14 0 23 9t9 23zM896 1056v160q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-160q0 -14 9 -23 t23 -9h192q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1197 512h-109q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h109q-32 108 -112.5 188.5t-188.5 112.5v-109q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v109q-108 -32 -188.5 -112.5t-112.5 -188.5h109q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-109 q32 -108 112.5 -188.5t188.5 -112.5v109q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-109q108 32 188.5 112.5t112.5 188.5zM1536 704v-128q0 -26 -19 -45t-45 -19h-143q-37 -161 -154.5 -278.5t-278.5 -154.5v-143q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v143 q-161 37 -278.5 154.5t-154.5 278.5h-143q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h143q37 161 154.5 278.5t278.5 154.5v143q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-143q161 -37 278.5 -154.5t154.5 -278.5h143q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1097 457l-146 -146q-10 -10 -23 -10t-23 10l-137 137l-137 -137q-10 -10 -23 -10t-23 10l-146 146q-10 10 -10 23t10 23l137 137l-137 137q-10 10 -10 23t10 23l146 146q10 10 23 10t23 -10l137 -137l137 137q10 10 23 10t23 -10l146 -146q10 -10 10 -23t-10 -23 l-137 -137l137 -137q10 -10 10 -23t-10 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5 t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1171 723l-422 -422q-19 -19 -45 -19t-45 19l-294 294q-19 19 -19 45t19 45l102 102q19 19 45 19t45 -19l147 -147l275 275q19 19 45 19t45 -19l102 -102q19 -19 19 -45t-19 -45zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198 t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1312 643q0 161 -87 295l-754 -753q137 -89 297 -89q111 0 211.5 43.5t173.5 116.5t116 174.5t43 212.5zM313 344l755 754q-135 91 -300 91q-148 0 -273 -73t-198 -199t-73 -274q0 -162 89 -299zM1536 643q0 -157 -61 -300t-163.5 -246t-245 -164t-298.5 -61t-298.5 61 t-245 164t-163.5 246t-61 300t61 299.5t163.5 245.5t245 164t298.5 61t298.5 -61t245 -164t163.5 -245.5t61 -299.5z" /> -<glyph unicode="" d="M1536 640v-128q0 -53 -32.5 -90.5t-84.5 -37.5h-704l293 -294q38 -36 38 -90t-38 -90l-75 -76q-37 -37 -90 -37q-52 0 -91 37l-651 652q-37 37 -37 90q0 52 37 91l651 650q38 38 91 38q52 0 90 -38l75 -74q38 -38 38 -91t-38 -91l-293 -293h704q52 0 84.5 -37.5 t32.5 -90.5z" /> -<glyph unicode="" d="M1472 576q0 -54 -37 -91l-651 -651q-39 -37 -91 -37q-51 0 -90 37l-75 75q-38 38 -38 91t38 91l293 293h-704q-52 0 -84.5 37.5t-32.5 90.5v128q0 53 32.5 90.5t84.5 37.5h704l-293 294q-38 36 -38 90t38 90l75 75q38 38 90 38q53 0 91 -38l651 -651q37 -35 37 -90z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1611 565q0 -51 -37 -90l-75 -75q-38 -38 -91 -38q-54 0 -90 38l-294 293v-704q0 -52 -37.5 -84.5t-90.5 -32.5h-128q-53 0 -90.5 32.5t-37.5 84.5v704l-294 -293q-36 -38 -90 -38t-90 38l-75 75q-38 38 -38 90q0 53 38 91l651 651q35 37 90 37q54 0 91 -37l651 -651 q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1611 704q0 -53 -37 -90l-651 -652q-39 -37 -91 -37q-53 0 -90 37l-651 652q-38 36 -38 90q0 53 38 91l74 75q39 37 91 37q53 0 90 -37l294 -294v704q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-704l294 294q37 37 90 37q52 0 91 -37l75 -75q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 896q0 -26 -19 -45l-512 -512q-19 -19 -45 -19t-45 19t-19 45v256h-224q-98 0 -175.5 -6t-154 -21.5t-133 -42.5t-105.5 -69.5t-80 -101t-48.5 -138.5t-17.5 -181q0 -55 5 -123q0 -6 2.5 -23.5t2.5 -26.5q0 -15 -8.5 -25t-23.5 -10q-16 0 -28 17q-7 9 -13 22 t-13.5 30t-10.5 24q-127 285 -127 451q0 199 53 333q162 403 875 403h224v256q0 26 19 45t45 19t45 -19l512 -512q19 -19 19 -45z" /> -<glyph unicode="" d="M755 480q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23zM1536 1344v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332 q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M768 576v-448q0 -26 -19 -45t-45 -19t-45 19l-144 144l-332 -332q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l332 332l-144 144q-19 19 -19 45t19 45t45 19h448q26 0 45 -19t19 -45zM1523 1248q0 -13 -10 -23l-332 -332l144 -144q19 -19 19 -45t-19 -45 t-45 -19h-448q-26 0 -45 19t-19 45v448q0 26 19 45t45 19t45 -19l144 -144l332 332q10 10 23 10t23 -10l114 -114q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-416v-416q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v416h-416q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68 -28t28 -68v-416h416q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 800v-192q0 -40 -28 -68t-68 -28h-1216q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h1216q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1482 486q46 -26 59.5 -77.5t-12.5 -97.5l-64 -110q-26 -46 -77.5 -59.5t-97.5 12.5l-266 153v-307q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v307l-266 -153q-46 -26 -97.5 -12.5t-77.5 59.5l-64 110q-26 46 -12.5 97.5t59.5 77.5l266 154l-266 154 q-46 26 -59.5 77.5t12.5 97.5l64 110q26 46 77.5 59.5t97.5 -12.5l266 -153v307q0 52 38 90t90 38h128q52 0 90 -38t38 -90v-307l266 153q46 26 97.5 12.5t77.5 -59.5l64 -110q26 -46 12.5 -97.5t-59.5 -77.5l-266 -154z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM896 161v190q0 14 -9 23.5t-22 9.5h-192q-13 0 -23 -10t-10 -23v-190q0 -13 10 -23t23 -10h192 q13 0 22 9.5t9 23.5zM894 505l18 621q0 12 -10 18q-10 8 -24 8h-220q-14 0 -24 -8q-10 -6 -10 -18l17 -621q0 -10 10 -17.5t24 -7.5h185q14 0 23.5 7.5t10.5 17.5z" /> -<glyph unicode="" d="M928 180v56v468v192h-320v-192v-468v-56q0 -25 18 -38.5t46 -13.5h192q28 0 46 13.5t18 38.5zM472 1024h195l-126 161q-26 31 -69 31q-40 0 -68 -28t-28 -68t28 -68t68 -28zM1160 1120q0 40 -28 68t-68 28q-43 0 -69 -31l-125 -161h194q40 0 68 28t28 68zM1536 864v-320 q0 -14 -9 -23t-23 -9h-96v-416q0 -40 -28 -68t-68 -28h-1088q-40 0 -68 28t-28 68v416h-96q-14 0 -23 9t-9 23v320q0 14 9 23t23 9h440q-93 0 -158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5q107 0 168 -77l128 -165l128 165q61 77 168 77q93 0 158.5 -65.5t65.5 -158.5 t-65.5 -158.5t-158.5 -65.5h440q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19q-172 0 -318 -49.5t-259.5 -134t-235.5 -219.5q-19 -21 -19 -45q0 -26 19 -45t45 -19q24 0 45 19q27 24 74 71t67 66q137 124 268.5 176t313.5 52q26 0 45 19t19 45zM1792 1030q0 -95 -20 -193q-46 -224 -184.5 -383t-357.5 -268 q-214 -108 -438 -108q-148 0 -286 47q-15 5 -88 42t-96 37q-16 0 -39.5 -32t-45 -70t-52.5 -70t-60 -32q-30 0 -51 11t-31 24t-27 42q-2 4 -6 11t-5.5 10t-3 9.5t-1.5 13.5q0 35 31 73.5t68 65.5t68 56t31 48q0 4 -14 38t-16 44q-9 51 -9 104q0 115 43.5 220t119 184.5 t170.5 139t204 95.5q55 18 145 25.5t179.5 9t178.5 6t163.5 24t113.5 56.5l29.5 29.5t29.5 28t27 20t36.5 16t43.5 4.5q39 0 70.5 -46t47.5 -112t24 -124t8 -96z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 -160v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-1344q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h1344q13 0 22.5 -9.5t9.5 -22.5zM1152 896q0 -78 -24.5 -144t-64 -112.5t-87.5 -88t-96 -77.5t-87.5 -72t-64 -81.5t-24.5 -96.5q0 -96 67 -224l-4 1l1 -1 q-90 41 -160 83t-138.5 100t-113.5 122.5t-72.5 150.5t-27.5 184q0 78 24.5 144t64 112.5t87.5 88t96 77.5t87.5 72t64 81.5t24.5 96.5q0 94 -66 224l3 -1l-1 1q90 -41 160 -83t138.5 -100t113.5 -122.5t72.5 -150.5t27.5 -184z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 576q-152 236 -381 353q61 -104 61 -225q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 121 61 225q-229 -117 -381 -353q133 -205 333.5 -326.5t434.5 -121.5t434.5 121.5t333.5 326.5zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5 t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1792 576q0 -34 -20 -69q-140 -230 -376.5 -368.5t-499.5 -138.5t-499.5 139t-376.5 368q-20 35 -20 69t20 69q140 229 376.5 368t499.5 139t499.5 -139t376.5 -368q20 -35 20 -69z" /> -<glyph unicode="" horiz-adv-x="1792" d="M555 201l78 141q-87 63 -136 159t-49 203q0 121 61 225q-229 -117 -381 -353q167 -258 427 -375zM944 960q0 20 -14 34t-34 14q-125 0 -214.5 -89.5t-89.5 -214.5q0 -20 14 -34t34 -14t34 14t14 34q0 86 61 147t147 61q20 0 34 14t14 34zM1307 1151q0 -7 -1 -9 q-105 -188 -315 -566t-316 -567l-49 -89q-10 -16 -28 -16q-12 0 -134 70q-16 10 -16 28q0 12 44 87q-143 65 -263.5 173t-208.5 245q-20 31 -20 69t20 69q153 235 380 371t496 136q89 0 180 -17l54 97q10 16 28 16q5 0 18 -6t31 -15.5t33 -18.5t31.5 -18.5t19.5 -11.5 q16 -10 16 -27zM1344 704q0 -139 -79 -253.5t-209 -164.5l280 502q8 -45 8 -84zM1792 576q0 -35 -20 -69q-39 -64 -109 -145q-150 -172 -347.5 -267t-419.5 -95l74 132q212 18 392.5 137t301.5 307q-115 179 -282 294l63 112q95 -64 182.5 -153t144.5 -184q20 -34 20 -69z " /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 161v190q0 14 -9.5 23.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -23.5v-190q0 -14 9.5 -23.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 23.5zM1022 535l18 459q0 12 -10 19q-13 11 -24 11h-220q-11 0 -24 -11q-10 -7 -10 -21l17 -457q0 -10 10 -16.5t24 -6.5h185 q14 0 23.5 6.5t10.5 16.5zM1008 1469l768 -1408q35 -63 -2 -126q-17 -29 -46.5 -46t-63.5 -17h-1536q-34 0 -63.5 17t-46.5 46q-37 63 -2 126l768 1408q17 31 47 49t65 18t65 -18t47 -49z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1376 1376q44 -52 12 -148t-108 -172l-161 -161l160 -696q5 -19 -12 -33l-128 -96q-7 -6 -19 -6q-4 0 -7 1q-15 3 -21 16l-279 508l-259 -259l53 -194q5 -17 -8 -31l-96 -96q-9 -9 -23 -9h-2q-15 2 -24 13l-189 252l-252 189q-11 7 -13 23q-1 13 9 25l96 97q9 9 23 9 q6 0 8 -1l194 -53l259 259l-508 279q-14 8 -17 24q-2 16 9 27l128 128q14 13 30 8l665 -159l160 160q76 76 172 108t148 -12z" /> -<glyph unicode="" horiz-adv-x="1664" d="M128 -128h288v288h-288v-288zM480 -128h320v288h-320v-288zM128 224h288v320h-288v-320zM480 224h320v320h-320v-320zM128 608h288v288h-288v-288zM864 -128h320v288h-320v-288zM480 608h320v288h-320v-288zM1248 -128h288v288h-288v-288zM864 224h320v320h-320v-320z M512 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1248 224h288v320h-288v-320zM864 608h320v288h-320v-288zM1248 608h288v288h-288v-288zM1280 1088v288q0 13 -9.5 22.5t-22.5 9.5h-64 q-13 0 -22.5 -9.5t-9.5 -22.5v-288q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47 h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M666 1055q-60 -92 -137 -273q-22 45 -37 72.5t-40.5 63.5t-51 56.5t-63 35t-81.5 14.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q250 0 410 -225zM1792 256q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192q-32 0 -85 -0.5t-81 -1t-73 1 t-71 5t-64 10.5t-63 18.5t-58 28.5t-59 40t-55 53.5t-56 69.5q59 93 136 273q22 -45 37 -72.5t40.5 -63.5t51 -56.5t63 -35t81.5 -14.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1792 1152q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5 v192h-256q-48 0 -87 -15t-69 -45t-51 -61.5t-45 -77.5q-32 -62 -78 -171q-29 -66 -49.5 -111t-54 -105t-64 -100t-74 -83t-90 -68.5t-106.5 -42t-128 -16.5h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224q48 0 87 15t69 45t51 61.5t45 77.5q32 62 78 171q29 66 49.5 111 t54 105t64 100t74 83t90 68.5t106.5 42t128 16.5h256v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22q-17 -2 -30.5 9t-17.5 29v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281 q0 130 71 248.5t191 204.5t286 136.5t348 50.5q244 0 450 -85.5t326 -233t120 -321.5z" /> -<glyph unicode="" d="M1536 704v-128q0 -201 -98.5 -362t-274 -251.5t-395.5 -90.5t-395.5 90.5t-274 251.5t-98.5 362v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-128q0 -52 23.5 -90t53.5 -57t71 -30t64 -13t44 -2t44 2t64 13t71 30t53.5 57t23.5 90v128q0 26 19 45t45 19h384 q26 0 45 -19t19 -45zM512 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45zM1536 1344v-384q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h384q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1683 205l-166 -165q-19 -19 -45 -19t-45 19l-531 531l-531 -531q-19 -19 -45 -19t-45 19l-166 165q-19 19 -19 45.5t19 45.5l742 741q19 19 45 19t45 -19l742 -741q19 -19 19 -45.5t-19 -45.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1683 728l-742 -741q-19 -19 -45 -19t-45 19l-742 741q-19 19 -19 45.5t19 45.5l166 165q19 19 45 19t45 -19l531 -531l531 531q19 19 45 19t45 -19l166 -165q19 -19 19 -45.5t-19 -45.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " /> -<glyph unicode="" horiz-adv-x="1664" d="M640 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1536 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1664 1088v-512q0 -24 -16.5 -42.5t-40.5 -21.5l-1044 -122q13 -60 13 -70q0 -16 -24 -64h920q26 0 45 -19t19 -45 t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 11 8 31.5t16 36t21.5 40t15.5 29.5l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t19.5 -15.5t13 -24.5t8 -26t5.5 -29.5t4.5 -26h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" /> -<glyph unicode="" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-188v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-532q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960z" /> -<glyph unicode="" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" /> -<glyph unicode="" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1408 768q0 -139 -94 -257t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224 q0 139 94 257t256.5 186.5t353.5 68.5t353.5 -68.5t256.5 -186.5t94 -257zM1792 512q0 -120 -71 -224.5t-195 -176.5q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7 q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230z" /> -<glyph unicode="" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 768q0 51 -39 89.5t-89 38.5h-352q0 58 48 159.5t48 160.5q0 98 -32 145t-128 47q-26 -26 -38 -85t-30.5 -125.5t-59.5 -109.5q-22 -23 -77 -91q-4 -5 -23 -30t-31.5 -41t-34.5 -42.5 t-40 -44t-38.5 -35.5t-40 -27t-35.5 -9h-32v-640h32q13 0 31.5 -3t33 -6.5t38 -11t35 -11.5t35.5 -12.5t29 -10.5q211 -73 342 -73h121q192 0 192 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5q32 1 53.5 47t21.5 81zM1536 769 q0 -89 -49 -163q9 -33 9 -69q0 -77 -38 -144q3 -21 3 -43q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5h-36h-93q-96 0 -189.5 22.5t-216.5 65.5q-116 40 -138 40h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h274q36 24 137 155q58 75 107 128 q24 25 35.5 85.5t30.5 126.5t62 108q39 37 90 37q84 0 151 -32.5t102 -101.5t35 -186q0 -93 -48 -192h176q104 0 180 -76t76 -179z" /> -<glyph unicode="" d="M256 1088q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 512q0 35 -21.5 81t-53.5 47q15 17 25 47.5t10 55.5q0 69 -53 119q18 32 18 69t-17.5 73.5t-47.5 52.5q5 30 5 56q0 85 -49 126t-136 41h-128q-131 0 -342 -73q-5 -2 -29 -10.5 t-35.5 -12.5t-35 -11.5t-38 -11t-33 -6.5t-31.5 -3h-32v-640h32q16 0 35.5 -9t40 -27t38.5 -35.5t40 -44t34.5 -42.5t31.5 -41t23 -30q55 -68 77 -91q41 -43 59.5 -109.5t30.5 -125.5t38 -85q96 0 128 47t32 145q0 59 -48 160.5t-48 159.5h352q50 0 89 38.5t39 89.5z M1536 511q0 -103 -76 -179t-180 -76h-176q48 -99 48 -192q0 -118 -35 -186q-35 -69 -102 -101.5t-151 -32.5q-51 0 -90 37q-34 33 -54 82t-25.5 90.5t-17.5 84.5t-31 64q-48 50 -107 127q-101 131 -137 155h-274q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5 h288q22 0 138 40q128 44 223 66t200 22h112q140 0 226.5 -79t85.5 -216v-5q60 -77 60 -178q0 -22 -3 -43q38 -67 38 -144q0 -36 -9 -69q49 -74 49 -163z" /> -<glyph unicode="" horiz-adv-x="896" d="M832 1504v-1339l-449 -236q-22 -12 -40 -12q-21 0 -31.5 14.5t-10.5 35.5q0 6 2 20l86 500l-364 354q-25 27 -25 48q0 37 56 46l502 73l225 455q19 41 49 41z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 940q0 81 -21.5 143t-55 98.5t-81.5 59.5t-94 31t-98 8t-112 -25.5t-110.5 -64t-86.5 -72t-60 -61.5q-18 -22 -49 -22t-49 22q-24 28 -60 61.5t-86.5 72t-110.5 64t-112 25.5t-98 -8t-94 -31t-81.5 -59.5t-55 -98.5t-21.5 -143q0 -168 187 -355l581 -560l580 559 q188 188 188 356zM1792 940q0 -221 -229 -450l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-10 8 -27.5 26t-55.5 65.5t-68 97.5t-53.5 121t-23.5 138q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5 q224 0 351 -124t127 -344z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 96q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-119 0 -203.5 84.5t-84.5 203.5v704q0 119 84.5 203.5t203.5 84.5h320q13 0 22.5 -9.5t9.5 -22.5q0 -4 1 -20t0.5 -26.5t-3 -23.5t-10 -19.5t-20.5 -6.5h-320q-66 0 -113 -47t-47 -113v-704 q0 -66 47 -113t113 -47h288h11h13t11.5 -1t11.5 -3t8 -5.5t7 -9t2 -13.5zM1568 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45z" /> -<glyph unicode="" d="M237 122h231v694h-231v-694zM483 1030q-1 52 -36 86t-93 34t-94.5 -34t-36.5 -86q0 -51 35.5 -85.5t92.5 -34.5h1q59 0 95 34.5t36 85.5zM1068 122h231v398q0 154 -73 233t-193 79q-136 0 -209 -117h2v101h-231q3 -66 0 -694h231v388q0 38 7 56q15 35 45 59.5t74 24.5 q116 0 116 -157v-371zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1152" d="M480 672v448q0 14 -9 23t-23 9t-23 -9t-9 -23v-448q0 -14 9 -23t23 -9t23 9t9 23zM1152 320q0 -26 -19 -45t-45 -19h-429l-51 -483q-2 -12 -10.5 -20.5t-20.5 -8.5h-1q-27 0 -32 27l-76 485h-404q-26 0 -45 19t-19 45q0 123 78.5 221.5t177.5 98.5v512q-52 0 -90 38 t-38 90t38 90t90 38h640q52 0 90 -38t38 -90t-38 -90t-90 -38v-512q99 0 177.5 -98.5t78.5 -221.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" /> -<glyph unicode="" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" /> -<glyph unicode="" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1120 1280h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v832q0 66 -47 113t-113 47zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832 q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 1280h-1024v-1242l423 406l89 85l89 -85l423 -406v1242zM1164 1408q23 0 44 -9q33 -13 52.5 -41t19.5 -62v-1289q0 -34 -19.5 -62t-52.5 -41q-19 -8 -44 -8q-48 0 -83 32l-441 424l-441 -424q-36 -33 -83 -33q-23 0 -44 9q-33 13 -52.5 41t-19.5 62v1289 q0 34 19.5 62t52.5 41q21 9 44 9h1048z" /> -<glyph unicode="" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" /> -<glyph unicode="" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" /> -<glyph unicode="" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" /> -<glyph unicode="" d="M1040 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1296 320q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5zM1408 160v320q0 13 -9.5 22.5t-22.5 9.5 h-1216q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h1216q13 0 22.5 9.5t9.5 22.5zM178 640h1180l-157 482q-4 13 -16 21.5t-26 8.5h-782q-14 0 -26 -8.5t-16 -21.5zM1536 480v-320q0 -66 -47 -113t-113 -47h-1216q-66 0 -113 47t-47 113v320q0 25 16 75 l197 606q17 53 63 86t101 33h782q55 0 101 -33t63 -86l197 -606q16 -50 16 -75z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 896q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5v-384q0 -52 -38 -90t-90 -38q-417 347 -812 380q-58 -19 -91 -66t-31 -100.5t40 -92.5q-20 -33 -23 -65.5t6 -58t33.5 -55t48 -50t61.5 -50.5q-29 -58 -111.5 -83t-168.5 -11.5t-132 55.5q-7 23 -29.5 87.5 t-32 94.5t-23 89t-15 101t3.5 98.5t22 110.5h-122q-66 0 -113 47t-47 113v192q0 66 47 113t113 47h480q435 0 896 384q52 0 90 -38t38 -90v-384zM1536 292v954q-394 -302 -768 -343v-270q377 -42 768 -341z" /> -<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM246 128h1300q-266 300 -266 832q0 51 -24 105t-69 103t-121.5 80.5t-169.5 31.5t-169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -532 -266 -832z M1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5 t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> -<glyph unicode="" d="M1376 640l138 -135q30 -28 20 -70q-12 -41 -52 -51l-188 -48l53 -186q12 -41 -19 -70q-29 -31 -70 -19l-186 53l-48 -188q-10 -40 -51 -52q-12 -2 -19 -2q-31 0 -51 22l-135 138l-135 -138q-28 -30 -70 -20q-41 11 -51 52l-48 188l-186 -53q-41 -12 -70 19q-31 29 -19 70 l53 186l-188 48q-40 10 -52 51q-10 42 20 70l138 135l-138 135q-30 28 -20 70q12 41 52 51l188 48l-53 186q-12 41 19 70q29 31 70 19l186 -53l48 188q10 41 51 51q41 12 70 -19l135 -139l135 139q29 30 70 19q41 -10 51 -51l48 -188l186 53q41 12 70 -19q31 -29 19 -70 l-53 -186l188 -48q40 -10 52 -51q10 -42 -20 -70z" /> -<glyph unicode="" horiz-adv-x="1792" d="M256 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 768q0 51 -39 89.5t-89 38.5h-576q0 20 15 48.5t33 55t33 68t15 84.5q0 67 -44.5 97.5t-115.5 30.5q-24 0 -90 -139q-24 -44 -37 -65q-40 -64 -112 -145q-71 -81 -101 -106 q-69 -57 -140 -57h-32v-640h32q72 0 167 -32t193.5 -64t179.5 -32q189 0 189 167q0 26 -5 56q30 16 47.5 52.5t17.5 73.5t-18 69q53 50 53 119q0 25 -10 55.5t-25 47.5h331q52 0 90 38t38 90zM1792 769q0 -105 -75.5 -181t-180.5 -76h-169q-4 -62 -37 -119q3 -21 3 -43 q0 -101 -60 -178q1 -139 -85 -219.5t-227 -80.5q-133 0 -322 69q-164 59 -223 59h-288q-53 0 -90.5 37.5t-37.5 90.5v640q0 53 37.5 90.5t90.5 37.5h288q10 0 21.5 4.5t23.5 14t22.5 18t24 22.5t20.5 21.5t19 21.5t14 17q65 74 100 129q13 21 33 62t37 72t40.5 63t55 49.5 t69.5 17.5q125 0 206.5 -67t81.5 -189q0 -68 -22 -128h374q104 0 180 -76t76 -179z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1376 128h32v640h-32q-35 0 -67.5 12t-62.5 37t-50 46t-49 54q-2 3 -3.5 4.5t-4 4.5t-4.5 5q-72 81 -112 145q-14 22 -38 68q-1 3 -10.5 22.5t-18.5 36t-20 35.5t-21.5 30.5t-18.5 11.5q-71 0 -115.5 -30.5t-44.5 -97.5q0 -43 15 -84.5t33 -68t33 -55t15 -48.5h-576 q-50 0 -89 -38.5t-39 -89.5q0 -52 38 -90t90 -38h331q-15 -17 -25 -47.5t-10 -55.5q0 -69 53 -119q-18 -32 -18 -69t17.5 -73.5t47.5 -52.5q-4 -24 -4 -56q0 -85 48.5 -126t135.5 -41q84 0 183 32t194 64t167 32zM1664 192q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45 t45 -19t45 19t19 45zM1792 768v-640q0 -53 -37.5 -90.5t-90.5 -37.5h-288q-59 0 -223 -59q-190 -69 -317 -69q-142 0 -230 77.5t-87 217.5l1 5q-61 76 -61 178q0 22 3 43q-33 57 -37 119h-169q-105 0 -180.5 76t-75.5 181q0 103 76 179t180 76h374q-22 60 -22 128 q0 122 81.5 189t206.5 67q38 0 69.5 -17.5t55 -49.5t40.5 -63t37 -72t33 -62q35 -55 100 -129q2 -3 14 -17t19 -21.5t20.5 -21.5t24 -22.5t22.5 -18t23.5 -14t21.5 -4.5h288q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" d="M1280 -64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 700q0 189 -167 189q-26 0 -56 -5q-16 30 -52.5 47.5t-73.5 17.5t-69 -18q-50 53 -119 53q-25 0 -55.5 -10t-47.5 -25v331q0 52 -38 90t-90 38q-51 0 -89.5 -39t-38.5 -89v-576 q-20 0 -48.5 15t-55 33t-68 33t-84.5 15q-67 0 -97.5 -44.5t-30.5 -115.5q0 -24 139 -90q44 -24 65 -37q64 -40 145 -112q81 -71 106 -101q57 -69 57 -140v-32h640v32q0 72 32 167t64 193.5t32 179.5zM1536 705q0 -133 -69 -322q-59 -164 -59 -223v-288q0 -53 -37.5 -90.5 t-90.5 -37.5h-640q-53 0 -90.5 37.5t-37.5 90.5v288q0 10 -4.5 21.5t-14 23.5t-18 22.5t-22.5 24t-21.5 20.5t-21.5 19t-17 14q-74 65 -129 100q-21 13 -62 33t-72 37t-63 40.5t-49.5 55t-17.5 69.5q0 125 67 206.5t189 81.5q68 0 128 -22v374q0 104 76 180t179 76 q105 0 181 -75.5t76 -180.5v-169q62 -4 119 -37q21 3 43 3q101 0 178 -60q139 1 219.5 -85t80.5 -227z" /> -<glyph unicode="" d="M1408 576q0 84 -32 183t-64 194t-32 167v32h-640v-32q0 -35 -12 -67.5t-37 -62.5t-46 -50t-54 -49q-9 -8 -14 -12q-81 -72 -145 -112q-22 -14 -68 -38q-3 -1 -22.5 -10.5t-36 -18.5t-35.5 -20t-30.5 -21.5t-11.5 -18.5q0 -71 30.5 -115.5t97.5 -44.5q43 0 84.5 15t68 33 t55 33t48.5 15v-576q0 -50 38.5 -89t89.5 -39q52 0 90 38t38 90v331q46 -35 103 -35q69 0 119 53q32 -18 69 -18t73.5 17.5t52.5 47.5q24 -4 56 -4q85 0 126 48.5t41 135.5zM1280 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 580 q0 -142 -77.5 -230t-217.5 -87l-5 1q-76 -61 -178 -61q-22 0 -43 3q-54 -30 -119 -37v-169q0 -105 -76 -180.5t-181 -75.5q-103 0 -179 76t-76 180v374q-54 -22 -128 -22q-121 0 -188.5 81.5t-67.5 206.5q0 38 17.5 69.5t49.5 55t63 40.5t72 37t62 33q55 35 129 100 q3 2 17 14t21.5 19t21.5 20.5t22.5 24t18 22.5t14 23.5t4.5 21.5v288q0 53 37.5 90.5t90.5 37.5h640q53 0 90.5 -37.5t37.5 -90.5v-288q0 -59 59 -223q69 -190 69 -317z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-502l189 189q19 19 19 45t-19 45l-91 91q-18 18 -45 18t-45 -18l-362 -362l-91 -91q-18 -18 -18 -45t18 -45l91 -91l362 -362q18 -18 45 -18t45 18l91 91q18 18 18 45t-18 45l-189 189h502q26 0 45 19t19 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1285 640q0 27 -18 45l-91 91l-362 362q-18 18 -45 18t-45 -18l-91 -91q-18 -18 -18 -45t18 -45l189 -189h-502q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h502l-189 -189q-19 -19 -19 -45t19 -45l91 -91q18 -18 45 -18t45 18l362 362l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 641q0 27 -18 45l-362 362l-91 91q-18 18 -45 18t-45 -18l-91 -91l-362 -362q-18 -18 -18 -45t18 -45l91 -91q18 -18 45 -18t45 18l189 189v-502q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v502l189 -189q19 -19 45 -19t45 19l91 91q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1284 639q0 27 -18 45l-91 91q-18 18 -45 18t-45 -18l-189 -189v502q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-502l-189 189q-19 19 -45 19t-45 -19l-91 -91q-18 -18 -18 -45t18 -45l362 -362l91 -91q18 -18 45 -18t45 18l91 91l362 362q18 18 18 45zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1042 887q-2 -1 -9.5 -9.5t-13.5 -9.5q2 0 4.5 5t5 11t3.5 7q6 7 22 15q14 6 52 12q34 8 51 -11 q-2 2 9.5 13t14.5 12q3 2 15 4.5t15 7.5l2 22q-12 -1 -17.5 7t-6.5 21q0 -2 -6 -8q0 7 -4.5 8t-11.5 -1t-9 -1q-10 3 -15 7.5t-8 16.5t-4 15q-2 5 -9.5 10.5t-9.5 10.5q-1 2 -2.5 5.5t-3 6.5t-4 5.5t-5.5 2.5t-7 -5t-7.5 -10t-4.5 -5q-3 2 -6 1.5t-4.5 -1t-4.5 -3t-5 -3.5 q-3 -2 -8.5 -3t-8.5 -2q15 5 -1 11q-10 4 -16 3q9 4 7.5 12t-8.5 14h5q-1 4 -8.5 8.5t-17.5 8.5t-13 6q-8 5 -34 9.5t-33 0.5q-5 -6 -4.5 -10.5t4 -14t3.5 -12.5q1 -6 -5.5 -13t-6.5 -12q0 -7 14 -15.5t10 -21.5q-3 -8 -16 -16t-16 -12q-5 -8 -1.5 -18.5t10.5 -16.5 q2 -2 1.5 -4t-3.5 -4.5t-5.5 -4t-6.5 -3.5l-3 -2q-11 -5 -20.5 6t-13.5 26q-7 25 -16 30q-23 8 -29 -1q-5 13 -41 26q-25 9 -58 4q6 1 0 15q-7 15 -19 12q3 6 4 17.5t1 13.5q3 13 12 23q1 1 7 8.5t9.5 13.5t0.5 6q35 -4 50 11q5 5 11.5 17t10.5 17q9 6 14 5.5t14.5 -5.5 t14.5 -5q14 -1 15.5 11t-7.5 20q12 -1 3 17q-5 7 -8 9q-12 4 -27 -5q-8 -4 2 -8q-1 1 -9.5 -10.5t-16.5 -17.5t-16 5q-1 1 -5.5 13.5t-9.5 13.5q-8 0 -16 -15q3 8 -11 15t-24 8q19 12 -8 27q-7 4 -20.5 5t-19.5 -4q-5 -7 -5.5 -11.5t5 -8t10.5 -5.5t11.5 -4t8.5 -3 q14 -10 8 -14q-2 -1 -8.5 -3.5t-11.5 -4.5t-6 -4q-3 -4 0 -14t-2 -14q-5 5 -9 17.5t-7 16.5q7 -9 -25 -6l-10 1q-4 0 -16 -2t-20.5 -1t-13.5 8q-4 8 0 20q1 4 4 2q-4 3 -11 9.5t-10 8.5q-46 -15 -94 -41q6 -1 12 1q5 2 13 6.5t10 5.5q34 14 42 7l5 5q14 -16 20 -25 q-7 4 -30 1q-20 -6 -22 -12q7 -12 5 -18q-4 3 -11.5 10t-14.5 11t-15 5q-16 0 -22 -1q-146 -80 -235 -222q7 -7 12 -8q4 -1 5 -9t2.5 -11t11.5 3q9 -8 3 -19q1 1 44 -27q19 -17 21 -21q3 -11 -10 -18q-1 2 -9 9t-9 4q-3 -5 0.5 -18.5t10.5 -12.5q-7 0 -9.5 -16t-2.5 -35.5 t-1 -23.5l2 -1q-3 -12 5.5 -34.5t21.5 -19.5q-13 -3 20 -43q6 -8 8 -9q3 -2 12 -7.5t15 -10t10 -10.5q4 -5 10 -22.5t14 -23.5q-2 -6 9.5 -20t10.5 -23q-1 0 -2.5 -1t-2.5 -1q3 -7 15.5 -14t15.5 -13q1 -3 2 -10t3 -11t8 -2q2 20 -24 62q-15 25 -17 29q-3 5 -5.5 15.5 t-4.5 14.5q2 0 6 -1.5t8.5 -3.5t7.5 -4t2 -3q-3 -7 2 -17.5t12 -18.5t17 -19t12 -13q6 -6 14 -19.5t0 -13.5q9 0 20 -10t17 -20q5 -8 8 -26t5 -24q2 -7 8.5 -13.5t12.5 -9.5l16 -8t13 -7q5 -2 18.5 -10.5t21.5 -11.5q10 -4 16 -4t14.5 2.5t13.5 3.5q15 2 29 -15t21 -21 q36 -19 55 -11q-2 -1 0.5 -7.5t8 -15.5t9 -14.5t5.5 -8.5q5 -6 18 -15t18 -15q6 4 7 9q-3 -8 7 -20t18 -10q14 3 14 32q-31 -15 -49 18q0 1 -2.5 5.5t-4 8.5t-2.5 8.5t0 7.5t5 3q9 0 10 3.5t-2 12.5t-4 13q-1 8 -11 20t-12 15q-5 -9 -16 -8t-16 9q0 -1 -1.5 -5.5t-1.5 -6.5 q-13 0 -15 1q1 3 2.5 17.5t3.5 22.5q1 4 5.5 12t7.5 14.5t4 12.5t-4.5 9.5t-17.5 2.5q-19 -1 -26 -20q-1 -3 -3 -10.5t-5 -11.5t-9 -7q-7 -3 -24 -2t-24 5q-13 8 -22.5 29t-9.5 37q0 10 2.5 26.5t3 25t-5.5 24.5q3 2 9 9.5t10 10.5q2 1 4.5 1.5t4.5 0t4 1.5t3 6q-1 1 -4 3 q-3 3 -4 3q7 -3 28.5 1.5t27.5 -1.5q15 -11 22 2q0 1 -2.5 9.5t-0.5 13.5q5 -27 29 -9q3 -3 15.5 -5t17.5 -5q3 -2 7 -5.5t5.5 -4.5t5 0.5t8.5 6.5q10 -14 12 -24q11 -40 19 -44q7 -3 11 -2t4.5 9.5t0 14t-1.5 12.5l-1 8v18l-1 8q-15 3 -18.5 12t1.5 18.5t15 18.5q1 1 8 3.5 t15.5 6.5t12.5 8q21 19 15 35q7 0 11 9q-1 0 -5 3t-7.5 5t-4.5 2q9 5 2 16q5 3 7.5 11t7.5 10q9 -12 21 -2q7 8 1 16q5 7 20.5 10.5t18.5 9.5q7 -2 8 2t1 12t3 12q4 5 15 9t13 5l17 11q3 4 0 4q18 -2 31 11q10 11 -6 20q3 6 -3 9.5t-15 5.5q3 1 11.5 0.5t10.5 1.5 q15 10 -7 16q-17 5 -43 -12zM879 10q206 36 351 189q-3 3 -12.5 4.5t-12.5 3.5q-18 7 -24 8q1 7 -2.5 13t-8 9t-12.5 8t-11 7q-2 2 -7 6t-7 5.5t-7.5 4.5t-8.5 2t-10 -1l-3 -1q-3 -1 -5.5 -2.5t-5.5 -3t-4 -3t0 -2.5q-21 17 -36 22q-5 1 -11 5.5t-10.5 7t-10 1.5t-11.5 -7 q-5 -5 -6 -15t-2 -13q-7 5 0 17.5t2 18.5q-3 6 -10.5 4.5t-12 -4.5t-11.5 -8.5t-9 -6.5t-8.5 -5.5t-8.5 -7.5q-3 -4 -6 -12t-5 -11q-2 4 -11.5 6.5t-9.5 5.5q2 -10 4 -35t5 -38q7 -31 -12 -48q-27 -25 -29 -40q-4 -22 12 -26q0 -7 -8 -20.5t-7 -21.5q0 -6 2 -16z" /> -<glyph unicode="" horiz-adv-x="1664" d="M384 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1028 484l-682 -682q-37 -37 -90 -37q-52 0 -91 37l-106 108q-38 36 -38 90q0 53 38 91l681 681q39 -98 114.5 -173.5t173.5 -114.5zM1662 919q0 -39 -23 -106q-47 -134 -164.5 -217.5 t-258.5 -83.5q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q58 0 121.5 -16.5t107.5 -46.5q16 -11 16 -28t-16 -28l-293 -169v-224l193 -107q5 3 79 48.5t135.5 81t70.5 35.5q15 0 23.5 -10t8.5 -25z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 128h640v128h-640v-128zM640 640h1024v128h-1024v-128zM1280 1152h384v128h-384v-128zM1792 320v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 832v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19 t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45zM1792 1344v-256q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1664q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1403 1241q17 -41 -14 -70l-493 -493v-742q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-256 256q-19 19 -19 45v486l-493 493q-31 29 -14 70q17 39 59 39h1280q42 0 59 -39z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 1280h512v128h-512v-128zM1792 640v-480q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v480h672v-160q0 -26 19 -45t45 -19h320q26 0 45 19t19 45v160h672zM1024 640v-128h-256v128h256zM1792 1120v-384h-1792v384q0 66 47 113t113 47h352v160q0 40 28 68 t68 28h576q40 0 68 -28t28 -68v-160h352q66 0 113 -47t47 -113z" /> -<glyph unicode="" d="M1283 995l-355 -355l355 -355l144 144q29 31 70 14q39 -17 39 -59v-448q0 -26 -19 -45t-45 -19h-448q-42 0 -59 40q-17 39 14 69l144 144l-355 355l-355 -355l144 -144q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l144 -144 l355 355l-355 355l-144 -144q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v448q0 26 19 45t45 19h448q42 0 59 -40q17 -39 -14 -69l-144 -144l355 -355l355 355l-144 144q-31 30 -14 69q17 40 59 40h448q26 0 45 -19t19 -45v-448q0 -42 -39 -59q-13 -5 -25 -5q-26 0 -45 19z " /> -<glyph unicode="" horiz-adv-x="1920" d="M593 640q-162 -5 -265 -128h-134q-82 0 -138 40.5t-56 118.5q0 353 124 353q6 0 43.5 -21t97.5 -42.5t119 -21.5q67 0 133 23q-5 -37 -5 -66q0 -139 81 -256zM1664 3q0 -120 -73 -189.5t-194 -69.5h-874q-121 0 -194 69.5t-73 189.5q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q10 0 43 -21.5t73 -48t107 -48t135 -21.5t135 21.5t107 48t73 48t43 21.5q61 0 111.5 -20t85.5 -53.5t62 -81t43 -97.5t26.5 -108.5t14 -109t3.5 -103.5zM640 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75 t75 -181zM1344 896q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5zM1920 671q0 -78 -56 -118.5t-138 -40.5h-134q-103 123 -265 128q81 117 81 256q0 29 -5 66q66 -23 133 -23q59 0 119 21.5t97.5 42.5 t43.5 21q124 0 124 -353zM1792 1280q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1456 320q0 40 -28 68l-208 208q-28 28 -68 28q-42 0 -72 -32q3 -3 19 -18.5t21.5 -21.5t15 -19t13 -25.5t3.5 -27.5q0 -40 -28 -68t-68 -28q-15 0 -27.5 3.5t-25.5 13t-19 15t-21.5 21.5t-18.5 19q-33 -31 -33 -73q0 -40 28 -68l206 -207q27 -27 68 -27q40 0 68 26 l147 146q28 28 28 67zM753 1025q0 40 -28 68l-206 207q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l208 -208q27 -27 68 -27q42 0 72 31q-3 3 -19 18.5t-21.5 21.5t-15 19t-13 25.5t-3.5 27.5q0 40 28 68t68 28q15 0 27.5 -3.5t25.5 -13t19 -15 t21.5 -21.5t18.5 -19q33 31 33 73zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-206 207q-83 83 -83 203q0 123 88 209l-88 88q-86 -88 -208 -88q-120 0 -204 84l-208 208q-84 84 -84 204t85 203l147 146q83 83 203 83q121 0 204 -85l206 -207 q83 -83 83 -203q0 -123 -88 -209l88 -88q86 88 208 88q120 0 204 -84l208 -208q84 -84 84 -204z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088q-185 0 -316.5 131.5t-131.5 316.5q0 132 71 241.5t187 163.5q-2 28 -2 43q0 212 150 362t362 150q158 0 286.5 -88t187.5 -230q70 62 166 62q106 0 181 -75t75 -181q0 -75 -41 -138q129 -30 213 -134.5t84 -239.5z " /> -<glyph unicode="" horiz-adv-x="1664" d="M1527 88q56 -89 21.5 -152.5t-140.5 -63.5h-1152q-106 0 -140.5 63.5t21.5 152.5l503 793v399h-64q-26 0 -45 19t-19 45t19 45t45 19h512q26 0 45 -19t19 -45t-19 -45t-45 -19h-64v-399zM748 813l-272 -429h712l-272 429l-20 31v37v399h-128v-399v-37z" /> -<glyph unicode="" horiz-adv-x="1792" d="M960 640q26 0 45 -19t19 -45t-19 -45t-45 -19t-45 19t-19 45t19 45t45 19zM1260 576l507 -398q28 -20 25 -56q-5 -35 -35 -51l-128 -64q-13 -7 -29 -7q-17 0 -31 8l-690 387l-110 -66q-8 -4 -12 -5q14 -49 10 -97q-7 -77 -56 -147.5t-132 -123.5q-132 -84 -277 -84 q-136 0 -222 78q-90 84 -79 207q7 76 56 147t131 124q132 84 278 84q83 0 151 -31q9 13 22 22l122 73l-122 73q-13 9 -22 22q-68 -31 -151 -31q-146 0 -278 84q-82 53 -131 124t-56 147q-5 59 15.5 113t63.5 93q85 79 222 79q145 0 277 -84q83 -52 132 -123t56 -148 q4 -48 -10 -97q4 -1 12 -5l110 -66l690 387q14 8 31 8q16 0 29 -7l128 -64q30 -16 35 -51q3 -36 -25 -56zM579 836q46 42 21 108t-106 117q-92 59 -192 59q-74 0 -113 -36q-46 -42 -21 -108t106 -117q92 -59 192 -59q74 0 113 36zM494 91q81 51 106 117t-21 108 q-39 36 -113 36q-100 0 -192 -59q-81 -51 -106 -117t21 -108q39 -36 113 -36q100 0 192 59zM672 704l96 -58v11q0 36 33 56l14 8l-79 47l-26 -26q-3 -3 -10 -11t-12 -12q-2 -2 -4 -3.5t-3 -2.5zM896 480l96 -32l736 576l-128 64l-768 -431v-113l-160 -96l9 -8q2 -2 7 -6 q4 -4 11 -12t11 -12l26 -26zM1600 64l128 64l-520 408l-177 -138q-2 -3 -13 -7z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1696 1152q40 0 68 -28t28 -68v-1216q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v288h-544q-40 0 -68 28t-28 68v672q0 40 20 88t48 76l408 408q28 28 76 48t88 20h416q40 0 68 -28t28 -68v-328q68 40 128 40h416zM1152 939l-299 -299h299v299zM512 1323l-299 -299 h299v299zM708 676l316 316v416h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h512v256q0 40 20 88t48 76zM1664 -128v1152h-384v-416q0 -40 -28 -68t-68 -28h-416v-640h896z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1404 151q0 -117 -79 -196t-196 -79q-135 0 -235 100l-777 776q-113 115 -113 271q0 159 110 270t269 111q158 0 273 -113l605 -606q10 -10 10 -22q0 -16 -30.5 -46.5t-46.5 -30.5q-13 0 -23 10l-606 607q-79 77 -181 77q-106 0 -179 -75t-73 -181q0 -105 76 -181 l776 -777q63 -63 145 -63q64 0 106 42t42 106q0 82 -63 145l-581 581q-26 24 -60 24q-29 0 -48 -19t-19 -48q0 -32 25 -59l410 -410q10 -10 10 -22q0 -16 -31 -47t-47 -31q-12 0 -22 10l-410 410q-63 61 -63 149q0 82 57 139t139 57q88 0 149 -63l581 -581q100 -98 100 -235 z" /> -<glyph unicode="" d="M384 0h768v384h-768v-384zM1280 0h128v896q0 14 -10 38.5t-20 34.5l-281 281q-10 10 -34 20t-39 10v-416q0 -40 -28 -68t-68 -28h-576q-40 0 -68 28t-28 68v416h-128v-1280h128v416q0 40 28 68t68 28h832q40 0 68 -28t28 -68v-416zM896 928v320q0 13 -9.5 22.5t-22.5 9.5 h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5zM1536 896v-928q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h928q40 0 88 -20t76 -48l280 -280q28 -28 48 -76t20 -88z" /> -<glyph unicode="" d="M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1536 192v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 704v-128q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1536 1216v-128q0 -26 -19 -45 t-45 -19h-1408q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 128q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 640q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 224v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5 t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1152q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z M1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M381 -84q0 -80 -54.5 -126t-135.5 -46q-106 0 -172 66l57 88q49 -45 106 -45q29 0 50.5 14.5t21.5 42.5q0 64 -105 56l-26 56q8 10 32.5 43.5t42.5 54t37 38.5v1q-16 0 -48.5 -1t-48.5 -1v-53h-106v152h333v-88l-95 -115q51 -12 81 -49t30 -88zM383 543v-159h-362 q-6 36 -6 54q0 51 23.5 93t56.5 68t66 47.5t56.5 43.5t23.5 45q0 25 -14.5 38.5t-39.5 13.5q-46 0 -81 -58l-85 59q24 51 71.5 79.5t105.5 28.5q73 0 123 -41.5t50 -112.5q0 -50 -34 -91.5t-75 -64.5t-75.5 -50.5t-35.5 -52.5h127v60h105zM1792 224v-192q0 -13 -9.5 -22.5 t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM384 1123v-99h-335v99h107q0 41 0.5 122t0.5 121v12h-2q-8 -17 -50 -54l-71 76l136 127h106v-404h108zM1792 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5 t-9.5 22.5v192q0 14 9 23t23 9h1216q13 0 22.5 -9.5t9.5 -22.5zM1792 1248v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1216q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1216q13 0 22.5 -9.5t9.5 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1760 640q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1728q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h1728zM483 704q-28 35 -51 80q-48 97 -48 188q0 181 134 309q133 127 393 127q50 0 167 -19q66 -12 177 -48q10 -38 21 -118q14 -123 14 -183q0 -18 -5 -45l-12 -3l-84 6 l-14 2q-50 149 -103 205q-88 91 -210 91q-114 0 -182 -59q-67 -58 -67 -146q0 -73 66 -140t279 -129q69 -20 173 -66q58 -28 95 -52h-743zM990 448h411q7 -39 7 -92q0 -111 -41 -212q-23 -55 -71 -104q-37 -35 -109 -81q-80 -48 -153 -66q-80 -21 -203 -21q-114 0 -195 23 l-140 40q-57 16 -72 28q-8 8 -8 22v13q0 108 -2 156q-1 30 0 68l2 37v44l102 2q15 -34 30 -71t22.5 -56t12.5 -27q35 -57 80 -94q43 -36 105 -57q59 -22 132 -22q64 0 139 27q77 26 122 86q47 61 47 129q0 84 -81 157q-34 29 -137 71z" /> -<glyph unicode="" d="M48 1313q-37 2 -45 4l-3 88q13 1 40 1q60 0 112 -4q132 -7 166 -7q86 0 168 3q116 4 146 5q56 0 86 2l-1 -14l2 -64v-9q-60 -9 -124 -9q-60 0 -79 -25q-13 -14 -13 -132q0 -13 0.5 -32.5t0.5 -25.5l1 -229l14 -280q6 -124 51 -202q35 -59 96 -92q88 -47 177 -47 q104 0 191 28q56 18 99 51q48 36 65 64q36 56 53 114q21 73 21 229q0 79 -3.5 128t-11 122.5t-13.5 159.5l-4 59q-5 67 -24 88q-34 35 -77 34l-100 -2l-14 3l2 86h84l205 -10q76 -3 196 10l18 -2q6 -38 6 -51q0 -7 -4 -31q-45 -12 -84 -13q-73 -11 -79 -17q-15 -15 -15 -41 q0 -7 1.5 -27t1.5 -31q8 -19 22 -396q6 -195 -15 -304q-15 -76 -41 -122q-38 -65 -112 -123q-75 -57 -182 -89q-109 -33 -255 -33q-167 0 -284 46q-119 47 -179 122q-61 76 -83 195q-16 80 -16 237v333q0 188 -17 213q-25 36 -147 39zM1536 -96v64q0 14 -9 23t-23 9h-1472 q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h1472q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M512 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 160v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23 v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM512 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 160v192 q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1024 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 544v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192 q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1536 928v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM1664 1248v-1088q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1344q66 0 113 -47t47 -113 z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1190 955l293 293l-107 107l-293 -293zM1637 1248q0 -27 -18 -45l-1286 -1286q-18 -18 -45 -18t-45 18l-198 198q-18 18 -18 45t18 45l1286 1286q18 18 45 18t45 -18l198 -198q18 -18 18 -45zM286 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM636 1276 l196 -60l-196 -60l-60 -196l-60 196l-196 60l196 60l60 196zM1566 798l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98zM926 1438l98 -30l-98 -30l-30 -98l-30 98l-98 30l98 30l30 98z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" /> -<glyph unicode="" d="M829 318q0 -76 -58.5 -112.5t-139.5 -36.5q-41 0 -80.5 9.5t-75.5 28.5t-58 53t-22 78q0 46 25 80t65.5 51.5t82 25t84.5 7.5q20 0 31 -2q2 -1 23 -16.5t26 -19t23 -18t24.5 -22t19 -22.5t17 -26t9 -26.5t4.5 -31.5zM755 863q0 -60 -33 -99.5t-92 -39.5q-53 0 -93 42.5 t-57.5 96.5t-17.5 106q0 61 32 104t92 43q53 0 93.5 -45t58 -101t17.5 -107zM861 1120l88 64h-265q-85 0 -161 -32t-127.5 -98t-51.5 -153q0 -93 64.5 -154.5t158.5 -61.5q22 0 43 3q-13 -29 -13 -54q0 -44 40 -94q-175 -12 -257 -63q-47 -29 -75.5 -73t-28.5 -95 q0 -43 18.5 -77.5t48.5 -56.5t69 -37t77.5 -21t76.5 -6q60 0 120.5 15.5t113.5 46t86 82.5t33 117q0 49 -20 89.5t-49 66.5t-58 47.5t-49 44t-20 44.5t15.5 42.5t37.5 39.5t44 42t37.5 59.5t15.5 82.5q0 60 -22.5 99.5t-72.5 90.5h83zM1152 672h128v64h-128v128h-64v-128 h-128v-64h128v-160h64v160zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M735 740q0 -36 32 -70.5t77.5 -68t90.5 -73.5t77 -104t32 -142q0 -90 -48 -173q-72 -122 -211 -179.5t-298 -57.5q-132 0 -246.5 41.5t-171.5 137.5q-37 60 -37 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 42 -47.5 74t-15.5 73q0 36 21 85q-46 -4 -68 -4 q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q77 66 182.5 98t217.5 32h418l-138 -88h-131q74 -63 112 -133t38 -160q0 -72 -24.5 -129.5t-59 -93t-69.5 -65t-59.5 -61.5t-24.5 -66zM589 836q38 0 78 16.5t66 43.5q53 57 53 159q0 58 -17 125t-48.5 129.5 t-84.5 103.5t-117 41q-42 0 -82.5 -19.5t-65.5 -52.5q-47 -59 -47 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26zM591 -37q58 0 111.5 13t99 39t73 73t27.5 109q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -48 2 q-53 0 -105 -7t-107.5 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -70 35 -123.5t91.5 -83t119 -44t127.5 -14.5zM1401 839h213v-108h-213v-219h-105v219h-212v108h212v217h105v-217z" /> -<glyph unicode="" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M640 1088v-896q0 -26 -19 -45t-45 -19t-45 19l-448 448q-19 19 -19 45t19 45l448 448q19 19 45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M576 640q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19t-19 45v896q0 26 19 45t45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M160 0h608v1152h-640v-1120q0 -13 9.5 -22.5t22.5 -9.5zM1536 32v1120h-640v-1152h608q13 0 22.5 9.5t9.5 22.5zM1664 1248v-1216q0 -66 -47 -113t-113 -47h-1344q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1344q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45zM1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 448q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 826v-794q0 -66 -47 -113t-113 -47h-1472q-66 0 -113 47t-47 113v794q44 -49 101 -87q362 -246 497 -345q57 -42 92.5 -65.5t94.5 -48t110 -24.5h1h1q51 0 110 24.5t94.5 48t92.5 65.5q170 123 498 345q57 39 100 87zM1792 1120q0 -79 -49 -151t-122 -123 q-376 -261 -468 -325q-10 -7 -42.5 -30.5t-54 -38t-52 -32.5t-57.5 -27t-50 -9h-1h-1q-23 0 -50 9t-57.5 27t-52 32.5t-54 38t-42.5 30.5q-91 64 -262 182.5t-205 142.5q-62 42 -117 115.5t-55 136.5q0 78 41.5 130t118.5 52h1472q65 0 112.5 -47t47.5 -113z" /> -<glyph unicode="" d="M349 911v-991h-330v991h330zM370 1217q1 -73 -50.5 -122t-135.5 -49h-2q-82 0 -132 49t-50 122q0 74 51.5 122.5t134.5 48.5t133 -48.5t51 -122.5zM1536 488v-568h-329v530q0 105 -40.5 164.5t-126.5 59.5q-63 0 -105.5 -34.5t-63.5 -85.5q-11 -30 -11 -81v-553h-329 q2 399 2 647t-1 296l-1 48h329v-144h-2q20 32 41 56t56.5 52t87 43.5t114.5 15.5q171 0 275 -113.5t104 -332.5z" /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1771 0q0 -53 -37 -90l-107 -108q-39 -37 -91 -37q-53 0 -90 37l-363 364q-38 36 -38 90q0 53 43 96l-256 256l-126 -126q-14 -14 -34 -14t-34 14q2 -2 12.5 -12t12.5 -13t10 -11.5t10 -13.5t6 -13.5t5.5 -16.5t1.5 -18q0 -38 -28 -68q-3 -3 -16.5 -18t-19 -20.5 t-18.5 -16.5t-22 -15.5t-22 -9t-26 -4.5q-40 0 -68 28l-408 408q-28 28 -28 68q0 13 4.5 26t9 22t15.5 22t16.5 18.5t20.5 19t18 16.5q30 28 68 28q10 0 18 -1.5t16.5 -5.5t13.5 -6t13.5 -10t11.5 -10t13 -12.5t12 -12.5q-14 14 -14 34t14 34l348 348q14 14 34 14t34 -14 q-2 2 -12.5 12t-12.5 13t-10 11.5t-10 13.5t-6 13.5t-5.5 16.5t-1.5 18q0 38 28 68q3 3 16.5 18t19 20.5t18.5 16.5t22 15.5t22 9t26 4.5q40 0 68 -28l408 -408q28 -28 28 -68q0 -13 -4.5 -26t-9 -22t-15.5 -22t-16.5 -18.5t-20.5 -19t-18 -16.5q-30 -28 -68 -28 q-10 0 -18 1.5t-16.5 5.5t-13.5 6t-13.5 10t-11.5 10t-13 12.5t-12 12.5q14 -14 14 -34t-14 -34l-126 -126l256 -256q43 43 96 43q52 0 91 -37l363 -363q37 -39 37 -91z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM576 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1004 351l101 382q6 26 -7.5 48.5t-38.5 29.5 t-48 -6.5t-30 -39.5l-101 -382q-60 -5 -107 -43.5t-63 -98.5q-20 -77 20 -146t117 -89t146 20t89 117q16 60 -6 117t-72 91zM1664 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 1024q0 53 -37.5 90.5 t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1472 832q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 384q0 -261 -141 -483q-19 -29 -54 -29h-1402q-35 0 -54 29 q-141 221 -141 483q0 182 71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640 q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 174 120 321.5 t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M704 1152q-153 0 -286 -52t-211.5 -141t-78.5 -191q0 -82 53 -158t149 -132l97 -56l-35 -84q34 20 62 39l44 31l53 -10q78 -14 153 -14q153 0 286 52t211.5 141t78.5 191t-78.5 191t-211.5 141t-286 52zM704 1280q191 0 353.5 -68.5t256.5 -186.5t94 -257t-94 -257 t-256.5 -186.5t-353.5 -68.5q-86 0 -176 16q-124 -88 -278 -128q-36 -9 -86 -16h-3q-11 0 -20.5 8t-11.5 21q-1 3 -1 6.5t0.5 6.5t2 6l2.5 5t3.5 5.5t4 5t4.5 5t4 4.5q5 6 23 25t26 29.5t22.5 29t25 38.5t20.5 44q-124 72 -195 177t-71 224q0 139 94 257t256.5 186.5 t353.5 68.5zM1526 111q10 -24 20.5 -44t25 -38.5t22.5 -29t26 -29.5t23 -25q1 -1 4 -4.5t4.5 -5t4 -5t3.5 -5.5l2.5 -5t2 -6t0.5 -6.5t-1 -6.5q-3 -14 -13 -22t-22 -7q-50 7 -86 16q-154 40 -278 128q-90 -16 -176 -16q-271 0 -472 132q58 -4 88 -4q161 0 309 45t264 129 q125 92 192 212t67 254q0 77 -23 152q129 -71 204 -178t75 -230q0 -120 -71 -224.5t-195 -176.5z" /> -<glyph unicode="" horiz-adv-x="896" d="M885 970q18 -20 7 -44l-540 -1157q-13 -25 -42 -25q-4 0 -14 2q-17 5 -25.5 19t-4.5 30l197 808l-406 -101q-4 -1 -12 -1q-18 0 -31 11q-18 15 -13 39l201 825q4 14 16 23t28 9h328q19 0 32 -12.5t13 -29.5q0 -8 -5 -18l-171 -463l396 98q8 2 12 2q19 0 34 -15z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 288v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192h-512v-192h96q40 0 68 -28t28 -68v-320 q0 -40 -28 -68t-68 -28h-320q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h96v192q0 52 38 90t90 38h512v192h-96q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h320q40 0 68 -28t28 -68v-320q0 -40 -28 -68t-68 -28h-96v-192h512q52 0 90 -38t38 -90v-192h96q40 0 68 -28t28 -68 z" /> -<glyph unicode="" horiz-adv-x="1664" d="M896 708v-580q0 -104 -76 -180t-180 -76t-180 76t-76 180q0 26 19 45t45 19t45 -19t19 -45q0 -50 39 -89t89 -39t89 39t39 89v580q33 11 64 11t64 -11zM1664 681q0 -13 -9.5 -22.5t-22.5 -9.5q-11 0 -23 10q-49 46 -93 69t-102 23q-68 0 -128 -37t-103 -97 q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -28 -17q-18 0 -29 17q-4 6 -14.5 24t-17.5 28q-43 60 -102.5 97t-127.5 37t-127.5 -37t-102.5 -97q-7 -10 -17.5 -28t-14.5 -24q-11 -17 -29 -17q-17 0 -28 17q-4 6 -14.5 24t-17.5 28q-43 60 -103 97t-128 37q-58 0 -102 -23t-93 -69 q-12 -10 -23 -10q-13 0 -22.5 9.5t-9.5 22.5q0 5 1 7q45 183 172.5 319.5t298 204.5t360.5 68q140 0 274.5 -40t246.5 -113.5t194.5 -187t115.5 -251.5q1 -2 1 -7zM896 1408v-98q-42 2 -64 2t-64 -2v98q0 26 19 45t45 19t45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M768 -128h896v640h-416q-40 0 -68 28t-28 68v416h-384v-1152zM1024 1312v64q0 13 -9.5 22.5t-22.5 9.5h-704q-13 0 -22.5 -9.5t-9.5 -22.5v-64q0 -13 9.5 -22.5t22.5 -9.5h704q13 0 22.5 9.5t9.5 22.5zM1280 640h299l-299 299v-299zM1792 512v-672q0 -40 -28 -68t-68 -28 h-960q-40 0 -68 28t-28 68v160h-544q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1088q40 0 68 -28t28 -68v-328q21 -13 36 -28l408 -408q28 -28 48 -76t20 -88z" /> -<glyph unicode="" horiz-adv-x="1024" d="M736 960q0 -13 -9.5 -22.5t-22.5 -9.5t-22.5 9.5t-9.5 22.5q0 46 -54 71t-106 25q-13 0 -22.5 9.5t-9.5 22.5t9.5 22.5t22.5 9.5q50 0 99.5 -16t87 -54t37.5 -90zM896 960q0 72 -34.5 134t-90 101.5t-123 62t-136.5 22.5t-136.5 -22.5t-123 -62t-90 -101.5t-34.5 -134 q0 -101 68 -180q10 -11 30.5 -33t30.5 -33q128 -153 141 -298h228q13 145 141 298q10 11 30.5 33t30.5 33q68 79 68 180zM1024 960q0 -155 -103 -268q-45 -49 -74.5 -87t-59.5 -95.5t-34 -107.5q47 -28 47 -82q0 -37 -25 -64q25 -27 25 -64q0 -52 -45 -81q13 -23 13 -47 q0 -46 -31.5 -71t-77.5 -25q-20 -44 -60 -70t-87 -26t-87 26t-60 70q-46 0 -77.5 25t-31.5 71q0 24 13 47q-45 29 -45 81q0 37 25 64q-25 27 -25 64q0 54 47 82q-4 50 -34 107.5t-59.5 95.5t-74.5 87q-103 113 -103 268q0 99 44.5 184.5t117 142t164 89t186.5 32.5 t186.5 -32.5t164 -89t117 -142t44.5 -184.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 352v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-1376v-192q0 -13 -9.5 -22.5t-22.5 -9.5q-12 0 -24 10l-319 320q-9 9 -9 22q0 14 9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h1376q13 0 22.5 -9.5t9.5 -22.5zM1792 896q0 -14 -9 -23l-320 -320q-9 -9 -23 -9 q-13 0 -22.5 9.5t-9.5 22.5v192h-1376q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h1376v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 608q0 14 -9 23t-23 9h-224v352q0 13 -9.5 22.5t-22.5 9.5h-192q-13 0 -22.5 -9.5t-9.5 -22.5v-352h-224q-13 0 -22.5 -9.5t-9.5 -22.5q0 -14 9 -23l352 -352q9 -9 23 -9t23 9l351 351q10 12 10 24zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1280 672q0 14 -9 23l-352 352q-9 9 -23 9t-23 -9l-351 -351q-10 -12 -10 -24q0 -14 9 -23t23 -9h224v-352q0 -13 9.5 -22.5t22.5 -9.5h192q13 0 22.5 9.5t9.5 22.5v352h224q13 0 22.5 9.5t9.5 22.5zM1920 384q0 -159 -112.5 -271.5t-271.5 -112.5h-1088 q-185 0 -316.5 131.5t-131.5 316.5q0 130 70 240t188 165q-2 30 -2 43q0 212 150 362t362 150q156 0 285.5 -87t188.5 -231q71 62 166 62q106 0 181 -75t75 -181q0 -76 -41 -138q130 -31 213.5 -135.5t83.5 -238.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 68 5.5 131t24 138t47.5 132.5t81 103t120 60.5q-22 -52 -22 -120v-203q-58 -20 -93 -70t-35 -111q0 -80 56 -136t136 -56 t136 56t56 136q0 61 -35.5 111t-92.5 70v203q0 62 25 93q132 -104 295 -104t295 104q25 -31 25 -93v-64q-106 0 -181 -75t-75 -181v-89q-32 -29 -32 -71q0 -40 28 -68t68 -28t68 28t28 68q0 42 -32 71v89q0 52 38 90t90 38t90 -38t38 -90v-89q-32 -29 -32 -71q0 -40 28 -68 t68 -28t68 28t28 68q0 42 -32 71v89q0 68 -34.5 127.5t-93.5 93.5q0 10 0.5 42.5t0 48t-2.5 41.5t-7 47t-13 40q68 -15 120 -60.5t81 -103t47.5 -132.5t24 -138t5.5 -131zM1088 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1280 832q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 832q0 -62 -35.5 -111t-92.5 -70v-395q0 -159 -131.5 -271.5t-316.5 -112.5t-316.5 112.5t-131.5 271.5v132q-164 20 -274 128t-110 252v512q0 26 19 45t45 19q6 0 16 -2q17 30 47 48 t65 18q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5q-33 0 -64 18v-402q0 -106 94 -181t226 -75t226 75t94 181v402q-31 -18 -64 -18q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5q35 0 65 -18t47 -48q10 2 16 2q26 0 45 -19t19 -45v-512q0 -144 -110 -252 t-274 -128v-132q0 -106 94 -181t226 -75t226 75t94 181v395q-57 21 -92.5 70t-35.5 111q0 80 56 136t136 56t136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 1152h512v128h-512v-128zM288 1152v-1280h-64q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h64zM1408 1152v-1280h-1024v1280h128v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h128zM1792 928v-832q0 -92 -66 -158t-158 -66h-64v1280h64q92 0 158 -66 t66 -158z" /> -<glyph unicode="" horiz-adv-x="1792" d="M912 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM1728 128q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-181 75t-75 181h-448q-52 0 -90 38t-38 90q50 42 91 88t85 119.5t74.5 158.5 t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q190 -28 307 -158.5t117 -282.5q0 -139 19.5 -260t50 -206t74.5 -158.5t85 -119.5t91 -88z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1664 896q0 80 -56 136t-136 56h-64v-384h64q80 0 136 56t56 136zM0 128h1792q0 -106 -75 -181t-181 -75h-1280q-106 0 -181 75t-75 181zM1856 896q0 -159 -112.5 -271.5t-271.5 -112.5h-64v-32q0 -92 -66 -158t-158 -66h-704q-92 0 -158 66t-66 158v736q0 26 19 45 t45 19h1152q159 0 271.5 -112.5t112.5 -271.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M640 1472v-640q0 -61 -35.5 -111t-92.5 -70v-779q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v779q-57 20 -92.5 70t-35.5 111v640q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45v-416q0 -26 19 -45 t45 -19t45 19t19 45v416q0 26 19 45t45 19t45 -19t19 -45zM1408 1472v-1600q0 -52 -38 -90t-90 -38h-128q-52 0 -90 38t-38 90v512h-224q-13 0 -22.5 9.5t-9.5 22.5v800q0 132 94 226t226 94h256q26 0 45 -19t19 -45z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M384 736q0 14 9 23t23 9h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64zM1120 512q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704zM1120 256q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704 q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h704z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 992v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 1248v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1536h-1152v-1536h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM1408 1472v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM384 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M1152 224v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM896 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M640 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 480v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5zM1152 736v-64q0 -13 -9.5 -22.5t-22.5 -9.5h-64q-13 0 -22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h64q13 0 22.5 -9.5t9.5 -22.5z M896 -128h384v1152h-256v-32q0 -40 -28 -68t-68 -28h-448q-40 0 -68 28t-28 68v32h-256v-1152h384v224q0 13 9.5 22.5t22.5 9.5h320q13 0 22.5 -9.5t9.5 -22.5v-224zM896 1056v320q0 13 -9.5 22.5t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-96h-128v96q0 13 -9.5 22.5 t-22.5 9.5h-64q-13 0 -22.5 -9.5t-9.5 -22.5v-320q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5v96h128v-96q0 -13 9.5 -22.5t22.5 -9.5h64q13 0 22.5 9.5t9.5 22.5zM1408 1088v-1280q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1280q0 26 19 45t45 19h320 v288q0 40 28 68t68 28h448q40 0 68 -28t28 -68v-288h320q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1920" d="M640 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM256 640h384v256h-158q-14 -2 -22 -9l-195 -195q-7 -12 -9 -22v-30zM1536 128q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1664 800v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM1920 1344v-1152 q0 -26 -19 -45t-45 -19h-192q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-128q-26 0 -45 19t-19 45t19 45t45 19v416q0 26 13 58t32 51l198 198q19 19 51 32t58 13h160v320q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 416v192q0 14 -9 23t-23 9h-224v224q0 14 -9 23t-23 9h-192q-14 0 -23 -9t-9 -23v-224h-224q-14 0 -23 -9t-9 -23v-192q0 -14 9 -23t23 -9h224v-224q0 -14 9 -23t23 -9h192q14 0 23 9t9 23v224h224q14 0 23 9t9 23zM640 1152h512v128h-512v-128zM256 1152v-1280h-32 q-92 0 -158 66t-66 158v832q0 92 66 158t158 66h32zM1440 1152v-1280h-1088v1280h160v160q0 40 28 68t68 28h576q40 0 68 -28t28 -68v-160h160zM1792 928v-832q0 -92 -66 -158t-158 -66h-32v1280h32q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1920 576q-1 -32 -288 -96l-352 -32l-224 -64h-64l-293 -352h69q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-96h-160h-64v32h64v416h-160l-192 -224h-96l-32 32v192h32v32h128v8l-192 24v128l192 24v8h-128v32h-32v192l32 32h96l192 -224h160v416h-64v32h64h160h96 q26 0 45 -4.5t19 -11.5t-19 -11.5t-45 -4.5h-69l293 -352h64l224 -64l352 -32q261 -58 287 -93z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 640v384h-256v-256q0 -53 37.5 -90.5t90.5 -37.5h128zM1664 192v-192h-1152v192l128 192h-128q-159 0 -271.5 112.5t-112.5 271.5v320l-64 64l32 128h480l32 128h960l32 -192l-64 -32v-800z" /> -<glyph unicode="" d="M1280 192v896q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-512v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-896q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h512v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M627 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23zM1011 160q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM979 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23 l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 224q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23zM1075 608q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393 q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 672q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23zM1075 1056q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23 t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="640" d="M627 992q0 -13 -10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="640" d="M595 576q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 352q0 -13 -10 -23l-50 -50q-10 -10 -23 -10t-23 10l-393 393l-393 -393q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l466 -466q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1075 800q0 -13 -10 -23l-466 -466q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l393 -393l393 393q10 10 23 10t23 -10l50 -50q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1792 544v832q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-832q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1376v-1088q0 -66 -47 -113t-113 -47h-544q0 -37 16 -77.5t32 -71t16 -43.5q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19 t-19 45q0 14 16 44t32 70t16 78h-544q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="1920" d="M416 256q-66 0 -113 47t-47 113v704q0 66 47 113t113 47h1088q66 0 113 -47t47 -113v-704q0 -66 -47 -113t-113 -47h-1088zM384 1120v-704q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5v704q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5z M1760 192h160v-96q0 -40 -47 -68t-113 -28h-1600q-66 0 -113 28t-47 68v96h160h1600zM1040 96q16 0 16 16t-16 16h-160q-16 0 -16 -16t16 -16h160z" /> -<glyph unicode="" horiz-adv-x="1152" d="M640 128q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1024 288v960q0 13 -9.5 22.5t-22.5 9.5h-832q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h832q13 0 22.5 9.5t9.5 22.5zM1152 1248v-1088q0 -66 -47 -113t-113 -47h-832 q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h832q66 0 113 -47t47 -113z" /> -<glyph unicode="" horiz-adv-x="768" d="M464 128q0 33 -23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5t56.5 23.5t23.5 56.5zM672 288v704q0 13 -9.5 22.5t-22.5 9.5h-512q-13 0 -22.5 -9.5t-9.5 -22.5v-704q0 -13 9.5 -22.5t22.5 -9.5h512q13 0 22.5 9.5t9.5 22.5zM480 1136 q0 16 -16 16h-160q-16 0 -16 -16t16 -16h160q16 0 16 16zM768 1152v-1024q0 -52 -38 -90t-90 -38h-512q-52 0 -90 38t-38 90v1024q0 52 38 90t90 38h512q52 0 90 -38t38 -90z" /> -<glyph unicode="" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M526 142q0 -53 -37.5 -90.5t-90.5 -37.5q-52 0 -90 38t-38 90q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 -64q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1522 142q0 -52 -38 -90t-90 -38q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM558 1138q0 -66 -47 -113t-113 -47t-113 47t-47 113t47 113t113 47t113 -47t47 -113z M1728 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1088 1344q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1618 1138q0 -93 -66 -158.5t-158 -65.5q-93 0 -158.5 65.5t-65.5 158.5 q0 92 65.5 158t158.5 66q92 0 158 -66t66 -158z" /> -<glyph unicode="" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" /> -<glyph unicode="" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM1664 496q0 -207 -61 -331q-38 -77 -105.5 -133t-141 -86 t-170 -47.5t-171.5 -22t-167 -4.5q-78 0 -142 3t-147.5 12.5t-152.5 30t-137 51.5t-121 81t-86 115q-62 123 -62 331q0 237 136 396q-27 82 -27 170q0 116 51 218q108 0 190 -39.5t189 -123.5q147 35 309 35q148 0 280 -32q105 82 187 121t189 39q51 -102 51 -218 q0 -87 -27 -168q136 -160 136 -398z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1536 224v704q0 40 -28 68t-68 28h-704q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68v-960q0 -40 28 -68t68 -28h1216q40 0 68 28t28 68zM1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320 q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1781 605q0 35 -53 35h-1088q-40 0 -85.5 -21.5t-71.5 -52.5l-294 -363q-18 -24 -18 -40q0 -35 53 -35h1088q40 0 86 22t71 53l294 363q18 22 18 39zM640 768h768v160q0 40 -28 68t-68 28h-576q-40 0 -68 28t-28 68v64q0 40 -28 68t-68 28h-320q-40 0 -68 -28t-28 -68 v-853l256 315q44 53 116 87.5t140 34.5zM1909 605q0 -62 -46 -120l-295 -363q-43 -53 -116 -87.5t-140 -34.5h-1088q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158v-160h192q54 0 99 -24.5t67 -70.5q15 -32 15 -68z " /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" d="M1134 461q-37 -121 -138 -195t-228 -74t-228 74t-138 195q-8 25 4 48.5t38 31.5q25 8 48.5 -4t31.5 -38q25 -80 92.5 -129.5t151.5 -49.5t151.5 49.5t92.5 129.5q8 26 32 38t49 4t37 -31.5t4 -48.5zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5 t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1134 307q8 -25 -4 -48.5t-37 -31.5t-49 4t-32 38q-25 80 -92.5 129.5t-151.5 49.5t-151.5 -49.5t-92.5 -129.5q-8 -26 -31.5 -38t-48.5 -4q-26 8 -38 31.5t-4 48.5q37 121 138 195t228 74t228 -74t138 -195zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204 t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1152 448q0 -26 -19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h640q26 0 45 -19t19 -45zM640 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1152 896q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M832 448v128q0 14 -9 23t-23 9h-192v192q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-192h-192q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h192v-192q0 -14 9 -23t23 -9h128q14 0 23 9t9 23v192h192q14 0 23 9t9 23zM1408 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1920 512q0 -212 -150 -362t-362 -150q-192 0 -338 128h-220q-146 -128 -338 -128q-212 0 -362 150 t-150 362t150 362t362 150h896q212 0 362 -150t150 -362z" /> -<glyph unicode="" horiz-adv-x="1920" d="M384 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM512 624v-96q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h224q16 0 16 -16zM384 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 368v-96q0 -16 -16 -16 h-864q-16 0 -16 16v96q0 16 16 16h864q16 0 16 -16zM768 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM640 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1024 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16 h96q16 0 16 -16zM896 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1280 624v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 368v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1152 880v-96 q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1408 880v-96q0 -16 -16 -16h-96q-16 0 -16 16v96q0 16 16 16h96q16 0 16 -16zM1664 880v-352q0 -16 -16 -16h-224q-16 0 -16 16v96q0 16 16 16h112v240q0 16 16 16h96q16 0 16 -16zM1792 128v896h-1664v-896 h1664zM1920 1024v-896q0 -53 -37.5 -90.5t-90.5 -37.5h-1664q-53 0 -90.5 37.5t-37.5 90.5v896q0 53 37.5 90.5t90.5 37.5h1664q53 0 90.5 -37.5t37.5 -90.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1664 491v616q-169 -91 -306 -91q-82 0 -145 32q-100 49 -184 76.5t-178 27.5q-173 0 -403 -127v-599q245 113 433 113q55 0 103.5 -7.5t98 -26t77 -31t82.5 -39.5l28 -14q44 -22 101 -22q120 0 293 92zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9 h-64q-14 0 -23 9t-9 23v1266q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102 q-15 -9 -33 -9q-16 0 -32 8q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> -<glyph unicode="" horiz-adv-x="1792" d="M832 536v192q-181 -16 -384 -117v-185q205 96 384 110zM832 954v197q-172 -8 -384 -126v-189q215 111 384 118zM1664 491v184q-235 -116 -384 -71v224q-20 6 -39 15q-5 3 -33 17t-34.5 17t-31.5 15t-34.5 15.5t-32.5 13t-36 12.5t-35 8.5t-39.5 7.5t-39.5 4t-44 2 q-23 0 -49 -3v-222h19q102 0 192.5 -29t197.5 -82q19 -9 39 -15v-188q42 -17 91 -17q120 0 293 92zM1664 918v189q-169 -91 -306 -91q-45 0 -78 8v-196q148 -42 384 90zM320 1280q0 -35 -17.5 -64t-46.5 -46v-1266q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v1266 q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1792 1216v-763q0 -39 -35 -57q-10 -5 -17 -9q-218 -116 -369 -116q-88 0 -158 35l-28 14q-64 33 -99 48t-91 29t-114 14q-102 0 -235.5 -44t-228.5 -102q-15 -9 -33 -9q-16 0 -32 8 q-32 19 -32 56v742q0 35 31 55q35 21 78.5 42.5t114 52t152.5 49.5t155 19q112 0 209 -31t209 -86q38 -19 89 -19q122 0 310 112q22 12 31 17q31 16 62 -2q31 -20 31 -55z" /> -<glyph unicode="" horiz-adv-x="1664" d="M585 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23zM1664 96v-64q0 -14 -9 -23t-23 -9h-960q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h960q14 0 23 -9 t9 -23z" /> -<glyph unicode="" horiz-adv-x="1920" d="M617 137l-50 -50q-10 -10 -23 -10t-23 10l-466 466q-10 10 -10 23t10 23l466 466q10 10 23 10t23 -10l50 -50q10 -10 10 -23t-10 -23l-393 -393l393 -393q10 -10 10 -23t-10 -23zM1208 1204l-373 -1291q-4 -13 -15.5 -19.5t-23.5 -2.5l-62 17q-13 4 -19.5 15.5t-2.5 24.5 l373 1291q4 13 15.5 19.5t23.5 2.5l62 -17q13 -4 19.5 -15.5t2.5 -24.5zM1865 553l-466 -466q-10 -10 -23 -10t-23 10l-50 50q-10 10 -10 23t10 23l393 393l-393 393q-10 10 -10 23t10 23l50 50q10 10 23 10t23 -10l466 -466q10 -10 10 -23t-10 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M640 454v-70q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-69l-397 -398q-19 -19 -19 -45t19 -45zM1792 416q0 -58 -17 -133.5t-38.5 -138t-48 -125t-40.5 -90.5l-20 -40q-8 -17 -28 -17q-6 0 -9 1 q-25 8 -23 34q43 400 -106 565q-64 71 -170.5 110.5t-267.5 52.5v-251q0 -42 -39 -59q-13 -5 -25 -5q-27 0 -45 19l-512 512q-19 19 -19 45t19 45l512 512q29 31 70 14q39 -17 39 -59v-262q411 -28 599 -221q169 -173 169 -509z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1186 579l257 250l-356 52l-66 10l-30 60l-159 322v-963l59 -31l318 -168l-60 355l-12 66zM1638 841l-363 -354l86 -500q5 -33 -6 -51.5t-34 -18.5q-17 0 -40 12l-449 236l-449 -236q-23 -12 -40 -12q-23 0 -34 18.5t-6 51.5l86 500l-364 354q-32 32 -23 59.5t54 34.5 l502 73l225 455q20 41 49 41q28 0 49 -41l225 -455l502 -73q45 -7 54 -34.5t-24 -59.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1401 1187l-640 -1280q-17 -35 -57 -35q-5 0 -15 2q-22 5 -35.5 22.5t-13.5 39.5v576h-576q-22 0 -39.5 13.5t-22.5 35.5t4 42t29 30l1280 640q13 7 29 7q27 0 45 -19q15 -14 18.5 -34.5t-6.5 -39.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M557 256h595v595zM512 301l595 595h-595v-595zM1664 224v-192q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v224h-864q-14 0 -23 9t-9 23v864h-224q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h224v224q0 14 9 23t23 9h192q14 0 23 -9t9 -23 v-224h851l246 247q10 9 23 9t23 -9q9 -10 9 -23t-9 -23l-247 -246v-851h224q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M288 64q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM288 1216q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM928 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1024 1088q0 -52 -26 -96.5t-70 -69.5 q-2 -287 -226 -414q-68 -38 -203 -81q-128 -40 -169.5 -71t-41.5 -100v-26q44 -25 70 -69.5t26 -96.5q0 -80 -56 -136t-136 -56t-136 56t-56 136q0 52 26 96.5t70 69.5v820q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136q0 -52 -26 -96.5t-70 -69.5v-497 q54 26 154 57q55 17 87.5 29.5t70.5 31t59 39.5t40.5 51t28 69.5t8.5 91.5q-44 25 -70 69.5t-26 96.5q0 80 56 136t136 56t136 -56t56 -136z" /> -<glyph unicode="" horiz-adv-x="1664" d="M439 265l-256 -256q-10 -9 -23 -9q-12 0 -23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23zM608 224v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM384 448q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23t9 23t23 9h320 q14 0 23 -9t9 -23zM1648 320q0 -120 -85 -203l-147 -146q-83 -83 -203 -83q-121 0 -204 85l-334 335q-21 21 -42 56l239 18l273 -274q27 -27 68 -27.5t68 26.5l147 146q28 28 28 67q0 40 -28 68l-274 275l18 239q35 -21 56 -42l336 -336q84 -86 84 -204zM1031 1044l-239 -18 l-273 274q-28 28 -68 28q-39 0 -68 -27l-147 -146q-28 -28 -28 -67q0 -40 28 -68l274 -274l-18 -240q-35 21 -56 42l-336 336q-84 86 -84 204q0 120 85 203l147 146q83 83 203 83q121 0 204 -85l334 -335q21 -21 42 -56zM1664 960q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9 t-9 23t9 23t23 9h320q14 0 23 -9t9 -23zM1120 1504v-320q0 -14 -9 -23t-23 -9t-23 9t-9 23v320q0 14 9 23t23 9t23 -9t9 -23zM1527 1353l-256 -256q-11 -9 -23 -9t-23 9q-9 10 -9 23t9 23l256 256q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> -<glyph unicode="" horiz-adv-x="1024" d="M704 280v-240q0 -16 -12 -28t-28 -12h-240q-16 0 -28 12t-12 28v240q0 16 12 28t28 12h240q16 0 28 -12t12 -28zM1020 880q0 -54 -15.5 -101t-35 -76.5t-55 -59.5t-57.5 -43.5t-61 -35.5q-41 -23 -68.5 -65t-27.5 -67q0 -17 -12 -32.5t-28 -15.5h-240q-15 0 -25.5 18.5 t-10.5 37.5v45q0 83 65 156.5t143 108.5q59 27 84 56t25 76q0 42 -46.5 74t-107.5 32q-65 0 -108 -29q-35 -25 -107 -115q-13 -16 -31 -16q-12 0 -25 8l-164 125q-13 10 -15.5 25t5.5 28q160 266 464 266q80 0 161 -31t146 -83t106 -127.5t41 -158.5z" /> -<glyph unicode="" horiz-adv-x="640" d="M640 192v-128q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64v384h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h384q26 0 45 -19t19 -45v-576h64q26 0 45 -19t19 -45zM512 1344v-192q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v192 q0 26 19 45t45 19h256q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="640" d="M512 288v-224q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v224q0 26 19 45t45 19h256q26 0 45 -19t19 -45zM542 1344l-28 -768q-1 -26 -20.5 -45t-45.5 -19h-256q-26 0 -45.5 19t-20.5 45l-28 768q-1 26 17.5 45t44.5 19h320q26 0 44.5 -19t17.5 -45z" /> -<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1534 846v-206h-514l-3 27 q-4 28 -4 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q83 65 188 65q110 0 178 -59.5t68 -158.5q0 -56 -24.5 -103t-62 -76.5t-81.5 -58.5t-82 -50.5t-65.5 -51.5t-30.5 -63h232v80 h126z" /> -<glyph unicode="" d="M897 167v-167h-248l-159 252l-24 42q-8 9 -11 21h-3l-9 -21q-10 -20 -25 -44l-155 -250h-258v167h128l197 291l-185 272h-137v168h276l139 -228q2 -4 23 -42q8 -9 11 -21h3q3 9 11 21l25 42l140 228h257v-168h-125l-184 -267l204 -296h109zM1536 -50v-206h-514l-4 27 q-3 45 -3 46q0 64 26 117t65 86.5t84 65t84 54.5t65 54t26 64q0 38 -29.5 62.5t-70.5 24.5q-51 0 -97 -39q-14 -11 -36 -38l-105 92q26 37 63 66q80 65 188 65q110 0 178 -59.5t68 -158.5q0 -66 -34.5 -118.5t-84 -86t-99.5 -62.5t-87 -63t-41 -73h232v80h126z" /> -<glyph unicode="" horiz-adv-x="1920" d="M896 128l336 384h-768l-336 -384h768zM1909 1205q15 -34 9.5 -71.5t-30.5 -65.5l-896 -1024q-38 -44 -96 -44h-768q-38 0 -69.5 20.5t-47.5 54.5q-15 34 -9.5 71.5t30.5 65.5l896 1024q38 44 96 44h768q38 0 69.5 -20.5t47.5 -54.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1664 438q0 -81 -44.5 -135t-123.5 -54q-41 0 -77.5 17.5t-59 38t-56.5 38t-71 17.5q-110 0 -110 -124q0 -39 16 -115t15 -115v-5q-22 0 -33 -1q-34 -3 -97.5 -11.5t-115.5 -13.5t-98 -5q-61 0 -103 26.5t-42 83.5q0 37 17.5 71t38 56.5t38 59t17.5 77.5q0 79 -54 123.5 t-135 44.5q-84 0 -143 -45.5t-59 -127.5q0 -43 15 -83t33.5 -64.5t33.5 -53t15 -50.5q0 -45 -46 -89q-37 -35 -117 -35q-95 0 -245 24q-9 2 -27.5 4t-27.5 4l-13 2q-1 0 -3 1q-2 0 -2 1v1024q2 -1 17.5 -3.5t34 -5t21.5 -3.5q150 -24 245 -24q80 0 117 35q46 44 46 89 q0 22 -15 50.5t-33.5 53t-33.5 64.5t-15 83q0 82 59 127.5t144 45.5q80 0 134 -44.5t54 -123.5q0 -41 -17.5 -77.5t-38 -59t-38 -56.5t-17.5 -71q0 -57 42 -83.5t103 -26.5q64 0 180 15t163 17v-2q-1 -2 -3.5 -17.5t-5 -34t-3.5 -21.5q-24 -150 -24 -245q0 -80 35 -117 q44 -46 89 -46q22 0 50.5 15t53 33.5t64.5 33.5t83 15q82 0 127.5 -59t45.5 -143z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1152 832v-128q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-217 24 -364.5 187.5t-147.5 384.5v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -185 131.5 -316.5t316.5 -131.5 t316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45zM896 1216v-512q0 -132 -94 -226t-226 -94t-226 94t-94 226v512q0 132 94 226t226 94t226 -94t94 -226z" /> -<glyph unicode="" horiz-adv-x="1408" d="M271 591l-101 -101q-42 103 -42 214v128q0 26 19 45t45 19t45 -19t19 -45v-128q0 -53 15 -113zM1385 1193l-361 -361v-128q0 -132 -94 -226t-226 -94q-55 0 -109 19l-96 -96q97 -51 205 -51q185 0 316.5 131.5t131.5 316.5v128q0 26 19 45t45 19t45 -19t19 -45v-128 q0 -221 -147.5 -384.5t-364.5 -187.5v-132h256q26 0 45 -19t19 -45t-19 -45t-45 -19h-640q-26 0 -45 19t-19 45t19 45t45 19h256v132q-125 13 -235 81l-254 -254q-10 -10 -23 -10t-23 10l-82 82q-10 10 -10 23t10 23l1234 1234q10 10 23 10t23 -10l82 -82q10 -10 10 -23 t-10 -23zM1005 1325l-621 -621v512q0 132 94 226t226 94q102 0 184.5 -59t116.5 -152z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1088 576v640h-448v-1137q119 63 213 137q235 184 235 360zM1280 1344v-768q0 -86 -33.5 -170.5t-83 -150t-118 -127.5t-126.5 -103t-121 -77.5t-89.5 -49.5t-42.5 -20q-12 -6 -26 -6t-26 6q-16 7 -42.5 20t-89.5 49.5t-121 77.5t-126.5 103t-118 127.5t-83 150 t-33.5 170.5v768q0 26 19 45t45 19h1152q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 1344q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1408 1376v-320q0 -16 -12 -25q-8 -7 -20 -7q-4 0 -7 1l-448 96q-11 2 -18 11t-7 20h-256v-102q111 -23 183.5 -111t72.5 -203v-800q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v800 q0 106 62.5 190.5t161.5 114.5v111h-32q-59 0 -115 -23.5t-91.5 -53t-66 -66.5t-40.5 -53.5t-14 -24.5q-17 -35 -57 -35q-16 0 -29 7q-23 12 -31.5 37t3.5 49q5 10 14.5 26t37.5 53.5t60.5 70t85 67t108.5 52.5q-25 42 -25 86q0 66 47 113t113 47t113 -47t47 -113 q0 -33 -14 -64h302q0 11 7 20t18 11l448 96q3 1 7 1q12 0 20 -7q12 -9 12 -25z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1440 1088q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1664 1376q0 -249 -75.5 -430.5t-253.5 -360.5q-81 -80 -195 -176l-20 -379q-2 -16 -16 -26l-384 -224q-7 -4 -16 -4q-12 0 -23 9l-64 64q-13 14 -8 32l85 276l-281 281l-276 -85q-3 -1 -9 -1 q-14 0 -23 9l-64 64q-17 19 -5 39l224 384q10 14 26 16l379 20q96 114 176 195q188 187 358 258t431 71q14 0 24 -9.5t10 -22.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1745 763l-164 -763h-334l178 832q13 56 -15 88q-27 33 -83 33h-169l-204 -953h-334l204 953h-286l-204 -953h-334l204 953l-153 327h1276q101 0 189.5 -40.5t147.5 -113.5q60 -73 81 -168.5t0 -194.5z" /> -<glyph unicode="" d="M909 141l102 102q19 19 19 45t-19 45l-307 307l307 307q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M717 141l454 454q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l307 -307l-307 -307q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1165 397l102 102q19 19 19 45t-19 45l-454 454q-19 19 -45 19t-45 -19l-454 -454q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l307 307l307 -307q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M813 237l454 454q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-307 -307l-307 307q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l454 -454q19 -19 45 -19t45 19zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5 t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1130 939l16 175h-884l47 -534h612l-22 -228l-197 -53l-196 53l-13 140h-175l22 -278l362 -100h4v1l359 99l50 544h-644l-15 181h674zM0 1408h1408l-128 -1438l-578 -162l-574 162z" /> -<glyph unicode="" horiz-adv-x="1792" d="M275 1408h1505l-266 -1333l-804 -267l-698 267l71 356h297l-29 -147l422 -161l486 161l68 339h-1208l58 297h1209l38 191h-1208z" /> -<glyph unicode="" horiz-adv-x="1792" d="M960 1280q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1792 352v-352q0 -22 -20 -30q-8 -2 -12 -2q-13 0 -23 9l-93 93q-119 -143 -318.5 -226.5t-429.5 -83.5t-429.5 83.5t-318.5 226.5l-93 -93q-9 -9 -23 -9q-4 0 -12 2q-20 8 -20 30v352 q0 14 9 23t23 9h352q22 0 30 -20q8 -19 -7 -35l-100 -100q67 -91 189.5 -153.5t271.5 -82.5v647h-192q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h192v163q-58 34 -93 92.5t-35 128.5q0 106 75 181t181 75t181 -75t75 -181q0 -70 -35 -128.5t-93 -92.5v-163h192q26 0 45 -19 t19 -45v-128q0 -26 -19 -45t-45 -19h-192v-647q149 20 271.5 82.5t189.5 153.5l-100 100q-15 16 -7 35q8 20 30 20h352q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1056 768q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h32v320q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45q0 106 -75 181t-181 75t-181 -75t-75 -181 v-320h736z" /> -<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM1152 640q0 159 -112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM1280 640q0 -212 -150 -362t-362 -150t-362 150 t-150 362t150 362t362 150t362 -150t150 -362zM1408 640q0 130 -51 248.5t-136.5 204t-204 136.5t-248.5 51t-248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5zM1536 640 q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM896 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM1408 800v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> -<glyph unicode="" horiz-adv-x="384" d="M384 288v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 800v-192q0 -40 -28 -68t-68 -28h-192q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68zM384 1312v-192q0 -40 -28 -68t-68 -28h-192 q-40 0 -68 28t-28 68v192q0 40 28 68t68 28h192q40 0 68 -28t28 -68z" /> -<glyph unicode="" d="M512 256q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM863 162q-13 232 -177 396t-396 177q-14 1 -24 -9t-10 -23v-128q0 -13 8.5 -22t21.5 -10q154 -11 264 -121t121 -264q1 -13 10 -21.5t22 -8.5h128q13 0 23 10 t9 24zM1247 161q-5 154 -56 297.5t-139.5 260t-205 205t-260 139.5t-297.5 56q-14 1 -23 -9q-10 -10 -10 -23v-128q0 -13 9 -22t22 -10q204 -7 378 -111.5t278.5 -278.5t111.5 -378q1 -13 10 -22t22 -9h128q13 0 23 10q11 9 9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM1152 585q32 18 32 55t-32 55l-544 320q-31 19 -64 1q-32 -19 -32 -56v-640q0 -37 32 -56 q16 -8 32 -8q17 0 32 9z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1024 1084l316 -316l-572 -572l-316 316zM813 105l618 618q19 19 19 45t-19 45l-362 362q-18 18 -45 18t-45 -18l-618 -618q-19 -19 -19 -45t19 -45l362 -362q18 -18 45 -18t45 18zM1702 742l-907 -908q-37 -37 -90.5 -37t-90.5 37l-126 126q56 56 56 136t-56 136 t-136 56t-136 -56l-125 126q-37 37 -37 90.5t37 90.5l907 906q37 37 90.5 37t90.5 -37l125 -125q-56 -56 -56 -136t56 -136t136 -56t136 56l126 -125q37 -37 37 -90.5t-37 -90.5z" /> -<glyph unicode="" d="M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h832q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5 t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1018 933q-18 -37 -58 -37h-192v-864q0 -14 -9 -23t-23 -9h-704q-21 0 -29 18q-8 20 4 35l160 192q9 11 25 11h320v640h-192q-40 0 -58 37q-17 37 9 68l320 384q18 22 49 22t49 -22l320 -384q27 -32 9 -68z" /> -<glyph unicode="" horiz-adv-x="1024" d="M32 1280h704q13 0 22.5 -9.5t9.5 -23.5v-863h192q40 0 58 -37t-9 -69l-320 -384q-18 -22 -49 -22t-49 22l-320 384q-26 31 -9 69q18 37 58 37h192v640h-320q-14 0 -25 11l-160 192q-13 14 -4 34q9 19 29 19z" /> -<glyph unicode="" d="M685 237l614 614q19 19 19 45t-19 45l-102 102q-19 19 -45 19t-45 -19l-467 -467l-211 211q-19 19 -45 19t-45 -19l-102 -102q-19 -19 -19 -45t19 -45l358 -358q19 -19 45 -19t45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5 t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M404 428l152 -152l-52 -52h-56v96h-96v56zM818 818q14 -13 -3 -30l-291 -291q-17 -17 -30 -3q-14 13 3 30l291 291q17 17 30 3zM544 128l544 544l-288 288l-544 -544v-288h288zM1152 736l92 92q28 28 28 68t-28 68l-152 152q-28 28 -68 28t-68 -28l-92 -92zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1280 608v480q0 26 -19 45t-45 19h-480q-42 0 -59 -39q-17 -41 14 -70l144 -144l-534 -534q-19 -19 -19 -45t19 -45l102 -102q19 -19 45 -19t45 19l534 534l144 -144q18 -19 45 -19q12 0 25 5q39 17 39 59zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1005 435l352 352q19 19 19 45t-19 45l-352 352q-30 31 -69 14q-40 -17 -40 -59v-160q-119 0 -216 -19.5t-162.5 -51t-114 -79t-76.5 -95.5t-44.5 -109t-21.5 -111.5t-5 -110.5q0 -181 167 -404q10 -12 25 -12q7 0 13 3q22 9 19 33q-44 354 62 473q46 52 130 75.5 t224 23.5v-160q0 -42 40 -59q12 -5 24 -5q26 0 45 19zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M640 448l256 128l-256 128v-256zM1024 1039v-542l-512 -256v542zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1145 861q18 -35 -5 -66l-320 -448q-19 -27 -52 -27t-52 27l-320 448q-23 31 -5 66q17 35 57 35h640q40 0 57 -35zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1145 419q-17 -35 -57 -35h-640q-40 0 -57 35q-18 35 5 66l320 448q19 27 52 27t52 -27l320 -448q23 -31 5 -66zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1088 640q0 -33 -27 -52l-448 -320q-31 -23 -66 -5q-35 17 -35 57v640q0 40 35 57q35 18 66 -5l448 -320q27 -19 27 -52zM1280 160v960q0 14 -9 23t-23 9h-960q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h960q14 0 23 9t9 23zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M976 229l35 -159q3 -12 -3 -22.5t-17 -14.5l-5 -1q-4 -2 -10.5 -3.5t-16 -4.5t-21.5 -5.5t-25.5 -5t-30 -5t-33.5 -4.5t-36.5 -3t-38.5 -1q-234 0 -409 130.5t-238 351.5h-95q-13 0 -22.5 9.5t-9.5 22.5v113q0 13 9.5 22.5t22.5 9.5h66q-2 57 1 105h-67q-14 0 -23 9 t-9 23v114q0 14 9 23t23 9h98q67 210 243.5 338t400.5 128q102 0 194 -23q11 -3 20 -15q6 -11 3 -24l-43 -159q-3 -13 -14 -19.5t-24 -2.5l-4 1q-4 1 -11.5 2.5l-17.5 3.5t-22.5 3.5t-26 3t-29 2.5t-29.5 1q-126 0 -226 -64t-150 -176h468q16 0 25 -12q10 -12 7 -26 l-24 -114q-5 -26 -32 -26h-488q-3 -37 0 -105h459q15 0 25 -12q9 -12 6 -27l-24 -112q-2 -11 -11 -18.5t-20 -7.5h-387q48 -117 149.5 -185.5t228.5 -68.5q18 0 36 1.5t33.5 3.5t29.5 4.5t24.5 5t18.5 4.5l12 3l5 2q13 5 26 -2q12 -7 15 -21z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1020 399v-367q0 -14 -9 -23t-23 -9h-956q-14 0 -23 9t-9 23v150q0 13 9.5 22.5t22.5 9.5h97v383h-95q-14 0 -23 9.5t-9 22.5v131q0 14 9 23t23 9h95v223q0 171 123.5 282t314.5 111q185 0 335 -125q9 -8 10 -20.5t-7 -22.5l-103 -127q-9 -11 -22 -12q-13 -2 -23 7 q-5 5 -26 19t-69 32t-93 18q-85 0 -137 -47t-52 -123v-215h305q13 0 22.5 -9t9.5 -23v-131q0 -13 -9.5 -22.5t-22.5 -9.5h-305v-379h414v181q0 13 9 22.5t23 9.5h162q14 0 23 -9.5t9 -22.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M978 351q0 -153 -99.5 -263.5t-258.5 -136.5v-175q0 -14 -9 -23t-23 -9h-135q-13 0 -22.5 9.5t-9.5 22.5v175q-66 9 -127.5 31t-101.5 44.5t-74 48t-46.5 37.5t-17.5 18q-17 21 -2 41l103 135q7 10 23 12q15 2 24 -9l2 -2q113 -99 243 -125q37 -8 74 -8q81 0 142.5 43 t61.5 122q0 28 -15 53t-33.5 42t-58.5 37.5t-66 32t-80 32.5q-39 16 -61.5 25t-61.5 26.5t-62.5 31t-56.5 35.5t-53.5 42.5t-43.5 49t-35.5 58t-21 66.5t-8.5 78q0 138 98 242t255 134v180q0 13 9.5 22.5t22.5 9.5h135q14 0 23 -9t9 -23v-176q57 -6 110.5 -23t87 -33.5 t63.5 -37.5t39 -29t15 -14q17 -18 5 -38l-81 -146q-8 -15 -23 -16q-14 -3 -27 7q-3 3 -14.5 12t-39 26.5t-58.5 32t-74.5 26t-85.5 11.5q-95 0 -155 -43t-60 -111q0 -26 8.5 -48t29.5 -41.5t39.5 -33t56 -31t60.5 -27t70 -27.5q53 -20 81 -31.5t76 -35t75.5 -42.5t62 -50 t53 -63.5t31.5 -76.5t13 -94z" /> -<glyph unicode="" horiz-adv-x="898" d="M898 1066v-102q0 -14 -9 -23t-23 -9h-168q-23 -144 -129 -234t-276 -110q167 -178 459 -536q14 -16 4 -34q-8 -18 -29 -18h-195q-16 0 -25 12q-306 367 -498 571q-9 9 -9 22v127q0 13 9.5 22.5t22.5 9.5h112q132 0 212.5 43t102.5 125h-427q-14 0 -23 9t-9 23v102 q0 14 9 23t23 9h413q-57 113 -268 113h-145q-13 0 -22.5 9.5t-9.5 22.5v133q0 14 9 23t23 9h832q14 0 23 -9t9 -23v-102q0 -14 -9 -23t-23 -9h-233q47 -61 64 -144h171q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1027" d="M603 0h-172q-13 0 -22.5 9t-9.5 23v330h-288q-13 0 -22.5 9t-9.5 23v103q0 13 9.5 22.5t22.5 9.5h288v85h-288q-13 0 -22.5 9t-9.5 23v104q0 13 9.5 22.5t22.5 9.5h214l-321 578q-8 16 0 32q10 16 28 16h194q19 0 29 -18l215 -425q19 -38 56 -125q10 24 30.5 68t27.5 61 l191 420q8 19 29 19h191q17 0 27 -16q9 -14 1 -31l-313 -579h215q13 0 22.5 -9.5t9.5 -22.5v-104q0 -14 -9.5 -23t-22.5 -9h-290v-85h290q13 0 22.5 -9.5t9.5 -22.5v-103q0 -14 -9.5 -23t-22.5 -9h-290v-330q0 -13 -9.5 -22.5t-22.5 -9.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1043 971q0 100 -65 162t-171 62h-320v-448h320q106 0 171 62t65 162zM1280 971q0 -193 -126.5 -315t-326.5 -122h-340v-118h505q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-505v-192q0 -14 -9.5 -23t-22.5 -9h-167q-14 0 -23 9t-9 23v192h-224q-14 0 -23 9t-9 23v128 q0 14 9 23t23 9h224v118h-224q-14 0 -23 9t-9 23v149q0 13 9 22.5t23 9.5h224v629q0 14 9 23t23 9h539q200 0 326.5 -122t126.5 -315z" /> -<glyph unicode="" horiz-adv-x="1792" d="M514 341l81 299h-159l75 -300q1 -1 1 -3t1 -3q0 1 0.5 3.5t0.5 3.5zM630 768l35 128h-292l32 -128h225zM822 768h139l-35 128h-70zM1271 340l78 300h-162l81 -299q0 -1 0.5 -3.5t1.5 -3.5q0 1 0.5 3t0.5 3zM1382 768l33 128h-297l34 -128h230zM1792 736v-64q0 -14 -9 -23 t-23 -9h-213l-164 -616q-7 -24 -31 -24h-159q-24 0 -31 24l-166 616h-209l-167 -616q-7 -24 -31 -24h-159q-11 0 -19.5 7t-10.5 17l-160 616h-208q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h175l-33 128h-142q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h109l-89 344q-5 15 5 28 q10 12 26 12h137q26 0 31 -24l90 -360h359l97 360q7 24 31 24h126q24 0 31 -24l98 -360h365l93 360q5 24 31 24h137q16 0 26 -12q10 -13 5 -28l-91 -344h111q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-145l-34 -128h179q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1167 896q18 -182 -131 -258q117 -28 175 -103t45 -214q-7 -71 -32.5 -125t-64.5 -89t-97 -58.5t-121.5 -34.5t-145.5 -15v-255h-154v251q-80 0 -122 1v-252h-154v255q-18 0 -54 0.5t-55 0.5h-200l31 183h111q50 0 58 51v402h16q-6 1 -16 1v287q-13 68 -89 68h-111v164 l212 -1q64 0 97 1v252h154v-247q82 2 122 2v245h154v-252q79 -7 140 -22.5t113 -45t82.5 -78t36.5 -114.5zM952 351q0 36 -15 64t-37 46t-57.5 30.5t-65.5 18.5t-74 9t-69 3t-64.5 -1t-47.5 -1v-338q8 0 37 -0.5t48 -0.5t53 1.5t58.5 4t57 8.5t55.5 14t47.5 21t39.5 30 t24.5 40t9.5 51zM881 827q0 33 -12.5 58.5t-30.5 42t-48 28t-55 16.5t-61.5 8t-58 2.5t-54 -1t-39.5 -0.5v-307q5 0 34.5 -0.5t46.5 0t50 2t55 5.5t51.5 11t48.5 18.5t37 27t27 38.5t9 51z" /> -<glyph unicode="" d="M1024 1024v472q22 -14 36 -28l408 -408q14 -14 28 -36h-472zM896 992q0 -40 28 -68t68 -28h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544z" /> -<glyph unicode="" d="M1468 1060q14 -14 28 -36h-472v472q22 -14 36 -28zM992 896h544v-1056q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h800v-544q0 -40 28 -68t68 -28zM1152 160v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-704q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h704q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1191 1128h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1572 -23 v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -11v-2l14 2q9 2 30 2h248v119h121zM1661 874v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162 l230 -662h70z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1191 104h177l-72 218l-12 47q-2 16 -2 20h-4l-3 -20q0 -1 -3.5 -18t-7.5 -29zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1661 -150 v-106h-288v106h75l-47 144h-243l-47 -144h75v-106h-287v106h70l230 662h162l230 -662h70zM1572 1001v-233h-584v90l369 529q12 18 21 27l11 9v3q-2 0 -6.5 -0.5t-7.5 -0.5q-12 -3 -30 -3h-232v-115h-120v229h567v-89l-369 -530q-6 -8 -21 -26l-11 -10v-3l14 3q9 1 30 1h248 v119h121z" /> -<glyph unicode="" horiz-adv-x="1792" d="M736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23zM1792 -32v-192q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832 q14 0 23 -9t9 -23zM1600 480v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1408 992v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1216 1504v-192q0 -14 -9 -23t-23 -9h-256 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1216 -32v-192q0 -14 -9 -23t-23 -9h-256q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h256q14 0 23 -9t9 -23zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192 q14 0 23 -9t9 -23zM1408 480v-192q0 -14 -9 -23t-23 -9h-448q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h448q14 0 23 -9t9 -23zM1600 992v-192q0 -14 -9 -23t-23 -9h-640q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h640q14 0 23 -9t9 -23zM1792 1504v-192q0 -14 -9 -23t-23 -9h-832 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h832q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1346 223q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9t9 -23 zM1486 165q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5 t82 -252.5zM1456 882v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165z" /> -<glyph unicode="" d="M1346 1247q0 63 -44 116t-103 53q-52 0 -83 -37t-31 -94t36.5 -95t104.5 -38q50 0 85 27t35 68zM736 96q0 -12 -10 -24l-319 -319q-10 -9 -23 -9q-12 0 -23 9l-320 320q-15 16 -7 35q8 20 30 20h192v1376q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1376h192q14 0 23 -9 t9 -23zM1456 -142v-114h-469v114h167v432q0 7 0.5 19t0.5 17v16h-2l-7 -12q-8 -13 -26 -31l-62 -58l-82 86l192 185h123v-654h165zM1486 1189q0 -62 -13 -121.5t-41 -114t-68 -95.5t-98.5 -65.5t-127.5 -24.5q-62 0 -108 16q-24 8 -42 15l39 113q15 -7 31 -11q37 -13 75 -13 q84 0 134.5 58.5t66.5 145.5h-2q-21 -23 -61.5 -37t-84.5 -14q-106 0 -173 71.5t-67 172.5q0 105 72 178t181 73q123 0 205 -94.5t82 -252.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M256 192q0 26 -19 45t-45 19q-27 0 -45.5 -19t-18.5 -45q0 -27 18.5 -45.5t45.5 -18.5q26 0 45 18.5t19 45.5zM416 704v-640q0 -26 -19 -45t-45 -19h-288q-26 0 -45 19t-19 45v640q0 26 19 45t45 19h288q26 0 45 -19t19 -45zM1600 704q0 -86 -55 -149q15 -44 15 -76 q3 -76 -43 -137q17 -56 0 -117q-15 -57 -54 -94q9 -112 -49 -181q-64 -76 -197 -78h-36h-76h-17q-66 0 -144 15.5t-121.5 29t-120.5 39.5q-123 43 -158 44q-26 1 -45 19.5t-19 44.5v641q0 25 18 43.5t43 20.5q24 2 76 59t101 121q68 87 101 120q18 18 31 48t17.5 48.5 t13.5 60.5q7 39 12.5 61t19.5 52t34 50q19 19 45 19q46 0 82.5 -10.5t60 -26t40 -40.5t24 -45t12 -50t5 -45t0.5 -39q0 -38 -9.5 -76t-19 -60t-27.5 -56q-3 -6 -10 -18t-11 -22t-8 -24h277q78 0 135 -57t57 -135z" /> -<glyph unicode="" horiz-adv-x="1664" d="M256 960q0 -26 -19 -45t-45 -19q-27 0 -45.5 19t-18.5 45q0 27 18.5 45.5t45.5 18.5q26 0 45 -18.5t19 -45.5zM416 448v640q0 26 -19 45t-45 19h-288q-26 0 -45 -19t-19 -45v-640q0 -26 19 -45t45 -19h288q26 0 45 19t19 45zM1545 597q55 -61 55 -149q-1 -78 -57.5 -135 t-134.5 -57h-277q4 -14 8 -24t11 -22t10 -18q18 -37 27 -57t19 -58.5t10 -76.5q0 -24 -0.5 -39t-5 -45t-12 -50t-24 -45t-40 -40.5t-60 -26t-82.5 -10.5q-26 0 -45 19q-20 20 -34 50t-19.5 52t-12.5 61q-9 42 -13.5 60.5t-17.5 48.5t-31 48q-33 33 -101 120q-49 64 -101 121 t-76 59q-25 2 -43 20.5t-18 43.5v641q0 26 19 44.5t45 19.5q35 1 158 44q77 26 120.5 39.5t121.5 29t144 15.5h17h76h36q133 -2 197 -78q58 -69 49 -181q39 -37 54 -94q17 -61 0 -117q46 -61 43 -137q0 -32 -15 -76z" /> -<glyph unicode="" d="M919 233v157q0 50 -29 50q-17 0 -33 -16v-224q16 -16 33 -16q29 0 29 49zM1103 355h66v34q0 51 -33 51t-33 -51v-34zM532 621v-70h-80v-423h-74v423h-78v70h232zM733 495v-367h-67v40q-39 -45 -76 -45q-33 0 -42 28q-6 16 -6 54v290h66v-270q0 -24 1 -26q1 -15 15 -15 q20 0 42 31v280h67zM985 384v-146q0 -52 -7 -73q-12 -42 -53 -42q-35 0 -68 41v-36h-67v493h67v-161q32 40 68 40q41 0 53 -42q7 -21 7 -74zM1236 255v-9q0 -29 -2 -43q-3 -22 -15 -40q-27 -40 -80 -40q-52 0 -81 38q-21 27 -21 86v129q0 59 20 86q29 38 80 38t78 -38 q21 -28 21 -86v-76h-133v-65q0 -51 34 -51q24 0 30 26q0 1 0.5 7t0.5 16.5v21.5h68zM785 1079v-156q0 -51 -32 -51t-32 51v156q0 52 32 52t32 -52zM1318 366q0 177 -19 260q-10 44 -43 73.5t-76 34.5q-136 15 -412 15q-275 0 -411 -15q-44 -5 -76.5 -34.5t-42.5 -73.5 q-20 -87 -20 -260q0 -176 20 -260q10 -43 42.5 -73t75.5 -35q137 -15 412 -15t412 15q43 5 75.5 35t42.5 73q20 84 20 260zM563 1017l90 296h-75l-51 -195l-53 195h-78l24 -69t23 -69q35 -103 46 -158v-201h74v201zM852 936v130q0 58 -21 87q-29 38 -78 38q-51 0 -78 -38 q-21 -29 -21 -87v-130q0 -58 21 -87q27 -38 78 -38q49 0 78 38q21 27 21 87zM1033 816h67v370h-67v-283q-22 -31 -42 -31q-15 0 -16 16q-1 2 -1 26v272h-67v-293q0 -37 6 -55q11 -27 43 -27q36 0 77 45v-40zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960 q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M971 292v-211q0 -67 -39 -67q-23 0 -45 22v301q22 22 45 22q39 0 39 -67zM1309 291v-46h-90v46q0 68 45 68t45 -68zM343 509h107v94h-312v-94h105v-569h100v569zM631 -60h89v494h-89v-378q-30 -42 -57 -42q-18 0 -21 21q-1 3 -1 35v364h-89v-391q0 -49 8 -73 q12 -37 58 -37q48 0 102 61v-54zM1060 88v197q0 73 -9 99q-17 56 -71 56q-50 0 -93 -54v217h-89v-663h89v48q45 -55 93 -55q54 0 71 55q9 27 9 100zM1398 98v13h-91q0 -51 -2 -61q-7 -36 -40 -36q-46 0 -46 69v87h179v103q0 79 -27 116q-39 51 -106 51q-68 0 -107 -51 q-28 -37 -28 -116v-173q0 -79 29 -116q39 -51 108 -51q72 0 108 53q18 27 21 54q2 9 2 58zM790 1011v210q0 69 -43 69t-43 -69v-210q0 -70 43 -70t43 70zM1509 260q0 -234 -26 -350q-14 -59 -58 -99t-102 -46q-184 -21 -555 -21t-555 21q-58 6 -102.5 46t-57.5 99 q-26 112 -26 350q0 234 26 350q14 59 58 99t103 47q183 20 554 20t555 -20q58 -7 102.5 -47t57.5 -99q26 -112 26 -350zM511 1536h102l-121 -399v-271h-100v271q-14 74 -61 212q-37 103 -65 187h106l71 -263zM881 1203v-175q0 -81 -28 -118q-37 -51 -106 -51q-67 0 -105 51 q-28 38 -28 118v175q0 80 28 117q38 51 105 51q69 0 106 -51q28 -37 28 -117zM1216 1365v-499h-91v55q-53 -62 -103 -62q-46 0 -59 37q-8 24 -8 75v394h91v-367q0 -33 1 -35q3 -22 21 -22q27 0 57 43v381h91z" /> -<glyph unicode="" horiz-adv-x="1408" d="M597 869q-10 -18 -257 -456q-27 -46 -65 -46h-239q-21 0 -31 17t0 36l253 448q1 0 0 1l-161 279q-12 22 -1 37q9 15 32 15h239q40 0 66 -45zM1403 1511q11 -16 0 -37l-528 -934v-1l336 -615q11 -20 1 -37q-10 -15 -32 -15h-239q-42 0 -66 45l-339 622q18 32 531 942 q25 45 64 45h241q22 0 31 -15z" /> -<glyph unicode="" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" /> -<glyph unicode="" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" /> -<glyph unicode="" horiz-adv-x="1408" d="M928 135v-151l-707 -1v151zM1169 481v-701l-1 -35v-1h-1132l-35 1h-1v736h121v-618h928v618h120zM241 393l704 -65l-13 -150l-705 65zM309 709l683 -183l-39 -146l-683 183zM472 1058l609 -360l-77 -130l-609 360zM832 1389l398 -585l-124 -85l-399 584zM1285 1536 l121 -697l-149 -26l-121 697z" /> -<glyph unicode="" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" /> -<glyph unicode="" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" /> -<glyph unicode="" d="M848 666q0 43 -41 66t-77 1q-43 -20 -42.5 -72.5t43.5 -70.5q39 -23 81 4t36 72zM928 682q8 -66 -36 -121t-110 -61t-119 40t-56 113q-2 49 25.5 93t72.5 64q70 31 141.5 -10t81.5 -118zM1100 1073q-20 -21 -53.5 -34t-53 -16t-63.5 -8q-155 -20 -324 0q-44 6 -63 9.5 t-52.5 16t-54.5 32.5q13 19 36 31t40 15.5t47 8.5q198 35 408 1q33 -5 51 -8.5t43 -16t39 -31.5zM1142 327q0 7 5.5 26.5t3 32t-17.5 16.5q-161 -106 -365 -106t-366 106l-12 -6l-5 -12q26 -154 41 -210q47 -81 204 -108q249 -46 428 53q34 19 49 51.5t22.5 85.5t12.5 71z M1272 1020q9 53 -8 75q-43 55 -155 88q-216 63 -487 36q-132 -12 -226 -46q-38 -15 -59.5 -25t-47 -34t-29.5 -54q8 -68 19 -138t29 -171t24 -137q1 -5 5 -31t7 -36t12 -27t22 -28q105 -80 284 -100q259 -28 440 63q24 13 39.5 23t31 29t19.5 40q48 267 80 473zM1536 1120 v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M944 207l80 -237q-23 -35 -111 -66t-177 -32q-104 -2 -190.5 26t-142.5 74t-95 106t-55.5 120t-16.5 118v544h-168v215q72 26 129 69.5t91 90t58 102t34 99t15 88.5q1 5 4.5 8.5t7.5 3.5h244v-424h333v-252h-334v-518q0 -30 6.5 -56t22.5 -52.5t49.5 -41.5t81.5 -14 q78 2 134 29z" /> -<glyph unicode="" d="M1136 75l-62 183q-44 -22 -103 -22q-36 -1 -62 10.5t-38.5 31.5t-17.5 40.5t-5 43.5v398h257v194h-256v326h-188q-8 0 -9 -10q-5 -44 -17.5 -87t-39 -95t-77 -95t-118.5 -68v-165h130v-418q0 -57 21.5 -115t65 -111t121 -85.5t176.5 -30.5q69 1 136.5 25t85.5 50z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="768" d="M765 237q8 -19 -5 -35l-350 -384q-10 -10 -23 -10q-14 0 -24 10l-355 384q-13 16 -5 35q9 19 29 19h224v1248q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-1248h224q21 0 29 -19z" /> -<glyph unicode="" horiz-adv-x="768" d="M765 1043q-9 -19 -29 -19h-224v-1248q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v1248h-224q-21 0 -29 19t5 35l350 384q10 10 23 10q14 0 24 -10l355 -384q13 -16 5 -35z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 736v-192q0 -14 -9 -23t-23 -9h-1248v-224q0 -21 -19 -29t-35 5l-384 350q-10 10 -10 23q0 14 10 24l384 354q16 14 35 6q19 -9 19 -29v-224h1248q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1728 643q0 -14 -10 -24l-384 -354q-16 -14 -35 -6q-19 9 -19 29v224h-1248q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h1248v224q0 21 19 29t35 -5l384 -350q10 -10 10 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1393 321q-39 -125 -123 -250q-129 -196 -257 -196q-49 0 -140 32q-86 32 -151 32q-61 0 -142 -33q-81 -34 -132 -34q-152 0 -301 259q-147 261 -147 503q0 228 113 374q112 144 284 144q72 0 177 -30q104 -30 138 -30q45 0 143 34q102 34 173 34q119 0 213 -65 q52 -36 104 -100q-79 -67 -114 -118q-65 -94 -65 -207q0 -124 69 -223t158 -126zM1017 1494q0 -61 -29 -136q-30 -75 -93 -138q-54 -54 -108 -72q-37 -11 -104 -17q3 149 78 257q74 107 250 148q1 -3 2.5 -11t2.5 -11q0 -4 0.5 -10t0.5 -10z" /> -<glyph unicode="" horiz-adv-x="1664" d="M682 530v-651l-682 94v557h682zM682 1273v-659h-682v565zM1664 530v-786l-907 125v661h907zM1664 1408v-794h-907v669z" /> -<glyph unicode="" horiz-adv-x="1408" d="M493 1053q16 0 27.5 11.5t11.5 27.5t-11.5 27.5t-27.5 11.5t-27 -11.5t-11 -27.5t11 -27.5t27 -11.5zM915 1053q16 0 27 11.5t11 27.5t-11 27.5t-27 11.5t-27.5 -11.5t-11.5 -27.5t11.5 -27.5t27.5 -11.5zM103 869q42 0 72 -30t30 -72v-430q0 -43 -29.5 -73t-72.5 -30 t-73 30t-30 73v430q0 42 30 72t73 30zM1163 850v-666q0 -46 -32 -78t-77 -32h-75v-227q0 -43 -30 -73t-73 -30t-73 30t-30 73v227h-138v-227q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73l-1 227h-74q-46 0 -78 32t-32 78v666h918zM931 1255q107 -55 171 -153.5t64 -215.5 h-925q0 117 64 215.5t172 153.5l-71 131q-7 13 5 20q13 6 20 -6l72 -132q95 42 201 42t201 -42l72 132q7 12 20 6q12 -7 5 -20zM1408 767v-430q0 -43 -30 -73t-73 -30q-42 0 -72 30t-30 73v430q0 43 30 72.5t72 29.5q43 0 73 -29.5t30 -72.5z" /> -<glyph unicode="" d="M663 1125q-11 -1 -15.5 -10.5t-8.5 -9.5q-5 -1 -5 5q0 12 19 15h10zM750 1111q-4 -1 -11.5 6.5t-17.5 4.5q24 11 32 -2q3 -6 -3 -9zM399 684q-4 1 -6 -3t-4.5 -12.5t-5.5 -13.5t-10 -13q-7 -10 -1 -12q4 -1 12.5 7t12.5 18q1 3 2 7t2 6t1.5 4.5t0.5 4v3t-1 2.5t-3 2z M1254 325q0 18 -55 42q4 15 7.5 27.5t5 26t3 21.5t0.5 22.5t-1 19.5t-3.5 22t-4 20.5t-5 25t-5.5 26.5q-10 48 -47 103t-72 75q24 -20 57 -83q87 -162 54 -278q-11 -40 -50 -42q-31 -4 -38.5 18.5t-8 83.5t-11.5 107q-9 39 -19.5 69t-19.5 45.5t-15.5 24.5t-13 15t-7.5 7 q-14 62 -31 103t-29.5 56t-23.5 33t-15 40q-4 21 6 53.5t4.5 49.5t-44.5 25q-15 3 -44.5 18t-35.5 16q-8 1 -11 26t8 51t36 27q37 3 51 -30t4 -58q-11 -19 -2 -26.5t30 -0.5q13 4 13 36v37q-5 30 -13.5 50t-21 30.5t-23.5 15t-27 7.5q-107 -8 -89 -134q0 -15 -1 -15 q-9 9 -29.5 10.5t-33 -0.5t-15.5 5q1 57 -16 90t-45 34q-27 1 -41.5 -27.5t-16.5 -59.5q-1 -15 3.5 -37t13 -37.5t15.5 -13.5q10 3 16 14q4 9 -7 8q-7 0 -15.5 14.5t-9.5 33.5q-1 22 9 37t34 14q17 0 27 -21t9.5 -39t-1.5 -22q-22 -15 -31 -29q-8 -12 -27.5 -23.5 t-20.5 -12.5q-13 -14 -15.5 -27t7.5 -18q14 -8 25 -19.5t16 -19t18.5 -13t35.5 -6.5q47 -2 102 15q2 1 23 7t34.5 10.5t29.5 13t21 17.5q9 14 20 8q5 -3 6.5 -8.5t-3 -12t-16.5 -9.5q-20 -6 -56.5 -21.5t-45.5 -19.5q-44 -19 -70 -23q-25 -5 -79 2q-10 2 -9 -2t17 -19 q25 -23 67 -22q17 1 36 7t36 14t33.5 17.5t30 17t24.5 12t17.5 2.5t8.5 -11q0 -2 -1 -4.5t-4 -5t-6 -4.5t-8.5 -5t-9 -4.5t-10 -5t-9.5 -4.5q-28 -14 -67.5 -44t-66.5 -43t-49 -1q-21 11 -63 73q-22 31 -25 22q-1 -3 -1 -10q0 -25 -15 -56.5t-29.5 -55.5t-21 -58t11.5 -63 q-23 -6 -62.5 -90t-47.5 -141q-2 -18 -1.5 -69t-5.5 -59q-8 -24 -29 -3q-32 31 -36 94q-2 28 4 56q4 19 -1 18l-4 -5q-36 -65 10 -166q5 -12 25 -28t24 -20q20 -23 104 -90.5t93 -76.5q16 -15 17.5 -38t-14 -43t-45.5 -23q8 -15 29 -44.5t28 -54t7 -70.5q46 24 7 92 q-4 8 -10.5 16t-9.5 12t-2 6q3 5 13 9.5t20 -2.5q46 -52 166 -36q133 15 177 87q23 38 34 30q12 -6 10 -52q-1 -25 -23 -92q-9 -23 -6 -37.5t24 -15.5q3 19 14.5 77t13.5 90q2 21 -6.5 73.5t-7.5 97t23 70.5q15 18 51 18q1 37 34.5 53t72.5 10.5t60 -22.5zM626 1152 q3 17 -2.5 30t-11.5 15q-9 2 -9 -7q2 -5 5 -6q10 0 7 -15q-3 -20 8 -20q3 0 3 3zM1045 955q-2 8 -6.5 11.5t-13 5t-14.5 5.5q-5 3 -9.5 8t-7 8t-5.5 6.5t-4 4t-4 -1.5q-14 -16 7 -43.5t39 -31.5q9 -1 14.5 8t3.5 20zM867 1168q0 11 -5 19.5t-11 12.5t-9 3q-14 -1 -7 -7l4 -2 q14 -4 18 -31q0 -3 8 2zM921 1401q0 2 -2.5 5t-9 7t-9.5 6q-15 15 -24 15q-9 -1 -11.5 -7.5t-1 -13t-0.5 -12.5q-1 -4 -6 -10.5t-6 -9t3 -8.5q4 -3 8 0t11 9t15 9q1 1 9 1t15 2t9 7zM1486 60q20 -12 31 -24.5t12 -24t-2.5 -22.5t-15.5 -22t-23.5 -19.5t-30 -18.5 t-31.5 -16.5t-32 -15.5t-27 -13q-38 -19 -85.5 -56t-75.5 -64q-17 -16 -68 -19.5t-89 14.5q-18 9 -29.5 23.5t-16.5 25.5t-22 19.5t-47 9.5q-44 1 -130 1q-19 0 -57 -1.5t-58 -2.5q-44 -1 -79.5 -15t-53.5 -30t-43.5 -28.5t-53.5 -11.5q-29 1 -111 31t-146 43q-19 4 -51 9.5 t-50 9t-39.5 9.5t-33.5 14.5t-17 19.5q-10 23 7 66.5t18 54.5q1 16 -4 40t-10 42.5t-4.5 36.5t10.5 27q14 12 57 14t60 12q30 18 42 35t12 51q21 -73 -32 -106q-32 -20 -83 -15q-34 3 -43 -10q-13 -15 5 -57q2 -6 8 -18t8.5 -18t4.5 -17t1 -22q0 -15 -17 -49t-14 -48 q3 -17 37 -26q20 -6 84.5 -18.5t99.5 -20.5q24 -6 74 -22t82.5 -23t55.5 -4q43 6 64.5 28t23 48t-7.5 58.5t-19 52t-20 36.5q-121 190 -169 242q-68 74 -113 40q-11 -9 -15 15q-3 16 -2 38q1 29 10 52t24 47t22 42q8 21 26.5 72t29.5 78t30 61t39 54q110 143 124 195 q-12 112 -16 310q-2 90 24 151.5t106 104.5q39 21 104 21q53 1 106 -13.5t89 -41.5q57 -42 91.5 -121.5t29.5 -147.5q-5 -95 30 -214q34 -113 133 -218q55 -59 99.5 -163t59.5 -191q8 -49 5 -84.5t-12 -55.5t-20 -22q-10 -2 -23.5 -19t-27 -35.5t-40.5 -33.5t-61 -14 q-18 1 -31.5 5t-22.5 13.5t-13.5 15.5t-11.5 20.5t-9 19.5q-22 37 -41 30t-28 -49t7 -97q20 -70 1 -195q-10 -65 18 -100.5t73 -33t85 35.5q59 49 89.5 66.5t103.5 42.5q53 18 77 36.5t18.5 34.5t-25 28.5t-51.5 23.5q-33 11 -49.5 48t-15 72.5t15.5 47.5q1 -31 8 -56.5 t14.5 -40.5t20.5 -28.5t21 -19t21.5 -13t16.5 -9.5z" /> -<glyph unicode="" d="M1024 36q-42 241 -140 498h-2l-2 -1q-16 -6 -43 -16.5t-101 -49t-137 -82t-131 -114.5t-103 -148l-15 11q184 -150 418 -150q132 0 256 52zM839 643q-21 49 -53 111q-311 -93 -673 -93q-1 -7 -1 -21q0 -124 44 -236.5t124 -201.5q50 89 123.5 166.5t142.5 124.5t130.5 81 t99.5 48l37 13q4 1 13 3.5t13 4.5zM732 855q-120 213 -244 378q-138 -65 -234 -186t-128 -272q302 0 606 80zM1416 536q-210 60 -409 29q87 -239 128 -469q111 75 185 189.5t96 250.5zM611 1277q-1 0 -2 -1q1 1 2 1zM1201 1132q-185 164 -433 164q-76 0 -155 -19 q131 -170 246 -382q69 26 130 60.5t96.5 61.5t65.5 57t37.5 40.5zM1424 647q-3 232 -149 410l-1 -1q-9 -12 -19 -24.5t-43.5 -44.5t-71 -60.5t-100 -65t-131.5 -64.5q25 -53 44 -95q2 -6 6.5 -17.5t7.5 -16.5q36 5 74.5 7t73.5 2t69 -1.5t64 -4t56.5 -5.5t48 -6.5t36.5 -6 t25 -4.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1173 473q0 50 -19.5 91.5t-48.5 68.5t-73 49t-82.5 34t-87.5 23l-104 24q-30 7 -44 10.5t-35 11.5t-30 16t-16.5 21t-7.5 30q0 77 144 77q43 0 77 -12t54 -28.5t38 -33.5t40 -29t48 -12q47 0 75.5 32t28.5 77q0 55 -56 99.5t-142 67.5t-182 23q-68 0 -132 -15.5 t-119.5 -47t-89 -87t-33.5 -128.5q0 -61 19 -106.5t56 -75.5t80 -48.5t103 -32.5l146 -36q90 -22 112 -36q32 -20 32 -60q0 -39 -40 -64.5t-105 -25.5q-51 0 -91.5 16t-65 38.5t-45.5 45t-46 38.5t-54 16q-50 0 -75.5 -30t-25.5 -75q0 -92 122 -157.5t291 -65.5 q73 0 140 18.5t122.5 53.5t88.5 93.5t33 131.5zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5q-130 0 -234 80q-77 -16 -150 -16q-143 0 -273.5 55.5t-225 150t-150 225t-55.5 273.5q0 73 16 150q-80 104 -80 234q0 159 112.5 271.5t271.5 112.5q130 0 234 -80 q77 16 150 16q143 0 273.5 -55.5t225 -150t150 -225t55.5 -273.5q0 -73 -16 -150q80 -104 80 -234z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1000 1102l37 194q5 23 -9 40t-35 17h-712q-23 0 -38.5 -17t-15.5 -37v-1101q0 -7 6 -1l291 352q23 26 38 33.5t48 7.5h239q22 0 37 14.5t18 29.5q24 130 37 191q4 21 -11.5 40t-36.5 19h-294q-29 0 -48 19t-19 48v42q0 29 19 47.5t48 18.5h346q18 0 35 13.5t20 29.5z M1227 1324q-15 -73 -53.5 -266.5t-69.5 -350t-35 -173.5q-6 -22 -9 -32.5t-14 -32.5t-24.5 -33t-38.5 -21t-58 -10h-271q-13 0 -22 -10q-8 -9 -426 -494q-22 -25 -58.5 -28.5t-48.5 5.5q-55 22 -55 98v1410q0 55 38 102.5t120 47.5h888q95 0 127 -53t10 -159zM1227 1324 l-158 -790q4 17 35 173.5t69.5 350t53.5 266.5z" /> -<glyph unicode="" d="M704 192v1024q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-1024q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1376 576v640q0 14 -9 23t-23 9h-480q-14 0 -23 -9t-9 -23v-640q0 -14 9 -23t23 -9h480q14 0 23 9t9 23zM1536 1344v-1408q0 -26 -19 -45t-45 -19h-1408 q-26 0 -45 19t-19 45v1408q0 26 19 45t45 19h1408q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1280 480q0 -40 -28 -68t-68 -28q-51 0 -80 43l-227 341h-45v-132l247 -411q9 -15 9 -33q0 -26 -19 -45t-45 -19h-192v-272q0 -46 -33 -79t-79 -33h-160q-46 0 -79 33t-33 79v272h-192q-26 0 -45 19t-19 45q0 18 9 33l247 411v132h-45l-227 -341q-29 -43 -80 -43 q-40 0 -68 28t-28 68q0 29 16 53l256 384q73 107 176 107h384q103 0 176 -107l256 -384q16 -24 16 -53zM864 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 832v-416q0 -40 -28 -68t-68 -28t-68 28t-28 68v352h-64v-912q0 -46 -33 -79t-79 -33t-79 33t-33 79v464h-64v-464q0 -46 -33 -79t-79 -33t-79 33t-33 79v912h-64v-352q0 -40 -28 -68t-68 -28t-68 28t-28 68v416q0 80 56 136t136 56h640q80 0 136 -56t56 -136z M736 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" d="M773 234l350 473q16 22 24.5 59t-6 85t-61.5 79q-40 26 -83 25.5t-73.5 -17.5t-54.5 -45q-36 -40 -96 -40q-59 0 -95 40q-24 28 -54.5 45t-73.5 17.5t-84 -25.5q-46 -31 -60.5 -79t-6 -85t24.5 -59zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1472 640q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5zM1748 363q-4 -15 -20 -20l-292 -96v-306q0 -16 -13 -26q-15 -10 -29 -4 l-292 94l-180 -248q-10 -13 -26 -13t-26 13l-180 248l-292 -94q-14 -6 -29 4q-13 10 -13 26v306l-292 96q-16 5 -20 20q-5 17 4 29l180 248l-180 248q-9 13 -4 29q4 15 20 20l292 96v306q0 16 13 26q15 10 29 4l292 -94l180 248q9 12 26 12t26 -12l180 -248l292 94 q14 6 29 -4q13 -10 13 -26v-306l292 -96q16 -5 20 -20q5 -16 -4 -29l-180 -248l180 -248q9 -12 4 -29z" /> -<glyph unicode="" d="M1262 233q-54 -9 -110 -9q-182 0 -337 90t-245 245t-90 337q0 192 104 357q-201 -60 -328.5 -229t-127.5 -384q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51q144 0 273.5 61.5t220.5 171.5zM1465 318q-94 -203 -283.5 -324.5t-413.5 -121.5q-156 0 -298 61 t-245 164t-164 245t-61 298q0 153 57.5 292.5t156 241.5t235.5 164.5t290 68.5q44 2 61 -39q18 -41 -15 -72q-86 -78 -131.5 -181.5t-45.5 -218.5q0 -148 73 -273t198 -198t273 -73q118 0 228 51q41 18 72 -13q14 -14 17.5 -34t-4.5 -38z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1088 704q0 26 -19 45t-45 19h-256q-26 0 -45 -19t-19 -45t19 -45t45 -19h256q26 0 45 19t19 45zM1664 896v-960q0 -26 -19 -45t-45 -19h-1408q-26 0 -45 19t-19 45v960q0 26 19 45t45 19h1408q26 0 45 -19t19 -45zM1728 1344v-256q0 -26 -19 -45t-45 -19h-1536 q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h1536q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1632 576q0 -26 -19 -45t-45 -19h-224q0 -171 -67 -290l208 -209q19 -19 19 -45t-19 -45q-18 -19 -45 -19t-45 19l-198 197q-5 -5 -15 -13t-42 -28.5t-65 -36.5t-82 -29t-97 -13v896h-128v-896q-51 0 -101.5 13.5t-87 33t-66 39t-43.5 32.5l-15 14l-183 -207 q-20 -21 -48 -21q-24 0 -43 16q-19 18 -20.5 44.5t15.5 46.5l202 227q-58 114 -58 274h-224q-26 0 -45 19t-19 45t19 45t45 19h224v294l-173 173q-19 19 -19 45t19 45t45 19t45 -19l173 -173h844l173 173q19 19 45 19t45 -19t19 -45t-19 -45l-173 -173v-294h224q26 0 45 -19 t19 -45zM1152 1152h-640q0 133 93.5 226.5t226.5 93.5t226.5 -93.5t93.5 -226.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1917 1016q23 -64 -150 -294q-24 -32 -65 -85q-78 -100 -90 -131q-17 -41 14 -81q17 -21 81 -82h1l1 -1l1 -1l2 -2q141 -131 191 -221q3 -5 6.5 -12.5t7 -26.5t-0.5 -34t-25 -27.5t-59 -12.5l-256 -4q-24 -5 -56 5t-52 22l-20 12q-30 21 -70 64t-68.5 77.5t-61 58 t-56.5 15.5q-3 -1 -8 -3.5t-17 -14.5t-21.5 -29.5t-17 -52t-6.5 -77.5q0 -15 -3.5 -27.5t-7.5 -18.5l-4 -5q-18 -19 -53 -22h-115q-71 -4 -146 16.5t-131.5 53t-103 66t-70.5 57.5l-25 24q-10 10 -27.5 30t-71.5 91t-106 151t-122.5 211t-130.5 272q-6 16 -6 27t3 16l4 6 q15 19 57 19l274 2q12 -2 23 -6.5t16 -8.5l5 -3q16 -11 24 -32q20 -50 46 -103.5t41 -81.5l16 -29q29 -60 56 -104t48.5 -68.5t41.5 -38.5t34 -14t27 5q2 1 5 5t12 22t13.5 47t9.5 81t0 125q-2 40 -9 73t-14 46l-6 12q-25 34 -85 43q-13 2 5 24q17 19 38 30q53 26 239 24 q82 -1 135 -13q20 -5 33.5 -13.5t20.5 -24t10.5 -32t3.5 -45.5t-1 -55t-2.5 -70.5t-1.5 -82.5q0 -11 -1 -42t-0.5 -48t3.5 -40.5t11.5 -39t22.5 -24.5q8 -2 17 -4t26 11t38 34.5t52 67t68 107.5q60 104 107 225q4 10 10 17.5t11 10.5l4 3l5 2.5t13 3t20 0.5l288 2 q39 5 64 -2.5t31 -16.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M675 252q21 34 11 69t-45 50q-34 14 -73 1t-60 -46q-22 -34 -13 -68.5t43 -50.5t74.5 -2.5t62.5 47.5zM769 373q8 13 3.5 26.5t-17.5 18.5q-14 5 -28.5 -0.5t-21.5 -18.5q-17 -31 13 -45q14 -5 29 0.5t22 18.5zM943 266q-45 -102 -158 -150t-224 -12 q-107 34 -147.5 126.5t6.5 187.5q47 93 151.5 139t210.5 19q111 -29 158.5 -119.5t2.5 -190.5zM1255 426q-9 96 -89 170t-208.5 109t-274.5 21q-223 -23 -369.5 -141.5t-132.5 -264.5q9 -96 89 -170t208.5 -109t274.5 -21q223 23 369.5 141.5t132.5 264.5zM1563 422 q0 -68 -37 -139.5t-109 -137t-168.5 -117.5t-226 -83t-270.5 -31t-275 33.5t-240.5 93t-171.5 151t-65 199.5q0 115 69.5 245t197.5 258q169 169 341.5 236t246.5 -7q65 -64 20 -209q-4 -14 -1 -20t10 -7t14.5 0.5t13.5 3.5l6 2q139 59 246 59t153 -61q45 -63 0 -178 q-2 -13 -4.5 -20t4.5 -12.5t12 -7.5t17 -6q57 -18 103 -47t80 -81.5t34 -116.5zM1489 1046q42 -47 54.5 -108.5t-6.5 -117.5q-8 -23 -29.5 -34t-44.5 -4q-23 8 -34 29.5t-4 44.5q20 63 -24 111t-107 35q-24 -5 -45 8t-25 37q-5 24 8 44.5t37 25.5q60 13 119 -5.5t101 -65.5z M1670 1209q87 -96 112.5 -222.5t-13.5 -241.5q-9 -27 -34 -40t-52 -4t-40 34t-5 52q28 82 10 172t-80 158q-62 69 -148 95.5t-173 8.5q-28 -6 -52 9.5t-30 43.5t9.5 51.5t43.5 29.5q123 26 244 -11.5t208 -134.5z" /> -<glyph unicode="" d="M1133 -34q-171 -94 -368 -94q-196 0 -367 94q138 87 235.5 211t131.5 268q35 -144 132.5 -268t235.5 -211zM638 1394v-485q0 -252 -126.5 -459.5t-330.5 -306.5q-181 215 -181 495q0 187 83.5 349.5t229.5 269.5t325 137zM1536 638q0 -280 -181 -495 q-204 99 -330.5 306.5t-126.5 459.5v485q179 -30 325 -137t229.5 -269.5t83.5 -349.5z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1402 433q-32 -80 -76 -138t-91 -88.5t-99 -46.5t-101.5 -14.5t-96.5 8.5t-86.5 22t-69.5 27.5t-46 22.5l-17 10q-113 -228 -289.5 -359.5t-384.5 -132.5q-19 0 -32 13t-13 32t13 31.5t32 12.5q173 1 322.5 107.5t251.5 294.5q-36 -14 -72 -23t-83 -13t-91 2.5t-93 28.5 t-92 59t-84.5 100t-74.5 146q114 47 214 57t167.5 -7.5t124.5 -56.5t88.5 -77t56.5 -82q53 131 79 291q-7 -1 -18 -2.5t-46.5 -2.5t-69.5 0.5t-81.5 10t-88.5 23t-84 42.5t-75 65t-54.5 94.5t-28.5 127.5q70 28 133.5 36.5t112.5 -1t92 -30t73.5 -50t56 -61t42 -63t27.5 -56 t16 -39.5l4 -16q12 122 12 195q-8 6 -21.5 16t-49 44.5t-63.5 71.5t-54 93t-33 112.5t12 127t70 138.5q73 -25 127.5 -61.5t84.5 -76.5t48 -85t20.5 -89t-0.5 -85.5t-13 -76.5t-19 -62t-17 -42l-7 -15q1 -5 1 -50.5t-1 -71.5q3 7 10 18.5t30.5 43t50.5 58t71 55.5t91.5 44.5 t112 14.5t132.5 -24q-2 -78 -21.5 -141.5t-50 -104.5t-69.5 -71.5t-81.5 -45.5t-84.5 -24t-80 -9.5t-67.5 1t-46.5 4.5l-17 3q-23 -147 -73 -283q6 7 18 18.5t49.5 41t77.5 52.5t99.5 42t117.5 20t129 -23.5t137 -77.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1259 283v-66q0 -85 -57.5 -144.5t-138.5 -59.5h-57l-260 -269v269h-529q-81 0 -138.5 59.5t-57.5 144.5v66h1238zM1259 609v-255h-1238v255h1238zM1259 937v-255h-1238v255h1238zM1259 1077v-67h-1238v67q0 84 57.5 143.5t138.5 59.5h846q81 0 138.5 -59.5t57.5 -143.5z " /> -<glyph unicode="" d="M1152 640q0 -14 -9 -23l-320 -320q-9 -9 -23 -9q-13 0 -22.5 9.5t-9.5 22.5v192h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v192q0 14 9 23t23 9q12 0 24 -10l319 -319q9 -9 9 -23zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1152 736v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-192q0 -14 -9 -23t-23 -9q-12 0 -24 10l-319 319q-9 9 -9 23t9 23l320 320q9 9 23 9q13 0 22.5 -9.5t9.5 -22.5v-192h352q13 0 22.5 -9.5t9.5 -22.5zM1312 640q0 148 -73 273t-198 198t-273 73t-273 -73t-198 -198 t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455q40 0 57 -35l228 -455z" /> -<glyph unicode="" d="M1254 899q16 85 -21 132q-52 65 -187 45q-17 -3 -41 -12.5t-57.5 -30.5t-64.5 -48.5t-59.5 -70t-44.5 -91.5q80 7 113.5 -16t26.5 -99q-5 -52 -52 -143q-43 -78 -71 -99q-44 -32 -87 14q-23 24 -37.5 64.5t-19 73t-10 84t-8.5 71.5q-23 129 -34 164q-12 37 -35.5 69 t-50.5 40q-57 16 -127 -25q-54 -32 -136.5 -106t-122.5 -102v-7q16 -8 25.5 -26t21.5 -20q21 -3 54.5 8.5t58 10.5t41.5 -30q11 -18 18.5 -38.5t15 -48t12.5 -40.5q17 -46 53 -187q36 -146 57 -197q42 -99 103 -125q43 -12 85 -1.5t76 31.5q131 77 250 237 q104 139 172.5 292.5t82.5 226.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2176" d="M620 416q-110 -64 -268 -64h-128v64h-64q-13 0 -22.5 23.5t-9.5 56.5q0 24 7 49q-58 2 -96.5 10.5t-38.5 20.5t38.5 20.5t96.5 10.5q-7 25 -7 49q0 33 9.5 56.5t22.5 23.5h64v64h128q158 0 268 -64h1113q42 -7 106.5 -18t80.5 -14q89 -15 150 -40.5t83.5 -47.5t22.5 -40 t-22.5 -40t-83.5 -47.5t-150 -40.5q-16 -3 -80.5 -14t-106.5 -18h-1113zM1739 668q53 -36 53 -92t-53 -92l81 -30q68 48 68 122t-68 122zM625 400h1015q-217 -38 -456 -80q-57 0 -113 -24t-83 -48l-28 -24l-288 -288q-26 -26 -70.5 -45t-89.5 -19h-96l-93 464h29 q157 0 273 64zM352 816h-29l93 464h96q46 0 90 -19t70 -45l288 -288q4 -4 11 -10.5t30.5 -23t48.5 -29t61.5 -23t72.5 -10.5l456 -80h-1015q-116 64 -273 64z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1519 760q62 0 103.5 -40.5t41.5 -101.5q0 -97 -93 -130l-172 -59l56 -167q7 -21 7 -47q0 -59 -42 -102t-101 -43q-47 0 -85.5 27t-53.5 72l-55 165l-310 -106l55 -164q8 -24 8 -47q0 -59 -42 -102t-102 -43q-47 0 -85 27t-53 72l-55 163l-153 -53q-29 -9 -50 -9 q-61 0 -101.5 40t-40.5 101q0 47 27.5 85t71.5 53l156 53l-105 313l-156 -54q-26 -8 -48 -8q-60 0 -101 40.5t-41 100.5q0 47 27.5 85t71.5 53l157 53l-53 159q-8 24 -8 47q0 60 42 102.5t102 42.5q47 0 85 -27t53 -72l54 -160l310 105l-54 160q-8 24 -8 47q0 59 42.5 102 t101.5 43q47 0 85.5 -27.5t53.5 -71.5l53 -161l162 55q21 6 43 6q60 0 102.5 -39.5t42.5 -98.5q0 -45 -30 -81.5t-74 -51.5l-157 -54l105 -316l164 56q24 8 46 8zM725 498l310 105l-105 315l-310 -107z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM1280 352v436q-31 -35 -64 -55q-34 -22 -132.5 -85t-151.5 -99q-98 -69 -164 -69v0v0q-66 0 -164 69 q-46 32 -141.5 92.5t-142.5 92.5q-12 8 -33 27t-31 27v-436q0 -40 28 -68t68 -28h832q40 0 68 28t28 68zM1280 925q0 41 -27.5 70t-68.5 29h-832q-40 0 -68 -28t-28 -68q0 -37 30.5 -76.5t67.5 -64.5q47 -32 137.5 -89t129.5 -83q3 -2 17 -11.5t21 -14t21 -13t23.5 -13 t21.5 -9.5t22.5 -7.5t20.5 -2.5t20.5 2.5t22.5 7.5t21.5 9.5t23.5 13t21 13t21 14t17 11.5l267 174q35 23 66.5 62.5t31.5 73.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M127 640q0 163 67 313l367 -1005q-196 95 -315 281t-119 411zM1415 679q0 -19 -2.5 -38.5t-10 -49.5t-11.5 -44t-17.5 -59t-17.5 -58l-76 -256l-278 826q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-75 1 -202 10q-12 1 -20.5 -5t-11.5 -15t-1.5 -18.5t9 -16.5 t19.5 -8l80 -8l120 -328l-168 -504l-280 832q46 3 88 8q19 2 26 18.5t-2.5 31t-28.5 13.5l-205 -10q-7 0 -23 0.5t-26 0.5q105 160 274.5 253.5t367.5 93.5q147 0 280.5 -53t238.5 -149h-10q-55 0 -92 -40.5t-37 -95.5q0 -12 2 -24t4 -21.5t8 -23t9 -21t12 -22.5t12.5 -21 t14.5 -24t14 -23q63 -107 63 -212zM909 573l237 -647q1 -6 5 -11q-126 -44 -255 -44q-112 0 -217 32zM1570 1009q95 -174 95 -369q0 -209 -104 -385.5t-279 -278.5l235 678q59 169 59 276q0 42 -6 79zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286 t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 -215q173 0 331.5 68t273 182.5t182.5 273t68 331.5t-68 331.5t-182.5 273t-273 182.5t-331.5 68t-331.5 -68t-273 -182.5t-182.5 -273t-68 -331.5t68 -331.5t182.5 -273 t273 -182.5t331.5 -68z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1086 1536v-1536l-272 -128q-228 20 -414 102t-293 208.5t-107 272.5q0 140 100.5 263.5t275 205.5t391.5 108v-172q-217 -38 -356.5 -150t-139.5 -255q0 -152 154.5 -267t388.5 -145v1360zM1755 954l37 -390l-525 114l147 83q-119 70 -280 99v172q277 -33 481 -157z" /> -<glyph unicode="" horiz-adv-x="2048" d="M960 1536l960 -384v-128h-128q0 -26 -20.5 -45t-48.5 -19h-1526q-28 0 -48.5 19t-20.5 45h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59q28 0 48.5 -19t20.5 -45v-64h-1664v64q0 26 20.5 45t48.5 19h59v768zM1851 -64 q28 0 48.5 -19t20.5 -45v-128h-1920v128q0 26 20.5 45t48.5 19h1782z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1774 700l18 -316q4 -69 -82 -128t-235 -93.5t-323 -34.5t-323 34.5t-235 93.5t-82 128l18 316l574 -181q22 -7 48 -7t48 7zM2304 1024q0 -23 -22 -31l-1120 -352q-4 -1 -10 -1t-10 1l-652 206q-43 -34 -71 -111.5t-34 -178.5q63 -36 63 -109q0 -69 -58 -107l58 -433 q2 -14 -8 -25q-9 -11 -24 -11h-192q-15 0 -24 11q-10 11 -8 25l58 433q-58 38 -58 107q0 73 65 111q11 207 98 330l-333 104q-22 8 -22 31t22 31l1120 352q4 1 10 1t10 -1l1120 -352q22 -8 22 -31z" /> -<glyph unicode="" d="M859 579l13 -707q-62 11 -105 11q-41 0 -105 -11l13 707q-40 69 -168.5 295.5t-216.5 374.5t-181 287q58 -15 108 -15q43 0 111 15q63 -111 133.5 -229.5t167 -276.5t138.5 -227q37 61 109.5 177.5t117.5 190t105 176t107 189.5q54 -14 107 -14q56 0 114 14v0 q-28 -39 -60 -88.5t-49.5 -78.5t-56.5 -96t-49 -84q-146 -248 -353 -610z" /> -<glyph unicode="" horiz-adv-x="1280" d="M981 197q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -49 2q-53 0 -104.5 -7t-107 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -56 23.5 -102t61 -75.5t87 -50t100 -29t101.5 -8.5q58 0 111.5 13t99 39t73 73t27.5 109zM864 1055 q0 59 -17 125.5t-48 129t-84 103.5t-117 41q-42 0 -82.5 -19.5t-66.5 -52.5q-46 -59 -46 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26q37 0 77.5 16.5t65.5 43.5q53 56 53 159zM752 1536h417l-137 -88h-132q75 -63 113 -133t38 -160q0 -72 -24.5 -129.5 t-59.5 -93t-69.5 -65t-59 -61.5t-24.5 -66q0 -36 32 -70.5t77 -68t90.5 -73.5t77.5 -104t32 -142q0 -91 -49 -173q-71 -122 -209.5 -179.5t-298.5 -57.5q-132 0 -246.5 41.5t-172.5 137.5q-36 59 -36 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 41 -47.5 73.5 t-15.5 73.5q0 40 21 85q-46 -4 -68 -4q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q76 66 182 98t218 32z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1509 107q0 -14 -12 -29q-52 -59 -147.5 -83t-196.5 -24q-252 0 -346 107q-12 15 -12 29q0 17 12 29.5t29 12.5q15 0 30 -12q58 -49 125.5 -66t159.5 -17t160 17t127 66q15 12 30 12q17 0 29 -12.5t12 -29.5zM978 498q0 -61 -43 -104t-104 -43q-60 0 -104.5 43.5 t-44.5 103.5q0 61 44 105t105 44t104 -44t43 -105zM1622 498q0 -61 -43 -104t-104 -43q-60 0 -104.5 43.5t-44.5 103.5q0 61 44 105t105 44t104 -44t43 -105zM415 793q-39 27 -88 27q-66 0 -113 -47t-47 -113q0 -72 54 -121q53 141 194 254zM2020 382q0 222 -249 387 q-128 85 -291.5 126.5t-331.5 41.5t-331.5 -41.5t-292.5 -126.5q-249 -165 -249 -387t249 -387q129 -85 292.5 -126.5t331.5 -41.5t331.5 41.5t291.5 126.5q249 165 249 387zM2137 660q0 66 -47 113t-113 47q-50 0 -93 -30q140 -114 192 -256q61 48 61 126zM1993 1335 q0 49 -34.5 83.5t-82.5 34.5q-49 0 -83.5 -34.5t-34.5 -83.5q0 -48 34.5 -82.5t83.5 -34.5q48 0 82.5 34.5t34.5 82.5zM2220 660q0 -65 -33 -122t-89 -90q5 -35 5 -66q0 -139 -79 -255.5t-208 -201.5q-140 -92 -313.5 -136.5t-354.5 -44.5t-355 44.5t-314 136.5 q-129 85 -208 201.5t-79 255.5q0 36 6 71q-53 33 -83.5 88.5t-30.5 118.5q0 100 71 171.5t172 71.5q91 0 159 -60q265 170 638 177l144 456q10 29 40 29q24 0 384 -90q24 55 74 88t110 33q82 0 141 -59t59 -142t-59 -141.5t-141 -58.5q-83 0 -141.5 58.5t-59.5 140.5 l-339 80l-125 -395q349 -15 603 -179q71 63 163 63q101 0 172 -71.5t71 -171.5z" /> -<glyph unicode="" d="M950 393q7 7 17.5 7t17.5 -7t7 -18t-7 -18q-65 -64 -208 -64h-1h-1q-143 0 -207 64q-8 7 -8 18t8 18q7 7 17.5 7t17.5 -7q49 -51 172 -51h1h1q122 0 173 51zM671 613q0 -37 -26 -64t-63 -27t-63 27t-26 64t26 63t63 26t63 -26t26 -63zM1214 1049q-29 0 -50 21t-21 50 q0 30 21 51t50 21q30 0 51 -21t21 -51q0 -29 -21 -50t-51 -21zM1216 1408q132 0 226 -94t94 -227v-894q0 -133 -94 -227t-226 -94h-896q-132 0 -226 94t-94 227v894q0 133 94 227t226 94h896zM1321 596q35 14 57 45.5t22 70.5q0 51 -36 87.5t-87 36.5q-60 0 -98 -48 q-151 107 -375 115l83 265l206 -49q1 -50 36.5 -85t84.5 -35q50 0 86 35.5t36 85.5t-36 86t-86 36q-36 0 -66 -20.5t-45 -53.5l-227 54q-9 2 -17.5 -2.5t-11.5 -14.5l-95 -302q-224 -4 -381 -113q-36 43 -93 43q-51 0 -87 -36.5t-36 -87.5q0 -37 19.5 -67.5t52.5 -45.5 q-7 -25 -7 -54q0 -98 74 -181.5t201.5 -132t278.5 -48.5q150 0 277.5 48.5t201.5 132t74 181.5q0 27 -6 54zM971 702q37 0 63 -26t26 -63t-26 -64t-63 -27t-63 27t-26 64t26 63t63 26z" /> -<glyph unicode="" d="M866 697l90 27v62q0 79 -58 135t-138 56t-138 -55.5t-58 -134.5v-283q0 -20 -14 -33.5t-33 -13.5t-32.5 13.5t-13.5 33.5v120h-151v-122q0 -82 57.5 -139t139.5 -57q81 0 138.5 56.5t57.5 136.5v280q0 19 13.5 33t33.5 14q19 0 32.5 -14t13.5 -33v-54zM1199 502v122h-150 v-126q0 -20 -13.5 -33.5t-33.5 -13.5q-19 0 -32.5 14t-13.5 33v123l-90 -26l-60 28v-123q0 -80 58 -137t139 -57t138.5 57t57.5 139zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103 t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1062 824v118q0 42 -30 72t-72 30t-72 -30t-30 -72v-612q0 -175 -126 -299t-303 -124q-178 0 -303.5 125.5t-125.5 303.5v266h328v-262q0 -43 30 -72.5t72 -29.5t72 29.5t30 72.5v620q0 171 126.5 292t301.5 121q176 0 302 -122t126 -294v-136l-195 -58zM1592 602h328 v-266q0 -178 -125.5 -303.5t-303.5 -125.5q-177 0 -303 124.5t-126 300.5v268l131 -61l195 58v-270q0 -42 30 -71.5t72 -29.5t72 29.5t30 71.5v275z" /> -<glyph unicode="" d="M1472 160v480h-704v704h-480q-93 0 -158.5 -65.5t-65.5 -158.5v-480h704v-704h480q93 0 158.5 65.5t65.5 158.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M328 1254h204v-983h-532v697h328v286zM328 435v369h-123v-369h123zM614 968v-697h205v697h-205zM614 1254v-204h205v204h-205zM901 968h533v-942h-533v163h328v82h-328v697zM1229 435v369h-123v-369h123zM1516 968h532v-942h-532v163h327v82h-327v697zM1843 435v369h-123 v-369h123z" /> -<glyph unicode="" d="M1046 516q0 -64 -38 -109t-91 -45q-43 0 -70 15v277q28 17 70 17q53 0 91 -45.5t38 -109.5zM703 944q0 -64 -38 -109.5t-91 -45.5q-43 0 -70 15v277q28 17 70 17q53 0 91 -45t38 -109zM1265 513q0 134 -88 229t-213 95q-20 0 -39 -3q-23 -78 -78 -136q-87 -95 -211 -101 v-636l211 41v206q51 -19 117 -19q125 0 213 95t88 229zM922 940q0 134 -88.5 229t-213.5 95q-74 0 -141 -36h-186v-840l211 41v206q55 -19 116 -19q125 0 213.5 95t88.5 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2038" d="M1222 607q75 3 143.5 -20.5t118 -58.5t101 -94.5t84 -108t75.5 -120.5q33 -56 78.5 -109t75.5 -80.5t99 -88.5q-48 -30 -108.5 -57.5t-138.5 -59t-114 -47.5q-44 37 -74 115t-43.5 164.5t-33 180.5t-42.5 168.5t-72.5 123t-122.5 48.5l-10 -2l-6 -4q4 -5 13 -14 q6 -5 28 -23.5t25.5 -22t19 -18t18 -20.5t11.5 -21t10.5 -27.5t4.5 -31t4 -40.5l1 -33q1 -26 -2.5 -57.5t-7.5 -52t-12.5 -58.5t-11.5 -53q-35 1 -101 -9.5t-98 -10.5q-39 0 -72 10q-2 16 -2 47q0 74 3 96q2 13 31.5 41.5t57 59t26.5 51.5q-24 2 -43 -24 q-36 -53 -111.5 -99.5t-136.5 -46.5q-25 0 -75.5 63t-106.5 139.5t-84 96.5q-6 4 -27 30q-482 -112 -513 -112q-16 0 -28 11t-12 27q0 15 8.5 26.5t22.5 14.5l486 106q-8 14 -8 25t5.5 17.5t16 11.5t20 7t23 4.5t18.5 4.5q4 1 15.5 7.5t17.5 6.5q15 0 28 -16t20 -33 q163 37 172 37q17 0 29.5 -11t12.5 -28q0 -15 -8.5 -26t-23.5 -14l-182 -40l-1 -16q-1 -26 81.5 -117.5t104.5 -91.5q47 0 119 80t72 129q0 36 -23.5 53t-51 18.5t-51 11.5t-23.5 34q0 16 10 34l-68 19q43 44 43 117q0 26 -5 58q82 16 144 16q44 0 71.5 -1.5t48.5 -8.5 t31 -13.5t20.5 -24.5t15.5 -33.5t17 -47.5t24 -60l50 25q-3 -40 -23 -60t-42.5 -21t-40 -6.5t-16.5 -20.5zM1282 842q-5 5 -13.5 15.5t-12 14.5t-10.5 11.5t-10 10.5l-8 8t-8.5 7.5t-8 5t-8.5 4.5q-7 3 -14.5 5t-20.5 2.5t-22 0.5h-32.5h-37.5q-126 0 -217 -43 q16 30 36 46.5t54 29.5t65.5 36t46 36.5t50 55t43.5 50.5q12 -9 28 -31.5t32 -36.5t38 -13l12 1v-76l22 -1q247 95 371 190q28 21 50 39t42.5 37.5t33 31t29.5 34t24 31t24.5 37t23 38t27 47.5t29.5 53l7 9q-2 -53 -43 -139q-79 -165 -205 -264t-306 -142q-14 -3 -42 -7.5 t-50 -9.5t-39 -14q3 -19 24.5 -46t21.5 -34q0 -11 -26 -30zM1061 -79q39 26 131.5 47.5t146.5 21.5q9 0 22.5 -15.5t28 -42.5t26 -50t24 -51t14.5 -33q-121 -45 -244 -45q-61 0 -125 11zM822 568l48 12l109 -177l-73 -48zM1323 51q3 -15 3 -16q0 -7 -17.5 -14.5t-46 -13 t-54 -9.5t-53.5 -7.5t-32 -4.5l-7 43q21 2 60.5 8.5t72 10t60.5 3.5h14zM866 679l-96 -20l-6 17q10 1 32.5 7t34.5 6q19 0 35 -10zM1061 45h31l10 -83l-41 -12v95zM1950 1535v1v-1zM1950 1535l-1 -5l-2 -2l1 3zM1950 1535l1 1z" /> -<glyph unicode="" d="M1167 -50q-5 19 -24 5q-30 -22 -87 -39t-131 -17q-129 0 -193 49q-5 4 -13 4q-11 0 -26 -12q-7 -6 -7.5 -16t7.5 -20q34 -32 87.5 -46t102.5 -12.5t99 4.5q41 4 84.5 20.5t65 30t28.5 20.5q12 12 7 29zM1128 65q-19 47 -39 61q-23 15 -76 15q-47 0 -71 -10 q-29 -12 -78 -56q-26 -24 -12 -44q9 -8 17.5 -4.5t31.5 23.5q3 2 10.5 8.5t10.5 8.5t10 7t11.5 7t12.5 5t15 4.5t16.5 2.5t20.5 1q27 0 44.5 -7.5t23 -14.5t13.5 -22q10 -17 12.5 -20t12.5 1q23 12 14 34zM1483 346q0 22 -5 44.5t-16.5 45t-34 36.5t-52.5 14 q-33 0 -97 -41.5t-129 -83.5t-101 -42q-27 -1 -63.5 19t-76 49t-83.5 58t-100 49t-111 19q-115 -1 -197 -78.5t-84 -178.5q-2 -112 74 -164q29 -20 62.5 -28.5t103.5 -8.5q57 0 132 32.5t134 71t120 70.5t93 31q26 -1 65 -31.5t71.5 -67t68 -67.5t55.5 -32q35 -3 58.5 14 t55.5 63q28 41 42.5 101t14.5 106zM1536 506q0 -164 -62 -304.5t-166 -236t-242.5 -149.5t-290.5 -54t-293 57.5t-247.5 157t-170.5 241.5t-64 302q0 89 19.5 172.5t49 145.5t70.5 118.5t78.5 94t78.5 69.5t64.5 46.5t42.5 24.5q14 8 51 26.5t54.5 28.5t48 30t60.5 44 q36 28 58 72.5t30 125.5q129 -155 186 -193q44 -29 130 -68t129 -66q21 -13 39 -25t60.5 -46.5t76 -70.5t75 -95t69 -122t47 -148.5t19.5 -177.5z" /> -<glyph unicode="" d="M1070 463l-160 -160l-151 -152l-30 -30q-65 -64 -151.5 -87t-171.5 -2q-16 -70 -72 -115t-129 -45q-85 0 -145 60.5t-60 145.5q0 72 44.5 128t113.5 72q-22 86 1 173t88 152l12 12l151 -152l-11 -11q-37 -37 -37 -89t37 -90q37 -37 89 -37t89 37l30 30l151 152l161 160z M729 1145l12 -12l-152 -152l-12 12q-37 37 -89 37t-89 -37t-37 -89.5t37 -89.5l29 -29l152 -152l160 -160l-151 -152l-161 160l-151 152l-30 30q-68 67 -90 159.5t5 179.5q-70 15 -115 71t-45 129q0 85 60 145.5t145 60.5q76 0 133.5 -49t69.5 -123q84 20 169.5 -3.5 t149.5 -87.5zM1536 78q0 -85 -60 -145.5t-145 -60.5q-74 0 -131 47t-71 118q-86 -28 -179.5 -6t-161.5 90l-11 12l151 152l12 -12q37 -37 89 -37t89 37t37 89t-37 89l-30 30l-152 152l-160 160l152 152l160 -160l152 -152l29 -30q64 -64 87.5 -150.5t2.5 -171.5 q76 -11 126.5 -68.5t50.5 -134.5zM1534 1202q0 -77 -51 -135t-127 -69q26 -85 3 -176.5t-90 -158.5l-12 -12l-151 152l12 12q37 37 37 89t-37 89t-89 37t-89 -37l-30 -30l-152 -152l-160 -160l-152 152l161 160l152 152l29 30q67 67 159 89.5t178 -3.5q11 75 68.5 126 t135.5 51q85 0 145 -60.5t60 -145.5z" /> -<glyph unicode="" d="M654 458q-1 -3 -12.5 0.5t-31.5 11.5l-20 9q-44 20 -87 49q-7 5 -41 31.5t-38 28.5q-67 -103 -134 -181q-81 -95 -105 -110q-4 -2 -19.5 -4t-18.5 0q6 4 82 92q21 24 85.5 115t78.5 118q17 30 51 98.5t36 77.5q-8 1 -110 -33q-8 -2 -27.5 -7.5t-34.5 -9.5t-17 -5 q-2 -2 -2 -10.5t-1 -9.5q-5 -10 -31 -15q-23 -7 -47 0q-18 4 -28 21q-4 6 -5 23q6 2 24.5 5t29.5 6q58 16 105 32q100 35 102 35q10 2 43 19.5t44 21.5q9 3 21.5 8t14.5 5.5t6 -0.5q2 -12 -1 -33q0 -2 -12.5 -27t-26.5 -53.5t-17 -33.5q-25 -50 -77 -131l64 -28 q12 -6 74.5 -32t67.5 -28q4 -1 10.5 -25.5t4.5 -30.5zM449 944q3 -15 -4 -28q-12 -23 -50 -38q-30 -12 -60 -12q-26 3 -49 26q-14 15 -18 41l1 3q3 -3 19.5 -5t26.5 0t58 16q36 12 55 14q17 0 21 -17zM1147 815l63 -227l-139 42zM39 15l694 232v1032l-694 -233v-1031z M1280 332l102 -31l-181 657l-100 31l-216 -536l102 -31l45 110l211 -65zM777 1294l573 -184v380zM1088 -29l158 -13l-54 -160l-40 66q-130 -83 -276 -108q-58 -12 -91 -12h-84q-79 0 -199.5 39t-183.5 85q-8 7 -8 16q0 8 5 13.5t13 5.5q4 0 18 -7.5t30.5 -16.5t20.5 -11 q73 -37 159.5 -61.5t157.5 -24.5q95 0 167 14.5t157 50.5q15 7 30.5 15.5t34 19t28.5 16.5zM1536 1050v-1079l-774 246q-14 -6 -375 -127.5t-368 -121.5q-13 0 -18 13q0 1 -1 3v1078q3 9 4 10q5 6 20 11q106 35 149 50v384l558 -198q2 0 160.5 55t316 108.5t161.5 53.5 q20 0 20 -21v-418z" /> -<glyph unicode="" horiz-adv-x="1792" d="M288 1152q66 0 113 -47t47 -113v-1088q0 -66 -47 -113t-113 -47h-128q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h128zM1664 989q58 -34 93 -93t35 -128v-768q0 -106 -75 -181t-181 -75h-864q-66 0 -113 47t-47 113v1536q0 40 28 68t68 28h672q40 0 88 -20t76 -48 l152 -152q28 -28 48 -76t20 -88v-163zM928 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM928 512v128q0 14 -9 23 t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1184 256v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128 q14 0 23 9t9 23zM1184 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 0v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 256v128q0 14 -9 23t-23 9h-128 q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1440 512v128q0 14 -9 23t-23 9h-128q-14 0 -23 -9t-9 -23v-128q0 -14 9 -23t23 -9h128q14 0 23 9t9 23zM1536 896v256h-160q-40 0 -68 28t-28 68v160h-640v-512h896z" /> -<glyph unicode="" d="M1344 1536q26 0 45 -19t19 -45v-1664q0 -26 -19 -45t-45 -19h-1280q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h1280zM512 1248v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 992v-64q0 -14 9 -23t23 -9h64q14 0 23 9 t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 736v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM512 480v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 160v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM384 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM384 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 -96v192q0 14 -9 23t-23 9h-320q-14 0 -23 -9 t-9 -23v-192q0 -14 9 -23t23 -9h320q14 0 23 9t9 23zM896 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 928v64 q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM896 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 160v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64 q14 0 23 9t9 23zM1152 416v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 672v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 928v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9 t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1152 1184v64q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-64q0 -14 9 -23t23 -9h64q14 0 23 9t9 23z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1188 988l-292 -292v-824q0 -46 -33 -79t-79 -33t-79 33t-33 79v384h-64v-384q0 -46 -33 -79t-79 -33t-79 33t-33 79v824l-292 292q-28 28 -28 68t28 68t68 28t68 -28l228 -228h368l228 228q28 28 68 28t68 -28t28 -68t-28 -68zM864 1152q0 -93 -65.5 -158.5 t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1664" d="M780 1064q0 -60 -19 -113.5t-63 -92.5t-105 -39q-76 0 -138 57.5t-92 135.5t-30 151q0 60 19 113.5t63 92.5t105 39q77 0 138.5 -57.5t91.5 -135t30 -151.5zM438 581q0 -80 -42 -139t-119 -59q-76 0 -141.5 55.5t-100.5 133.5t-35 152q0 80 42 139.5t119 59.5 q76 0 141.5 -55.5t100.5 -134t35 -152.5zM832 608q118 0 255 -97.5t229 -237t92 -254.5q0 -46 -17 -76.5t-48.5 -45t-64.5 -20t-76 -5.5q-68 0 -187.5 45t-182.5 45q-66 0 -192.5 -44.5t-200.5 -44.5q-183 0 -183 146q0 86 56 191.5t139.5 192.5t187.5 146t193 59zM1071 819 q-61 0 -105 39t-63 92.5t-19 113.5q0 74 30 151.5t91.5 135t138.5 57.5q61 0 105 -39t63 -92.5t19 -113.5q0 -73 -30 -151t-92 -135.5t-138 -57.5zM1503 923q77 0 119 -59.5t42 -139.5q0 -74 -35 -152t-100.5 -133.5t-141.5 -55.5q-77 0 -119 59t-42 139q0 74 35 152.5 t100.5 134t141.5 55.5z" /> -<glyph unicode="" horiz-adv-x="768" d="M704 1008q0 -145 -57 -243.5t-152 -135.5l45 -821q2 -26 -16 -45t-44 -19h-192q-26 0 -44 19t-16 45l45 821q-95 37 -152 135.5t-57 243.5q0 128 42.5 249.5t117.5 200t160 78.5t160 -78.5t117.5 -200t42.5 -249.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 -93l640 349v636l-640 -233v-752zM832 772l698 254l-698 254l-698 -254zM1664 1024v-768q0 -35 -18 -65t-49 -47l-704 -384q-28 -16 -61 -16t-61 16l-704 384q-31 17 -49 47t-18 65v768q0 40 23 73t61 47l704 256q22 8 44 8t44 -8l704 -256q38 -14 61 -47t23 -73z " /> -<glyph unicode="" horiz-adv-x="2304" d="M640 -96l384 192v314l-384 -164v-342zM576 358l404 173l-404 173l-404 -173zM1664 -96l384 192v314l-384 -164v-342zM1600 358l404 173l-404 173l-404 -173zM1152 651l384 165v266l-384 -164v-267zM1088 1030l441 189l-441 189l-441 -189zM2176 512v-416q0 -36 -19 -67 t-52 -47l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-5 2 -7 4q-2 -2 -7 -4l-448 -224q-25 -14 -57 -14t-57 14l-448 224q-33 16 -52 47t-19 67v416q0 38 21.5 70t56.5 48l434 186v400q0 38 21.5 70t56.5 48l448 192q23 10 50 10t50 -10l448 -192q35 -16 56.5 -48t21.5 -70 v-400l434 -186q36 -16 57 -48t21 -70z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1848 1197h-511v-124h511v124zM1596 771q-90 0 -146 -52.5t-62 -142.5h408q-18 195 -200 195zM1612 186q63 0 122 32t76 87h221q-100 -307 -427 -307q-214 0 -340.5 132t-126.5 347q0 208 130.5 345.5t336.5 137.5q138 0 240.5 -68t153 -179t50.5 -248q0 -17 -2 -47h-658 q0 -111 57.5 -171.5t166.5 -60.5zM277 236h296q205 0 205 167q0 180 -199 180h-302v-347zM277 773h281q78 0 123.5 36.5t45.5 113.5q0 144 -190 144h-260v-294zM0 1282h594q87 0 155 -14t126.5 -47.5t90 -96.5t31.5 -154q0 -181 -172 -263q114 -32 172 -115t58 -204 q0 -75 -24.5 -136.5t-66 -103.5t-98.5 -71t-121 -42t-134 -13h-611v1260z" /> -<glyph unicode="" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM499 1041h-371v-787h382q117 0 197 57.5t80 170.5q0 158 -143 200q107 52 107 164q0 57 -19.5 96.5 t-56.5 60.5t-79 29.5t-97 8.5zM477 723h-176v184h163q119 0 119 -90q0 -94 -106 -94zM486 388h-185v217h189q124 0 124 -113q0 -104 -128 -104zM1136 356q-68 0 -104 38t-36 107h411q1 10 1 30q0 132 -74.5 220.5t-203.5 88.5q-128 0 -210 -86t-82 -216q0 -135 79 -217 t213 -82q205 0 267 191h-138q-11 -34 -47.5 -54t-75.5 -20zM1126 722q113 0 124 -122h-254q4 56 39 89t91 33zM964 988h319v-77h-319v77z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1582 954q0 -101 -71.5 -172.5t-172.5 -71.5t-172.5 71.5t-71.5 172.5t71.5 172.5t172.5 71.5t172.5 -71.5t71.5 -172.5zM812 212q0 104 -73 177t-177 73q-27 0 -54 -6l104 -42q77 -31 109.5 -106.5t1.5 -151.5q-31 -77 -107 -109t-152 -1q-21 8 -62 24.5t-61 24.5 q32 -60 91 -96.5t130 -36.5q104 0 177 73t73 177zM1642 953q0 126 -89.5 215.5t-215.5 89.5q-127 0 -216.5 -89.5t-89.5 -215.5q0 -127 89.5 -216t216.5 -89q126 0 215.5 89t89.5 216zM1792 953q0 -189 -133.5 -322t-321.5 -133l-437 -319q-12 -129 -109 -218t-229 -89 q-121 0 -214 76t-118 192l-230 92v429l389 -157q79 48 173 48q13 0 35 -2l284 407q2 187 135.5 319t320.5 132q188 0 321.5 -133.5t133.5 -321.5z" /> -<glyph unicode="" d="M1242 889q0 80 -57 136.5t-137 56.5t-136.5 -57t-56.5 -136q0 -80 56.5 -136.5t136.5 -56.5t137 56.5t57 136.5zM632 301q0 -83 -58 -140.5t-140 -57.5q-56 0 -103 29t-72 77q52 -20 98 -40q60 -24 120 1.5t85 86.5q24 60 -1.5 120t-86.5 84l-82 33q22 5 42 5 q82 0 140 -57.5t58 -140.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v153l172 -69q20 -92 93.5 -152t168.5 -60q104 0 181 70t87 173l345 252q150 0 255.5 105.5t105.5 254.5q0 150 -105.5 255.5t-255.5 105.5 q-148 0 -253 -104.5t-107 -252.5l-225 -322q-9 1 -28 1q-75 0 -137 -37l-297 119v468q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5zM1289 887q0 -100 -71 -170.5t-171 -70.5t-170.5 70.5t-70.5 170.5t70.5 171t170.5 71q101 0 171.5 -70.5t70.5 -171.5z " /> -<glyph unicode="" horiz-adv-x="1792" d="M836 367l-15 -368l-2 -22l-420 29q-36 3 -67 31.5t-47 65.5q-11 27 -14.5 55t4 65t12 55t21.5 64t19 53q78 -12 509 -28zM449 953l180 -379l-147 92q-63 -72 -111.5 -144.5t-72.5 -125t-39.5 -94.5t-18.5 -63l-4 -21l-190 357q-17 26 -18 56t6 47l8 18q35 63 114 188 l-140 86zM1680 436l-188 -359q-12 -29 -36.5 -46.5t-43.5 -20.5l-18 -4q-71 -7 -219 -12l8 -164l-230 367l211 362l7 -173q170 -16 283 -5t170 33zM895 1360q-47 -63 -265 -435l-317 187l-19 12l225 356q20 31 60 45t80 10q24 -2 48.5 -12t42 -21t41.5 -33t36 -34.5 t36 -39.5t32 -35zM1550 1053l212 -363q18 -37 12.5 -76t-27.5 -74q-13 -20 -33 -37t-38 -28t-48.5 -22t-47 -16t-51.5 -14t-46 -12q-34 72 -265 436l313 195zM1407 1279l142 83l-220 -373l-419 20l151 86q-34 89 -75 166t-75.5 123.5t-64.5 80t-47 46.5l-17 13l405 -1 q31 3 58 -10.5t39 -28.5l11 -15q39 -61 112 -190z" /> -<glyph unicode="" horiz-adv-x="2048" d="M480 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM516 768h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5zM1888 448q0 66 -47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47t113 47t47 113zM2048 544v-384 q0 -14 -9 -23t-23 -9h-96v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-1024v-128q0 -80 -56 -136t-136 -56t-136 56t-56 136v128h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5t179 63.5h768q98 0 179 -63.5t104 -157.5 l105 -419h28q93 0 158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1824 640q93 0 158.5 -65.5t65.5 -158.5v-384q0 -14 -9 -23t-23 -9h-96v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-1024v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5 t179 63.5h128v224q0 14 9 23t23 9h448q14 0 23 -9t9 -23v-224h128q98 0 179 -63.5t104 -157.5l105 -419h28zM320 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM516 640h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5z M1728 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47z" /> -<glyph unicode="" d="M1504 64q0 -26 -19 -45t-45 -19h-462q1 -17 6 -87.5t5 -108.5q0 -25 -18 -42.5t-43 -17.5h-320q-25 0 -43 17.5t-18 42.5q0 38 5 108.5t6 87.5h-462q-26 0 -45 19t-19 45t19 45l402 403h-229q-26 0 -45 19t-19 45t19 45l402 403h-197q-26 0 -45 19t-19 45t19 45l384 384 q19 19 45 19t45 -19l384 -384q19 -19 19 -45t-19 -45t-45 -19h-197l402 -403q19 -19 19 -45t-19 -45t-45 -19h-229l402 -403q19 -19 19 -45z" /> -<glyph unicode="" d="M1127 326q0 32 -30 51q-193 115 -447 115q-133 0 -287 -34q-42 -9 -42 -52q0 -20 13.5 -34.5t35.5 -14.5q5 0 37 8q132 27 243 27q226 0 397 -103q19 -11 33 -11q19 0 33 13.5t14 34.5zM1223 541q0 40 -35 61q-237 141 -548 141q-153 0 -303 -42q-48 -13 -48 -64 q0 -25 17.5 -42.5t42.5 -17.5q7 0 37 8q122 33 251 33q279 0 488 -124q24 -13 38 -13q25 0 42.5 17.5t17.5 42.5zM1331 789q0 47 -40 70q-126 73 -293 110.5t-343 37.5q-204 0 -364 -47q-23 -7 -38.5 -25.5t-15.5 -48.5q0 -31 20.5 -52t51.5 -21q11 0 40 8q133 37 307 37 q159 0 309.5 -34t253.5 -95q21 -12 40 -12q29 0 50.5 20.5t21.5 51.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1024" d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273l-30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273l30 30h301v-303z" /> -<glyph unicode="" horiz-adv-x="2304" d="M784 164l16 241l-16 523q-1 10 -7.5 17t-16.5 7q-9 0 -16 -7t-7 -17l-14 -523l14 -241q1 -10 7.5 -16.5t15.5 -6.5q22 0 24 23zM1080 193l11 211l-12 586q0 16 -13 24q-8 5 -16 5t-16 -5q-13 -8 -13 -24l-1 -6l-10 -579q0 -1 11 -236v-1q0 -10 6 -17q9 -11 23 -11 q11 0 20 9q9 7 9 20zM35 533l20 -128l-20 -126q-2 -9 -9 -9t-9 9l-17 126l17 128q2 9 9 9t9 -9zM121 612l26 -207l-26 -203q-2 -9 -10 -9q-9 0 -9 10l-23 202l23 207q0 9 9 9q8 0 10 -9zM401 159zM213 650l25 -245l-25 -237q0 -11 -11 -11q-10 0 -12 11l-21 237l21 245 q2 12 12 12q11 0 11 -12zM307 657l23 -252l-23 -244q-2 -13 -14 -13q-13 0 -13 13l-21 244l21 252q0 13 13 13q12 0 14 -13zM401 639l21 -234l-21 -246q-2 -16 -16 -16q-6 0 -10.5 4.5t-4.5 11.5l-20 246l20 234q0 6 4.5 10.5t10.5 4.5q14 0 16 -15zM784 164zM495 785 l21 -380l-21 -246q0 -7 -5 -12.5t-12 -5.5q-16 0 -18 18l-18 246l18 380q2 18 18 18q7 0 12 -5.5t5 -12.5zM589 871l19 -468l-19 -244q0 -8 -5.5 -13.5t-13.5 -5.5q-18 0 -20 19l-16 244l16 468q2 19 20 19q8 0 13.5 -5.5t5.5 -13.5zM687 911l18 -506l-18 -242 q-2 -21 -22 -21q-19 0 -21 21l-16 242l16 506q0 9 6.5 15.5t14.5 6.5q9 0 15 -6.5t7 -15.5zM1079 169v0v0zM881 915l15 -510l-15 -239q0 -10 -7.5 -17.5t-17.5 -7.5t-17 7t-8 18l-14 239l14 510q0 11 7.5 18t17.5 7t17.5 -7t7.5 -18zM980 896l14 -492l-14 -236q0 -11 -8 -19 t-19 -8t-19 8t-9 19l-12 236l12 492q1 12 9 20t19 8t18.5 -8t8.5 -20zM1192 404l-14 -231v0q0 -13 -9 -22t-22 -9t-22 9t-10 22l-6 114l-6 117l12 636v3q2 15 12 24q9 7 20 7q8 0 15 -5q14 -8 16 -26zM2304 423q0 -117 -83 -199.5t-200 -82.5h-786q-13 2 -22 11t-9 22v899 q0 23 28 33q85 34 181 34q195 0 338 -131.5t160 -323.5q53 22 110 22q117 0 200 -83t83 -201z" /> -<glyph unicode="" d="M768 768q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 0q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127 t443 -43zM768 384q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 1536q208 0 385 -34.5t280 -93.5t103 -128v-128q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5 t-103 128v128q0 69 103 128t280 93.5t385 34.5z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M894 465q33 -26 84 -56q59 7 117 7q147 0 177 -49q16 -22 2 -52q0 -1 -1 -2l-2 -2v-1q-6 -38 -71 -38q-48 0 -115 20t-130 53q-221 -24 -392 -83q-153 -262 -242 -262q-15 0 -28 7l-24 12q-1 1 -6 5q-10 10 -6 36q9 40 56 91.5t132 96.5q14 9 23 -6q2 -2 2 -4q52 85 107 197 q68 136 104 262q-24 82 -30.5 159.5t6.5 127.5q11 40 42 40h21h1q23 0 35 -15q18 -21 9 -68q-2 -6 -4 -8q1 -3 1 -8v-30q-2 -123 -14 -192q55 -164 146 -238zM318 54q52 24 137 158q-51 -40 -87.5 -84t-49.5 -74zM716 974q-15 -42 -2 -132q1 7 7 44q0 3 7 43q1 4 4 8 q-1 1 -1 2t-0.5 1.5t-0.5 1.5q-1 22 -13 36q0 -1 -1 -2v-2zM592 313q135 54 284 81q-2 1 -13 9.5t-16 13.5q-76 67 -127 176q-27 -86 -83 -197q-30 -56 -45 -83zM1238 329q-24 24 -140 24q76 -28 124 -28q14 0 18 1q0 1 -2 3z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M233 768v-107h70l164 -661h159l128 485q7 20 10 46q2 16 2 24h4l3 -24q1 -3 3.5 -20t5.5 -26l128 -485h159l164 661h70v107h-300v-107h90l-99 -438q-5 -20 -7 -46l-2 -21h-4l-3 21q-1 5 -4 21t-5 25l-144 545h-114l-144 -545q-2 -9 -4.5 -24.5t-3.5 -21.5l-4 -21h-4l-2 21 q-2 26 -7 46l-99 438h90v107h-300z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M429 106v-106h281v106h-75l103 161q5 7 10 16.5t7.5 13.5t3.5 4h2q1 -4 5 -10q2 -4 4.5 -7.5t6 -8t6.5 -8.5l107 -161h-76v-106h291v106h-68l-192 273l195 282h67v107h-279v-107h74l-103 -159q-4 -7 -10 -16.5t-9 -13.5l-2 -3h-2q-1 4 -5 10q-6 11 -17 23l-106 159h76v107 h-290v-107h68l189 -272l-194 -283h-68z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M416 106v-106h327v106h-93v167h137q76 0 118 15q67 23 106.5 87t39.5 146q0 81 -37 141t-100 87q-48 19 -130 19h-368v-107h92v-555h-92zM769 386h-119v268h120q52 0 83 -18q56 -33 56 -115q0 -89 -62 -120q-31 -15 -78 -15z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M1280 320v-320h-1024v192l192 192l128 -128l384 384zM448 512q-80 0 -136 56t-56 136t56 136t136 56t136 -56t56 -136t-56 -136t-136 -56z" /> -<glyph unicode="" d="M640 1152v128h-128v-128h128zM768 1024v128h-128v-128h128zM640 896v128h-128v-128h128zM768 768v128h-128v-128h128zM1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400 v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-128v-128h-128v128h-512v-1536h1280zM781 593l107 -349q8 -27 8 -52q0 -83 -72.5 -137.5t-183.5 -54.5t-183.5 54.5t-72.5 137.5q0 25 8 52q21 63 120 396v128h128v-128h79 q22 0 39 -13t23 -34zM640 128q53 0 90.5 19t37.5 45t-37.5 45t-90.5 19t-90.5 -19t-37.5 -45t37.5 -45t90.5 -19z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M620 686q20 -8 20 -30v-544q0 -22 -20 -30q-8 -2 -12 -2q-12 0 -23 9l-166 167h-131q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h131l166 167q16 15 35 7zM1037 -3q31 0 50 24q129 159 129 363t-129 363q-16 21 -43 24t-47 -14q-21 -17 -23.5 -43.5t14.5 -47.5 q100 -123 100 -282t-100 -282q-17 -21 -14.5 -47.5t23.5 -42.5q18 -15 40 -15zM826 145q27 0 47 20q87 93 87 219t-87 219q-18 19 -45 20t-46 -17t-20 -44.5t18 -46.5q52 -57 52 -131t-52 -131q-19 -20 -18 -46.5t20 -44.5q20 -17 44 -17z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M768 768q52 0 90 -38t38 -90v-384q0 -52 -38 -90t-90 -38h-384q-52 0 -90 38t-38 90v384q0 52 38 90t90 38h384zM1260 766q20 -8 20 -30v-576q0 -22 -20 -30q-8 -2 -12 -2q-14 0 -23 9l-265 266v90l265 266q9 9 23 9q4 0 12 -2z" /> -<glyph unicode="" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M480 768q8 11 21 12.5t24 -6.5l51 -38q11 -8 12.5 -21t-6.5 -24l-182 -243l182 -243q8 -11 6.5 -24t-12.5 -21l-51 -38q-11 -8 -24 -6.5t-21 12.5l-226 301q-14 19 0 38zM1282 467q14 -19 0 -38l-226 -301q-8 -11 -21 -12.5t-24 6.5l-51 38q-11 8 -12.5 21t6.5 24l182 243 l-182 243q-8 11 -6.5 24t12.5 21l51 38q11 8 24 6.5t21 -12.5zM662 6q-13 2 -20.5 13t-5.5 24l138 831q2 13 13 20.5t24 5.5l63 -10q13 -2 20.5 -13t5.5 -24l-138 -831q-2 -13 -13 -20.5t-24 -5.5z" /> -<glyph unicode="" d="M1497 709v-198q-101 -23 -198 -23q-65 -136 -165.5 -271t-181.5 -215.5t-128 -106.5q-80 -45 -162 3q-28 17 -60.5 43.5t-85 83.5t-102.5 128.5t-107.5 184t-105.5 244t-91.5 314.5t-70.5 390h283q26 -218 70 -398.5t104.5 -317t121.5 -235.5t140 -195q169 169 287 406 q-142 72 -223 220t-81 333q0 192 104 314.5t284 122.5q178 0 273 -105.5t95 -297.5q0 -159 -58 -286q-7 -1 -19.5 -3t-46 -2t-63 6t-62 25.5t-50.5 51.5q31 103 31 184q0 87 -29 132t-79 45q-53 0 -85 -49.5t-32 -140.5q0 -186 105 -293.5t267 -107.5q62 0 121 14z" /> -<glyph unicode="" horiz-adv-x="1792" d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546 q0 -41 -34 -64l-819 -546q-21 -13 -43 -13t-43 13l-819 546q-34 23 -34 64v546q0 41 34 64l819 546q21 13 43 13t43 -13l819 -546q34 -23 34 -64z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1800 764q111 -46 179.5 -145.5t68.5 -221.5q0 -164 -118 -280.5t-285 -116.5q-4 0 -11.5 0.5t-10.5 0.5h-1209h-1h-2h-5q-170 10 -288 125.5t-118 280.5q0 110 55 203t147 147q-12 39 -12 82q0 115 82 196t199 81q95 0 172 -58q75 154 222.5 248t326.5 94 q166 0 306 -80.5t221.5 -218.5t81.5 -301q0 -6 -0.5 -18t-0.5 -18zM468 498q0 -122 84 -193t208 -71q137 0 240 99q-16 20 -47.5 56.5t-43.5 50.5q-67 -65 -144 -65q-55 0 -93.5 33.5t-38.5 87.5q0 53 38.5 87t91.5 34q44 0 84.5 -21t73 -55t65 -75t69 -82t77 -75t97 -55 t121.5 -21q121 0 204.5 71.5t83.5 190.5q0 121 -84 192t-207 71q-143 0 -241 -97q14 -16 29.5 -34t34.5 -40t29 -34q66 64 142 64q52 0 92 -33t40 -84q0 -57 -37 -91.5t-94 -34.5q-43 0 -82.5 21t-72 55t-65.5 75t-69.5 82t-77.5 75t-96.5 55t-118.5 21q-122 0 -207 -70.5 t-85 -189.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 1408q-190 0 -361 -90l194 -194q82 28 167 28t167 -28l194 194q-171 90 -361 90zM218 279l194 194 q-28 82 -28 167t28 167l-194 194q-90 -171 -90 -361t90 -361zM896 -128q190 0 361 90l-194 194q-82 -28 -167 -28t-167 28l-194 -194q171 -90 361 -90zM896 256q159 0 271.5 112.5t112.5 271.5t-112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5 t271.5 -112.5zM1380 473l194 -194q90 171 90 361t-90 361l-194 -194q28 -82 28 -167t-28 -167z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348q0 222 101 414.5t276.5 317t390.5 155.5v-260q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 q0 230 -145.5 406t-366.5 221v260q215 -31 390.5 -155.5t276.5 -317t101 -414.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M19 662q8 217 116 406t305 318h5q0 -1 -1 -3q-8 -8 -28 -33.5t-52 -76.5t-60 -110.5t-44.5 -135.5t-14 -150.5t39 -157.5t108.5 -154q50 -50 102 -69.5t90.5 -11.5t69.5 23.5t47 32.5l16 16q39 51 53 116.5t6.5 122.5t-21 107t-26.5 80l-14 29q-10 25 -30.5 49.5t-43 41 t-43.5 29.5t-35 19l-13 6l104 115q39 -17 78 -52t59 -61l19 -27q1 48 -18.5 103.5t-40.5 87.5l-20 31l161 183l160 -181q-33 -46 -52.5 -102.5t-22.5 -90.5l-4 -33q22 37 61.5 72.5t67.5 52.5l28 17l103 -115q-44 -14 -85 -50t-60 -65l-19 -29q-31 -56 -48 -133.5t-7 -170 t57 -156.5q33 -45 77.5 -60.5t85 -5.5t76 26.5t57.5 33.5l21 16q60 53 96.5 115t48.5 121.5t10 121.5t-18 118t-37 107.5t-45.5 93t-45 72t-34.5 47.5l-13 17q-14 13 -7 13l10 -3q40 -29 62.5 -46t62 -50t64 -58t58.5 -65t55.5 -77t45.5 -88t38 -103t23.5 -117t10.5 -136 q3 -259 -108 -465t-312 -321t-456 -115q-185 0 -351 74t-283.5 198t-184 293t-60.5 353z" /> -<glyph unicode="" horiz-adv-x="1792" d="M874 -102v-66q-208 6 -385 109.5t-283 275.5l58 34q29 -49 73 -99l65 57q148 -168 368 -212l-17 -86q65 -12 121 -13zM276 428l-83 -28q22 -60 49 -112l-57 -33q-98 180 -98 385t98 385l57 -33q-30 -56 -49 -112l82 -28q-35 -100 -35 -212q0 -109 36 -212zM1528 251 l58 -34q-106 -172 -283 -275.5t-385 -109.5v66q56 1 121 13l-17 86q220 44 368 212l65 -57q44 50 73 99zM1377 805l-233 -80q14 -42 14 -85t-14 -85l232 -80q-31 -92 -98 -169l-185 162q-57 -67 -147 -85l48 -241q-52 -10 -98 -10t-98 10l48 241q-90 18 -147 85l-185 -162 q-67 77 -98 169l232 80q-14 42 -14 85t14 85l-233 80q33 93 99 169l185 -162q59 68 147 86l-48 240q44 10 98 10t98 -10l-48 -240q88 -18 147 -86l185 162q66 -76 99 -169zM874 1448v-66q-65 -2 -121 -13l17 -86q-220 -42 -368 -211l-65 56q-38 -42 -73 -98l-57 33 q106 172 282 275.5t385 109.5zM1705 640q0 -205 -98 -385l-57 33q27 52 49 112l-83 28q36 103 36 212q0 112 -35 212l82 28q-19 56 -49 112l57 33q98 -180 98 -385zM1585 1063l-57 -33q-35 56 -73 98l-65 -56q-148 169 -368 211l17 86q-56 11 -121 13v66q209 -6 385 -109.5 t282 -275.5zM1748 640q0 173 -67.5 331t-181.5 272t-272 181.5t-331 67.5t-331 -67.5t-272 -181.5t-181.5 -272t-67.5 -331t67.5 -331t181.5 -272t272 -181.5t331 -67.5t331 67.5t272 181.5t181.5 272t67.5 331zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71 t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" /> -<glyph unicode="" d="M582 228q0 -66 -93 -66q-107 0 -107 63q0 64 98 64q102 0 102 -61zM546 694q0 -85 -74 -85q-77 0 -77 84q0 90 77 90q36 0 55 -25.5t19 -63.5zM712 769v125q-78 -29 -135 -29q-50 29 -110 29q-86 0 -145 -57t-59 -143q0 -50 29.5 -102t73.5 -67v-3q-38 -17 -38 -85 q0 -53 41 -77v-3q-113 -37 -113 -139q0 -45 20 -78.5t54 -51t72 -25.5t81 -8q224 0 224 188q0 67 -48 99t-126 46q-27 5 -51.5 20.5t-24.5 39.5q0 44 49 52q77 15 122 70t45 134q0 24 -10 52q37 9 49 13zM771 350h137q-2 27 -2 82v387q0 46 2 69h-137q3 -23 3 -71v-392 q0 -50 -3 -75zM1280 366v121q-30 -21 -68 -21q-53 0 -53 82v225h52q9 0 26.5 -1t26.5 -1v117h-105q0 82 3 102h-140q4 -24 4 -55v-47h-60v-117q36 3 37 3q3 0 11 -0.5t12 -0.5v-2h-2v-217q0 -37 2.5 -64t11.5 -56.5t24.5 -48.5t43.5 -31t66 -12q64 0 108 24zM924 1072 q0 36 -24 63.5t-60 27.5t-60.5 -27t-24.5 -64q0 -36 25 -62.5t60 -26.5t59.5 27t24.5 62zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M595 22q0 100 -165 100q-158 0 -158 -104q0 -101 172 -101q151 0 151 105zM536 777q0 61 -30 102t-89 41q-124 0 -124 -145q0 -135 124 -135q119 0 119 137zM805 1101v-202q-36 -12 -79 -22q16 -43 16 -84q0 -127 -73 -216.5t-197 -112.5q-40 -8 -59.5 -27t-19.5 -58 q0 -31 22.5 -51.5t58 -32t78.5 -22t86 -25.5t78.5 -37.5t58 -64t22.5 -98.5q0 -304 -363 -304q-69 0 -130 12.5t-116 41t-87.5 82t-32.5 127.5q0 165 182 225v4q-67 41 -67 126q0 109 63 137v4q-72 24 -119.5 108.5t-47.5 165.5q0 139 95 231.5t235 92.5q96 0 178 -47 q98 0 218 47zM1123 220h-222q4 45 4 134v609q0 94 -4 128h222q-4 -33 -4 -124v-613q0 -89 4 -134zM1724 442v-196q-71 -39 -174 -39q-62 0 -107 20t-70 50t-39.5 78t-18.5 92t-4 103v351h2v4q-7 0 -19 1t-18 1q-21 0 -59 -6v190h96v76q0 54 -6 89h227q-6 -41 -6 -165h171 v-190q-15 0 -43.5 2t-42.5 2h-85v-365q0 -131 87 -131q61 0 109 33zM1148 1389q0 -58 -39 -101.5t-96 -43.5q-58 0 -98 43.5t-40 101.5q0 59 39.5 103t98.5 44q58 0 96.5 -44.5t38.5 -102.5z" /> -<glyph unicode="" d="M825 547l343 588h-150q-21 -39 -63.5 -118.5t-68 -128.5t-59.5 -118.5t-60 -128.5h-3q-21 48 -44.5 97t-52 105.5t-46.5 92t-54 104.5t-49 95h-150l323 -589v-435h134v436zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M842 964q0 -80 -57 -136.5t-136 -56.5q-60 0 -111 35q-62 -67 -115 -146q-247 -371 -202 -859q1 -22 -12.5 -38.5t-34.5 -18.5h-5q-20 0 -35 13.5t-17 33.5q-14 126 -3.5 247.5t29.5 217t54 186t69 155.5t74 125q61 90 132 165q-16 35 -16 77q0 80 56.5 136.5t136.5 56.5 t136.5 -56.5t56.5 -136.5zM1223 953q0 -158 -78 -292t-212.5 -212t-292.5 -78q-64 0 -131 14q-21 5 -32.5 23.5t-6.5 39.5q5 20 23 31.5t39 7.5q51 -13 108 -13q97 0 186 38t153 102t102 153t38 186t-38 186t-102 153t-153 102t-186 38t-186 -38t-153 -102t-102 -153 t-38 -186q0 -114 52 -218q10 -20 3.5 -40t-25.5 -30t-39.5 -3t-30.5 26q-64 123 -64 265q0 119 46.5 227t124.5 186t186 124t226 46q158 0 292.5 -78t212.5 -212.5t78 -292.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M270 730q-8 19 -8 52q0 20 11 49t24 45q-1 22 7.5 53t22.5 43q0 139 92.5 288.5t217.5 209.5q139 66 324 66q133 0 266 -55q49 -21 90 -48t71 -56t55 -68t42 -74t32.5 -84.5t25.5 -89.5t22 -98l1 -5q55 -83 55 -150q0 -14 -9 -40t-9 -38q0 -1 1.5 -3.5t3.5 -5t2 -3.5 q77 -114 120.5 -214.5t43.5 -208.5q0 -43 -19.5 -100t-55.5 -57q-9 0 -19.5 7.5t-19 17.5t-19 26t-16 26.5t-13.5 26t-9 17.5q-1 1 -3 1l-5 -4q-59 -154 -132 -223q20 -20 61.5 -38.5t69 -41.5t35.5 -65q-2 -4 -4 -16t-7 -18q-64 -97 -302 -97q-53 0 -110.5 9t-98 20 t-104.5 30q-15 5 -23 7q-14 4 -46 4.5t-40 1.5q-41 -45 -127.5 -65t-168.5 -20q-35 0 -69 1.5t-93 9t-101 20.5t-74.5 40t-32.5 64q0 40 10 59.5t41 48.5q11 2 40.5 13t49.5 12q4 0 14 2q2 2 2 4l-2 3q-48 11 -108 105.5t-73 156.5l-5 3q-4 0 -12 -20q-18 -41 -54.5 -74.5 t-77.5 -37.5h-1q-4 0 -6 4.5t-5 5.5q-23 54 -23 100q0 275 252 466z" /> -<glyph unicode="" horiz-adv-x="2048" d="M580 1075q0 41 -25 66t-66 25q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 66 24.5t25 65.5zM1323 568q0 28 -25.5 50t-65.5 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q40 0 65.5 22t25.5 51zM1087 1075q0 41 -24.5 66t-65.5 25 q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 65.5 24.5t24.5 65.5zM1722 568q0 28 -26 50t-65 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q39 0 65 22t26 51zM1456 965q-31 4 -70 4q-169 0 -311 -77t-223.5 -208.5t-81.5 -287.5 q0 -78 23 -152q-35 -3 -68 -3q-26 0 -50 1.5t-55 6.5t-44.5 7t-54.5 10.5t-50 10.5l-253 -127l72 218q-290 203 -290 490q0 169 97.5 311t264 223.5t363.5 81.5q176 0 332.5 -66t262 -182.5t136.5 -260.5zM2048 404q0 -117 -68.5 -223.5t-185.5 -193.5l55 -181l-199 109 q-150 -37 -218 -37q-169 0 -311 70.5t-223.5 191.5t-81.5 264t81.5 264t223.5 191.5t311 70.5q161 0 303 -70.5t227.5 -192t85.5 -263.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-453 185l-242 -295q-18 -23 -49 -23q-13 0 -22 4q-19 7 -30.5 23.5t-11.5 36.5v349l864 1059l-1069 -925l-395 162q-37 14 -40 55q-2 40 32 59l1664 960q15 9 32 9q20 0 36 -11z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1764 1525q33 -24 27 -64l-256 -1536q-5 -29 -32 -45q-14 -8 -31 -8q-11 0 -24 5l-527 215l-298 -327q-18 -21 -47 -21q-14 0 -23 4q-19 7 -30 23.5t-11 36.5v452l-472 193q-37 14 -40 55q-3 39 32 59l1664 960q35 21 68 -2zM1422 26l221 1323l-1434 -827l336 -137 l863 639l-478 -797z" /> -<glyph unicode="" d="M1536 640q0 -156 -61 -298t-164 -245t-245 -164t-298 -61q-172 0 -327 72.5t-264 204.5q-7 10 -6.5 22.5t8.5 20.5l137 138q10 9 25 9q16 -2 23 -12q73 -95 179 -147t225 -52q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5 t-163.5 109.5t-198.5 40.5q-98 0 -188 -35.5t-160 -101.5l137 -138q31 -30 14 -69q-17 -40 -59 -40h-448q-26 0 -45 19t-19 45v448q0 42 40 59q39 17 69 -14l130 -129q107 101 244.5 156.5t284.5 55.5q156 0 298 -61t245 -164t164 -245t61 -298zM896 928v-448q0 -14 -9 -23 t-23 -9h-320q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103 t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1682 -128q-44 0 -132.5 3.5t-133.5 3.5q-44 0 -132 -3.5t-132 -3.5q-24 0 -37 20.5t-13 45.5q0 31 17 46t39 17t51 7t45 15q33 21 33 140l-1 391q0 21 -1 31q-13 4 -50 4h-675q-38 0 -51 -4q-1 -10 -1 -31l-1 -371q0 -142 37 -164q16 -10 48 -13t57 -3.5t45 -15 t20 -45.5q0 -26 -12.5 -48t-36.5 -22q-47 0 -139.5 3.5t-138.5 3.5q-43 0 -128 -3.5t-127 -3.5q-23 0 -35.5 21t-12.5 45q0 30 15.5 45t36 17.5t47.5 7.5t42 15q33 23 33 143l-1 57v813q0 3 0.5 26t0 36.5t-1.5 38.5t-3.5 42t-6.5 36.5t-11 31.5t-16 18q-15 10 -45 12t-53 2 t-41 14t-18 45q0 26 12 48t36 22q46 0 138.5 -3.5t138.5 -3.5q42 0 126.5 3.5t126.5 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17 -43.5t-38.5 -14.5t-49.5 -4t-43 -13q-35 -21 -35 -160l1 -320q0 -21 1 -32q13 -3 39 -3h699q25 0 38 3q1 11 1 32l1 320q0 139 -35 160 q-18 11 -58.5 12.5t-66 13t-25.5 49.5q0 26 12.5 48t37.5 22q44 0 132 -3.5t132 -3.5q43 0 129 3.5t129 3.5q25 0 37.5 -22t12.5 -48q0 -30 -17.5 -44t-40 -14.5t-51.5 -3t-44 -12.5q-35 -23 -35 -161l1 -943q0 -119 34 -140q16 -10 46 -13.5t53.5 -4.5t41.5 -15.5t18 -44.5 q0 -26 -12 -48t-36 -22z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1278 1347v-73q0 -29 -18.5 -61t-42.5 -32q-50 0 -54 -1q-26 -6 -32 -31q-3 -11 -3 -64v-1152q0 -25 -18 -43t-43 -18h-108q-25 0 -43 18t-18 43v1218h-143v-1218q0 -25 -17.5 -43t-43.5 -18h-108q-26 0 -43.5 18t-17.5 43v496q-147 12 -245 59q-126 58 -192 179 q-64 117 -64 259q0 166 88 286q88 118 209 159q111 37 417 37h479q25 0 43 -18t18 -43z" /> -<glyph unicode="" d="M352 128v-128h-352v128h352zM704 256q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM864 640v-128h-864v128h864zM224 1152v-128h-224v128h224zM1536 128v-128h-736v128h736zM576 1280q26 0 45 -19t19 -45v-256 q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1216 768q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h256zM1536 640v-128h-224v128h224zM1536 1152v-128h-864v128h864z" /> -<glyph unicode="" d="M1216 512q133 0 226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5t-226.5 93.5t-93.5 226.5q0 12 2 34l-360 180q-92 -86 -218 -86q-133 0 -226.5 93.5t-93.5 226.5t93.5 226.5t226.5 93.5q126 0 218 -86l360 180q-2 22 -2 34q0 133 93.5 226.5t226.5 93.5 t226.5 -93.5t93.5 -226.5t-93.5 -226.5t-226.5 -93.5q-126 0 -218 86l-360 -180q2 -22 2 -34t-2 -34l360 -180q92 86 218 86z" /> -<glyph unicode="" d="M1280 341q0 88 -62.5 151t-150.5 63q-84 0 -145 -58l-241 120q2 16 2 23t-2 23l241 120q61 -58 145 -58q88 0 150.5 63t62.5 151t-62.5 150.5t-150.5 62.5t-151 -62.5t-63 -150.5q0 -7 2 -23l-241 -120q-62 57 -145 57q-88 0 -150.5 -62.5t-62.5 -150.5t62.5 -150.5 t150.5 -62.5q83 0 145 57l241 -120q-2 -16 -2 -23q0 -88 63 -150.5t151 -62.5t150.5 62.5t62.5 150.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M571 947q-10 25 -34 35t-49 0q-108 -44 -191 -127t-127 -191q-10 -25 0 -49t35 -34q13 -5 24 -5q42 0 60 40q34 84 98.5 148.5t148.5 98.5q25 11 35 35t0 49zM1513 1303l46 -46l-244 -243l68 -68q19 -19 19 -45.5t-19 -45.5l-64 -64q89 -161 89 -343q0 -143 -55.5 -273.5 t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5q182 0 343 -89l64 64q19 19 45.5 19t45.5 -19l68 -68zM1521 1359q-10 -10 -22 -10q-13 0 -23 10l-91 90q-9 10 -9 23t9 23q10 9 23 9t23 -9l90 -91 q10 -9 10 -22.5t-10 -22.5zM1751 1129q-11 -9 -23 -9t-23 9l-90 91q-10 9 -10 22.5t10 22.5q9 10 22.5 10t22.5 -10l91 -90q9 -10 9 -23t-9 -23zM1792 1312q0 -14 -9 -23t-23 -9h-96q-14 0 -23 9t-9 23t9 23t23 9h96q14 0 23 -9t9 -23zM1600 1504v-96q0 -14 -9 -23t-23 -9 t-23 9t-9 23v96q0 14 9 23t23 9t23 -9t9 -23zM1751 1449l-91 -90q-10 -10 -22 -10q-13 0 -23 10q-10 9 -10 22.5t10 22.5l90 91q10 9 23 9t23 -9q9 -10 9 -23t-9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M609 720l287 208l287 -208l-109 -336h-355zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM1515 186q149 203 149 454v3l-102 -89l-240 224l63 323 l134 -12q-150 206 -389 282l53 -124l-287 -159l-287 159l53 124q-239 -76 -389 -282l135 12l62 -323l-240 -224l-102 89v-3q0 -251 149 -454l30 132l326 -40l139 -298l-116 -69q117 -39 240 -39t240 39l-116 69l139 298l326 40z" /> -<glyph unicode="" horiz-adv-x="1792" d="M448 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM256 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM832 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23 v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM640 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM66 768q-28 0 -47 19t-19 46v129h514v-129q0 -27 -19 -46t-46 -19h-383zM1216 224v-192q0 -14 -9 -23t-23 -9h-192 q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1024 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1600 224v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23 zM1408 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 1016v-13h-514v10q0 104 -382 102q-382 -1 -382 -102v-10h-514v13q0 17 8.5 43t34 64t65.5 75.5t110.5 76t160 67.5t224 47.5t293.5 18.5t293 -18.5t224 -47.5 t160.5 -67.5t110.5 -76t65.5 -75.5t34 -64t8.5 -43zM1792 608v-192q0 -14 -9 -23t-23 -9h-192q-14 0 -23 9t-9 23v192q0 14 9 23t23 9h192q14 0 23 -9t9 -23zM1792 962v-129q0 -27 -19 -46t-46 -19h-384q-27 0 -46 19t-19 46v129h514z" /> -<glyph unicode="" horiz-adv-x="1792" d="M704 1216v-768q0 -26 -19 -45t-45 -19v-576q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v512l249 873q7 23 31 23h424zM1024 1216v-704h-256v704h256zM1792 320v-512q0 -26 -19 -45t-45 -19h-512q-26 0 -45 19t-19 45v576q-26 0 -45 19t-19 45v768h424q24 0 31 -23z M736 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23zM1408 1504v-224h-352v224q0 14 9 23t23 9h288q14 0 23 -9t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1755 1083q37 -37 37 -90t-37 -91l-401 -400l150 -150l-160 -160q-163 -163 -389.5 -186.5t-411.5 100.5l-362 -362h-181v181l362 362q-124 185 -100.5 411.5t186.5 389.5l160 160l150 -150l400 401q38 37 91 37t90 -37t37 -90.5t-37 -90.5l-400 -401l234 -234l401 400 q38 37 91 37t90 -37z" /> -<glyph unicode="" horiz-adv-x="1792" d="M873 796q0 -83 -63.5 -142.5t-152.5 -59.5t-152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59t152.5 -59t63.5 -143zM1375 796q0 -83 -63 -142.5t-153 -59.5q-89 0 -152.5 59.5t-63.5 142.5q0 84 63.5 143t152.5 59q90 0 153 -59t63 -143zM1600 616v667q0 87 -32 123.5 t-111 36.5h-1112q-83 0 -112.5 -34t-29.5 -126v-673q43 -23 88.5 -40t81 -28t81 -18.5t71 -11t70 -4t58.5 -0.5t56.5 2t44.5 2q68 1 95 -27q6 -6 10 -9q26 -25 61 -51q7 91 118 87q5 0 36.5 -1.5t43 -2t45.5 -1t53 1t54.5 4.5t61 8.5t62 13.5t67 19.5t67.5 27t72 34.5z M1763 621q-121 -149 -372 -252q84 -285 -23 -465q-66 -113 -183 -148q-104 -32 -182 15q-86 51 -82 164l-1 326v1q-8 2 -24.5 6t-23.5 5l-1 -338q4 -114 -83 -164q-79 -47 -183 -15q-117 36 -182 150q-105 180 -22 463q-251 103 -372 252q-25 37 -4 63t60 -1q3 -2 11 -7 t11 -8v694q0 72 47 123t114 51h1257q67 0 114 -51t47 -123v-694l21 15q39 27 60 1t-4 -63z" /> -<glyph unicode="" horiz-adv-x="1792" d="M896 1102v-434h-145v434h145zM1294 1102v-434h-145v434h145zM1294 342l253 254v795h-1194v-1049h326v-217l217 217h398zM1692 1536v-1013l-434 -434h-326l-217 -217h-217v217h-398v1158l109 289h1483z" /> -<glyph unicode="" d="M773 217v-127q-1 -292 -6 -305q-12 -32 -51 -40q-54 -9 -181.5 38t-162.5 89q-13 15 -17 36q-1 12 4 26q4 10 34 47t181 216q1 0 60 70q15 19 39.5 24.5t49.5 -3.5q24 -10 37.5 -29t12.5 -42zM624 468q-3 -55 -52 -70l-120 -39q-275 -88 -292 -88q-35 2 -54 36 q-12 25 -17 75q-8 76 1 166.5t30 124.5t56 32q13 0 202 -77q70 -29 115 -47l84 -34q23 -9 35.5 -30.5t11.5 -48.5zM1450 171q-7 -54 -91.5 -161t-135.5 -127q-37 -14 -63 7q-14 10 -184 287l-47 77q-14 21 -11.5 46t19.5 46q35 43 83 26q1 -1 119 -40q203 -66 242 -79.5 t47 -20.5q28 -22 22 -61zM778 803q5 -102 -54 -122q-58 -17 -114 71l-378 598q-8 35 19 62q41 43 207.5 89.5t224.5 31.5q40 -10 49 -45q3 -18 22 -305.5t24 -379.5zM1440 695q3 -39 -26 -59q-15 -10 -329 -86q-67 -15 -91 -23l1 2q-23 -6 -46 4t-37 32q-30 47 0 87 q1 1 75 102q125 171 150 204t34 39q28 19 65 2q48 -23 123 -133.5t81 -167.5v-3z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960q0 -26 19 -45t45 -19t45 19 t19 45zM1920 192v1088h-1536v-1088q0 -33 -11 -64h1483q26 0 45 19t19 45zM2048 1408v-1216q0 -80 -56 -136t-136 -56h-1664q-80 0 -136 56t-56 136v1088h256v128h1792z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" /> -<glyph unicode="" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1112 1090q0 159 -237 159h-70q-32 0 -59.5 -21.5t-34.5 -52.5l-63 -276q-2 -5 -2 -16q0 -24 17 -39.5t41 -15.5h53q69 0 128.5 13t112.5 41t83.5 81.5t30.5 126.5zM1716 938q0 -265 -220 -428q-219 -161 -612 -161h-61q-32 0 -59 -21.5t-34 -52.5l-73 -316 q-8 -36 -40.5 -61.5t-69.5 -25.5h-213q-31 0 -53 20t-22 51q0 10 13 65h151q34 0 64 23.5t38 56.5l73 316q8 33 37.5 57t63.5 24h61q390 0 607 160t217 421q0 129 -51 207q183 -92 183 -335zM1533 1123q0 -264 -221 -428q-218 -161 -612 -161h-60q-32 0 -59.5 -22t-34.5 -53 l-73 -315q-8 -36 -40 -61.5t-69 -25.5h-214q-31 0 -52.5 19.5t-21.5 51.5q0 8 2 20l300 1301q8 36 40.5 61.5t69.5 25.5h444q68 0 125 -4t120.5 -15t113.5 -30t96.5 -50.5t77.5 -74t49.5 -103.5t18.5 -136z" /> -<glyph unicode="" horiz-adv-x="1792" d="M602 949q19 -61 31 -123.5t17 -141.5t-14 -159t-62 -145q-21 81 -67 157t-95.5 127t-99 90.5t-78.5 57.5t-33 19q-62 34 -81.5 100t14.5 128t101 81.5t129 -14.5q138 -83 238 -177zM927 1236q11 -25 20.5 -46t36.5 -100.5t42.5 -150.5t25.5 -179.5t0 -205.5t-47.5 -209.5 t-105.5 -208.5q-51 -72 -138 -72q-54 0 -98 31q-57 40 -69 109t28 127q60 85 81 195t13 199.5t-32 180.5t-39 128t-22 52q-31 63 -8.5 129.5t85.5 97.5q34 17 75 17q47 0 88.5 -25t63.5 -69zM1248 567q-17 -160 -72 -311q-17 131 -63 246q25 174 -5 361q-27 178 -94 342 q114 -90 212 -211q9 -37 15 -80q26 -179 7 -347zM1520 1440q9 -17 23.5 -49.5t43.5 -117.5t50.5 -178t34 -227.5t5 -269t-47 -300t-112.5 -323.5q-22 -48 -66 -75.5t-95 -27.5q-39 0 -74 16q-67 31 -92.5 100t4.5 136q58 126 90 257.5t37.5 239.5t-3.5 213.5t-26.5 180.5 t-38.5 138.5t-32.5 90t-15.5 32.5q-34 65 -11.5 135.5t87.5 104.5q37 20 81 20q49 0 91.5 -25.5t66.5 -70.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" /> -<glyph unicode="" horiz-adv-x="2304" d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453q0 33 -40 33h-84v-69h83q41 0 41 36zM1475 457q0 29 -42 29h-82v-61h81q43 0 43 32zM1197 923q0 29 -42 29h-82v-60h81q43 0 43 31zM1656 854h89l-44 108z M699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453q0 -20 -5.5 -35t-14 -25t-22.5 -16.5t-26 -10t-31.5 -4.5t-31.5 -1t-32.5 0.5t-29.5 0.5v-91h-126l-80 90l-83 -90h-256v271h260 l80 -89l82 89h207q109 0 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229q0 -55 -38.5 -94.5t-93.5 -39.5h-2040q-55 0 -93.5 39.5t-38.5 94.5v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1q10 0 10 -14v-86h279 v23q23 -12 55 -18t52.5 -6.5t63 0.5t51.5 1l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249q-69 0 -109 -22v22h-172v-22q-24 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391q0 55 38.5 94.5t93.5 39.5h2040 q55 0 93.5 -39.5t38.5 -94.5v-678h-120q-51 0 -81 -22v22h-177q-55 0 -78 -22v22h-316v-22q-31 22 -87 22h-209v-22q-23 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21q59 0 90 13v-102h174v99h8q8 0 10 -2t2 -10v-87h529q57 0 88 24v-24h168 q60 0 95 17zM1546 469q0 -23 -12 -43t-34 -29q25 -9 34 -26t9 -46v-54h-65v45q0 33 -12 43.5t-46 10.5h-69v-99h-65v271h154q48 0 77 -15t29 -58zM1269 936q0 -24 -12.5 -44t-33.5 -29q26 -9 34.5 -25.5t8.5 -46.5v-53h-65q0 9 0.5 26.5t0 25t-3 18.5t-8.5 16t-17.5 8.5 t-29.5 3.5h-70v-98h-64v271l153 -1q49 0 78 -14.5t29 -57.5zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357q0 -86 -102 -86h-126v58h126q34 0 34 25q0 16 -17 21t-41.5 5t-49.5 3.5t-42 22.5t-17 55q0 39 26 60t66 21 h130v-57h-119q-36 0 -36 -25q0 -16 17.5 -20.5t42 -4t49 -2.5t42 -21.5t17.5 -54.5zM2304 407v-101q-24 -35 -88 -35h-125v58h125q33 0 33 25q0 13 -12.5 19t-31 5.5t-40 2t-40 8t-31 24t-12.5 48.5q0 39 26.5 60t66.5 21h129v-57h-118q-36 0 -36 -25q0 -20 29 -22t68.5 -5 t56.5 -26zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75q-129 0 -129 133q0 138 133 138h63v-59q-7 0 -28 1t-28.5 0.5t-23 -2t-21.5 -6.5t-14.5 -13.5t-11.5 -23t-3 -33.5q0 -38 13.5 -58t49.5 -20h29l92 213h97l109 -256v256h99l114 -188v188h66z" /> -<glyph unicode="" horiz-adv-x="2304" d="M322 689h-15q-19 0 -19 18q0 28 19 85q5 15 15 19.5t28 4.5q77 0 77 -49q0 -41 -30.5 -59.5t-74.5 -18.5zM664 528q-47 0 -47 29q0 62 123 62l3 -3q-5 -88 -79 -88zM1438 687h-15q-19 0 -19 19q0 28 19 85q5 15 14.5 19t28.5 4q77 0 77 -49q0 -41 -30.5 -59.5 t-74.5 -18.5zM1780 527q-47 0 -47 30q0 62 123 62l3 -3q-5 -89 -79 -89zM373 894h-128q-8 0 -14.5 -4t-8.5 -7.5t-7 -12.5q-3 -7 -45 -190t-42 -192q0 -7 5.5 -12.5t13.5 -5.5h62q25 0 32.5 34.5l15 69t32.5 34.5q47 0 87.5 7.5t80.5 24.5t63.5 52.5t23.5 84.5 q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM719 798q-38 0 -74 -6q-2 0 -8.5 -1t-9 -1.5l-7.5 -1.5t-7.5 -2t-6.5 -3t-6.5 -4t-5 -5t-4.5 -7t-4 -9q-9 -29 -9 -39t9 -10q5 0 21.5 5t19.5 6q30 8 58 8q74 0 74 -36q0 -11 -10 -14q-8 -2 -18 -3t-21.5 -1.5t-17.5 -1.5 q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5q0 -38 26 -59.5t64 -21.5q24 0 45.5 6.5t33 13t38.5 23.5q-3 -7 -3 -15t5.5 -13.5t12.5 -5.5h56q1 1 7 3.5t7.5 3.5t5 3.5t5 5.5t2.5 8l45 194q4 13 4 30q0 81 -145 81zM1247 793h-74q-22 0 -39 -23q-5 -7 -29.5 -51 t-46.5 -81.5t-26 -38.5l-5 4q0 77 -27 166q-1 5 -3.5 8.5t-6 6.5t-6.5 5t-8.5 3t-8.5 1.5t-9.5 1t-9 0.5h-10h-8.5q-38 0 -38 -21l1 -5q5 -53 25 -151t25 -143q2 -16 2 -24q0 -19 -30.5 -61.5t-30.5 -58.5q0 -13 40 -13q61 0 76 25l245 415q10 20 10 26q0 9 -8 9zM1489 892 h-129q-18 0 -29 -23q-6 -13 -46.5 -191.5t-40.5 -190.5q0 -20 43 -20h7.5h9h9t9.5 1t8.5 2t8.5 3t6.5 4.5t5.5 6t3 8.5l21 91q2 10 10.5 17t19.5 7q47 0 87.5 7t80.5 24.5t63.5 52.5t23.5 84q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM1835 798q-26 0 -74 -6 q-38 -6 -48 -16q-7 -8 -11 -19q-8 -24 -8 -39q0 -10 8 -10q1 0 41 12q30 8 58 8q74 0 74 -36q0 -12 -10 -14q-4 -1 -57 -7q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5t26 -58.5t64 -21.5q24 0 45 6t34 13t38 24q-3 -15 -3 -16q0 -5 2 -8.5t6.5 -5.5t8 -3.5 t10.5 -2t9.5 -0.5h9.5h8q42 0 48 25l45 194q3 15 3 31q0 81 -145 81zM2157 889h-55q-25 0 -33 -40q-10 -44 -36.5 -167t-42.5 -190v-5q0 -16 16 -18h1h57q10 0 18.5 6.5t10.5 16.5l83 374h-1l1 5q0 7 -5.5 12.5t-13.5 5.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048 q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2304" d="M1597 633q0 -69 -21 -106q-19 -35 -52 -35q-23 0 -41 9v224q29 30 57 30q57 0 57 -122zM2035 669h-110q6 98 56 98q51 0 54 -98zM476 534q0 59 -33 91.5t-101 57.5q-36 13 -52 24t-16 25q0 26 38 26q58 0 124 -33l18 112q-67 32 -149 32q-77 0 -123 -38q-48 -39 -48 -109 q0 -58 32.5 -90.5t99.5 -56.5q39 -14 54.5 -25.5t15.5 -27.5q0 -31 -48 -31q-29 0 -70 12.5t-72 30.5l-18 -113q72 -41 168 -41q81 0 129 37q51 41 51 117zM771 749l19 111h-96v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219q0 -84 44 -120q38 -30 111 -30q32 0 79 11v118 q-32 -7 -44 -7q-42 0 -42 50v197h77zM1087 724v139q-15 3 -28 3q-32 0 -55.5 -16t-33.5 -46l-10 56h-131v-471h150v306q26 31 82 31q16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638q0 122 -45 179q-40 52 -111 52q-64 0 -117 -56l-8 47h-132v-645l150 25v151 q36 -11 68 -11q83 0 134 56q61 65 61 202zM1278 986q0 33 -23 56t-56 23t-56 -23t-23 -56t23 -56.5t56 -23.5t56 23.5t23 56.5zM2176 629q0 113 -48 176q-50 64 -144 64q-96 0 -151.5 -66t-55.5 -180q0 -128 63 -188q55 -55 161 -55q101 0 160 40l-16 103q-57 -31 -128 -31 q-43 0 -63 19q-23 19 -28 66h248q2 14 2 52zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1558 684q61 -356 298 -556q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5zM1024 -176q16 0 16 16t-16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5zM2026 1424q8 -10 7.5 -23.5t-10.5 -22.5 l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5 l418 363q10 8 23.5 7t21.5 -11z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1040 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM503 315l877 760q-42 88 -132.5 146.5t-223.5 58.5q-93 0 -169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -384 -137 -645zM1856 128 q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5l149 129h757q-166 187 -227 459l111 97q61 -356 298 -556zM1942 1520l84 -96q8 -10 7.5 -23.5t-10.5 -22.5l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161 q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5l418 363q10 8 23.5 7t21.5 -11z" /> -<glyph unicode="" horiz-adv-x="1408" d="M512 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM768 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1024 160v704q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-704 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM480 1152h448l-48 117q-7 9 -17 11h-317q-10 -2 -17 -11zM1408 1120v-64q0 -14 -9 -23t-23 -9h-96v-948q0 -83 -47 -143.5t-113 -60.5h-832q-66 0 -113 58.5t-47 141.5v952h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h309l70 167 q15 37 54 63t79 26h320q40 0 79 -26t54 -63l70 -167h309q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1150 462v-109q0 -50 -36.5 -89t-94 -60.5t-118 -32.5t-117.5 -11q-205 0 -342.5 139t-137.5 346q0 203 136 339t339 136q34 0 75.5 -4.5t93 -18t92.5 -34t69 -56.5t28 -81v-109q0 -16 -16 -16h-118q-16 0 -16 16v70q0 43 -65.5 67.5t-137.5 24.5q-140 0 -228.5 -91.5 t-88.5 -237.5q0 -151 91.5 -249.5t233.5 -98.5q68 0 138 24t70 66v70q0 7 4.5 11.5t10.5 4.5h119q6 0 11 -4.5t5 -11.5zM768 1280q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" d="M972 761q0 108 -53.5 169t-147.5 61q-63 0 -124 -30.5t-110 -84.5t-79.5 -137t-30.5 -180q0 -112 53.5 -173t150.5 -61q96 0 176 66.5t122.5 166t42.5 203.5zM1536 640q0 -111 -37 -197t-98.5 -135t-131.5 -74.5t-145 -27.5q-6 0 -15.5 -0.5t-16.5 -0.5q-95 0 -142 53 q-28 33 -33 83q-52 -66 -131.5 -110t-173.5 -44q-161 0 -249.5 95.5t-88.5 269.5q0 157 66 290t179 210.5t246 77.5q87 0 155 -35.5t106 -99.5l2 19l11 56q1 6 5.5 12t9.5 6h118q5 0 13 -11q5 -5 3 -16l-120 -614q-5 -24 -5 -48q0 -39 12.5 -52t44.5 -13q28 1 57 5.5t73 24 t77 50t57 89.5t24 137q0 292 -174 466t-466 174q-130 0 -248.5 -51t-204 -136.5t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51q228 0 405 144q11 9 24 8t21 -12l41 -49q8 -12 7 -24q-2 -13 -12 -22q-102 -83 -227.5 -128t-258.5 -45q-156 0 -298 61 t-245 164t-164 245t-61 298t61 298t164 245t245 164t298 61q344 0 556 -212t212 -556z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1698 1442q94 -94 94 -226.5t-94 -225.5l-225 -223l104 -104q10 -10 10 -23t-10 -23l-210 -210q-10 -10 -23 -10t-23 10l-105 105l-603 -603q-37 -37 -90 -37h-203l-256 -128l-64 64l128 256v203q0 53 37 90l603 603l-105 105q-10 10 -10 23t10 23l210 210q10 10 23 10 t23 -10l104 -104l223 225q93 94 225.5 94t226.5 -94zM512 64l576 576l-192 192l-576 -576v-192h192z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1615 1536q70 0 122.5 -46.5t52.5 -116.5q0 -63 -45 -151q-332 -629 -465 -752q-97 -91 -218 -91q-126 0 -216.5 92.5t-90.5 219.5q0 128 92 212l638 579q59 54 130 54zM706 502q39 -76 106.5 -130t150.5 -76l1 -71q4 -213 -129.5 -347t-348.5 -134q-123 0 -218 46.5 t-152.5 127.5t-86.5 183t-29 220q7 -5 41 -30t62 -44.5t59 -36.5t46 -17q41 0 55 37q25 66 57.5 112.5t69.5 76t88 47.5t103 25.5t125 10.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 128v-384h-1792v384q45 0 85 14t59 27.5t47 37.5q30 27 51.5 38t56.5 11t55.5 -11t52.5 -38q29 -25 47 -38t58 -27t86 -14q45 0 85 14.5t58 27t48 37.5q21 19 32.5 27t31 15t43.5 7q35 0 56.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14t85 14t59 27.5t47 37.5 q30 27 51.5 38t56.5 11q34 0 55.5 -11t51.5 -38q28 -24 47 -37.5t59 -27.5t85 -14zM1792 448v-192q-35 0 -55.5 11t-52.5 38q-29 25 -47 38t-58 27t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-22 -19 -33 -27t-31 -15t-44 -7q-35 0 -56.5 11t-51.5 38q-29 25 -47 38t-58 27 t-86 14q-45 0 -85 -14.5t-58 -27t-48 -37.5q-21 -19 -32.5 -27t-31 -15t-43.5 -7q-35 0 -56.5 11t-51.5 38q-28 24 -47 37.5t-59 27.5t-85 14q-46 0 -86 -14t-58 -27t-47 -38q-30 -27 -51.5 -38t-56.5 -11v192q0 80 56 136t136 56h64v448h256v-448h256v448h256v-448h256v448 h256v-448h64q80 0 136 -56t56 -136zM512 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1024 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51 t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150zM1536 1312q0 -77 -36 -118.5t-92 -41.5q-53 0 -90.5 37.5t-37.5 90.5q0 29 9.5 51t23.5 34t31 28t31 31.5t23.5 44.5t9.5 67q38 0 83 -74t45 -150z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1664 1024l256 -896h-1664v576l448 576l576 -576z" /> -<glyph unicode="" horiz-adv-x="1792" d="M768 646l546 -546q-106 -108 -247.5 -168t-298.5 -60q-209 0 -385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103v-762zM955 640h773q0 -157 -60 -298.5t-168 -247.5zM1664 768h-768v768q209 0 385.5 -103t279.5 -279.5t103 -385.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 0v-128h-2048v1536h128v-1408h1920zM1920 1248v-435q0 -21 -19.5 -29.5t-35.5 7.5l-121 121l-633 -633q-10 -10 -23 -10t-23 10l-233 233l-416 -416l-192 192l585 585q10 10 23 10t23 -10l233 -233l464 464l-121 121q-16 16 -7.5 35.5t29.5 19.5h435q14 0 23 -9 t9 -23z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1292 832q0 -6 10 -41q10 -29 25 -49.5t41 -34t44 -20t55 -16.5q325 -91 325 -332q0 -146 -105.5 -242.5t-254.5 -96.5q-59 0 -111.5 18.5t-91.5 45.5t-77 74.5t-63 87.5t-53.5 103.5t-43.5 103t-39.5 106.5t-35.5 95q-32 81 -61.5 133.5t-73.5 96.5t-104 64t-142 20 q-96 0 -183 -55.5t-138 -144.5t-51 -185q0 -160 106.5 -279.5t263.5 -119.5q177 0 258 95q56 63 83 116l84 -152q-15 -34 -44 -70l1 -1q-131 -152 -388 -152q-147 0 -269.5 79t-190.5 207.5t-68 274.5q0 105 43.5 206t116 176.5t172 121.5t204.5 46q87 0 159 -19t123.5 -50 t95 -80t72.5 -99t58.5 -117t50.5 -124.5t50 -130.5t55 -127q96 -200 233 -200q81 0 138.5 48.5t57.5 128.5q0 42 -19 72t-50.5 46t-72.5 31.5t-84.5 27t-87.5 34t-81 52t-65 82t-39 122.5q-3 16 -3 33q0 110 87.5 192t198.5 78q78 -3 120.5 -14.5t90.5 -53.5h-1 q12 -11 23 -24.5t26 -36t19 -27.5l-129 -99q-26 49 -54 70v1q-23 21 -97 21q-49 0 -84 -33t-35 -83z" /> -<glyph unicode="" d="M1432 484q0 173 -234 239q-35 10 -53 16.5t-38 25t-29 46.5q0 2 -2 8.5t-3 12t-1 7.5q0 36 24.5 59.5t60.5 23.5q54 0 71 -15h-1q20 -15 39 -51l93 71q-39 54 -49 64q-33 29 -67.5 39t-85.5 10q-80 0 -142 -57.5t-62 -137.5q0 -7 2 -23q16 -96 64.5 -140t148.5 -73 q29 -8 49 -15.5t45 -21.5t38.5 -34.5t13.5 -46.5v-5q1 -58 -40.5 -93t-100.5 -35q-97 0 -167 144q-23 47 -51.5 121.5t-48 125.5t-54 110.5t-74 95.5t-103.5 60.5t-147 24.5q-101 0 -192 -56t-144 -148t-50 -192v-1q4 -108 50.5 -199t133.5 -147.5t196 -56.5q186 0 279 110 q20 27 31 51l-60 109q-42 -80 -99 -116t-146 -36q-115 0 -191 87t-76 204q0 105 82 189t186 84q112 0 170 -53.5t104 -172.5q8 -21 25.5 -68.5t28.5 -76.5t31.5 -74.5t38.5 -74t45.5 -62.5t55.5 -53.5t66 -33t80 -13.5q107 0 183 69.5t76 174.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1152 640q0 104 -40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5t198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM1920 640q0 104 -40.5 198.5 t-109.5 163.5t-163.5 109.5t-198.5 40.5h-386q119 -90 188.5 -224t69.5 -288t-69.5 -288t-188.5 -224h386q104 0 198.5 40.5t163.5 109.5t109.5 163.5t40.5 198.5zM2048 640q0 -130 -51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5 t-136.5 204t-51 248.5t51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M0 640q0 130 51 248.5t136.5 204t204 136.5t248.5 51h768q130 0 248.5 -51t204 -136.5t136.5 -204t51 -248.5t-51 -248.5t-136.5 -204t-204 -136.5t-248.5 -51h-768q-130 0 -248.5 51t-204 136.5t-136.5 204t-51 248.5zM1408 128q104 0 198.5 40.5t163.5 109.5 t109.5 163.5t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M762 384h-314q-40 0 -57.5 35t6.5 67l188 251q-65 31 -137 31q-132 0 -226 -94t-94 -226t94 -226t226 -94q115 0 203 72.5t111 183.5zM576 512h186q-18 85 -75 148zM1056 512l288 384h-480l-99 -132q105 -103 126 -252h165zM2176 448q0 132 -94 226t-226 94 q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94t226 94t94 226zM2304 448q0 -185 -131.5 -316.5t-316.5 -131.5t-316.5 131.5t-131.5 316.5q0 97 39.5 183.5t109.5 149.5l-65 98l-353 -469 q-18 -26 -51 -26h-197q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5t-131.5 316.5t131.5 316.5t316.5 131.5q114 0 215 -55l137 183h-224q-26 0 -45 19t-19 45t19 45t45 19h384v-128h435l-85 128h-222q-26 0 -45 19t-19 45t19 45t45 19h256q33 0 53 -28l267 -400 q91 44 192 44q185 0 316.5 -131.5t131.5 -316.5z" /> -<glyph unicode="" d="M384 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 320q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1362 716l-72 384q-5 23 -22.5 37.5t-40.5 14.5 h-918q-23 0 -40.5 -14.5t-22.5 -37.5l-72 -384q-5 -30 14 -53t49 -23h1062q30 0 49 23t14 53zM1136 1328q0 20 -14 34t-34 14h-640q-20 0 -34 -14t-14 -34t14 -34t34 -14h640q20 0 34 14t14 34zM1536 603v-603h-128v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5 t-37.5 90.5v128h-768v-128q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5v128h-128v603q0 112 25 223l103 454q9 78 97.5 137t230 89t312.5 30t312.5 -30t230 -89t97.5 -137l105 -454q23 -102 23 -223z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1463 704q0 -35 -25 -60.5t-61 -25.5h-702q-36 0 -61 25.5t-25 60.5t25 60.5t61 25.5h702q36 0 61 -25.5t25 -60.5zM1677 704q0 86 -23 170h-982q-36 0 -61 25t-25 60q0 36 25 61t61 25h908q-88 143 -235 227t-320 84q-177 0 -327.5 -87.5t-238 -237.5t-87.5 -327 q0 -86 23 -170h982q36 0 61 -25t25 -60q0 -36 -25 -61t-61 -25h-908q88 -143 235.5 -227t320.5 -84q132 0 253 51.5t208 139t139 208t52 253.5zM2048 959q0 -35 -25 -60t-61 -25h-131q17 -85 17 -170q0 -167 -65.5 -319.5t-175.5 -263t-262.5 -176t-319.5 -65.5 q-246 0 -448.5 133t-301.5 350h-189q-36 0 -61 25t-25 61q0 35 25 60t61 25h132q-17 85 -17 170q0 167 65.5 319.5t175.5 263t262.5 176t320.5 65.5q245 0 447.5 -133t301.5 -350h188q36 0 61 -25t25 -61z" /> -<glyph unicode="" horiz-adv-x="1280" d="M953 1158l-114 -328l117 -21q165 451 165 518q0 56 -38 56q-57 0 -130 -225zM654 471l33 -88q37 42 71 67l-33 5.5t-38.5 7t-32.5 8.5zM362 1367q0 -98 159 -521q18 10 49 10q15 0 75 -5l-121 351q-75 220 -123 220q-19 0 -29 -17.5t-10 -37.5zM283 608q0 -36 51.5 -119 t117.5 -153t100 -70q14 0 25.5 13t11.5 27q0 24 -32 102q-13 32 -32 72t-47.5 89t-61.5 81t-62 32q-20 0 -45.5 -27t-25.5 -47zM125 273q0 -41 25 -104q59 -145 183.5 -227t281.5 -82q227 0 382 170q152 169 152 427q0 43 -1 67t-11.5 62t-30.5 56q-56 49 -211.5 75.5 t-270.5 26.5q-37 0 -49 -11q-12 -5 -12 -35q0 -34 21.5 -60t55.5 -40t77.5 -23.5t87.5 -11.5t85 -4t70 0h23q24 0 40 -19q15 -19 19 -55q-28 -28 -96 -54q-61 -22 -93 -46q-64 -46 -108.5 -114t-44.5 -137q0 -31 18.5 -88.5t18.5 -87.5l-3 -12q-4 -12 -4 -14 q-137 10 -146 216q-8 -2 -41 -2q2 -7 2 -21q0 -53 -40.5 -89.5t-94.5 -36.5q-82 0 -166.5 78t-84.5 159q0 34 33 67q52 -64 60 -76q77 -104 133 -104q12 0 26.5 8.5t14.5 20.5q0 34 -87.5 145t-116.5 111q-43 0 -70 -44.5t-27 -90.5zM11 264q0 101 42.5 163t136.5 88 q-28 74 -28 104q0 62 61 123t122 61q29 0 70 -15q-163 462 -163 567q0 80 41 130.5t119 50.5q131 0 325 -581q6 -17 8 -23q6 16 29 79.5t43.5 118.5t54 127.5t64.5 123t70.5 86.5t76.5 36q71 0 112 -49t41 -122q0 -108 -159 -550q61 -15 100.5 -46t58.5 -78t26 -93.5 t7 -110.5q0 -150 -47 -280t-132 -225t-211 -150t-278 -55q-111 0 -223 42q-149 57 -258 191.5t-109 286.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M785 528h207q-14 -158 -98.5 -248.5t-214.5 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-203q-5 64 -35.5 99t-81.5 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t40 -51.5t66 -18q95 0 109 139zM1497 528h206 q-14 -158 -98 -248.5t-214 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-204q-4 64 -35 99t-81 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t39.5 -51.5t65.5 -18q49 0 76.5 38t33.5 101zM1856 647q0 207 -15.5 307 t-60.5 161q-6 8 -13.5 14t-21.5 15t-16 11q-86 63 -697 63q-625 0 -710 -63q-5 -4 -17.5 -11.5t-21 -14t-14.5 -14.5q-45 -60 -60 -159.5t-15 -308.5q0 -208 15 -307.5t60 -160.5q6 -8 15 -15t20.5 -14t17.5 -12q44 -33 239.5 -49t470.5 -16q610 0 697 65q5 4 17 11t20.5 14 t13.5 16q46 60 61 159t15 309zM2048 1408v-1536h-2048v1536h2048z" /> -<glyph unicode="" d="M992 912v-496q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v496q0 112 -80 192t-192 80h-272v-1152q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v1344q0 14 9 23t23 9h464q135 0 249 -66.5t180.5 -180.5t66.5 -249zM1376 1376v-880q0 -135 -66.5 -249t-180.5 -180.5 t-249 -66.5h-464q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h160q14 0 23 -9t9 -23v-768h272q112 0 192 80t80 192v880q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" /> -<glyph unicode="" d="M1311 694v-114q0 -24 -13.5 -38t-37.5 -14h-202q-24 0 -38 14t-14 38v114q0 24 14 38t38 14h202q24 0 37.5 -14t13.5 -38zM821 464v250q0 53 -32.5 85.5t-85.5 32.5h-133q-68 0 -96 -52q-28 52 -96 52h-130q-53 0 -85.5 -32.5t-32.5 -85.5v-250q0 -22 21 -22h55 q22 0 22 22v230q0 24 13.5 38t38.5 14h94q24 0 38 -14t14 -38v-230q0 -22 21 -22h54q22 0 22 22v230q0 24 14 38t38 14h97q24 0 37.5 -14t13.5 -38v-230q0 -22 22 -22h55q21 0 21 22zM1410 560v154q0 53 -33 85.5t-86 32.5h-264q-53 0 -86 -32.5t-33 -85.5v-410 q0 -21 22 -21h55q21 0 21 21v180q31 -42 94 -42h191q53 0 86 32.5t33 85.5zM1536 1176v-1072q0 -96 -68 -164t-164 -68h-1072q-96 0 -164 68t-68 164v1072q0 96 68 164t164 68h1072q96 0 164 -68t68 -164z" /> -<glyph unicode="" d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960q0 -118 -85 -203t-203 -85h-960q-118 0 -203 85t-85 203v960q0 118 85 203t203 85h960q118 0 203 -85t85 -203z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2048 641q0 -21 -13 -36.5t-33 -19.5l-205 -356q3 -9 3 -18q0 -20 -12.5 -35.5t-32.5 -19.5l-193 -337q3 -8 3 -16q0 -23 -16.5 -40t-40.5 -17q-25 0 -41 18h-400q-17 -20 -43 -20t-43 20h-399q-17 -20 -43 -20q-23 0 -40 16.5t-17 40.5q0 8 4 20l-193 335 q-20 4 -32.5 19.5t-12.5 35.5q0 9 3 18l-206 356q-20 5 -32.5 20.5t-12.5 35.5q0 21 13.5 36.5t33.5 19.5l199 344q0 1 -0.5 3t-0.5 3q0 36 34 51l209 363q-4 10 -4 18q0 24 17 40.5t40 16.5q26 0 44 -21h396q16 21 43 21t43 -21h398q18 21 44 21q23 0 40 -16.5t17 -40.5 q0 -6 -4 -18l207 -358q23 -1 39 -17.5t16 -38.5q0 -13 -7 -27l187 -324q19 -4 31.5 -19.5t12.5 -35.5zM1063 -158h389l-342 354h-143l-342 -354h360q18 16 39 16t39 -16zM112 654q1 -4 1 -13q0 -10 -2 -15l208 -360q2 0 4.5 -1t5.5 -2.5l5 -2.5l188 199v347l-187 194 q-13 -8 -29 -10zM986 1438h-388l190 -200l554 200h-280q-16 -16 -38 -16t-38 16zM1689 226q1 6 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427l333 -343q8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6q14 -5 22 -13 zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177q4 -8 5 -12zM358 1051q0 -1 0.5 -2t0.5 -2q0 -16 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314l-223 236zM556 1425l-4 -8v-264l205 74l-191 201q-6 -2 -10 -3zM1447 1438h-16l-621 -224 l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018q-8 13 -8 29v2l-216 376q-5 1 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196 l-48 -227l130 227h-82zM1729 266l207 361q-2 10 -2 14q0 1 3 16l-171 296l-129 -612l77 -82q5 3 15 7z" /> -<glyph unicode="" d="M0 856q0 131 91.5 226.5t222.5 95.5h742l352 358v-1470q0 -132 -91.5 -227t-222.5 -95h-780q-131 0 -222.5 95t-91.5 227v790zM1232 102l-176 180v425q0 46 -32 79t-78 33h-484q-46 0 -78 -33t-32 -79v-492q0 -46 32.5 -79.5t77.5 -33.5h770z" /> -<glyph unicode="" d="M934 1386q-317 -121 -556 -362.5t-358 -560.5q-20 89 -20 176q0 208 102.5 384.5t278.5 279t384 102.5q82 0 169 -19zM1203 1267q93 -65 164 -155q-389 -113 -674.5 -400.5t-396.5 -676.5q-93 72 -155 162q112 386 395 671t667 399zM470 -67q115 356 379.5 622t619.5 384 q40 -92 54 -195q-292 -120 -516 -345t-343 -518q-103 14 -194 52zM1536 -125q-193 50 -367 115q-135 -84 -290 -107q109 205 274 370.5t369 275.5q-21 -152 -101 -284q65 -175 115 -370z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1893 1144l155 -1272q-131 0 -257 57q-200 91 -393 91q-226 0 -374 -148q-148 148 -374 148q-193 0 -393 -91q-128 -57 -252 -57h-5l155 1272q224 127 482 127q233 0 387 -106q154 106 387 106q258 0 482 -127zM1398 157q129 0 232 -28.5t260 -93.5l-124 1021 q-171 78 -368 78q-224 0 -374 -141q-150 141 -374 141q-197 0 -368 -78l-124 -1021q105 43 165.5 65t148.5 39.5t178 17.5q202 0 374 -108q172 108 374 108zM1438 191l-55 907q-211 -4 -359 -155q-152 155 -374 155q-176 0 -336 -66l-114 -941q124 51 228.5 76t221.5 25 q209 0 374 -102q172 107 374 102z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1500 165v733q0 21 -15 36t-35 15h-93q-20 0 -35 -15t-15 -36v-733q0 -20 15 -35t35 -15h93q20 0 35 15t15 35zM1216 165v531q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-531q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM924 165v429q0 20 -15 35t-35 15h-101 q-20 0 -35 -15t-15 -35v-429q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM632 165v362q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-362q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM2048 311q0 -166 -118 -284t-284 -118h-1244q-166 0 -284 118t-118 284 q0 116 63 214.5t168 148.5q-10 34 -10 73q0 113 80.5 193.5t193.5 80.5q102 0 180 -67q45 183 194 300t338 117q149 0 275 -73.5t199.5 -199.5t73.5 -275q0 -66 -14 -122q135 -33 221 -142.5t86 -247.5z" /> -<glyph unicode="" d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34 l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114 v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378 v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265q-129 0 -221 91.5t-92 221.5q0 129 92 221t221 92q130 0 221.5 -92t91.5 -221q0 -130 -91.5 -221.5t-221.5 -91.5zM595 646q0 -36 19.5 -56.5t49.5 -25t64 -7t64 -2t49.5 -9t19.5 -30.5q0 -49 -112 -49q-97 0 -123 51 h-3l-31 -63q67 -42 162 -42q29 0 56.5 5t55.5 16t45.5 33t17.5 53q0 46 -27.5 69.5t-67.5 27t-79.5 3t-67 5t-27.5 25.5q0 21 20.5 33t40.5 15t41 3q34 0 70.5 -11t51.5 -34h3l30 58q-3 1 -21 8.5t-22.5 9t-19.5 7t-22 7t-20 4.5t-24 4t-23 1q-29 0 -56.5 -5t-54 -16.5 t-43 -34t-16.5 -53.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M863 504q0 112 -79.5 191.5t-191.5 79.5t-191 -79.5t-79 -191.5t79 -191t191 -79t191.5 79t79.5 191zM1726 505q0 112 -79 191t-191 79t-191.5 -79t-79.5 -191q0 -113 79.5 -192t191.5 -79t191 79.5t79 191.5zM2048 1314v-1348q0 -44 -31.5 -75.5t-76.5 -31.5h-1832 q-45 0 -76.5 31.5t-31.5 75.5v1348q0 44 31.5 75.5t76.5 31.5h431q44 0 76 -31.5t32 -75.5v-161h754v161q0 44 32 75.5t76 31.5h431q45 0 76.5 -31.5t31.5 -75.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1430 953zM1690 749q148 0 253 -98.5t105 -244.5q0 -157 -109 -261.5t-267 -104.5q-85 0 -162 27.5t-138 73.5t-118 106t-109 126.5t-103.5 132.5t-108.5 126t-117 106t-136 73.5t-159 27.5q-154 0 -251.5 -91.5t-97.5 -244.5q0 -157 104 -250t263 -93q100 0 208 37.5 t193 98.5q5 4 21 18.5t30 24t22 9.5q14 0 24.5 -10.5t10.5 -24.5q0 -24 -60 -77q-101 -88 -234.5 -142t-260.5 -54q-133 0 -245.5 58t-180 165t-67.5 241q0 205 141.5 341t347.5 136q120 0 226.5 -43.5t185.5 -113t151.5 -153t139 -167.5t133.5 -153.5t149.5 -113 t172.5 -43.5q102 0 168.5 61.5t66.5 162.5q0 95 -64.5 159t-159.5 64q-30 0 -81.5 -18.5t-68.5 -18.5q-20 0 -35.5 15t-15.5 35q0 18 8.5 57t8.5 59q0 159 -107.5 263t-266.5 104q-58 0 -111.5 -18.5t-84 -40.5t-55.5 -40.5t-33 -18.5q-15 0 -25.5 10.5t-10.5 25.5 q0 19 25 46q59 67 147 103.5t182 36.5q191 0 318 -125.5t127 -315.5q0 -37 -4 -66q57 15 115 15z" /> -<glyph unicode="" horiz-adv-x="1664" d="M1216 832q0 26 -19 45t-45 19h-128v128q0 26 -19 45t-45 19t-45 -19t-19 -45v-128h-128q-26 0 -45 -19t-19 -45t19 -45t45 -19h128v-128q0 -26 19 -45t45 -19t45 19t19 45v128h128q26 0 45 19t19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 832q0 26 -19 45t-45 19t-45 -19l-147 -146v293q0 26 -19 45t-45 19t-45 -19t-19 -45v-293l-147 146q-19 19 -45 19t-45 -19t-19 -45t19 -45l256 -256q19 -19 45 -19t45 19l256 256q19 19 19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" /> -<glyph unicode="" horiz-adv-x="2048" d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512q14 -18 13 -41.5t-17 -40.5l-960 -1024q-18 -20 -47 -20t-47 20 l-960 1024q-16 17 -17 40.5t13 41.5l384 512q18 26 51 26h1152q33 0 51 -26z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1811 -19q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83 q19 19 45 19t45 -19l83 -83zM237 19q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -82l83 82q19 19 45 19t45 -19l83 -82l64 64v293l-210 314q-17 26 -7 56.5t40 40.5l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58q30 -10 40 -40.5t-7 -56.5l-210 -314 v-293l19 18q19 19 45 19t45 -19l83 -82l83 82q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83zM640 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" /> -<glyph unicode="" d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010q-2 4 -4 6q-10 8 -96 8q-70 0 -167 -19q-7 -2 -21 -2t-21 2q-97 19 -167 19q-86 0 -96 -8q-2 -2 -4 -6q2 -18 4 -27q2 -3 7.5 -6.5t7.5 -10.5q2 -4 7.5 -20.5t7 -20.5t7.5 -17t8.5 -17t9 -14 t12 -13.5t14 -9.5t17.5 -8t20.5 -4t24.5 -2q36 0 59 12.5t32.5 30t14.5 34.5t11.5 29.5t17.5 12.5h12q11 0 17.5 -12.5t11.5 -29.5t14.5 -34.5t32.5 -30t59 -12.5q13 0 24.5 2t20.5 4t17.5 8t14 9.5t12 13.5t9 14t8.5 17t7.5 17t7 20.5t7.5 20.5q2 7 7.5 10.5t7.5 6.5 q2 9 4 27zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 61 4.5 118t19 125.5t37.5 123.5t63.5 103.5t93.5 74.5l-90 220h214q-22 64 -22 128q0 12 2 32q-194 40 -194 96q0 57 210 99q17 62 51.5 134t70.5 114q32 37 76 37q30 0 84 -31t84 -31t84 31 t84 31q44 0 76 -37q36 -42 70.5 -114t51.5 -134q210 -42 210 -99q0 -56 -194 -96q7 -81 -20 -160h214l-82 -225q63 -33 107.5 -96.5t65.5 -143.5t29 -151.5t8 -148.5z" /> -<glyph unicode="" horiz-adv-x="2304" d="M2301 500q12 -103 -22 -198.5t-99 -163.5t-158.5 -106t-196.5 -31q-161 11 -279.5 125t-134.5 274q-12 111 27.5 210.5t118.5 170.5l-71 107q-96 -80 -151 -194t-55 -244q0 -27 -18.5 -46.5t-45.5 -19.5h-256h-69q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5 t-131.5 316.5t131.5 316.5t316.5 131.5q76 0 152 -27l24 45q-123 110 -304 110h-64q-26 0 -45 19t-19 45t19 45t45 19h128q78 0 145 -13.5t116.5 -38.5t71.5 -39.5t51 -36.5h512h115l-85 128h-222q-30 0 -49 22.5t-14 52.5q4 23 23 38t43 15h253q33 0 53 -28l70 -105 l114 114q19 19 46 19h101q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-179l115 -172q131 63 275 36q143 -26 244 -134.5t118 -253.5zM448 128q115 0 203 72.5t111 183.5h-314q-35 0 -55 31q-18 32 -1 63l147 277q-47 13 -91 13q-132 0 -226 -94t-94 -226t94 -226 t226 -94zM1856 128q132 0 226 94t94 226t-94 226t-226 94q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94z" /> -<glyph unicode="" d="M1408 0q0 -63 -61.5 -113.5t-164 -81t-225 -46t-253.5 -15.5t-253.5 15.5t-225 46t-164 81t-61.5 113.5q0 49 33 88.5t91 66.5t118 44.5t131 29.5q26 5 48 -10.5t26 -41.5q5 -26 -10.5 -48t-41.5 -26q-58 -10 -106 -23.5t-76.5 -25.5t-48.5 -23.5t-27.5 -19.5t-8.5 -12 q3 -11 27 -26.5t73 -33t114 -32.5t160.5 -25t201.5 -10t201.5 10t160.5 25t114 33t73 33.5t27 27.5q-1 4 -8.5 11t-27.5 19t-48.5 23.5t-76.5 25t-106 23.5q-26 4 -41.5 26t-10.5 48q4 26 26 41.5t48 10.5q71 -12 131 -29.5t118 -44.5t91 -66.5t33 -88.5zM1024 896v-384 q0 -26 -19 -45t-45 -19h-64v-384q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v384h-64q-26 0 -45 19t-19 45v384q0 53 37.5 90.5t90.5 37.5h384q53 0 90.5 -37.5t37.5 -90.5zM928 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5 t158.5 -65.5t65.5 -158.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 512h305q-5 -6 -10 -10.5t-9 -7.5l-3 -4l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-5 2 -21 20h369q22 0 39.5 13.5t22.5 34.5l70 281l190 -667q6 -20 23 -33t39 -13q21 0 38 13t23 33l146 485l56 -112q18 -35 57 -35zM1792 940q0 -145 -103 -300h-369l-111 221 q-8 17 -25.5 27t-36.5 8q-45 -5 -56 -46l-129 -430l-196 686q-6 20 -23.5 33t-39.5 13t-39 -13.5t-22 -34.5l-116 -464h-423q-103 155 -103 300q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124 t127 -344z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292 q11 134 80.5 249t182 188t245.5 88q170 19 319 -54t236 -212t87 -306zM128 960q0 -185 131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h416q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-419 -420q87 -104 129.5 -236.5t30.5 -276.5q-22 -250 -200.5 -431t-428.5 -206q-163 -17 -314 39.5t-256.5 162t-162 256.5t-39.5 314q25 250 206 428.5 t431 200.5q144 12 276.5 -30.5t236.5 -129.5l419 419h-261q-14 0 -23 9t-9 23v64zM704 -128q117 0 223.5 45.5t184 123t123 184t45.5 223.5t-45.5 223.5t-123 184t-184 123t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123 t223.5 -45.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M830 1220q145 -72 233.5 -210.5t88.5 -305.5q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5 t-147.5 384.5q0 167 88.5 305.5t233.5 210.5q-165 96 -228 273q-6 16 3.5 29.5t26.5 13.5h69q21 0 29 -20q44 -106 140 -171t214 -65t214 65t140 171q8 20 37 20h61q17 0 26.5 -13.5t3.5 -29.5q-63 -177 -228 -273zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" d="M1024 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-149 16 -270.5 103t-186.5 223.5t-53 291.5q16 204 160 353.5t347 172.5q118 14 228 -19t198 -103l255 254h-134q-14 0 -23 9t-9 23v64zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5t-147.5 384.5q0 201 126 359l-52 53l-101 -111q-9 -10 -22 -10.5t-23 7.5l-48 44q-10 8 -10.5 21.5t8.5 23.5l105 115l-111 112v-134q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9 t-9 23v288q0 26 19 45t45 19h288q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-133l106 -107l86 94q9 10 22 10.5t23 -7.5l48 -44q10 -8 10.5 -21.5t-8.5 -23.5l-90 -99l57 -56q158 126 359 126t359 -126l255 254h-134q-14 0 -23 9t-9 23v64zM832 256q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1790 1007q12 -155 -52.5 -292t-186 -224t-271.5 -103v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-512v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23 t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292q17 206 164.5 356.5t352.5 169.5q206 21 377 -94q171 115 377 94q205 -19 352.5 -169.5t164.5 -356.5zM896 647q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM576 512q115 0 218 57q-154 165 -154 391 q0 224 154 391q-103 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5zM1152 128v260q-137 15 -256 94q-119 -79 -256 -94v-260h512zM1216 512q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5q-115 0 -218 -57q154 -167 154 -391 q0 -226 -154 -391q103 -57 218 -57z" /> -<glyph unicode="" horiz-adv-x="1920" d="M1536 1120q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-31 -182 -166 -312t-318 -156q-210 -29 -384.5 80t-241.5 300q-117 6 -221 57.5t-177.5 133t-113.5 192.5t-32 230 q9 135 78 252t182 191.5t248 89.5q118 14 227.5 -19t198.5 -103l255 254h-134q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q59 -74 93 -169q182 -9 328 -124l255 254h-134q-14 0 -23 9 t-9 23v64zM1024 704q0 20 -4 58q-162 -25 -271 -150t-109 -292q0 -20 4 -58q162 25 271 150t109 292zM128 704q0 -168 111 -294t276 -149q-3 29 -3 59q0 210 135 369.5t338 196.5q-53 120 -163.5 193t-245.5 73q-185 0 -316.5 -131.5t-131.5 -316.5zM1088 -128 q185 0 316.5 131.5t131.5 316.5q0 168 -111 294t-276 149q3 -29 3 -59q0 -210 -135 -369.5t-338 -196.5q53 -120 163.5 -193t245.5 -73z" /> -<glyph unicode="" horiz-adv-x="2048" d="M1664 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-32 -180 -164.5 -310t-313.5 -157q-223 -34 -409 90q-117 -78 -256 -93v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23 t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-155 17 -279.5 109.5t-187 237.5t-39.5 307q25 187 159.5 322.5t320.5 164.5q224 34 410 -90q146 97 320 97q201 0 359 -126l255 254h-134q-14 0 -23 9 t-9 23v64zM896 391q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM128 704q0 -185 131.5 -316.5t316.5 -131.5q117 0 218 57q-154 167 -154 391t154 391q-101 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5zM1216 256q185 0 316.5 131.5t131.5 316.5 t-131.5 316.5t-316.5 131.5q-117 0 -218 -57q154 -167 154 -391t-154 -391q101 -57 218 -57z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1728 1536q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-229 -230l156 -156q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-156 157l-99 -100q87 -104 129.5 -236.5t30.5 -276.5q-22 -250 -200.5 -431t-428.5 -206q-163 -17 -314 39.5 t-256.5 162t-162 256.5t-39.5 314q25 250 206 428.5t431 200.5q144 12 276.5 -30.5t236.5 -129.5l99 99l-156 156q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l156 -156l229 229h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM1280 448q0 117 -45.5 223.5t-123 184t-184 123 t-223.5 45.5t-223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5t45.5 -223.5t123 -184t184 -123t223.5 -45.5t223.5 45.5t184 123t123 184t45.5 223.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M640 892q217 -24 364.5 -187.5t147.5 -384.5q0 -167 -87 -306t-236 -212t-319 -54q-133 15 -245.5 88t-182 188t-80.5 249q-12 155 52.5 292t186 224t271.5 103v132h-160q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h160v165l-92 -92q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22 t9 23l202 201q19 19 45 19t45 -19l202 -201q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-92 92v-165h160q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-160v-132zM576 -128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5 t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="2048" d="M2029 685q19 -19 19 -45t-19 -45l-294 -294q-9 -10 -22.5 -10t-22.5 10l-45 45q-10 9 -10 22.5t10 22.5l185 185h-294v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-131q-12 -119 -67 -226t-139 -183.5t-196.5 -121.5t-234.5 -45q-180 0 -330.5 91t-234.5 247 t-74 337q8 162 94 300t226.5 219.5t302.5 85.5q166 4 310.5 -71.5t235.5 -208.5t107 -296h131v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h294l-185 185q-10 9 -10 22.5t10 22.5l45 45q9 10 22.5 10t22.5 -10zM640 128q104 0 198.5 40.5t163.5 109.5t109.5 163.5 t40.5 198.5t-40.5 198.5t-109.5 163.5t-163.5 109.5t-198.5 40.5t-198.5 -40.5t-163.5 -109.5t-109.5 -163.5t-40.5 -198.5t40.5 -198.5t109.5 -163.5t163.5 -109.5t198.5 -40.5z" /> -<glyph unicode="" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-612q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v612q-217 24 -364.5 187.5t-147.5 384.5q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM576 512q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" d="M1451 1408q35 0 60 -25t25 -60v-1366q0 -35 -25 -60t-60 -25h-391v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-735q-35 0 -60 25t-25 60v1366q0 35 25 60t60 25h1366z" /> -<glyph unicode="" horiz-adv-x="1280" d="M0 939q0 108 37.5 203.5t103.5 166.5t152 123t185 78t202 26q158 0 294 -66.5t221 -193.5t85 -287q0 -96 -19 -188t-60 -177t-100 -149.5t-145 -103t-189 -38.5q-68 0 -135 32t-96 88q-10 -39 -28 -112.5t-23.5 -95t-20.5 -71t-26 -71t-32 -62.5t-46 -77.5t-62 -86.5 l-14 -5l-9 10q-15 157 -15 188q0 92 21.5 206.5t66.5 287.5t52 203q-32 65 -32 169q0 83 52 156t132 73q61 0 95 -40.5t34 -102.5q0 -66 -44 -191t-44 -187q0 -63 45 -104.5t109 -41.5q55 0 102 25t78.5 68t56 95t38 110.5t20 111t6.5 99.5q0 173 -109.5 269.5t-285.5 96.5 q-200 0 -334 -129.5t-134 -328.5q0 -44 12.5 -85t27 -65t27 -45.5t12.5 -30.5q0 -28 -15 -73t-37 -45q-2 0 -17 3q-51 15 -90.5 56t-61 94.5t-32.5 108t-11 106.5z" /> -<glyph unicode="" d="M985 562q13 0 97.5 -44t89.5 -53q2 -5 2 -15q0 -33 -17 -76q-16 -39 -71 -65.5t-102 -26.5q-57 0 -190 62q-98 45 -170 118t-148 185q-72 107 -71 194v8q3 91 74 158q24 22 52 22q6 0 18 -1.5t19 -1.5q19 0 26.5 -6.5t15.5 -27.5q8 -20 33 -88t25 -75q0 -21 -34.5 -57.5 t-34.5 -46.5q0 -7 5 -15q34 -73 102 -137q56 -53 151 -101q12 -7 22 -7q15 0 54 48.5t52 48.5zM782 32q127 0 243.5 50t200.5 134t134 200.5t50 243.5t-50 243.5t-134 200.5t-200.5 134t-243.5 50t-243.5 -50t-200.5 -134t-134 -200.5t-50 -243.5q0 -203 120 -368l-79 -233 l242 77q158 -104 345 -104zM782 1414q153 0 292.5 -60t240.5 -161t161 -240.5t60 -292.5t-60 -292.5t-161 -240.5t-240.5 -161t-292.5 -60q-195 0 -365 94l-417 -134l136 405q-108 178 -108 389q0 153 60 292.5t161 240.5t240.5 161t292.5 60z" /> -<glyph unicode="" horiz-adv-x="1792" d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM128 1152h1024v128h-1024v-128zM1696 704q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1696 1216 q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" /> -<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1664 512h352q13 0 22.5 -9.5t9.5 -22.5v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-352q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5 t-9.5 22.5v352h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v352q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5v-352zM928 288q0 -52 38 -90t90 -38h256v-238q-68 -50 -171 -50h-874q-121 0 -194 69t-73 190q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q79 -61 154.5 -91.5t164.5 -30.5t164.5 30.5t154.5 91.5q20 17 39 17q132 0 217 -96h-223q-52 0 -90 -38t-38 -90v-192z" /> -<glyph unicode="" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1781 320l249 -249q9 -9 9 -23q0 -13 -9 -22l-136 -136q-9 -9 -22 -9q-14 0 -23 9l-249 249l-249 -249q-9 -9 -23 -9q-13 0 -22 9l-136 136 q-9 9 -9 22q0 14 9 23l249 249l-249 249q-9 9 -9 23q0 13 9 22l136 136q9 9 22 9q14 0 23 -9l249 -249l249 249q9 9 23 9q13 0 22 -9l136 -136q9 -9 9 -22q0 -14 -9 -23zM1283 320l-181 -181q-37 -37 -37 -91q0 -53 37 -90l83 -83q-21 -3 -44 -3h-874q-121 0 -194 69 t-73 190q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q154 -122 319 -122t319 122q20 17 39 17q28 0 57 -6q-28 -27 -41 -50t-13 -56q0 -54 37 -91z" /> -<glyph unicode="" horiz-adv-x="2048" d="M256 512h1728q26 0 45 -19t19 -45v-448h-256v256h-1536v-256h-256v1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-704zM832 832q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM2048 576v64q0 159 -112.5 271.5t-271.5 112.5h-704 q-26 0 -45 -19t-19 -45v-384h1152z" /> -<glyph unicode="" d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" /> -<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM768 192q80 0 136 56t56 136t-56 136t-136 56 t-136 -56t-56 -136t56 -136t136 -56zM1344 768v512h-1152v-512h1152z" /> -<glyph unicode="" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM288 224q66 0 113 47t47 113t-47 113t-113 47 t-113 -47t-47 -113t47 -113t113 -47zM704 768v512h-544v-512h544zM1248 224q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM1408 768v512h-576v-512h576z" /> -<glyph unicode="" horiz-adv-x="1792" d="M1792 204v-209h-642v209h134v926h-6l-314 -1135h-243l-310 1135h-8v-926h135v-209h-538v209h69q21 0 43 19.5t22 37.5v881q0 18 -22 40t-43 22h-69v209h672l221 -821h6l223 821h670v-209h-71q-19 0 -41 -22t-22 -40v-881q0 -18 21.5 -37.5t41.5 -19.5h71z" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -<glyph unicode="" horiz-adv-x="1792" /> -</font> -</defs></svg> \ No newline at end of file diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.ttf b/src/UI/Content/FontAwesome/fontawesome-webfont.ttf deleted file mode 100644 index ed9372f8ea0fbaa04f42630a48887e4b38945345..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122092 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 +|+&Vdls@0SAya!5R?! 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 W2%>->3J9Wr?l-V&_Su?ry8j2p&Q&b{ diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.woff b/src/UI/Content/FontAwesome/fontawesome-webfont.woff deleted file mode 100644 index 8b280b98fa2fa261aa4b0f8fd061f772073ef83e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 71508 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 F003Px<E8)r diff --git a/src/UI/Content/FontAwesome/fontawesome-webfont.woff2 b/src/UI/Content/FontAwesome/fontawesome-webfont.woff2 deleted file mode 100644 index 3311d585145b1cc1b9581e914acbb32d8542b4f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56780 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 diff --git a/src/UI/Content/FontAwesome/icons.less b/src/UI/Content/FontAwesome/icons.less deleted file mode 100644 index c265de5a6..000000000 --- a/src/UI/Content/FontAwesome/icons.less +++ /dev/null @@ -1,596 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } -.@{fa-css-prefix}-music:before { content: @fa-var-music; } -.@{fa-css-prefix}-search:before { content: @fa-var-search; } -.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } -.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } -.@{fa-css-prefix}-star:before { content: @fa-var-star; } -.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } -.@{fa-css-prefix}-user:before { content: @fa-var-user; } -.@{fa-css-prefix}-film:before { content: @fa-var-film; } -.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } -.@{fa-css-prefix}-th:before { content: @fa-var-th; } -.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } -.@{fa-css-prefix}-check:before { content: @fa-var-check; } -.@{fa-css-prefix}-remove:before, -.@{fa-css-prefix}-close:before, -.@{fa-css-prefix}-times:before { content: @fa-var-times; } -.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } -.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } -.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } -.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } -.@{fa-css-prefix}-gear:before, -.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } -.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } -.@{fa-css-prefix}-home:before { content: @fa-var-home; } -.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } -.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } -.@{fa-css-prefix}-road:before { content: @fa-var-road; } -.@{fa-css-prefix}-download:before { content: @fa-var-download; } -.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } -.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } -.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } -.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } -.@{fa-css-prefix}-rotate-right:before, -.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } -.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } -.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } -.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } -.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } -.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } -.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } -.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } -.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } -.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } -.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } -.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } -.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } -.@{fa-css-prefix}-book:before { content: @fa-var-book; } -.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } -.@{fa-css-prefix}-print:before { content: @fa-var-print; } -.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } -.@{fa-css-prefix}-font:before { content: @fa-var-font; } -.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } -.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } -.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } -.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } -.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } -.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } -.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } -.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } -.@{fa-css-prefix}-list:before { content: @fa-var-list; } -.@{fa-css-prefix}-dedent:before, -.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } -.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } -.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } -.@{fa-css-prefix}-photo:before, -.@{fa-css-prefix}-image:before, -.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } -.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } -.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } -.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } -.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } -.@{fa-css-prefix}-edit:before, -.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } -.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } -.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } -.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } -.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } -.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } -.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } -.@{fa-css-prefix}-play:before { content: @fa-var-play; } -.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } -.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } -.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } -.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } -.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } -.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } -.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } -.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } -.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } -.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } -.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } -.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } -.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } -.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } -.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } -.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } -.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } -.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } -.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } -.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } -.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } -.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } -.@{fa-css-prefix}-mail-forward:before, -.@{fa-css-prefix}-share:before { content: @fa-var-share; } -.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } -.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } -.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } -.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } -.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } -.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } -.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } -.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } -.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } -.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } -.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } -.@{fa-css-prefix}-warning:before, -.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } -.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } -.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } -.@{fa-css-prefix}-random:before { content: @fa-var-random; } -.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } -.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } -.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } -.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } -.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } -.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } -.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } -.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } -.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } -.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } -.@{fa-css-prefix}-bar-chart-o:before, -.@{fa-css-prefix}-bar-chart:before { content: @fa-var-bar-chart; } -.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } -.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } -.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } -.@{fa-css-prefix}-key:before { content: @fa-var-key; } -.@{fa-css-prefix}-gears:before, -.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } -.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } -.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } -.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } -.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } -.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } -.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } -.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } -.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } -.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } -.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } -.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } -.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } -.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } -.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } -.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } -.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } -.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } -.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } -.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } -.@{fa-css-prefix}-facebook-f:before, -.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } -.@{fa-css-prefix}-github:before { content: @fa-var-github; } -.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } -.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } -.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } -.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } -.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } -.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } -.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } -.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } -.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } -.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } -.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } -.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } -.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } -.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } -.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } -.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } -.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } -.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } -.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } -.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } -.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } -.@{fa-css-prefix}-group:before, -.@{fa-css-prefix}-users:before { content: @fa-var-users; } -.@{fa-css-prefix}-chain:before, -.@{fa-css-prefix}-link:before { content: @fa-var-link; } -.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } -.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } -.@{fa-css-prefix}-cut:before, -.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } -.@{fa-css-prefix}-copy:before, -.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } -.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } -.@{fa-css-prefix}-save:before, -.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } -.@{fa-css-prefix}-square:before { content: @fa-var-square; } -.@{fa-css-prefix}-navicon:before, -.@{fa-css-prefix}-reorder:before, -.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } -.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } -.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } -.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } -.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } -.@{fa-css-prefix}-table:before { content: @fa-var-table; } -.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } -.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } -.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } -.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } -.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } -.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } -.@{fa-css-prefix}-money:before { content: @fa-var-money; } -.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } -.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } -.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } -.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } -.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } -.@{fa-css-prefix}-unsorted:before, -.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } -.@{fa-css-prefix}-sort-down:before, -.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } -.@{fa-css-prefix}-sort-up:before, -.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } -.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } -.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } -.@{fa-css-prefix}-rotate-left:before, -.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } -.@{fa-css-prefix}-legal:before, -.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } -.@{fa-css-prefix}-dashboard:before, -.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } -.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } -.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } -.@{fa-css-prefix}-flash:before, -.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } -.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } -.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } -.@{fa-css-prefix}-paste:before, -.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } -.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } -.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } -.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } -.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } -.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } -.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } -.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } -.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } -.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } -.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } -.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } -.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } -.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } -.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } -.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } -.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } -.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } -.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } -.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } -.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } -.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } -.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } -.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } -.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } -.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } -.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } -.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } -.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } -.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } -.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } -.@{fa-css-prefix}-mobile-phone:before, -.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } -.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } -.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } -.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } -.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } -.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } -.@{fa-css-prefix}-mail-reply:before, -.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } -.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } -.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } -.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } -.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } -.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } -.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } -.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } -.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } -.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } -.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } -.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } -.@{fa-css-prefix}-code:before { content: @fa-var-code; } -.@{fa-css-prefix}-mail-reply-all:before, -.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } -.@{fa-css-prefix}-star-half-empty:before, -.@{fa-css-prefix}-star-half-full:before, -.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } -.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } -.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } -.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } -.@{fa-css-prefix}-unlink:before, -.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } -.@{fa-css-prefix}-question:before { content: @fa-var-question; } -.@{fa-css-prefix}-info:before { content: @fa-var-info; } -.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } -.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } -.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } -.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } -.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } -.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } -.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } -.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } -.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } -.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } -.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } -.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } -.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } -.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } -.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } -.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } -.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } -.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } -.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } -.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } -.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } -.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } -.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } -.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } -.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } -.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } -.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } -.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } -.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } -.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } -.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } -.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } -.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } -.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } -.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } -.@{fa-css-prefix}-toggle-down:before, -.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } -.@{fa-css-prefix}-toggle-up:before, -.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } -.@{fa-css-prefix}-toggle-right:before, -.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } -.@{fa-css-prefix}-euro:before, -.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } -.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } -.@{fa-css-prefix}-dollar:before, -.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } -.@{fa-css-prefix}-rupee:before, -.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } -.@{fa-css-prefix}-cny:before, -.@{fa-css-prefix}-rmb:before, -.@{fa-css-prefix}-yen:before, -.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } -.@{fa-css-prefix}-ruble:before, -.@{fa-css-prefix}-rouble:before, -.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } -.@{fa-css-prefix}-won:before, -.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } -.@{fa-css-prefix}-bitcoin:before, -.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } -.@{fa-css-prefix}-file:before { content: @fa-var-file; } -.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } -.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } -.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } -.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } -.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } -.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } -.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } -.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } -.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } -.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } -.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } -.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } -.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } -.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } -.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } -.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } -.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } -.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } -.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } -.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } -.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } -.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } -.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } -.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } -.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } -.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } -.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } -.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } -.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } -.@{fa-css-prefix}-android:before { content: @fa-var-android; } -.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } -.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } -.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } -.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } -.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } -.@{fa-css-prefix}-female:before { content: @fa-var-female; } -.@{fa-css-prefix}-male:before { content: @fa-var-male; } -.@{fa-css-prefix}-gittip:before, -.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; } -.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } -.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } -.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } -.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } -.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } -.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } -.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } -.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } -.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } -.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } -.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } -.@{fa-css-prefix}-toggle-left:before, -.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } -.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } -.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } -.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } -.@{fa-css-prefix}-turkish-lira:before, -.@{fa-css-prefix}-try:before { content: @fa-var-try; } -.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } -.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } -.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } -.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } -.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } -.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } -.@{fa-css-prefix}-institution:before, -.@{fa-css-prefix}-bank:before, -.@{fa-css-prefix}-university:before { content: @fa-var-university; } -.@{fa-css-prefix}-mortar-board:before, -.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } -.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } -.@{fa-css-prefix}-google:before { content: @fa-var-google; } -.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } -.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } -.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } -.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } -.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } -.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } -.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } -.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } -.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } -.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } -.@{fa-css-prefix}-language:before { content: @fa-var-language; } -.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } -.@{fa-css-prefix}-building:before { content: @fa-var-building; } -.@{fa-css-prefix}-child:before { content: @fa-var-child; } -.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } -.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } -.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } -.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } -.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } -.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } -.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } -.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } -.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } -.@{fa-css-prefix}-automobile:before, -.@{fa-css-prefix}-car:before { content: @fa-var-car; } -.@{fa-css-prefix}-cab:before, -.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } -.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } -.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } -.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } -.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } -.@{fa-css-prefix}-database:before { content: @fa-var-database; } -.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } -.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } -.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } -.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } -.@{fa-css-prefix}-file-photo-o:before, -.@{fa-css-prefix}-file-picture-o:before, -.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } -.@{fa-css-prefix}-file-zip-o:before, -.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } -.@{fa-css-prefix}-file-sound-o:before, -.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } -.@{fa-css-prefix}-file-movie-o:before, -.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } -.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } -.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } -.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } -.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } -.@{fa-css-prefix}-life-bouy:before, -.@{fa-css-prefix}-life-buoy:before, -.@{fa-css-prefix}-life-saver:before, -.@{fa-css-prefix}-support:before, -.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } -.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } -.@{fa-css-prefix}-ra:before, -.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } -.@{fa-css-prefix}-ge:before, -.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } -.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } -.@{fa-css-prefix}-git:before { content: @fa-var-git; } -.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } -.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } -.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } -.@{fa-css-prefix}-wechat:before, -.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } -.@{fa-css-prefix}-send:before, -.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } -.@{fa-css-prefix}-send-o:before, -.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } -.@{fa-css-prefix}-history:before { content: @fa-var-history; } -.@{fa-css-prefix}-genderless:before, -.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } -.@{fa-css-prefix}-header:before { content: @fa-var-header; } -.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } -.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } -.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } -.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } -.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } -.@{fa-css-prefix}-soccer-ball-o:before, -.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol-o; } -.@{fa-css-prefix}-tty:before { content: @fa-var-tty; } -.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; } -.@{fa-css-prefix}-plug:before { content: @fa-var-plug; } -.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; } -.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; } -.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; } -.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper-o; } -.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; } -.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; } -.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; } -.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; } -.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; } -.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; } -.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; } -.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; } -.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; } -.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; } -.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; } -.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash-o; } -.@{fa-css-prefix}-trash:before { content: @fa-var-trash; } -.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; } -.@{fa-css-prefix}-at:before { content: @fa-var-at; } -.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eyedropper; } -.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; } -.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; } -.@{fa-css-prefix}-area-chart:before { content: @fa-var-area-chart; } -.@{fa-css-prefix}-pie-chart:before { content: @fa-var-pie-chart; } -.@{fa-css-prefix}-line-chart:before { content: @fa-var-line-chart; } -.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; } -.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; } -.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; } -.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; } -.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; } -.@{fa-css-prefix}-bus:before { content: @fa-var-bus; } -.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; } -.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; } -.@{fa-css-prefix}-cc:before { content: @fa-var-cc; } -.@{fa-css-prefix}-shekel:before, -.@{fa-css-prefix}-sheqel:before, -.@{fa-css-prefix}-ils:before { content: @fa-var-ils; } -.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; } -.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; } -.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; } -.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; } -.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; } -.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; } -.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; } -.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; } -.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; } -.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; } -.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; } -.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; } -.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; } -.@{fa-css-prefix}-ship:before { content: @fa-var-ship; } -.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; } -.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; } -.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; } -.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; } -.@{fa-css-prefix}-venus:before { content: @fa-var-venus; } -.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } -.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } -.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; } -.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } -.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } -.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; } -.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; } -.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; } -.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; } -.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } -.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; } -.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; } -.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } -.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } -.@{fa-css-prefix}-server:before { content: @fa-var-server; } -.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; } -.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; } -.@{fa-css-prefix}-hotel:before, -.@{fa-css-prefix}-bed:before { content: @fa-var-bed; } -.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; } -.@{fa-css-prefix}-train:before { content: @fa-var-train; } -.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } -.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } diff --git a/src/UI/Content/FontAwesome/larger.less b/src/UI/Content/FontAwesome/larger.less deleted file mode 100644 index c9d646770..000000000 --- a/src/UI/Content/FontAwesome/larger.less +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.@{fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.@{fa-css-prefix}-2x { font-size: 2em; } -.@{fa-css-prefix}-3x { font-size: 3em; } -.@{fa-css-prefix}-4x { font-size: 4em; } -.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/src/UI/Content/FontAwesome/list.less b/src/UI/Content/FontAwesome/list.less deleted file mode 100644 index 0b440382f..000000000 --- a/src/UI/Content/FontAwesome/list.less +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.@{fa-css-prefix}-ul { - padding-left: 0; - margin-left: @fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.@{fa-css-prefix}-li { - position: absolute; - left: -@fa-li-width; - width: @fa-li-width; - top: (2em / 14); - text-align: center; - &.@{fa-css-prefix}-lg { - left: (-@fa-li-width + (4em / 14)); - } -} diff --git a/src/UI/Content/FontAwesome/mixins.less b/src/UI/Content/FontAwesome/mixins.less deleted file mode 100644 index c97f4604c..000000000 --- a/src/UI/Content/FontAwesome/mixins.less +++ /dev/null @@ -1,27 +0,0 @@ -// Mixins -// -------------------------- - -.fa-icon() { - display: inline-block; - font: normal normal normal @fa-font-size-base/1 FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transform: translate(0, 0); // ensures no half-pixel rendering in firefox - -} - -.fa-icon-rotate(@degrees, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - transform: rotate(@degrees); -} - -.fa-icon-flip(@horiz, @vert, @rotation) { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); - -webkit-transform: scale(@horiz, @vert); - -ms-transform: scale(@horiz, @vert); - transform: scale(@horiz, @vert); -} diff --git a/src/UI/Content/FontAwesome/path.less b/src/UI/Content/FontAwesome/path.less deleted file mode 100644 index 9211e6659..000000000 --- a/src/UI/Content/FontAwesome/path.less +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); - src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), - url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), - url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), - url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), - url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); -// src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/src/UI/Content/FontAwesome/rotated-flipped.less b/src/UI/Content/FontAwesome/rotated-flipped.less deleted file mode 100644 index f6ba81475..000000000 --- a/src/UI/Content/FontAwesome/rotated-flipped.less +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } -.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } -.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } - -.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } -.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .@{fa-css-prefix}-rotate-90, -:root .@{fa-css-prefix}-rotate-180, -:root .@{fa-css-prefix}-rotate-270, -:root .@{fa-css-prefix}-flip-horizontal, -:root .@{fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/src/UI/Content/FontAwesome/stacked.less b/src/UI/Content/FontAwesome/stacked.less deleted file mode 100644 index fc53fb0e7..000000000 --- a/src/UI/Content/FontAwesome/stacked.less +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.@{fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.@{fa-css-prefix}-stack-1x { line-height: inherit; } -.@{fa-css-prefix}-stack-2x { font-size: 2em; } -.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/src/UI/Content/FontAwesome/variables.less b/src/UI/Content/FontAwesome/variables.less deleted file mode 100644 index 7d026a20d..000000000 --- a/src/UI/Content/FontAwesome/variables.less +++ /dev/null @@ -1,606 +0,0 @@ -// Variables -// -------------------------- - -@fa-font-path: "../Content/FontAwesome"; -@fa-font-size-base: 14px; -//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.3.0/fonts"; // for referencing Bootstrap CDN font files directly -@fa-css-prefix: fa; -@fa-version: "4.3.0"; -@fa-border-color: #eee; -@fa-inverse: #fff; -@fa-li-width: (30em / 14); - -@fa-var-adjust: "\f042"; -@fa-var-adn: "\f170"; -@fa-var-align-center: "\f037"; -@fa-var-align-justify: "\f039"; -@fa-var-align-left: "\f036"; -@fa-var-align-right: "\f038"; -@fa-var-ambulance: "\f0f9"; -@fa-var-anchor: "\f13d"; -@fa-var-android: "\f17b"; -@fa-var-angellist: "\f209"; -@fa-var-angle-double-down: "\f103"; -@fa-var-angle-double-left: "\f100"; -@fa-var-angle-double-right: "\f101"; -@fa-var-angle-double-up: "\f102"; -@fa-var-angle-down: "\f107"; -@fa-var-angle-left: "\f104"; -@fa-var-angle-right: "\f105"; -@fa-var-angle-up: "\f106"; -@fa-var-apple: "\f179"; -@fa-var-archive: "\f187"; -@fa-var-area-chart: "\f1fe"; -@fa-var-arrow-circle-down: "\f0ab"; -@fa-var-arrow-circle-left: "\f0a8"; -@fa-var-arrow-circle-o-down: "\f01a"; -@fa-var-arrow-circle-o-left: "\f190"; -@fa-var-arrow-circle-o-right: "\f18e"; -@fa-var-arrow-circle-o-up: "\f01b"; -@fa-var-arrow-circle-right: "\f0a9"; -@fa-var-arrow-circle-up: "\f0aa"; -@fa-var-arrow-down: "\f063"; -@fa-var-arrow-left: "\f060"; -@fa-var-arrow-right: "\f061"; -@fa-var-arrow-up: "\f062"; -@fa-var-arrows: "\f047"; -@fa-var-arrows-alt: "\f0b2"; -@fa-var-arrows-h: "\f07e"; -@fa-var-arrows-v: "\f07d"; -@fa-var-asterisk: "\f069"; -@fa-var-at: "\f1fa"; -@fa-var-automobile: "\f1b9"; -@fa-var-backward: "\f04a"; -@fa-var-ban: "\f05e"; -@fa-var-bank: "\f19c"; -@fa-var-bar-chart: "\f080"; -@fa-var-bar-chart-o: "\f080"; -@fa-var-barcode: "\f02a"; -@fa-var-bars: "\f0c9"; -@fa-var-bed: "\f236"; -@fa-var-beer: "\f0fc"; -@fa-var-behance: "\f1b4"; -@fa-var-behance-square: "\f1b5"; -@fa-var-bell: "\f0f3"; -@fa-var-bell-o: "\f0a2"; -@fa-var-bell-slash: "\f1f6"; -@fa-var-bell-slash-o: "\f1f7"; -@fa-var-bicycle: "\f206"; -@fa-var-binoculars: "\f1e5"; -@fa-var-birthday-cake: "\f1fd"; -@fa-var-bitbucket: "\f171"; -@fa-var-bitbucket-square: "\f172"; -@fa-var-bitcoin: "\f15a"; -@fa-var-bold: "\f032"; -@fa-var-bolt: "\f0e7"; -@fa-var-bomb: "\f1e2"; -@fa-var-book: "\f02d"; -@fa-var-bookmark: "\f02e"; -@fa-var-bookmark-o: "\f097"; -@fa-var-briefcase: "\f0b1"; -@fa-var-btc: "\f15a"; -@fa-var-bug: "\f188"; -@fa-var-building: "\f1ad"; -@fa-var-building-o: "\f0f7"; -@fa-var-bullhorn: "\f0a1"; -@fa-var-bullseye: "\f140"; -@fa-var-bus: "\f207"; -@fa-var-buysellads: "\f20d"; -@fa-var-cab: "\f1ba"; -@fa-var-calculator: "\f1ec"; -@fa-var-calendar: "\f073"; -@fa-var-calendar-o: "\f133"; -@fa-var-camera: "\f030"; -@fa-var-camera-retro: "\f083"; -@fa-var-car: "\f1b9"; -@fa-var-caret-down: "\f0d7"; -@fa-var-caret-left: "\f0d9"; -@fa-var-caret-right: "\f0da"; -@fa-var-caret-square-o-down: "\f150"; -@fa-var-caret-square-o-left: "\f191"; -@fa-var-caret-square-o-right: "\f152"; -@fa-var-caret-square-o-up: "\f151"; -@fa-var-caret-up: "\f0d8"; -@fa-var-cart-arrow-down: "\f218"; -@fa-var-cart-plus: "\f217"; -@fa-var-cc: "\f20a"; -@fa-var-cc-amex: "\f1f3"; -@fa-var-cc-discover: "\f1f2"; -@fa-var-cc-mastercard: "\f1f1"; -@fa-var-cc-paypal: "\f1f4"; -@fa-var-cc-stripe: "\f1f5"; -@fa-var-cc-visa: "\f1f0"; -@fa-var-certificate: "\f0a3"; -@fa-var-chain: "\f0c1"; -@fa-var-chain-broken: "\f127"; -@fa-var-check: "\f00c"; -@fa-var-check-circle: "\f058"; -@fa-var-check-circle-o: "\f05d"; -@fa-var-check-square: "\f14a"; -@fa-var-check-square-o: "\f046"; -@fa-var-chevron-circle-down: "\f13a"; -@fa-var-chevron-circle-left: "\f137"; -@fa-var-chevron-circle-right: "\f138"; -@fa-var-chevron-circle-up: "\f139"; -@fa-var-chevron-down: "\f078"; -@fa-var-chevron-left: "\f053"; -@fa-var-chevron-right: "\f054"; -@fa-var-chevron-up: "\f077"; -@fa-var-child: "\f1ae"; -@fa-var-circle: "\f111"; -@fa-var-circle-o: "\f10c"; -@fa-var-circle-o-notch: "\f1ce"; -@fa-var-circle-thin: "\f1db"; -@fa-var-clipboard: "\f0ea"; -@fa-var-clock-o: "\f017"; -@fa-var-close: "\f00d"; -@fa-var-cloud: "\f0c2"; -@fa-var-cloud-download: "\f0ed"; -@fa-var-cloud-upload: "\f0ee"; -@fa-var-cny: "\f157"; -@fa-var-code: "\f121"; -@fa-var-code-fork: "\f126"; -@fa-var-codepen: "\f1cb"; -@fa-var-coffee: "\f0f4"; -@fa-var-cog: "\f013"; -@fa-var-cogs: "\f085"; -@fa-var-columns: "\f0db"; -@fa-var-comment: "\f075"; -@fa-var-comment-o: "\f0e5"; -@fa-var-comments: "\f086"; -@fa-var-comments-o: "\f0e6"; -@fa-var-compass: "\f14e"; -@fa-var-compress: "\f066"; -@fa-var-connectdevelop: "\f20e"; -@fa-var-copy: "\f0c5"; -@fa-var-copyright: "\f1f9"; -@fa-var-credit-card: "\f09d"; -@fa-var-crop: "\f125"; -@fa-var-crosshairs: "\f05b"; -@fa-var-css3: "\f13c"; -@fa-var-cube: "\f1b2"; -@fa-var-cubes: "\f1b3"; -@fa-var-cut: "\f0c4"; -@fa-var-cutlery: "\f0f5"; -@fa-var-dashboard: "\f0e4"; -@fa-var-dashcube: "\f210"; -@fa-var-database: "\f1c0"; -@fa-var-dedent: "\f03b"; -@fa-var-delicious: "\f1a5"; -@fa-var-desktop: "\f108"; -@fa-var-deviantart: "\f1bd"; -@fa-var-diamond: "\f219"; -@fa-var-digg: "\f1a6"; -@fa-var-dollar: "\f155"; -@fa-var-dot-circle-o: "\f192"; -@fa-var-download: "\f019"; -@fa-var-dribbble: "\f17d"; -@fa-var-dropbox: "\f16b"; -@fa-var-drupal: "\f1a9"; -@fa-var-edit: "\f044"; -@fa-var-eject: "\f052"; -@fa-var-ellipsis-h: "\f141"; -@fa-var-ellipsis-v: "\f142"; -@fa-var-empire: "\f1d1"; -@fa-var-envelope: "\f0e0"; -@fa-var-envelope-o: "\f003"; -@fa-var-envelope-square: "\f199"; -@fa-var-eraser: "\f12d"; -@fa-var-eur: "\f153"; -@fa-var-euro: "\f153"; -@fa-var-exchange: "\f0ec"; -@fa-var-exclamation: "\f12a"; -@fa-var-exclamation-circle: "\f06a"; -@fa-var-exclamation-triangle: "\f071"; -@fa-var-expand: "\f065"; -@fa-var-external-link: "\f08e"; -@fa-var-external-link-square: "\f14c"; -@fa-var-eye: "\f06e"; -@fa-var-eye-slash: "\f070"; -@fa-var-eyedropper: "\f1fb"; -@fa-var-facebook: "\f09a"; -@fa-var-facebook-f: "\f09a"; -@fa-var-facebook-official: "\f230"; -@fa-var-facebook-square: "\f082"; -@fa-var-fast-backward: "\f049"; -@fa-var-fast-forward: "\f050"; -@fa-var-fax: "\f1ac"; -@fa-var-female: "\f182"; -@fa-var-fighter-jet: "\f0fb"; -@fa-var-file: "\f15b"; -@fa-var-file-archive-o: "\f1c6"; -@fa-var-file-audio-o: "\f1c7"; -@fa-var-file-code-o: "\f1c9"; -@fa-var-file-excel-o: "\f1c3"; -@fa-var-file-image-o: "\f1c5"; -@fa-var-file-movie-o: "\f1c8"; -@fa-var-file-o: "\f016"; -@fa-var-file-pdf-o: "\f1c1"; -@fa-var-file-photo-o: "\f1c5"; -@fa-var-file-picture-o: "\f1c5"; -@fa-var-file-powerpoint-o: "\f1c4"; -@fa-var-file-sound-o: "\f1c7"; -@fa-var-file-text: "\f15c"; -@fa-var-file-text-o: "\f0f6"; -@fa-var-file-video-o: "\f1c8"; -@fa-var-file-word-o: "\f1c2"; -@fa-var-file-zip-o: "\f1c6"; -@fa-var-files-o: "\f0c5"; -@fa-var-film: "\f008"; -@fa-var-filter: "\f0b0"; -@fa-var-fire: "\f06d"; -@fa-var-fire-extinguisher: "\f134"; -@fa-var-flag: "\f024"; -@fa-var-flag-checkered: "\f11e"; -@fa-var-flag-o: "\f11d"; -@fa-var-flash: "\f0e7"; -@fa-var-flask: "\f0c3"; -@fa-var-flickr: "\f16e"; -@fa-var-floppy-o: "\f0c7"; -@fa-var-folder: "\f07b"; -@fa-var-folder-o: "\f114"; -@fa-var-folder-open: "\f07c"; -@fa-var-folder-open-o: "\f115"; -@fa-var-font: "\f031"; -@fa-var-forumbee: "\f211"; -@fa-var-forward: "\f04e"; -@fa-var-foursquare: "\f180"; -@fa-var-frown-o: "\f119"; -@fa-var-futbol-o: "\f1e3"; -@fa-var-gamepad: "\f11b"; -@fa-var-gavel: "\f0e3"; -@fa-var-gbp: "\f154"; -@fa-var-ge: "\f1d1"; -@fa-var-gear: "\f013"; -@fa-var-gears: "\f085"; -@fa-var-genderless: "\f1db"; -@fa-var-gift: "\f06b"; -@fa-var-git: "\f1d3"; -@fa-var-git-square: "\f1d2"; -@fa-var-github: "\f09b"; -@fa-var-github-alt: "\f113"; -@fa-var-github-square: "\f092"; -@fa-var-gittip: "\f184"; -@fa-var-glass: "\f000"; -@fa-var-globe: "\f0ac"; -@fa-var-google: "\f1a0"; -@fa-var-google-plus: "\f0d5"; -@fa-var-google-plus-square: "\f0d4"; -@fa-var-google-wallet: "\f1ee"; -@fa-var-graduation-cap: "\f19d"; -@fa-var-gratipay: "\f184"; -@fa-var-group: "\f0c0"; -@fa-var-h-square: "\f0fd"; -@fa-var-hacker-news: "\f1d4"; -@fa-var-hand-o-down: "\f0a7"; -@fa-var-hand-o-left: "\f0a5"; -@fa-var-hand-o-right: "\f0a4"; -@fa-var-hand-o-up: "\f0a6"; -@fa-var-hdd-o: "\f0a0"; -@fa-var-header: "\f1dc"; -@fa-var-headphones: "\f025"; -@fa-var-heart: "\f004"; -@fa-var-heart-o: "\f08a"; -@fa-var-heartbeat: "\f21e"; -@fa-var-history: "\f1da"; -@fa-var-home: "\f015"; -@fa-var-hospital-o: "\f0f8"; -@fa-var-hotel: "\f236"; -@fa-var-html5: "\f13b"; -@fa-var-ils: "\f20b"; -@fa-var-image: "\f03e"; -@fa-var-inbox: "\f01c"; -@fa-var-indent: "\f03c"; -@fa-var-info: "\f129"; -@fa-var-info-circle: "\f05a"; -@fa-var-inr: "\f156"; -@fa-var-instagram: "\f16d"; -@fa-var-institution: "\f19c"; -@fa-var-ioxhost: "\f208"; -@fa-var-italic: "\f033"; -@fa-var-joomla: "\f1aa"; -@fa-var-jpy: "\f157"; -@fa-var-jsfiddle: "\f1cc"; -@fa-var-key: "\f084"; -@fa-var-keyboard-o: "\f11c"; -@fa-var-krw: "\f159"; -@fa-var-language: "\f1ab"; -@fa-var-laptop: "\f109"; -@fa-var-lastfm: "\f202"; -@fa-var-lastfm-square: "\f203"; -@fa-var-leaf: "\f06c"; -@fa-var-leanpub: "\f212"; -@fa-var-legal: "\f0e3"; -@fa-var-lemon-o: "\f094"; -@fa-var-level-down: "\f149"; -@fa-var-level-up: "\f148"; -@fa-var-life-bouy: "\f1cd"; -@fa-var-life-buoy: "\f1cd"; -@fa-var-life-ring: "\f1cd"; -@fa-var-life-saver: "\f1cd"; -@fa-var-lightbulb-o: "\f0eb"; -@fa-var-line-chart: "\f201"; -@fa-var-link: "\f0c1"; -@fa-var-linkedin: "\f0e1"; -@fa-var-linkedin-square: "\f08c"; -@fa-var-linux: "\f17c"; -@fa-var-list: "\f03a"; -@fa-var-list-alt: "\f022"; -@fa-var-list-ol: "\f0cb"; -@fa-var-list-ul: "\f0ca"; -@fa-var-location-arrow: "\f124"; -@fa-var-lock: "\f023"; -@fa-var-long-arrow-down: "\f175"; -@fa-var-long-arrow-left: "\f177"; -@fa-var-long-arrow-right: "\f178"; -@fa-var-long-arrow-up: "\f176"; -@fa-var-magic: "\f0d0"; -@fa-var-magnet: "\f076"; -@fa-var-mail-forward: "\f064"; -@fa-var-mail-reply: "\f112"; -@fa-var-mail-reply-all: "\f122"; -@fa-var-male: "\f183"; -@fa-var-map-marker: "\f041"; -@fa-var-mars: "\f222"; -@fa-var-mars-double: "\f227"; -@fa-var-mars-stroke: "\f229"; -@fa-var-mars-stroke-h: "\f22b"; -@fa-var-mars-stroke-v: "\f22a"; -@fa-var-maxcdn: "\f136"; -@fa-var-meanpath: "\f20c"; -@fa-var-medium: "\f23a"; -@fa-var-medkit: "\f0fa"; -@fa-var-meh-o: "\f11a"; -@fa-var-mercury: "\f223"; -@fa-var-microphone: "\f130"; -@fa-var-microphone-slash: "\f131"; -@fa-var-minus: "\f068"; -@fa-var-minus-circle: "\f056"; -@fa-var-minus-square: "\f146"; -@fa-var-minus-square-o: "\f147"; -@fa-var-mobile: "\f10b"; -@fa-var-mobile-phone: "\f10b"; -@fa-var-money: "\f0d6"; -@fa-var-moon-o: "\f186"; -@fa-var-mortar-board: "\f19d"; -@fa-var-motorcycle: "\f21c"; -@fa-var-music: "\f001"; -@fa-var-navicon: "\f0c9"; -@fa-var-neuter: "\f22c"; -@fa-var-newspaper-o: "\f1ea"; -@fa-var-openid: "\f19b"; -@fa-var-outdent: "\f03b"; -@fa-var-pagelines: "\f18c"; -@fa-var-paint-brush: "\f1fc"; -@fa-var-paper-plane: "\f1d8"; -@fa-var-paper-plane-o: "\f1d9"; -@fa-var-paperclip: "\f0c6"; -@fa-var-paragraph: "\f1dd"; -@fa-var-paste: "\f0ea"; -@fa-var-pause: "\f04c"; -@fa-var-paw: "\f1b0"; -@fa-var-paypal: "\f1ed"; -@fa-var-pencil: "\f040"; -@fa-var-pencil-square: "\f14b"; -@fa-var-pencil-square-o: "\f044"; -@fa-var-phone: "\f095"; -@fa-var-phone-square: "\f098"; -@fa-var-photo: "\f03e"; -@fa-var-picture-o: "\f03e"; -@fa-var-pie-chart: "\f200"; -@fa-var-pied-piper: "\f1a7"; -@fa-var-pied-piper-alt: "\f1a8"; -@fa-var-pinterest: "\f0d2"; -@fa-var-pinterest-p: "\f231"; -@fa-var-pinterest-square: "\f0d3"; -@fa-var-plane: "\f072"; -@fa-var-play: "\f04b"; -@fa-var-play-circle: "\f144"; -@fa-var-play-circle-o: "\f01d"; -@fa-var-plug: "\f1e6"; -@fa-var-plus: "\f067"; -@fa-var-plus-circle: "\f055"; -@fa-var-plus-square: "\f0fe"; -@fa-var-plus-square-o: "\f196"; -@fa-var-power-off: "\f011"; -@fa-var-print: "\f02f"; -@fa-var-puzzle-piece: "\f12e"; -@fa-var-qq: "\f1d6"; -@fa-var-qrcode: "\f029"; -@fa-var-question: "\f128"; -@fa-var-question-circle: "\f059"; -@fa-var-quote-left: "\f10d"; -@fa-var-quote-right: "\f10e"; -@fa-var-ra: "\f1d0"; -@fa-var-random: "\f074"; -@fa-var-rebel: "\f1d0"; -@fa-var-recycle: "\f1b8"; -@fa-var-reddit: "\f1a1"; -@fa-var-reddit-square: "\f1a2"; -@fa-var-refresh: "\f021"; -@fa-var-remove: "\f00d"; -@fa-var-renren: "\f18b"; -@fa-var-reorder: "\f0c9"; -@fa-var-repeat: "\f01e"; -@fa-var-reply: "\f112"; -@fa-var-reply-all: "\f122"; -@fa-var-retweet: "\f079"; -@fa-var-rmb: "\f157"; -@fa-var-road: "\f018"; -@fa-var-rocket: "\f135"; -@fa-var-rotate-left: "\f0e2"; -@fa-var-rotate-right: "\f01e"; -@fa-var-rouble: "\f158"; -@fa-var-rss: "\f09e"; -@fa-var-rss-square: "\f143"; -@fa-var-rub: "\f158"; -@fa-var-ruble: "\f158"; -@fa-var-rupee: "\f156"; -@fa-var-save: "\f0c7"; -@fa-var-scissors: "\f0c4"; -@fa-var-search: "\f002"; -@fa-var-search-minus: "\f010"; -@fa-var-search-plus: "\f00e"; -@fa-var-sellsy: "\f213"; -@fa-var-send: "\f1d8"; -@fa-var-send-o: "\f1d9"; -@fa-var-server: "\f233"; -@fa-var-share: "\f064"; -@fa-var-share-alt: "\f1e0"; -@fa-var-share-alt-square: "\f1e1"; -@fa-var-share-square: "\f14d"; -@fa-var-share-square-o: "\f045"; -@fa-var-shekel: "\f20b"; -@fa-var-sheqel: "\f20b"; -@fa-var-shield: "\f132"; -@fa-var-ship: "\f21a"; -@fa-var-shirtsinbulk: "\f214"; -@fa-var-shopping-cart: "\f07a"; -@fa-var-sign-in: "\f090"; -@fa-var-sign-out: "\f08b"; -@fa-var-signal: "\f012"; -@fa-var-simplybuilt: "\f215"; -@fa-var-sitemap: "\f0e8"; -@fa-var-skyatlas: "\f216"; -@fa-var-skype: "\f17e"; -@fa-var-slack: "\f198"; -@fa-var-sliders: "\f1de"; -@fa-var-slideshare: "\f1e7"; -@fa-var-smile-o: "\f118"; -@fa-var-soccer-ball-o: "\f1e3"; -@fa-var-sort: "\f0dc"; -@fa-var-sort-alpha-asc: "\f15d"; -@fa-var-sort-alpha-desc: "\f15e"; -@fa-var-sort-amount-asc: "\f160"; -@fa-var-sort-amount-desc: "\f161"; -@fa-var-sort-asc: "\f0de"; -@fa-var-sort-desc: "\f0dd"; -@fa-var-sort-down: "\f0dd"; -@fa-var-sort-numeric-asc: "\f162"; -@fa-var-sort-numeric-desc: "\f163"; -@fa-var-sort-up: "\f0de"; -@fa-var-soundcloud: "\f1be"; -@fa-var-space-shuttle: "\f197"; -@fa-var-spinner: "\f110"; -@fa-var-spoon: "\f1b1"; -@fa-var-spotify: "\f1bc"; -@fa-var-square: "\f0c8"; -@fa-var-square-o: "\f096"; -@fa-var-stack-exchange: "\f18d"; -@fa-var-stack-overflow: "\f16c"; -@fa-var-star: "\f005"; -@fa-var-star-half: "\f089"; -@fa-var-star-half-empty: "\f123"; -@fa-var-star-half-full: "\f123"; -@fa-var-star-half-o: "\f123"; -@fa-var-star-o: "\f006"; -@fa-var-steam: "\f1b6"; -@fa-var-steam-square: "\f1b7"; -@fa-var-step-backward: "\f048"; -@fa-var-step-forward: "\f051"; -@fa-var-stethoscope: "\f0f1"; -@fa-var-stop: "\f04d"; -@fa-var-street-view: "\f21d"; -@fa-var-strikethrough: "\f0cc"; -@fa-var-stumbleupon: "\f1a4"; -@fa-var-stumbleupon-circle: "\f1a3"; -@fa-var-subscript: "\f12c"; -@fa-var-subway: "\f239"; -@fa-var-suitcase: "\f0f2"; -@fa-var-sun-o: "\f185"; -@fa-var-superscript: "\f12b"; -@fa-var-support: "\f1cd"; -@fa-var-table: "\f0ce"; -@fa-var-tablet: "\f10a"; -@fa-var-tachometer: "\f0e4"; -@fa-var-tag: "\f02b"; -@fa-var-tags: "\f02c"; -@fa-var-tasks: "\f0ae"; -@fa-var-taxi: "\f1ba"; -@fa-var-tencent-weibo: "\f1d5"; -@fa-var-terminal: "\f120"; -@fa-var-text-height: "\f034"; -@fa-var-text-width: "\f035"; -@fa-var-th: "\f00a"; -@fa-var-th-large: "\f009"; -@fa-var-th-list: "\f00b"; -@fa-var-thumb-tack: "\f08d"; -@fa-var-thumbs-down: "\f165"; -@fa-var-thumbs-o-down: "\f088"; -@fa-var-thumbs-o-up: "\f087"; -@fa-var-thumbs-up: "\f164"; -@fa-var-ticket: "\f145"; -@fa-var-times: "\f00d"; -@fa-var-times-circle: "\f057"; -@fa-var-times-circle-o: "\f05c"; -@fa-var-tint: "\f043"; -@fa-var-toggle-down: "\f150"; -@fa-var-toggle-left: "\f191"; -@fa-var-toggle-off: "\f204"; -@fa-var-toggle-on: "\f205"; -@fa-var-toggle-right: "\f152"; -@fa-var-toggle-up: "\f151"; -@fa-var-train: "\f238"; -@fa-var-transgender: "\f224"; -@fa-var-transgender-alt: "\f225"; -@fa-var-trash: "\f1f8"; -@fa-var-trash-o: "\f014"; -@fa-var-tree: "\f1bb"; -@fa-var-trello: "\f181"; -@fa-var-trophy: "\f091"; -@fa-var-truck: "\f0d1"; -@fa-var-try: "\f195"; -@fa-var-tty: "\f1e4"; -@fa-var-tumblr: "\f173"; -@fa-var-tumblr-square: "\f174"; -@fa-var-turkish-lira: "\f195"; -@fa-var-twitch: "\f1e8"; -@fa-var-twitter: "\f099"; -@fa-var-twitter-square: "\f081"; -@fa-var-umbrella: "\f0e9"; -@fa-var-underline: "\f0cd"; -@fa-var-undo: "\f0e2"; -@fa-var-university: "\f19c"; -@fa-var-unlink: "\f127"; -@fa-var-unlock: "\f09c"; -@fa-var-unlock-alt: "\f13e"; -@fa-var-unsorted: "\f0dc"; -@fa-var-upload: "\f093"; -@fa-var-usd: "\f155"; -@fa-var-user: "\f007"; -@fa-var-user-md: "\f0f0"; -@fa-var-user-plus: "\f234"; -@fa-var-user-secret: "\f21b"; -@fa-var-user-times: "\f235"; -@fa-var-users: "\f0c0"; -@fa-var-venus: "\f221"; -@fa-var-venus-double: "\f226"; -@fa-var-venus-mars: "\f228"; -@fa-var-viacoin: "\f237"; -@fa-var-video-camera: "\f03d"; -@fa-var-vimeo-square: "\f194"; -@fa-var-vine: "\f1ca"; -@fa-var-vk: "\f189"; -@fa-var-volume-down: "\f027"; -@fa-var-volume-off: "\f026"; -@fa-var-volume-up: "\f028"; -@fa-var-warning: "\f071"; -@fa-var-wechat: "\f1d7"; -@fa-var-weibo: "\f18a"; -@fa-var-weixin: "\f1d7"; -@fa-var-whatsapp: "\f232"; -@fa-var-wheelchair: "\f193"; -@fa-var-wifi: "\f1eb"; -@fa-var-windows: "\f17a"; -@fa-var-won: "\f159"; -@fa-var-wordpress: "\f19a"; -@fa-var-wrench: "\f0ad"; -@fa-var-xing: "\f168"; -@fa-var-xing-square: "\f169"; -@fa-var-yahoo: "\f19e"; -@fa-var-yelp: "\f1e9"; -@fa-var-yen: "\f157"; -@fa-var-youtube: "\f167"; -@fa-var-youtube-play: "\f16a"; -@fa-var-youtube-square: "\f166"; - diff --git a/src/UI/Content/Images/background/logo.png b/src/UI/Content/Images/background/logo.png deleted file mode 100644 index dbbe0448f147cc6d2526eda28febd925ee585a3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15293 zcmX|o2{=^$_y7COY8Z@tXAIewv1DJyz9u0gY3!6wp@m9ZOB#jjRAMS5OH_!6u|`p% zMM_5YLX<2K|LOaCp8q`0%slt?oO9mieedg>d+xdKcZ#!<4JW%OI{<*w&eqZe00jK+ zi$b!HoyoTZ)}OGmqnp+A^fUxPtfLbrPF%l!T}4I3$;pWYW;tnVvrx9vE>5m^JixO) z*le`-8~}!nc9v#t(W46=j~u&1CUmb@2rTJz<)7mWK%#YJ*Y|und8)KD!SjA_me$}d zZ>*z9YQb)Et|t9@?(3r0vsE{V)PtR7rTk|08jMF@^CbiG?6HIFY96QM4xpAD)QfH- z=+zsp-003Fc<bX;-OERy^-iHv_-2Hlv(l+2=Mg7jCUDKr1NvG)^IOhZaCqWgEeE~5 zl<T=2mhm`S_iM^$$y(~s<?32h=9_Pvt?6P&$?7j779;$F;huBSVc2M$*!at}67*Y} z-4Y!^FyE`I=c`YkqkXsi)FyS{WaUfVJ)EHCPd9%U)cExnq-!3<*oyrzBz9YiIJuTp zQY_R0aYl({uUgNs_bzv4lw9N8Pq`wMCwo!28R10C;XARWv#Y>Mg>FmB&wo#qVWO-Z z(ELSarVNC=zWltpzb*rp(<4M*#>XO${qZe_UnDre7?({x=RX%;V#5*lBakd<Aqukc zHp1TY<T)T3qAKA0Cw>qbWGH)3=H=aWh#yYIK=!6CzaX<PD$-mk9R1MSVaW7*0ereG z0-3WjW``+cgpEMQe6US0=GaS&!rv#q5)HQI_X*JMfD>VDjIMjfqpEPdPdVp`a-PbE ze2Ce7_q|`$SVW4yv&nUK?9yMYcO`9wo!#zOLTp7gj{0_2-5u>c|0)|RJg@XV=HX5p z^wwL}h{|TF?FnQA-y$8|U;J7?TAvr0f`0!UqylSYTjU+asOuMm^&vk&LhQHfO*oTX z30m^__Dh>f_>&>81S6o-bXu4ilW)dFK)){z!;@QiU15dV9iv?i;Zh<bT*r7FnA}%c zaX#~hjfWp6CGe@zP4VZ#A8z{1>|Ird1(wTrD$DKrk#gUvyIa!4X4F@53b1cKbcQ{; z-&bjC@7R*y7cYO~!pQc>w1Ry<LhjE*XB{Xk|2gT<kH9cH-*PQ4zquHbBom`RJj&E) z{dzv{S`SoKrg8Sm>oMU8q{BbU^UbY?E04%UN;30FndS{$f8b}|z9~GYSHO9{PX}IO zcVyn<c+4&`j0M_0Er_JDU5{WaR%$%aFEMY7<)_|>T50w%A~mI4N1eXb>MPmG(l?ec zP2#q0L^&Z}6s8b<?wNu3N;jJ7C-yAw6Ey-aq@#~MbTJ?$1)$DRayJA#q?NB>WaP(T zu^)p|=dP;1GLVizF^kfrbe=!Z=8UY*#c;!yqC(W>8!C97|E%Ka`27PZS0^P{!`FAX zt^GGB*t-0DfBo>2-?QxvGN=^ZY}ok7_*YGBt^HW5�j16Rx|pb=L+~4Aa*hl8!kp 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 diff --git a/src/UI/Content/Images/cover-dark.png b/src/UI/Content/Images/cover-dark.png deleted file mode 100644 index b8924fb6ed2cbb53754dc0423df0715d367e9b69..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1797 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 diff --git a/src/UI/Content/Images/favicon-debug.ico b/src/UI/Content/Images/favicon-debug.ico deleted file mode 100644 index 0dfc41a11aa8d3b5c8a4790482b8855f1e24a078..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16958 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 diff --git a/src/UI/Content/Images/logos/128.png b/src/UI/Content/Images/logos/128.png deleted file mode 100644 index 7f0f927d1fac1090ab27f5dd09192b281fe32018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5799 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 G;{O5F@3O=I diff --git a/src/UI/Content/Images/logos/48.png b/src/UI/Content/Images/logos/48.png deleted file mode 100644 index 12d6fee534ad3aca72d6eab00bef52624c56bbc6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2223 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 PwB!KA#>u+T(kJ!5MVjyY diff --git a/src/UI/Content/Images/logos/64.png b/src/UI/Content/Images/logos/64.png deleted file mode 100644 index 49234e167db2e8274912ab79098698298c18f302..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2798 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 diff --git a/src/UI/Content/Images/safari/logo.svg b/src/UI/Content/Images/safari/logo.svg deleted file mode 100644 index d3eece392..000000000 --- a/src/UI/Content/Images/safari/logo.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" width="218px" height="218px" viewBox="0 0 218 218" enable-background="new 0 0 218 218" xml:space="preserve"><g display="none"/><g display="none"><path display="inline" fill-rule="evenodd" clip-rule="evenodd" fill="#EFEEEE" d="M217.5 108.95c0 29.833-10.533 55.399-31.6 76.7 -0.7 0.833-1.484 1.6-2.351 2.3 -3.466 3.399-7.134 6.483-11 9.25 -18.267 13.467-39.366 20.2-63.3 20.2 -23.967 0-45.033-6.733-63.2-20.2 -4.8-3.4-9.3-7.25-13.5-11.55 -16.367-16.267-26.417-35.167-30.15-56.7 -0.733-4.2-1.217-8.467-1.45-12.8 -0.1-2.4-0.15-4.801-0.15-7.2 0-2.534 0.05-4.95 0.15-7.25 0-0.233 0.066-0.467 0.2-0.7 1.567-26.6 12.033-49.583 31.4-68.95C53.85 11.017 79.417 0.5 109.25 0.5c29.934 0 55.483 10.517 76.65 31.55C206.967 53.483 217.5 79.117 217.5 108.95z"/></g><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="134.724 60.365 129.7 63.282 129.7 69.117 134.724 72.034 139.802 69.116 139.802 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.157 74.994 142.869 71.227 140.087 69.611 135.008 72.529 135.008 78.362 140.087 81.28 143.517 79.289 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 60.366 140.373 63.282 140.373 69.117 143.283 70.807 150.418 63.566 150.418 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="129.415 69.611 124.392 72.528 124.392 78.363 129.415 81.28 134.438 78.363 134.438 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="124.106 60.366 119.084 63.282 119.084 69.117 124.106 72.034 129.129 69.117 129.129 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="113.49 60.366 108.468 63.282 108.468 69.117 113.49 72.034 118.513 69.117 118.513 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M113.49 78.857l-0.479 0.278c0.423 0.05 0.843 0.109 1.259 0.176L113.49 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M123.821 72.528l-5.022-2.917 -5.023 2.917v5.835l2.188 1.27c-0.001 0-0.003 0-0.004 0 1.372 0.303 2.705 0.701 3.998 1.195 -0.076-0.029-0.152-0.059-0.229-0.087l4.093-2.377V72.528z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 51.175 145.681 54.037 145.681 59.871 150.703 62.787 151.831 62.132 155.726 58.18 155.726 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="182.551 32.684 177.528 35.6 177.528 40.433 184.392 33.753 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="163.429 163.159 166.342 166.172 166.342 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="182.551 51.175 177.528 54.037 177.528 59.871 178.193 60.257 185.561 52.89 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M175 157.7c-5.063-6.93-8.862-14.367-11.396-22.313 0.053 0.166 0.105 0.33 0.159 0.495l-2.159 1.254v5.836l5.022 2.916 0.941-0.546c0.083 0.172 0.166 0.343 0.25 0.515l-0.906 0.525v5.836l5.023 2.916 0.91-0.528c0.105 0.159 0.211 0.316 0.317 0.476l-0.942 0.547v5.834l5.022 2.862 2.8-1.596L175 157.7z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.681 151.549 145.681 152.219 147.293 153.155 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="157.737 153.715 159.54 155.54 157.05 158 157.769 157.305 161.034 160.683 161.034 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="164.581 53.033 161.319 51.175 156.297 54.037 156.297 57.981 157.888 59.548 157.901 59.535 158.597 60.247 157.902 59.533 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="158.808 42.894 161.319 44.352 166.342 41.436 166.342 35.6 166.202 35.519 166.62 35.102 166.627 35.106 171.65 32.19 171.65 30.085 172.173 29.564 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="186.188 34.796 178.68 42.104 182.551 44.352 187.573 41.436 187.573 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 32.684 166.912 35.6 166.912 41.436 171.936 44.352 175.817 42.098 176.958 40.988 176.958 35.6 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M113.205 72.528l-5.022-2.917 -5.022 2.917v5.835l1.447 0.84c-0.001 0-0.002 0-0.002 0 1.457-0.202 2.955-0.304 4.495-0.304 1.005 0 1.991 0.045 2.961 0.13 -0.002 0-0.003 0-0.004 0l1.148-0.667V72.528z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 97.292 66.004 100.209 66.004 106.044 71.026 108.961 76.049 106.044 76.049 100.209 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 106.539 60.639 109.457 60.639 115.29 65.718 118.208 70.741 115.291 70.741 109.456 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 115.785 66.004 118.702 66.004 124.535 71.026 127.397 76.049 124.535 76.049 118.702 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M79.5 114.5c-0.2-1.166-0.333-2.35-0.4-3.55 -0.033-0.667-0.05-1.333-0.05-2 0-0.456 0.019-0.875 0.033-1.302 -0.004 0.159-0.009 0.318-0.012 0.479l-2.735-1.588 -5.023 2.917v5.835l5.023 2.916 3.52-2.043c0.001 0.006 0.002 0.011 0.004 0.017C79.728 115.625 79.599 115.069 79.5 114.5z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 125.029 60.639 127.893 60.639 133.725 65.718 136.643 70.741 133.726 70.741 127.892 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M81.174 97.564l-4.554 2.645v5.835l2.467 1.433c0.006-0.171 0.004-0.36 0.013-0.527 0-0.067 0.017-0.134 0.05-0.2 0.192-3.263 0.87-6.329 2.03-9.199C81.178 97.556 81.176 97.56 81.174 97.564z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 134.221 66.004 137.137 66.004 142.973 69.161 144.806 74.729 139.022 76.049 140.294 76.049 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 88.102 60.639 90.965 60.639 96.797 65.718 99.715 70.741 96.798 70.741 90.964 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 78.857l-5.023 2.917v3.562c-0.007-0.021-0.014-0.043-0.021-0.064 0.298 0.951 0.577 1.909 0.838 2.874 -0.008-0.028-0.015-0.056-0.022-0.084l4.229 2.41 5.079-2.864v-5.832L60.354 78.857z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.802 137.138 134.724 134.22 129.7 137.137 129.7 142.973 134.724 145.89 138.07 143.967 136.979 142.88 139.802 140.048 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 125.03 71.312 127.892 71.312 133.726 76.335 136.643 81.357 133.726 81.357 127.892 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="102.875 60.366 97.852 63.282 97.852 69.117 102.875 72.034 107.897 69.117 107.897 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.068 78.363v-5.835l-5.023-2.917 -5.022 2.917v0.058c-0.099-0.188-0.199-0.374-0.299-0.561l0.014 0.008 5.022-2.917v-5.835l-5.022-2.916 -5.022 2.916v0.68c-0.121-0.171-0.243-0.342-0.365-0.512 4.843 6.734 8.48 13.96 10.913 21.677 -0.163-0.517-0.329-1.032-0.503-1.545v-1.809l-0.815-0.473c-0.106-0.284-0.214-0.566-0.324-0.849l1.425 0.828L60.068 78.363z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="61.124 153.154 60.354 152.712 55.331 155.629 55.331 161.463 56.063 161.88 59.78 158.156 57.979 156.422 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M99.735 80.313c0.003-0.001 0.007-0.002 0.011-0.004C99.743 80.311 99.739 80.312 99.735 80.313z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M103.72 79.348l-0.845-0.491 -1.861 1.081c0.906-0.244 1.83-0.444 2.771-0.601C103.763 79.341 103.742 79.345 103.72 79.348z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M97.566 69.611l-5.022 2.917v5.835l4.844 2.813c-0.076 0.032-0.15 0.067-0.226 0.1 0.439-0.189 0.884-0.368 1.332-0.534l4.095-2.378v-5.835L97.566 69.611z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="74.331 80.776 74.129 80.979 73.361 80.213 71.026 78.857 66.004 81.774 66.004 87.608 71.026 90.47 76.049 87.608 76.049 81.774 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 161.957 39.407 164.819 39.407 170.653 44.407 173.558 49.452 168.503 49.452 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 47.496 44.715 50.68 49.738 53.542 50.399 53.166 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.155 41.949 39.122 41.93 34.1 44.846 34.1 50.68 39.122 53.542 44.145 50.68 44.145 46.926 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="187.859 41.929 182.837 44.846 182.837 50.68 185.979 52.471 185.742 52.708 192.939 45.511 192.939 44.847 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="161.319 143.467 156.297 146.383 156.297 152.219 161.319 155.135 166.342 152.219 166.342 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.627 152.713 161.604 155.629 161.604 161.272 162.015 161.697 166.627 164.325 171.65 161.463 171.65 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 78.857 144.886 79.153 144.144 79.907 143.938 79.704 140.373 81.774 140.373 87.608 145.396 90.47 150.418 87.608 150.418 81.774 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 143.467 148.671 144.646 155.521 151.471 153.756 153.241 153.762 153.247 155.54 151.49 155.726 151.678 155.726 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 161.957 166.912 164.819 166.912 166.762 172.934 172.99 176.958 170.653 176.958 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="159.313 60.952 160.967 62.583 161.319 62.787 166.342 59.871 166.342 54.111 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.012 78.857 150.989 81.774 150.989 87.608 156.012 90.47 161.034 87.608 161.034 81.774 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M132.188 89.683c-0.002-0.003-0.005-0.005-0.007-0.008 0.826 0.988 1.577 2.005 2.256 3.052v-1.762L132.188 89.683z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 88.102 145.681 90.964 145.681 96.798 150.703 99.715 155.726 96.798 155.726 90.964 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="139.004 106.502 139.005 106.513 139.004 106.502 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M139.802 100.21l-2.347-1.349c-0.001-0.003-0.002-0.006-0.004-0.01 0.844 2.42 1.357 4.972 1.553 7.651l0.798-0.458V100.21zM140.087 106.539l-1.04 0.598c-0.012-0.209-0.027-0.416-0.042-0.623 0.058 0.802 0.095 1.612 0.095 2.437 0 3.003-0.39 5.849-1.16 8.539 0.041-0.148 0.08-0.297 0.12-0.446l2.027 1.165 5.023-2.917v-5.835L140.087 106.539z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 180.395 39.407 183.311 39.407 189.146 44.43 192.063 48.943 189.441 47.571 190.817 49.452 188.932 49.452 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M187.999 183.478c0.021-0.022 0.042-0.044 0.063-0.066C188.041 183.434 188.021 183.455 187.999 183.478z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 125.03 145.681 127.893 145.681 133.726 150.703 136.643 155.726 133.726 155.726 127.893 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.912 169.64 166.912 170.653 169.146 171.95 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M180.216 162.902l0.246 0.246 -2.934 1.671v5.834l5.022 2.917 5.022-2.917v-0.412l1.755 1.75 -1.469-0.844 -5.022 2.917v5.835l5.023 2.917 1.595-0.916c1.2-1.314 2.361-2.642 3.484-3.981v-2.326L180.216 162.902z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M188.145 183.311v0.011c0.008-0.008 0.015-0.016 0.021-0.023L188.145 183.311z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="63.821 70.701 60.639 72.529 60.639 78.362 65.718 81.28 70.741 78.363 70.741 77.601 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 51.174 60.639 54.038 60.639 59.871 60.789 59.957 61.871 58.872 65.769 62.758 70.741 59.871 70.741 54.037 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M31.319 34.132l-1.746 1.014c-0.264 0.283-0.522 0.569-0.782 0.854v5.436l5.023 2.917 4.898-2.845L31.319 34.132z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 192.557 44.715 193.681 47.379 191.01 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.012 134.221 150.989 137.137 150.989 142.973 156.012 145.889 161.034 142.973 161.034 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="167.349 53.131 171.65 50.68 171.65 48.944 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="172.221 44.846 172.221 45.599 174.137 43.733 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="160.711 63.094 153.397 70.516 156.012 72.034 161.034 69.117 161.034 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="178.255 42.517 172.221 48.39 172.221 50.68 177.243 53.542 182.266 50.68 182.266 44.846 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 51.175 166.912 54.037 166.912 59.871 171.936 62.787 176.958 59.871 176.958 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="166.627 41.93 161.604 44.847 161.604 50.68 165.007 52.619 171.65 46.153 171.65 44.846 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M134.724 78.857l-5.023 2.917v5.258c-0.003-0.003-0.006-0.005-0.009-0.008 0.22 0.207 0.441 0.41 0.658 0.625 0.23 0.235 0.449 0.474 0.671 0.713 -0.002-0.002-0.003-0.003-0.005-0.005l3.708 2.113 5.078-2.864v-5.832L134.724 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M124.106 78.857l-3.693 2.145c3.143 1.263 6.049 3.093 8.716 5.496v-4.724L124.106 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M133.052 127.103c0.055-0.071 0.109-0.143 0.164-0.214 -0.847 1.12-1.78 2.206-2.811 3.252l0.002 0.002 -0.058 0.058c-0.199 0.233-0.416 0.45-0.649 0.649l-0.707 0.707 -0.037-0.037c-0.744 0.669-1.506 1.306-2.306 1.881 -0.504 0.371-1.021 0.71-1.54 1.044 0.079-0.056 0.159-0.109 0.239-0.162l4.065 2.36 5.022-2.917v-5.833L133.052 127.103z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="152.983 70.936 145.681 78.347 145.681 78.363 150.703 81.28 155.726 78.363 155.726 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="135.008 93.631 135.008 93.631 135.007 93.629 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M140.087 88.102l-5.079 2.864v2.666c0.859 1.437 1.576 2.931 2.164 4.477 -0.011-0.028-0.021-0.057-0.031-0.085l2.945 1.693 5.024-2.917v-5.834L140.087 88.102z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 97.292 140.373 100.209 140.373 106.044 145.396 108.961 150.418 106.044 150.418 100.209 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M137.906 117.613c-0.944 3.243-2.448 6.259-4.515 9.046 0.004-0.005 0.007-0.009 0.011-0.014l1.321 0.753 5.078-2.863v-5.833L137.906 117.613z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="150.703 106.539 145.681 109.456 145.681 115.291 150.703 118.207 155.726 115.291 155.726 109.456 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 115.785 140.373 118.702 140.373 124.535 145.396 127.397 150.418 124.535 150.418 118.702 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="140.087 125.029 135.008 127.893 135.008 133.725 140.087 136.643 145.11 133.726 145.11 127.892 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M159.8 110c0-1.027 0.018-2.046 0.051-3.06 -0.005 0.153-0.011 0.306-0.015 0.46l-3.539 2.055v5.835l3.809 2.211c0.019 0.232 0.039 0.464 0.06 0.695l-4.153-2.412 -5.022 2.917v5.833l5.022 2.862 4.976-2.836c0.034 0.199 0.068 0.4 0.104 0.599l-4.794 2.731v5.834l5.022 2.917 2.266-1.315c0.004 0.013 0.008 0.025 0.012 0.038C161.065 127.418 159.8 118.963 159.8 110z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M169.602 70.966l-2.689 1.562v3.698c-0.068 0.148-0.133 0.297-0.2 0.445 0.96-2.113 2.02-4.185 3.18-6.213C169.795 70.628 169.697 70.796 169.602 70.966zM166.658 76.79c0.018-0.04 0.035-0.08 0.054-0.119C166.693 76.71 166.676 76.75 166.658 76.79zM166.658 76.79c-0.105 0.233-0.213 0.467-0.316 0.702v-4.964l-5.022-2.917 -5.022 2.917v5.835l5.022 2.917 4.523-2.627c-0.121 0.288-0.24 0.579-0.358 0.868l-3.88 2.253v5.834l0.979 0.558c-0.051 0.19-0.102 0.38-0.151 0.571l-1.113-0.635 -5.022 2.862v5.834l4.146 2.408c-0.024 0.206-0.049 0.411-0.073 0.617l-4.358-2.531 -5.022 2.917v5.835l5.022 2.917 3.846-2.233c-0.002 0.037-0.002 0.073-0.004 0.11C160.222 96.019 162.491 86.003 166.658 76.79z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M172.221 63.282v3.363c-0.192 0.296-0.383 0.591-0.57 0.889v-4.252l-5.023-2.916 -5.022 2.916v5.835l5.022 2.917 3.539-2.055c-0.059 0.102-0.116 0.205-0.175 0.307 0.953-1.657 1.972-3.285 3.059-4.885l4.726-4.726 -0.532-0.309L172.221 63.282z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="145.396 134.221 140.373 137.137 140.373 139.475 141.921 137.921 148.253 144.229 150.418 142.973 150.418 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="86.951 69.611 81.928 72.528 81.928 78.363 86.951 81.28 91.973 78.363 91.973 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="172.221 175.131 172.221 179.899 177.243 182.815 178.785 181.92 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="177.243 171.147 173.342 173.413 180.566 180.886 182.266 179.899 182.266 174.064 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="170.6 173.455 166.627 171.147 161.604 174.064 161.604 179.899 166.627 182.815 171.65 179.899 171.65 174.541 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="160.136 162.632 156.297 164.819 156.297 170.653 161.319 173.57 166.342 170.653 166.342 169.05 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="156.338 158.703 154.56 160.46 150.989 156.844 150.989 161.463 156.012 164.325 159.727 162.208 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M195.45 43c-2.051-2.664-4.251-5.266-6.585-7.813l-0.721 0.414v5.833l5.079 2.918 2.084-1.21 -0.755 0.755L195.45 43z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M158.39 43.311l0.146-0.146L158.1 43.6c-13.934 10.733-30.133 16.1-48.6 16.1 -2.231 0-4.431-0.08-6.598-0.238 0.086 0.006 0.172 0.013 0.258 0.019v0.39l5.022 2.916 5.022-2.916v-0.244c0.19-0.008 0.381-0.016 0.57-0.024v0.269l5.023 2.916 5.022-2.916V58.55c0.19-0.032 0.38-0.064 0.57-0.097v1.418l5.023 2.916 5.022-2.916v-3.853c0.189-0.06 0.381-0.121 0.57-0.182v4.034l5.079 2.917 5.023-2.917v-5.834l-2.209-1.258c0.213-0.097 0.426-0.195 0.638-0.293l1.856 1.058 5.022-2.862v-1.843c0.19-0.114 0.381-0.229 0.571-0.345v2.188l5.022 2.862 5.022-2.862v-5.834L158.39 43.311z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M177.4 24.35l-5.18 5.166v2.674l5.022 2.916 5.022-2.916v-3.614C180.664 27.105 179.044 25.692 177.4 24.35z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M61.9 44.35l-2.099-2.099 -4.47 2.595v5.834l5.023 2.863 5.079-2.864v-3.851c0.19 0.125 0.38 0.25 0.571 0.375v3.477l5.022 2.862 3.063-1.745c0.207 0.102 0.415 0.202 0.623 0.302l-3.4 1.938v5.834l5.023 2.916 5.022-2.916v-4.97c0.189 0.069 0.38 0.139 0.571 0.207v4.763l5.022 2.916 5.022-2.916v-1.935c0.191 0.04 0.379 0.079 0.571 0.117v1.818l5.022 2.916 5.023-2.916v-0.432c0.084 0.006 0.168 0.012 0.252 0.019C87.614 58.334 73.967 53.298 61.9 44.35z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="193.51 44.846 193.51 44.94 193.733 44.717 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M185.9 32.05c-1.012-1.011-2.034-1.986-3.063-2.943v3.083l1.979 1.15 1.086-1.057 1.395 1.434 -0.684 0.666 1.246 0.724 0.615-0.354C187.635 33.844 186.777 32.943 185.9 32.05z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="108.183 143.467 103.16 146.383 103.16 152.219 108.183 155.135 113.205 152.219 113.205 146.383 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M109.1 139c-0.341 0-0.671-0.026-1.008-0.036 0.125 0.005 0.25 0.011 0.376 0.014v3.995l5.022 2.916 5.022-2.916v-5.391c0.016-0.005 0.03-0.012 0.046-0.017C115.605 138.52 112.453 139 109.1 139z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="118.799 143.467 113.775 146.383 113.775 152.219 118.799 155.135 123.821 152.219 123.821 146.383 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M98.096 136.994l-0.244 0.143v5.836l5.023 2.916 5.022-2.916v-4.016C104.402 138.835 101.133 138.185 98.096 136.994z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="134.724 152.712 129.7 155.629 129.7 161.463 134.724 164.326 139.802 161.462 139.802 155.63 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="138.489 144.384 135.008 146.384 135.008 152.218 140.087 155.135 145.11 152.219 145.11 150.98 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M124.824 134.638c-1.817 1.14-3.734 2.043-5.74 2.735v5.6l5.022 2.916 5.022-2.916v-5.836L124.824 134.638z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="148.88 154.736 145.396 152.713 140.373 155.629 140.373 161.463 145.396 164.325 150.418 161.463 150.418 156.269 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="129.415 143.467 124.392 146.383 124.392 152.219 129.415 155.135 134.438 152.219 134.438 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="52.001 34.452 50.023 35.6 50.023 41.436 55.045 44.352 59.383 41.833 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="161.319 180.395 161.208 180.458 166.342 185.592 166.342 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M156.012 171.147l-3.688 2.143c-0.181-0.115-0.363-0.23-0.545-0.345l3.947-2.292v-5.834l-5.022-2.862 -5.022 2.862v4.685c-0.189-0.096-0.38-0.191-0.57-0.285v-4.399l-5.023-2.862 -5.079 2.863v0.293c-0.189-0.061-0.381-0.122-0.57-0.182v-0.112l-5.022-2.862 -1.981 1.129c-0.28-0.06-0.563-0.118-0.844-0.176l2.539-1.447v-5.834l-5.022-2.916 -5.022 2.916v5.834l0.633 0.361c-0.011-0.001-0.022-0.002-0.033-0.004 14.299 1.64 27.104 6.815 38.416 15.529l2.691 2.691 0.243-0.142v-5.835L156.012 171.147z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M118.168 161.66l0.345-0.197v-5.834l-5.022-2.916 -5.022 2.916v5.627c-0.19 0.002-0.38 0.005-0.57 0.008v-5.635l-5.022-2.916 -5.023 2.916v5.834l0.776 0.442c-0.314 0.039-0.627 0.079-0.939 0.121l-0.122-0.069 -0.198 0.113c-0.245 0.034-0.488 0.073-0.732 0.109 4.166-0.617 8.453-0.93 12.864-0.93 3.433 0 6.786 0.187 10.062 0.558C119.099 161.755 118.634 161.705 118.168 161.66z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M67.759 172.57c0.415-0.252 0.832-0.499 1.25-0.742C68.591 172.071 68.174 172.318 67.759 172.57z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M65.4 174.046l-5.045-2.898 -5.023 2.917v5.835l1.981 1.15 -0.417 0.418 -1.85-1.073 -5.022 2.916v5.048l-0.326 0.327L61.8 176.55c1.433-1.054 2.893-2.04 4.369-2.984C65.914 173.729 65.654 173.879 65.4 174.046z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M182.551 180.395l-1.575 0.914 4.443 4.596 -1.438 1.391 -4.787-4.951 -1.666 0.967v5.836l2.934 1.703c1.044-0.94 2.076-1.901 3.088-2.899 0.866-0.7 1.65-1.467 2.351-2.3 0.567-0.574 1.121-1.152 1.673-1.731v-0.608L182.551 180.395z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M193.51 176.161v1.075c0.162-0.195 0.33-0.39 0.49-0.586L193.51 176.161z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M39.122 189.641l-1.48 0.859c1.818 1.626 3.683 3.185 5.608 4.65l0.895-0.896v-1.697L39.122 189.641z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M177.243 189.641l-4.336 2.517 2.743 2.743c1.486-1.16 2.94-2.38 4.368-3.649L177.243 189.641z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="97.566 143.467 92.544 146.383 92.544 152.219 97.566 155.135 102.589 152.219 102.589 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="171.936 180.395 166.912 183.311 166.912 186.162 172.49 191.74 176.958 189.146 176.958 183.311 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M41.55 24c-0.044 0.036-0.088 0.074-0.132 0.11l0.58 0.337L41.55 24z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M43.57 26.021L40.951 24.5c-2.322 1.917-4.606 3.939-6.852 6.068v1.622l5.022 2.916 5.022-2.916v-5.595L43.57 26.021z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.715 27.166 44.715 32.19 49.738 35.106 51.584 34.034 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M32.55 32.05c-0.536 0.536-1.055 1.081-1.578 1.624l0.37-0.215 1.064-1.067 0.285 0.284 0.838-0.486v-1.076C33.203 31.426 32.875 31.733 32.55 32.05z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M29.823 171.913L24.7 177.05c1.479 1.784 3.036 3.54 4.652 5.274l4.176-2.425v-5.835L29.823 171.913z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M37.988 182.818l-4.126 4.134c1.09 1.081 2.206 2.123 3.335 3.146l1.64-0.951v-5.836L37.988 182.818z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M33.814 180.395l-4.062 2.357c0.88 0.937 1.772 1.868 2.692 2.79l3.755-3.763L33.814 180.395z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M30.321 34.344c-0.134 0.141-0.268 0.282-0.4 0.423C30.054 34.625 30.188 34.485 30.321 34.344z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M28.22 36.623c-1.713 1.903-3.359 3.843-4.92 5.827l1.282 1.279 -0.115-0.115 3.753-2.179V36.623z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.026 60.366 66.188 63.175 73.583 70.549 76.049 69.117 76.049 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="55.045 161.957 50.023 164.819 50.023 167.931 55.645 162.299 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M46.365 154.153c-0.026 0.041-0.052 0.081-0.078 0.121C46.313 154.234 46.339 154.194 46.365 154.153zM55.045 136.643l5.023-2.917v-5.834l-2.624-1.495c0.009-0.049 0.018-0.098 0.027-0.146 -1.89 10.016-5.593 19.316-11.107 27.903 0.049-0.076 0.1-0.152 0.148-0.229l2.939-1.706v-3.293c0.193-0.357 0.384-0.717 0.571-1.077v4.37l5.022 2.916 5.023-2.916v-5.836l-5.023-2.916 -3.985 2.314c0.146-0.303 0.291-0.607 0.433-0.912l3.268-1.896v-5.836l-0.126-0.073c0.063-0.185 0.124-0.367 0.186-0.552L55.045 136.643zM38.019 163.696l1.104 0.629 5.022-2.862v-4.024c0.192-0.27 0.382-0.54 0.571-0.812v4.836l5.022 2.862 5.022-2.862v-5.834l-5.022-2.916 -3.911 2.271c0.155-0.236 0.307-0.473 0.459-0.709 -0.777 1.206-1.588 2.397-2.437 3.575L38.019 163.696z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.407 58.519 39.407 58.519 38.896 58.008 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 51.175 39.407 54.037 39.407 58.519 42.65 61.754 44.43 62.787 49.452 59.871 49.452 54.037 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="50.818 53.583 50.023 54.037 50.023 59.871 55.045 62.787 58.206 60.953 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="56.929 63.829 59.003 61.748 58.624 61.37 55.331 63.282 55.331 69.117 60.354 72.034 63.402 70.283 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.312 78.17 71.312 78.363 71.776 78.633 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="92.258 134.221 87.236 137.137 87.236 142.973 92.258 145.889 97.281 142.973 97.281 137.137 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 152.713 76.62 155.629 76.62 161.463 81.643 164.325 86.665 161.463 86.665 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="86.951 143.467 81.928 146.383 81.928 152.219 86.951 155.135 91.973 152.219 91.973 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="78.793 144.895 71.588 152.379 76.335 155.135 81.357 152.219 81.357 146.383 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="71.18 152.803 66.004 158.18 66.004 161.463 71.026 164.325 76.049 161.463 76.049 155.629 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 143.466 60.639 146.384 60.639 152.218 61.532 152.73 68.754 145.229 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="60.354 134.22 55.331 137.137 55.331 142.973 60.354 145.89 65.433 142.972 65.433 137.138 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 115.784l-1.648 0.958c0.002-0.028 0.004-0.057 0.006-0.085 -0.236 3.253-0.646 6.437-1.232 9.552 0.025-0.135 0.051-0.271 0.076-0.406l2.799 1.596 5.079-2.863v-5.833L60.354 115.784z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 134.221 76.62 137.137 76.62 140.844 79.771 143.878 79.201 144.471 81.643 145.889 86.665 142.973 86.665 137.137 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M92.258 78.857l-5.022 2.917v5.834l0.423 0.241c0.064-0.066 0.125-0.134 0.191-0.199 2.713-2.682 5.679-4.74 8.892-6.188L92.258 78.857z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M79.983 116.749l-3.363 1.953v5.833l5.022 2.862 2.593-1.478c-1.984-2.82-3.409-5.875-4.253-9.173C79.982 116.747 79.983 116.748 79.983 116.749zM84.235 125.92c0.068 0.099 0.142 0.194 0.212 0.292 -0.068-0.099-0.136-0.198-0.205-0.296L84.235 125.92zM91.6 133.4c-1.333-0.934-2.583-2-3.75-3.2 -1.278-1.269-2.402-2.603-3.402-3.988 0.041 0.059 0.08 0.117 0.122 0.175l-2.641 1.505v5.834l5.022 2.917 5.022-2.917v-0.071C91.85 133.566 91.722 133.491 91.6 133.4z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 78.857 76.62 81.774 76.62 87.608 81.643 90.47 86.665 87.608 86.665 81.774 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M97.281 155.629l-5.022-2.916 -5.022 2.916v5.834l3.271 1.864c-0.085 0.02-0.17 0.04-0.255 0.06 1.974-0.458 3.978-0.841 6.011-1.151 -0.151 0.022-0.303 0.044-0.454 0.067l1.473-0.84V155.629z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 88.102 71.312 90.964 71.312 96.798 76.335 99.715 81.357 96.798 81.357 90.964 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M86.951 88.102l-5.022 2.862v4.912c1.315-2.72 3.09-5.254 5.323-7.603L86.951 88.102z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="49.738 171.147 49.498 171.287 44.715 176.078 44.715 179.899 49.738 182.815 54.76 179.899 54.76 174.064 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="39.122 171.147 34.1 174.064 34.1 179.899 36.617 181.361 43.99 173.975 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="38.406 182.399 39.122 182.815 44.145 179.899 44.145 176.65 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="32.688 51.816 32.688 51.816 32.383 51.512 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="33.529 50.68 33.529 44.846 28.506 41.93 24.886 44.032 32.383 51.512 32.269 51.398 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="38.836 54.037 33.814 51.175 32.688 51.816 38.836 57.95 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="37.601 164.115 30.24 171.495 33.814 173.57 38.836 170.653 38.836 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="57.445 163.324 50.091 170.692 55.045 173.57 60.068 170.653 60.068 164.819 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="65.718 161.957 60.639 164.82 60.639 170.652 65.718 173.57 70.741 170.653 70.741 164.819 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M89.689 163.519l-2.739-1.562 -5.022 2.862v0.997c-0.191 0.067-0.38 0.136-0.571 0.205v-1.202l-5.022-2.862 -5.023 2.862v5.704c-0.228 0.123-0.451 0.259-0.678 0.384 6.138-3.381 12.626-5.884 19.474-7.486C89.968 163.453 89.829 163.485 89.689 163.519z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="63.021 161.277 60.5 158.85 61.208 159.556 57.863 162.906 60.354 164.326 65.433 161.462 65.433 158.772 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="81.643 60.366 76.62 63.282 76.62 69.117 81.643 72.034 86.665 69.117 86.665 63.282 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="76.335 69.611 74.001 70.966 79.071 76.021 74.748 80.358 76.335 81.28 81.357 78.363 81.357 72.528 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="92.258 60.366 87.236 63.282 87.236 69.117 92.258 72.034 97.281 69.117 97.281 63.282 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M96.715 136.402c-0.133-0.061-0.266-0.122-0.398-0.185C96.449 136.28 96.582 136.342 96.715 136.402z"/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M95.012 135.559c-0.099-0.054-0.195-0.111-0.292-0.166C94.817 135.447 94.914 135.505 95.012 135.559z"/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="44.43 32.684 39.407 35.6 39.407 39.376 44.348 44.305 44.43 44.352 49.452 41.436 49.452 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="33.814 32.684 33.109 33.093 38.836 38.807 38.836 35.6 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="49.738 41.93 44.826 44.782 52.201 52.139 54.76 50.68 54.76 44.846 "/><polygon fill-rule="evenodd" clip-rule="evenodd" fill="#010101" points="55.045 51.175 52.621 52.557 59.996 59.914 60.068 59.871 60.068 54.037 "/><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M60.354 97.292l-2.138 1.241c-0.027-0.204-0.054-0.409-0.083-0.612l1.934-1.123v-5.834l-3.736-2.129c-0.055-0.21-0.111-0.419-0.167-0.629 1.856 6.886 2.786 14.15 2.786 21.793 0 2.23-0.079 4.43-0.234 6.599 0.013-0.182 0.026-0.363 0.038-0.545l1.314-0.763v-5.835l-1.126-0.654c-0.003-0.222-0.006-0.445-0.011-0.667l1.423 0.826 5.079-2.917v-5.834L60.354 97.292z"/></svg> \ No newline at end of file diff --git a/src/UI/Content/Images/touch/114.png b/src/UI/Content/Images/touch/114.png deleted file mode 100644 index e19a0815594f8bfcb1afec90d773ad53a736bb89..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5671 zcmc(jRaexF+l7CCguu`s3=YBoGqjS@(lE62&?%vGD-F^ygbW}h-H4>5bf?lO-7z5T z!|VT3yw_TL@3qhFgZpSlX{sxN2*HE^0Durma$5h=?SCS``R7eIo*Dpv18A!1$p5Qb z|F8Tz07%k$^yty%)+Qbv9tZ^5+1?=_AOKM6dpo;CghbR-R2=N=3=9llIxrU(7c(<6 z87UbPBO?tB4G^OShr^%J(*wy`tgNi$6y&c;-_xWbfmrp2lPe%e6JQ{NGC^OwcyX|^ zx4*wHARquFs8CW;3JD3(($ZqbX-d6%b@GqBgTcjL0Kp?5RR>7Zl@J#P>_ve$BAERn z+_=}zd3b<Ogp8Cl5UwIBDk>}@3=rc1xHyN0hs*1$?yZ?}a&mlpd|1p>_y88*CW$2g z0eI+es6keZiLzz>K$bp0MzFTAPW9Q8nG$?<c2@hdS6y8lh*JVg_)qr_6Z@-y3_bi$ zdY}|tz)5^=ZK-o^ta*Acpd+8P$Pq}@1iZw%ySwET708_BvEGVf2|okCBuBgZOG`_i zha19rN<u?J`3l^qaxKA`mU_Cn%F4<_UW)&~3R1v&FDEG}30MjNGAw-ylga%x+1c5^ zYi?3eI3w*-FeT03<3BftnCrtcpN^bY<$idXh9I;d5Uv13$-?!dvGiYHDZugB=~OC% zDa|CnFeYLw+~eJYh2JYxQ(aY)T|a*O$Qx-+9jqk`*Puj-0SLA~2d4uA1F^BOv_-b2 zrlyo1)PN{uQ7%5P%nNQN7$+SQJ&5uFb9Z@pSuobp*w|QBR_4^4jz*(}i#$2=otSg% zfP6y+H+2DdNftFR4n7`cPB__fCUVxNkDt*@j87C67Rprxx;Q%1piSYyhB&HR5H8k- z`-k!6*^=@0mX?;<+S;#`RTUK#df%dbeSJB<ILbvhSX*0jSR#J@{5d>0L?5Q7ub?KO zi=bB#Vto`5`|kreJharcfk#rLxA%>eUlJS@uP}$h?Vl5)JnhX*48&QPs86c{{A=cB zYxQ&m`2AMdm+k<7q5>f&tz);+W`yfUX-j4MHLi1Mx$QZ+#rVCehQgC1Aw;s5&X*M# zlRahoQbtZqLt0hyIU(zzaN;!n?c;x3wr{$5*EGCaUOAaPnT6dPuU<(6t`4X9=l|`? za+{hvkC*W8O2mhFY=-?zknlc46vr+)j|9r!#Ys4=ht(!o?pEZ(7+!k)nj~{xmsZTo zNI{WEtmj7M4<F9CU%q0rrxi7Sa94<%O-^{l#n5vhTn+KMAIt}QPU&2IlU9HEXfM4S z6TUXX&h}aRa+bh*lbs#2b=LcSE$RW=W$8MMqIItxL69z0W*dK390Y%wq^X}ehGGqq zTX+ZyeDwY_`0N=R4QEg4@KY1oo8Pp?z2W46T$9SZ_wb{kMlbn<`VCbm6j$t&-@Q>w zTy@!Cjc$4-zPamPf8V`<GnUn{MtJhBdTORTWj<5+{!cXS`A#3cgkO$d_e~9{l3JGc z6^7H<Z0<Aq3i$`TXkGigsP)T#wYt7-M9v*<HxlPs+E!PYuiSd9E)%w$_D4kG5(+97 zB~QPv5@^oW6OIJiUI0tlpVPj<y{m@XzqyW$lt$Zei@qs{RAiN({I$n#*y3<7yi)UI zf(l)=nEdznXK)I5$ZA%*u3cH>Q%7z6<;)D7w_4na@2>$ejTl!){*-M$2%`<E_f+9h zw8<lbE|B~2e`=}N14A!4ZI`IquIdum#;yk>(8eEpFS|$p6%olM0@TWb9CA!{d!2ME z*ToY@tG_FAxVnwS?cw_OCpPd|^RV6tS9d?|cVk2<sCpKM6v^&_Bn$)3ta{CRzD_GX zS}f&A%D5FJEYsAuU=RzM(NdP~J}4gw^5-ze<Cv|&{c39m{A%_90t+KJ%%-Np7V};x zR*7KPYezNm?n^G9U?fHuHJ`^|<6k8S8TlD)41kA~yIMc?=9|RiAZF>yiEK3z6O1F` zqhe`ZE@FnKJ+4pLm>C_%$B2~2GPQ6l=)S>>RSgLk1osm$+nbU2APdeNm7~{*elc!9 zLM#r;v;QW*#)gC~hA4f8q$j-jP?Vl@abt!vLQm=F{61_5%%tF(@I6C38J7h=Sb$=A zZ-HSQWP;U=L1jioY*Lq5Pb@Jfkl8`&<4()9t2mYlw+)h6(Rp+lJhIyIqLi)9B=3qW zD(k7VVE&2e=_2Rd&s1SvHIve=`&#k&Q}tfa4J}+Fb>&5J*Ncy8WQR6Vh8=1np?oD! z21?A@WS(Qdy}i5OfgUrnhT3Nj-Wy}XH@(%*s|DyH_2@lHrr!m}<0=33I<e6%@MD0y zlw^QPOqX8@ki}Wx`j&QHojRkhJX1o4NP2ial2w+Z-Ar_Ul8O|@rH&v1ao6i3#xGIA zXYQE-Q1}(qZK&8|mO?z1QEz+9=Z1n`8~lH;3#2fcCj)SM?7_7zEtKwDGFRMujc?au ztsbKhq#<<;C|t&0jt7zeD;yN-@aAD!qaoKS5a`X9%*aKC(u)LPj*SY^;5{$cYe;WK z=f#Zm;#SNB9pXshCan$cWj;d@yw$o5!~6UWbqamgs*Ivnuj~B%>t(y=>cJBElLoFE z3g@G4gOduiBBo`c4b8+f-}(7Ab<T1Ee30OF$=aH72I2T_E7=DWR4O`_#|Vqn+KM`k zuQLuusFW}+VMxDle&f)u2@hpVMW!!hW=76Oe*%UUqIjZET4hZEyo_HuKsPAAYJu6< z5ogwBcZ9Zu9>J6n<DWix$4=YTn|wFE$&xROTj?>X7E0^hwR$g2pp&@-I3hX}mA7S` zUz0L59={GNf2N#_Z)Uc3^jz`IgYx5$`MT`-@0GXsMHH+u=xZ#8hD(Iy{1+|R#9xqE z)62OxaXB^<L|+4l+AyZL)xAd8q4Ft<Jr2Kc@zmYzjg?q>XE~92`4)tBP2s%9$4!TZ zh&E_xC5sVH2X_op^D&&Lxb0<}#`x!V^nj+;q!-=r4ir2dOk@K`c`j}xUG+2{`XI8C zYfUXDPc2{%OE;Bok|K#oqcE-6Y{<EYit$99kNrd_%fG^&eO;-jRZOA@C&%W!`?1vH z^Dl!;GJX9O2EiJPa()W)U3A*Lx7sC%^3tmMPO@=K;2mU4q8;)&?xyw?>w`=S3A%n0 zp}`U+XP9c_CK#Zw7~kJ=1KRSK6z=xC6rq4=dngc1?7hRo-J5wbbv=8sM3MH6jdvkf z+h|$pNljs1U!SfKzCm5dj6>fMer?z&|IGl`ZF73%&G=k!8Xi(l{K<bBrWokCfJI`1 zj8f4yV%%$`R4FL^h7Al?8H>ISok_P0Ad8-+#2rr--I?K|?vpna+_Jb!n|0*epiIoQ zw3a~SoE*02+jlPEYi}4uhm4yGa4Db9h?IulXV8X;D{*;(-%4+h%C<-L4;`p`K_XF} zAF+x@V96p`4aYMgq_F<{G3UuUG9@s2L7_%Yl+C9~ksxsVLqT>!>I<qucHSUDa#?}$ zmea?Cf9v{z6#uubwW>0;uk4%I82nQj3E$SW>gqO~Rden<CR0QuLpZ%q#{u_{%EuRm zXdHnBbob>KpTcUai}%b#hRam}a15NaSSvj#FShWVkyk^dFa53Ehv5xxBC=wNED3e0 zPK;w=XYEF$nm<|m?C_mX80E}MecNHn(zoU=r<H?B)6-3@EjVb%^yUeVJB=oo?Zkk` z@~L8-BGv7ugK4rc_(HeCWp0HJpYu#87SdRYB*bXWZk>`mXr)Tj4`kK7!YMzU9hpb< zHspKb?8hmscmpMvGo!b#H+E8dR~<4nh;Ue3D#*aN4$oLAS}d)$h|5~nvvHhS3U>JN z%@Ptj`CLD+%G*GCXCf_kA4gs(>SWKW`H-rxzt!4A9XgQd*QSi?kkc2D5k!~!iL2a^ z-lW7iKydc!oAw{u^BaAcP=z7f`lmDu3iz6bmiqF$y)7ATWx?7=<eE_4Q%cJjZJs9_ z0#OC#Y=Uc;^vWl~SqV6OxyL4q3ZvS6nJhjV^wo?vpE-0!eX=(|JxKa0M6_cRlVk5x zM%eXks^DlVCoq%d-5VD&ER49=!_c?-Vq5nOi=*{LhJ6N_-^`Q!GLE1xL>+t%q+oox z#p#TS$^L81yE_sPq}0K<YlX%o?Ko#WDVFXil!n>JYW;qbwU)U!3BPO;MeAJ3p^0@r z*v15$wet28F1S~E=i@#bO9?`ZTZdbw*IxT4<9kSc66Jg$U@bMg88SF92Wm2E{GfLA zH{DB9Ox0oFl&;}@?;&Q6x0s0imdgf5w*Y#)iu;c2+qv$%sgAprjC;pGIBe&=xF++U z<V91fO^ZzSo|y_$%=dzb+)P%M<Kl#L#_Nl_%q{cZ<>`#*!Et9Kgya}yOOMBIriEui z&1uU$DliucqBI`Lx+)|L7ui<A!UWJ0pyGur8LI^}V1aQ2Qw?(1-<KEX(tj0EFm`%% z0KNsZ&8cHG<eki=CTq;7an3sxU=RGQR$qvislZ6Z>vL|QLDc@#b~(#_27Op&tq!r7 z*1)pvTFp17KI;qS0%N+lj~2^@GJC?IAEv*ejHwGJCuk~7I=-CvCV_RPM8}G861q>r zw6WPG4B$5r_bh1>uk=|wh2A$(%38U&wnU(}PI^Z9dX&H!W=wJQLQ6;l)zEH~xIyV{ zZq*sGbDCPkmOZ96^4R6erQC{(v19`sYG!?WpL-lm$wMc}nQM}`&n%d9_2}ehHmsxT zQV$fVmiA}NB6c%)wDoG9^Fuv<ntRQF`NUmQfr49j54Gx<Q^D!ORPFVbyy&}@EAcHx z`z*pIIpN?L>O(j3^HCck!mKYng2`0rFhaJRLiK$@C}agDW@T}tmruUkUdGk?PR`lJ zGj>wl^;T{B3)83uKbTpx?*g=f7M0~D87jg1G`Twq_ELtJrWjpm_Q3P3?n`|oCh^>c zQs4$t#KapjgJ=A^xS9Iw*BF|zkdT6jl0qj54xh(SO6S>~1M!hIO>4c*6`o^+5+~oM z)_$^4^QaVbazf><LgH7y%UuGBj3S8DVo5k}0p>}7m8Ux8s;_$YJ(QS^F0U|ZpzF#w zfW~-aJFY=OLG?KZmV@^qr)TZ0{C5v)xGLf8(KVe|z76*BRY5rM8q>SD>({otsdZc_ zFlgf5jKbQ56aMs9*qurAWs8Y`4Tlsb6RRMP^JhxDVLWp#T#~Xk#pn5@ztulx<ok(2 z6nXa>p;MIu1#Zp*JO^%ikB=V5%$3#&u~AC3kJDdlTF!_54mq(R?5bJ=*=x~#67^0= z&O*M~$q7xnkwmJ|jD+z%gGHV7bp}`Y6dPV=CpgDD?HVx%-N?U1u9;w6r3rY(^N<<- zC<t~O6-rv+w2Df>9%C~ZHP!E90^12rOk71njrGLe!R<u59MUli_Q63^ukkEiU1WLI zmtz==k{(_!-4^X7&y!!$`cTv{v=A<*IBC0HaY-xf-C`3~@ErEvH7cY?Pxh|t6R(>u z7lfYNH1LZssI^tqFT&iRfkL*qO(Z%;cy66RNE81{2<B^dK(IiLiW=MKC*$h6xfpKy zVXUyrFsM@^mxjN~aY&I^bJmUTfy!V6sEJHbJ9Sy&dHN2onw^GbcuA5_gA2M&_nD(g zv_`a;SWXtp65h@AVy@!~x=Ss%A?F50930EFWVJ}SDNUtV7mO7#ksWFCXC1-*n@XNY z`gA{q+dF)ba@7Et?dd00CPBU!wpVhtRR&7w%nF9o*vLxH{17z85;}?Mci}%;Y(4)7 zQ~e+voD<?znP{1_pWx8d(~uFpS}@sWlN;1TjMI=oA*22QTP>7(Ix*x&oM|p+g%~E9 zln#JBSuIjt=B+H63ty;Q{CFpDAvxPP7_mlC8pQQf6z<MBUeJ1UTW0>0pdJpTcZgaD z2V{uSOjA;6wcgX$b<Wi3*wGVmo4;^NbnJdKI_c@dC@=81stM7elnC&B{R~5Gc1lif z{z(1mzf^81K@}`FQ>x}WwY~c>%J7GXn2-@|){yn<?{xv%w{7;AYZMw>NzyiM$!CGu zzY2%ui95cIl0BMuTMzE&t#)%9iE_m17it#!ObC%9EHYQ_3bB7(Bp#gjqHrL-d%QCj z(=R6LX1w__J(8D?>sSv>tsu}$*?8V|!=(QM4n2&qKVy>D|Jo<^ku<5!MNrnbvw7Dm zU|l69_*8-dq;yL?JQW_~A?2^Xq59txw3VE|Q5V))Ck6J7yl_xJf@rjakbjz<y<Nz2 z=A(9Htp6^+;}B8lnT=iDP9vdAt8)-#VC*9~JvLXK-t$DW*ICP>RN2<NE(4n3o<0@$ zgQ&#rPiJ_CiEh{N7}|f8T|>J<^C#ZRWR;)tc#v+px|nAPZ9&R@qp1tpku#;mT{Iw$ zsiu^EZ`?oZMovk^Z3B@<Q54@=B`V>9>lu!7!#xW$>x2bQx{D5;Wu%uzy5ep@{Lz7@ zU!$G7Dz=>Ot)5dVdf0Fv8i}o4OBVFSSpus<#G?lI9wC>P0_#ni{u^6ks&&Y(QdRWg zZJi_dK|LS!P~OAp^V96YwQrdJOyN2P@$<hQjq~+;?AxizU8&_sUHqji^%H_K6<sAU zzsXGlpPUTT@7HkEL(9~Y<Il!XD#7_fy^|U60XA9IjqoE8XM<Bn5k%V4iA3j^n8fRm zg2&%D59uD(z<q9ZoB%1z>F-o$))KBJaS3i#S{FmN=v5-Y>zn^r4CrW26-+Q@FEFpe z4_movh-HSzoI|e>X|0(@;hIpW^^0)7#Yjau6ig2E&WFewc*JysT_B5%d~O8M;%;>L zYhrd72cAp&nqb8)0!gLdmPfj)$;C2kT653L3oyI`6?7~=ybpD#3B^X^gHlY5HM8q^ z;P1I??%PRM)9}3<r}CGPTza{O3SwX1x3u>?ZTf}%r~0ZzHFx>|B4u=_VCTc=b^Yf4 z6&>#oa*mvsv$NJ_Ac-M^oSn47m$Iw3!HY7U*5)CZoOzS~m{B*~hn^+2uwr<gF}_gv z_L=@zT6=Y<P98~be9W?;QDga2>M(LfZ++Y9aO{lSo8kSzrn!qW*P6!<ZfwU5@ohLc zMsb4Sqh1G2?&d$ss+w($IjmHc$8;MdFel;b6L6NNe~pr_3?9as@h<o5w+H9zjT*O} z&)1$_&TM)2L;kjy*Wo?=B)qokF_h9K^wdR%L~u>G%qsKBf124{h(w}KWT?Ara9lm9 z?Q#p%U6wDA6vJdFWM$vgNGf8nR6@$Yy!rMe>r?o%S56oG<>cRXvNsDRA=NbU?=6<_ zH;8@x*iC&-1BEzVN!VW~ZWRh#jwJdqt&P+??94K#Z*^S0SdeR9a`oa`n%^9T<VU@{ cHM6*(Pka^hA&(IMe={(EkXM(hk}(hYKX&!>djJ3c diff --git a/src/UI/Content/Images/touch/144.png b/src/UI/Content/Images/touch/144.png deleted file mode 100644 index 99fc3a98ea7c5d4316151a03c1649987e91b31c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6594 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 Pdx5AbAr;>%SVsOIDp^`( diff --git a/src/UI/Content/Images/touch/57.png b/src/UI/Content/Images/touch/57.png deleted file mode 100644 index 9f84befd61d407dc4b4e824119737ec0588dfae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2791 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 diff --git a/src/UI/Content/Images/touch/72.png b/src/UI/Content/Images/touch/72.png deleted file mode 100644 index 6b6808caa440a5d84cb34742d49b8e5bdc0d86f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3351 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 WpFwmy`y%vzkB0@>hE!|pk??<yy&7cz diff --git a/src/UI/Content/Messenger/messenger.css b/src/UI/Content/Messenger/messenger.css deleted file mode 100644 index 9fc58c936..000000000 --- a/src/UI/Content/Messenger/messenger.css +++ /dev/null @@ -1,101 +0,0 @@ -/* line 4, ../../src/sass/messenger.sass */ -ul.messenger { - margin: 0; - padding: 0; -} -/* line 8, ../../src/sass/messenger.sass */ -ul.messenger > li { - list-style: none; - margin: 0; - padding: 0; -} -/* line 14, ../../src/sass/messenger.sass */ -ul.messenger.messenger-empty { - display: none; -} -/* line 17, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message { - overflow: hidden; - *zoom: 1; -} -/* line 20, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message.messenger-hidden { - display: none; -} -/* line 23, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-phrase, ul.messenger .messenger-message .messenger-actions a { - padding-right: 5px; -} -/* line 26, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-actions { - float: right; -} -/* line 29, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message .messenger-actions a { - cursor: pointer; - text-decoration: underline; -} -/* line 33, ../../src/sass/messenger.sass */ -ul.messenger .messenger-message ul, ul.messenger .messenger-message ol { - margin: 10px 18px 0; -} -/* line 36, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed { - position: fixed; - z-index: 10000; -} -/* line 40, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed .messenger-message { - min-width: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -/* line 45, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed .message .messenger-actions { - float: left; -} -/* line 48, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top { - top: 20px; -} -/* line 51, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-bottom { - bottom: 20px; -} -/* line 54, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { - left: 50%; - width: 800px; - margin-left: -400px; -} -@media (max-width: 960px) { - /* line 54, ../../src/sass/messenger.sass */ - ul.messenger.messenger-fixed.messenger-on-top, ul.messenger.messenger-fixed.messenger-on-bottom { - left: 10%; - width: 80%; - margin-left: 0px; - } -} -/* line 64, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-right { - right: 20px; - left: auto; -} -/* line 68, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-top.messenger-on-left, ul.messenger.messenger-fixed.messenger-on-bottom.messenger-on-left { - left: 20px; - margin-left: 0px; -} -/* line 72, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-right, ul.messenger.messenger-fixed.messenger-on-left { - width: 350px; -} -/* line 75, ../../src/sass/messenger.sass */ -ul.messenger.messenger-fixed.messenger-on-right .messenger-actions, ul.messenger.messenger-fixed.messenger-on-left .messenger-actions { - float: left; -} -/* line 78, ../../src/sass/messenger.sass */ -ul.messenger .messenger-spinner { - display: none; -} diff --git a/src/UI/Content/Messenger/messenger.flat.css b/src/UI/Content/Messenger/messenger.flat.css deleted file mode 100644 index df8d35aeb..000000000 --- a/src/UI/Content/Messenger/messenger.flat.css +++ /dev/null @@ -1,462 +0,0 @@ -@-webkit-keyframes ui-spinner-rotate-right { - /* line 64, ../../src/sass/messenger-spinner.scss */ - 0% { - -webkit-transform: rotate(0deg); - } - - /* line 65, ../../src/sass/messenger-spinner.scss */ - 25% { - -webkit-transform: rotate(180deg); - } - - /* line 66, ../../src/sass/messenger-spinner.scss */ - 50% { - -webkit-transform: rotate(180deg); - } - - /* line 67, ../../src/sass/messenger-spinner.scss */ - 75% { - -webkit-transform: rotate(360deg); - } - - /* line 68, ../../src/sass/messenger-spinner.scss */ - 100% { - -webkit-transform: rotate(360deg); - } -} - -@-webkit-keyframes ui-spinner-rotate-left { - /* line 72, ../../src/sass/messenger-spinner.scss */ - 0% { - -webkit-transform: rotate(0deg); - } - - /* line 73, ../../src/sass/messenger-spinner.scss */ - 25% { - -webkit-transform: rotate(0deg); - } - - /* line 74, ../../src/sass/messenger-spinner.scss */ - 50% { - -webkit-transform: rotate(180deg); - } - - /* line 75, ../../src/sass/messenger-spinner.scss */ - 75% { - -webkit-transform: rotate(180deg); - } - - /* line 76, ../../src/sass/messenger-spinner.scss */ - 100% { - -webkit-transform: rotate(360deg); - } -} - -@-moz-keyframes ui-spinner-rotate-right { - /* line 80, ../../src/sass/messenger-spinner.scss */ - 0% { - -moz-transform: rotate(0deg); - } - - /* line 81, ../../src/sass/messenger-spinner.scss */ - 25% { - -moz-transform: rotate(180deg); - } - - /* line 82, ../../src/sass/messenger-spinner.scss */ - 50% { - -moz-transform: rotate(180deg); - } - - /* line 83, ../../src/sass/messenger-spinner.scss */ - 75% { - -moz-transform: rotate(360deg); - } - - /* line 84, ../../src/sass/messenger-spinner.scss */ - 100% { - -moz-transform: rotate(360deg); - } -} - -@-moz-keyframes ui-spinner-rotate-left { - /* line 88, ../../src/sass/messenger-spinner.scss */ - 0% { - -moz-transform: rotate(0deg); - } - - /* line 89, ../../src/sass/messenger-spinner.scss */ - 25% { - -moz-transform: rotate(0deg); - } - - /* line 90, ../../src/sass/messenger-spinner.scss */ - 50% { - -moz-transform: rotate(180deg); - } - - /* line 91, ../../src/sass/messenger-spinner.scss */ - 75% { - -moz-transform: rotate(180deg); - } - - /* line 92, ../../src/sass/messenger-spinner.scss */ - 100% { - -moz-transform: rotate(360deg); - } -} - -@keyframes ui-spinner-rotate-right { - /* line 96, ../../src/sass/messenger-spinner.scss */ - 0% { - transform: rotate(0deg); - } - - /* line 97, ../../src/sass/messenger-spinner.scss */ - 25% { - transform: rotate(180deg); - } - - /* line 98, ../../src/sass/messenger-spinner.scss */ - 50% { - transform: rotate(180deg); - } - - /* line 99, ../../src/sass/messenger-spinner.scss */ - 75% { - transform: rotate(360deg); - } - - /* line 100, ../../src/sass/messenger-spinner.scss */ - 100% { - transform: rotate(360deg); - } -} - -@keyframes ui-spinner-rotate-left { - /* line 104, ../../src/sass/messenger-spinner.scss */ - 0% { - transform: rotate(0deg); - } - - /* line 105, ../../src/sass/messenger-spinner.scss */ - 25% { - transform: rotate(0deg); - } - - /* line 106, ../../src/sass/messenger-spinner.scss */ - 50% { - transform: rotate(180deg); - } - - /* line 107, ../../src/sass/messenger-spinner.scss */ - 75% { - transform: rotate(180deg); - } - - /* line 108, ../../src/sass/messenger-spinner.scss */ - 100% { - transform: rotate(360deg); - } -} - -/* line 116, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner { - position: relative; - border-radius: 100%; -} -/* line 120, ../../src/sass/messenger-spinner.scss */ -ul.messenger.messenger-spinner-active .messenger-spinner .messenger-spinner { - display: block; -} -/* line 124, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side { - width: 50%; - height: 100%; - overflow: hidden; - position: absolute; -} -/* line 130, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - border-radius: 999px; - position: absolute; - width: 100%; - height: 100%; - -webkit-animation-iteration-count: infinite; - -moz-animation-iteration-count: infinite; - -ms-animation-iteration-count: infinite; - -o-animation-iteration-count: infinite; - animation-iteration-count: infinite; - -webkit-animation-timing-function: linear; - -moz-animation-timing-function: linear; - -ms-animation-timing-function: linear; - -o-animation-timing-function: linear; - animation-timing-function: linear; -} -/* line 140, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left { - left: 0; -} -/* line 143, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-left .messenger-spinner-fill { - left: 100%; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - -webkit-animation-name: ui-spinner-rotate-left; - -moz-animation-name: ui-spinner-rotate-left; - -ms-animation-name: ui-spinner-rotate-left; - -o-animation-name: ui-spinner-rotate-left; - animation-name: ui-spinner-rotate-left; - -webkit-transform-origin: 0 50%; - -moz-transform-origin: 0 50%; - -ms-transform-origin: 0 50%; - -o-transform-origin: 0 50%; - transform-origin: 0 50%; -} -/* line 152, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right { - left: 50%; -} -/* line 155, ../../src/sass/messenger-spinner.scss */ -.messenger-spinner .messenger-spinner-side-right .messenger-spinner-fill { - left: -100%; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - -webkit-animation-name: ui-spinner-rotate-right; - -moz-animation-name: ui-spinner-rotate-right; - -ms-animation-name: ui-spinner-rotate-right; - -o-animation-name: ui-spinner-rotate-right; - animation-name: ui-spinner-rotate-right; - -webkit-transform-origin: 100% 50%; - -moz-transform-origin: 100% 50%; - -ms-transform-origin: 100% 50%; - -o-transform-origin: 100% 50%; - transform-origin: 100% 50%; -} - -/* line 15, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -moz-user-select: none; - -webkit-user-select: none; - -o-user-select: none; - user-select: none; - background: #404040; -} -/* line 20, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat.messenger-empty { - display: none; -} -/* line 23, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message { - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -webkit-border-radius: 0px; - -moz-border-radius: 0px; - -ms-border-radius: 0px; - -o-border-radius: 0px; - border-radius: 0px; - position: relative; - border: 0px; - margin-bottom: 0px; - font-size: 13px; - background: transparent; - color: #f0f0f0; - font-weight: 500; - padding: 10px 30px 13px 65px; -} -/* line 35, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close { - position: absolute; - top: 0px; - right: 0px; - color: #888888; - opacity: 1; - font-weight: bold; - display: block; - font-size: 20px; - line-height: 20px; - padding: 8px 10px 7px 7px; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -/* line 51, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close:hover { - color: #bbbbbb; -} -/* line 54, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-close:active { - color: #777777; -} -/* line 57, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions { - float: none; - margin-top: 10px; -} -/* line 61, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - text-decoration: none; - color: #aaaaaa; - background: #2e2e2e; - display: inline-block; - padding: 10px; - margin-right: 10px; - padding: 4px 11px 6px; - text-transform: capitalize; -} -/* line 72, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a:hover { - color: #f0f0f0; - background: #2e2e2e; -} -/* line 76, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions a:active { - background: #292929; - color: #aaaaaa; -} -/* line 80, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-actions .messenger-phrase { - display: none; -} -/* line 83, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message .messenger-message-inner:before { - -webkit-border-radius: 50%; - -moz-border-radius: 50%; - -ms-border-radius: 50%; - -o-border-radius: 50%; - border-radius: 50%; - position: absolute; - left: 17px; - display: block; - content: " "; - top: 50%; - margin-top: -8px; - height: 13px; - width: 13px; - z-index: 20; -} -/* line 95, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-success .messenger-message-inner:before { - background: #5fca4a; -} -/* line 98, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { - background: #61c4b8; -} -/* line 103, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message.alert-error .messenger-message-inner:before { - background: #dd6a45; -} -/* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -webkit-animation-duration: 20s; - -moz-animation-duration: 20s; - -ms-animation-duration: 20s; - -o-animation-duration: 20s; - animation-duration: 20s; - opacity: 1; -} -/* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-soon .messenger-spinner:after { - content: ""; - background: #292929; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 32, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner { - width: 32px; - height: 32px; - background: transparent; -} -/* line 37, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner .messenger-spinner-side .messenger-spinner-fill { - background: #dd6a45; - -webkit-animation-duration: 600s; - -moz-animation-duration: 600s; - -ms-animation-duration: 600s; - -o-animation-duration: 600s; - animation-duration: 600s; - opacity: 1; -} -/* line 45, ../../src/sass/messenger-spinner.scss */ -ul.messenger-theme-flat .messenger-message.alert-error.messenger-retry-later .messenger-spinner:after { - content: ""; - background: #292929; - position: absolute; - width: 26px; - height: 26px; - border-radius: 50%; - top: 3px; - left: 3px; - display: block; -} -/* line 114, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-last .messenger-message { - -webkit-border-radius: 4px 4px 0px 0px; - -moz-border-radius: 4px 4px 0px 0px; - -ms-border-radius: 4px 4px 0px 0px; - -o-border-radius: 4px 4px 0px 0px; - border-radius: 4px 4px 0px 0px; - -webkit-box-shadow: inset 48px 0px 0px #292929; - -moz-box-shadow: inset 48px 0px 0px #292929; - box-shadow: inset 48px 0px 0px #292929; -} -/* line 118, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-first .messenger-message { - -webkit-border-radius: 0px 0px 4px 4px; - -moz-border-radius: 0px 0px 4px 4px; - -ms-border-radius: 0px 0px 4px 4px; - -o-border-radius: 0px 0px 4px 4px; - border-radius: 0px 0px 4px 4px; - -webkit-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - -moz-box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; - box-shadow: inset 0px 1px rgba(255, 255, 255, 0.13), inset 48px 0px 0px #292929; -} -/* line 122, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-message-slot.messenger-first.messenger-last .messenger-message { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - -ms-border-radius: 4px; - -o-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 48px 0px 0px #292929; - -moz-box-shadow: inset 48px 0px 0px #292929; - box-shadow: inset 48px 0px 0px #292929; -} -/* line 126, ../../src/sass/messenger-theme-flat.sass */ -ul.messenger-theme-flat .messenger-spinner { - display: block; - position: absolute; - left: 7px; - top: 50%; - margin-top: -18px; - z-index: 999; - height: 32px; - width: 32px; - z-index: 10; -} diff --git a/src/UI/Content/Overrides/bootstrap.less b/src/UI/Content/Overrides/bootstrap.less deleted file mode 100644 index a72e21e65..000000000 --- a/src/UI/Content/Overrides/bootstrap.less +++ /dev/null @@ -1,82 +0,0 @@ -@import "../prefixer"; -@import "../Bootstrap/variables"; -@import "../variables"; - -@input-border-focus: @droneTeal; -@font-family-sans-serif: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif; -@modal-md: 800px; -@modal-lg: 1000px; - -.label, .badge, i { - cursor : default; -} - -.slide-button { - min-width : 0px; -} - -.popover-title { - text-transform : none; -} - -.line &>[class^="icon-lidarr-"], .line &>[class*="icon-lidarr-"] { - margin-top : 1em; - height : 1em; - line-height : 1em; -} - -.tooltip-inner { - word-wrap: break-word; -} - -.dropdown-submenu { - position:relative; - & > .dropdown-menu { - top:0; - left:100%; - margin-top:-6px; - margin-left:-1px; - -webkit-border-radius:0 6px 6px 6px; - -moz-border-radius:0 6px 6px 6px; - border-radius:0 6px 6px 6px; - } - & > a:after { - display:block; - content:" "; - float:right; - width:0; - height:0; - border-color:transparent; - border-style:solid; - border-width:5px 0 5px 5px; - border-left-color:#cccccc; - margin-top:5px; - margin-right:-10px; - } -} -.dropdown-submenu:hover { - & > .dropdown-menu { - display:block; - } - & > a:after { - border-left-color:#ffffff; - } -} -.dropdown-submenu.pull-left { - float:none; - & > .dropdown-menu { - left:-100%; - margin-left:10px; - -webkit-border-radius:6px 0 6px 6px; - -moz-border-radius:6px 0 6px 6px; - border-radius:6px 0 6px 6px; - } -} - -.btn { - text-transform: capitalize; -} - -.table-responsive { - overflow-x: visible; -} diff --git a/src/UI/Content/Overrides/bootstrap.tagsinput.less b/src/UI/Content/Overrides/bootstrap.tagsinput.less deleted file mode 100644 index 85f726ae6..000000000 --- a/src/UI/Content/Overrides/bootstrap.tagsinput.less +++ /dev/null @@ -1,35 +0,0 @@ -@import "../Bootstrap/variables"; - -.bootstrap-tagsinput { - width : 100%; - - .twitter-typeahead { - width : auto; - } - - .tag { - margin-right: 0px; - - [data-role="remove"] { - &:hover { - color: @brand-danger; - } - } - } - - .tt-dropdown-menu { - - .opacity(0.95); - - .tt-suggestion { - color: #222222; - cursor: pointer; - - //selected item - &.tt-cursor { - background-color: @droneTeal; - color: #ffffff; - } - } - } -} \ No newline at end of file diff --git a/src/UI/Content/Overrides/bootstrap.toggle-switch.less b/src/UI/Content/Overrides/bootstrap.toggle-switch.less deleted file mode 100644 index 4e980373d..000000000 --- a/src/UI/Content/Overrides/bootstrap.toggle-switch.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../Bootstrap/variables"; -@import "../Bootstrap/mixins"; - -.toggle { - height: 34px; - box-sizing: border-box; - font-weight: normal; - - .slide-button { - .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); - - &.btn-danger, &.btn-warning { - //.buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight); - .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); - } - } - - input:first-of-type:checked ~ .slide-button { - .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); - - &.btn-danger { - .button-variant(@btn-danger-color, @btn-danger-bg, @btn-danger-border); - } - - &.btn-warning { - .button-variant(@btn-warning-color, @btn-warning-bg, @btn-warning-border); - } - } - - input:first-of-type:disabled ~ .slide-button { - opacity: 0.5; - } -} \ No newline at end of file diff --git a/src/UI/Content/Overrides/browser.less b/src/UI/Content/Overrides/browser.less deleted file mode 100644 index 236f2f8d8..000000000 --- a/src/UI/Content/Overrides/browser.less +++ /dev/null @@ -1,17 +0,0 @@ -html { - overflow : -moz-scrollbars-vertical; - overflow-y : scroll; -} - -button::-moz-focus-inner, a::-moz-focus-inner { - border : 0; -} - -a:focus { - outline : none; -} - -body h1, body h2, body h3, body h4, body h5, body h6 { - text-transform : capitalize; - font-weight : 300; -} diff --git a/src/UI/Content/Overrides/fullcalendar.less b/src/UI/Content/Overrides/fullcalendar.less deleted file mode 100644 index 76d1b32d5..000000000 --- a/src/UI/Content/Overrides/fullcalendar.less +++ /dev/null @@ -1,49 +0,0 @@ -.fc-view { - overflow: visible; -} - -.fc-time { - padding: 0 1px; -} - -.fc-title { - padding: 0 1px; - display: block; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -.fc-scroller { - overflow-y: visible; -} - -@media (max-width: @screen-xs-max) { - .fc-button { - padding: 0px 5px; - } - - .fc-header-space { - padding-left: 5px; - } -} - -.fc-header { - .fc-state-active { - z-index: 1; - } -} - -.fc-event-container { - .fc-event { - line-height : inherit; - } -} - -.fc-icon { - width: auto; -} - -.fc-icon::after { - margin: 0px; -} diff --git a/src/UI/Content/Overrides/messenger.less b/src/UI/Content/Overrides/messenger.less deleted file mode 100644 index 160ed9512..000000000 --- a/src/UI/Content/Overrides/messenger.less +++ /dev/null @@ -1,23 +0,0 @@ -@import "../variables"; - -body.control-panel-visible { - ul.messenger.messenger-fixed.messenger-on-bottom { - bottom: 95px; - } -} - -ul.messenger-theme-flat .messenger-message.alert-info .messenger-message-inner:before { - background: @droneTeal; -} - -@media (max-width: @screen-xs-max) { - ul.messenger.messenger-fixed.messenger-on-bottom { - width: 100%; - bottom: 0px; - .border-bottom-radius(0); - - &.messenger-on-right { - right : 0px; - } - } -} \ No newline at end of file diff --git a/src/UI/Content/badges.less b/src/UI/Content/badges.less deleted file mode 100644 index 68caf5c45..000000000 --- a/src/UI/Content/badges.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../Content/Bootstrap/variables"; - -.badge-inverse { - background-color: #eee; - border: 1px solid @badge-bg; - color: @badge-bg; -} - -.badge-primary { - .badge-variant(@label-primary-bg); -} - -.badge-success { - .badge-variant(@label-success-bg); -} - -.badge-info { - .badge-variant(@label-info-bg); -} - -.badge-warning { - .badge-variant(@label-warning-bg); -} - -.badge-danger { - .badge-variant(@label-danger-bg); -} - -.badge-variant(@color) { - background-color: @color; - &[href] { - &:hover, - &:focus { - background-color: darken(@color, 10%); - } - } -} \ No newline at end of file diff --git a/src/UI/Content/bootstrap.less b/src/UI/Content/bootstrap.less deleted file mode 100644 index 10e23ce63..000000000 --- a/src/UI/Content/bootstrap.less +++ /dev/null @@ -1,3 +0,0 @@ -@import "./Bootstrap/bootstrap"; -@import "./Overrides/bootstrap"; -@import "./bootstrap.tagsinput.less"; \ No newline at end of file diff --git a/src/UI/Content/bootstrap.tagsinput.less b/src/UI/Content/bootstrap.tagsinput.less deleted file mode 100644 index face63f18..000000000 --- a/src/UI/Content/bootstrap.tagsinput.less +++ /dev/null @@ -1,50 +0,0 @@ -.bootstrap-tagsinput { - background-color: #fff; - border: 1px solid #ccc; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - display: inline-block; - padding: 4px 6px; - margin-bottom: 10px; - color: #555; - vertical-align: middle; - border-radius: 4px; - max-width: 100%; - line-height: 22px; - cursor: text; - - input { - border: none; - box-shadow: none; - outline: none; - background-color: transparent; - padding: 0; - margin: 0; - width: auto !important; - max-width: inherit; - - &:focus { - border: none; - box-shadow: none; - } - } - - .tag { - margin-right: 2px; - color: white; - - [data-role="remove"] { - margin-left:8px; - cursor:pointer; - &:after{ - content: "x"; - padding:0px 2px; - } - &:hover { - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - &:active { - box-shadow: inset 0 3px 5px rgba(0,0,0,0.125); - } - } - } - } -} diff --git a/src/UI/Content/bootstrap.toggle-switch.css b/src/UI/Content/bootstrap.toggle-switch.css deleted file mode 100644 index ce924d3ce..000000000 --- a/src/UI/Content/bootstrap.toggle-switch.css +++ /dev/null @@ -1,228 +0,0 @@ -/* ------------------------------------------ -CSS TOGGLE SWITCHES (Ionuț Colceriu) -Licensed under Unlicense -https://github.com/ghinda/css-toggle-switch ------------------------------------------- */ - -/* Hide by default */ - -.switch .slide-button, -.toggle p span { - display: none; -} - -/* Toggle Switches */ - -/* We can't test for a specific feature, - * so we only target browsers with support for media queries. - */ -@media only screen { - - /* Checkbox - */ - .toggle { - position: relative; - padding: 0; - margin-left: 100px; - } - - /* Position the label over all the elements, except the slide-button - * Clicking anywhere on the label will change the switch-state - */ - .toggle label { - position: relative; - z-index: 3; - display: block; - width: 100%; - } - - /* Don't hide the input from screen-readers and keyboard access - */ - .toggle input { - position: absolute; - opacity: 0; - z-index: 5; - } - - .toggle p { - position: absolute; - left: -100px; - width: 100%; - margin: 0; - text-align: left; - } - - .toggle p span { - position: absolute; - top: 0; - left: 0; - z-index: 5; - display: block; - width: 50%; - margin-left: 100px; - text-align: center; - color: #F5F5F5; - } - - .toggle p span:last-child { - left: 50%; - } - - .toggle .slide-button { - position: absolute; - right: 0; - top: 0; - z-index: 4; - display: inline; - width: 50%; - height: 100%; - padding: 0; - } - - /* Radio Switch - */ - .switch { - position: relative; - padding: 0; - } - - .switch input { - position: absolute; - opacity: 0; - } - - .switch label { - position: relative; - z-index: 2; - - float: left; - width: 50%; - height: 100%; - - margin: 0; - text-align: center; - } - - .switch .slide-button { - position: absolute; - top: 0; - left: 0; - padding: 0; - z-index: 1; - - width: 50%; - height: 100%; - } - - .switch input:last-of-type:checked ~ .slide-button { - left: 50%; - } - - /* Switch with 3 items */ - .switch.switch-three label, - .switch.switch-three .slide-button { - width: 33.3%; - } - - .switch.switch-three input:checked:nth-of-type(2) ~ .slide-button { - left: 33.3%; - } - - .switch.switch-three input:checked:last-of-type ~ .slide-button { - left: 66.6%; - } - - /* Switch with 4 items */ - .switch.switch-four label, - .switch.switch-four .slide-button { - width: 25%; - } - - .switch.switch-four input:checked:nth-of-type(2) ~ .slide-button { - left: 25%; - } - - .switch.switch-four input:checked:nth-of-type(3) ~ .slide-button { - left: 50%; - } - - .switch.switch-four input:checked:last-of-type ~ .slide-button { - left: 75%; - } - - /* Switch with 5 items */ - .switch.switch-five label, - .switch.switch-five .slide-button { - width: 20%; - } - - .switch.switch-five input:checked:nth-of-type(2) ~ .slide-button { - left: 20%; - } - - .switch.switch-five input:checked:nth-of-type(3) ~ .slide-button { - left: 40%; - } - - .switch.switch-five input:checked:nth-of-type(4) ~ .slide-button { - left: 60%; - } - - .switch.switch-five input:checked:last-of-type ~ .slide-button { - left: 80%; - } - - /* Shared */ - .toggle, - .switch { - display: block; - height: 30px; - } - - .switch *, - .toggle * { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; - -o-box-sizing: border-box; - box-sizing: border-box; - } - - .switch .slide-button, - .toggle .slide-button { - display: block; - - -webkit-transition: all 0.3s ease-in 0s; - -moz-transition: all 0.3s ease-in 0s; - -ms-transition: all 0.3s ease-in 0s; - -o-transition: all 0.3s ease-in 0s; - transition: all 0.3s ease-in 0s; - } - - .toggle label, - .toggle p, - .switch label { - line-height: 30px; - vertical-align: middle; - } - - .toggle input:checked ~ .slide-button { - right: 50%; - } - - /* Outline the toggles when the inputs are focused */ - /*.toggle input:focus ~ .slide-button,*/ - /* WHY?! It looks awful and it seems to bug out in FF */ - /*.switch input:focus + label {*/ - /*outline: 1px dotted #888;*/ - /*}*/ - - /* Bugfix for older Webkit, including mobile Webkit. Adapted from: - * http://css-tricks.com/webkit-sibling-bug/ - */ - .switch, .toggle { - -webkit-animation: bugfix infinite 1s; - } - - @-webkit-keyframes bugfix { from { position: relative; } to { position: relative; } } -} diff --git a/src/UI/Content/checkbox-button.less b/src/UI/Content/checkbox-button.less deleted file mode 100644 index 51f54c7d8..000000000 --- a/src/UI/Content/checkbox-button.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; - -.checkbox-button div { - display: none; -} - -@media only screen { - .checkbox-button { - input { - position: absolute; - opacity: 0; - z-index: 5; - } - - div { - display: block; - } - - .btn { - .button-variant(@btn-default-color, @btn-default-bg, @btn-default-border); - color: #333333; - } - - .btn:hover { - color: #333333; - } - - input:first-of-type:checked ~ .btn-primary { - .button-variant(@btn-primary-color, @btn-primary-bg, @btn-primary-border); - } - } -} diff --git a/src/UI/Content/font.less b/src/UI/Content/font.less deleted file mode 100644 index f20b10dc1..000000000 --- a/src/UI/Content/font.less +++ /dev/null @@ -1,47 +0,0 @@ -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 300; - src: url('./fonts/opensans-light.eot'); - src: local('Open Sans Light'), - local('OpenSans-Light'), - url('./fonts/opensans-light.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-light.woff') format('woff'), - url('./fonts/opensans-light.ttf') format('truetype'); -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 400; - src: url('./fonts/opensans-regular.eot'); - src: local('Open Sans'), - local('OpenSans'), - url('./fonts/opensans-regular.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-regular.woff') format('woff'), - url('./fonts/opensans-regular.ttf') format('truetype') -} - -@font-face { - font-family: 'Open Sans'; - font-style: normal; - font-weight: 600; - src: url('./fonts/opensans-semibold.eot'); - src: local('Open Sans SemiBold'), - local('OpenSans-SemiBold'), - url('./fonts/opensans-semibold.eot?#iefix') format('embedded-opentype'), - url('./fonts/opensans-semibold.woff') format('woff'), - url('./fonts/opensans-semibold.ttf') format('truetype') -} - -@font-face { - font-family: 'Ubuntu Mono'; - font-style: normal; - font-weight: 400; - src: url('./fonts/ubuntumono-regular.eot'); - src: local('Open Sans'), - local('OpenSans'), - url('./fonts/ubuntumono-regular.eot?#iefix') format('embedded-opentype'), - url('./fonts/ubuntumono-regular.woff') format('woff'), - url('./fonts/ubuntumono-regular.ttf') format('truetype') -} \ No newline at end of file diff --git a/src/UI/Content/fonts/opensans-light.eot b/src/UI/Content/fonts/opensans-light.eot deleted file mode 100644 index 3c203d8e71dc546f24fa65f03bdc93d8a409a56b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19762 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 BB=7(L diff --git a/src/UI/Content/fonts/opensans-light.ttf b/src/UI/Content/fonts/opensans-light.ttf deleted file mode 100644 index 0d381897da20345fa63112f19042561f44ee3aa0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222412 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%0AG%b#%#Weyp=oz<}IGPqm z)8c4aT%)1Wt!Z(MiMFk2@iJ>#98HU(X>l|yj;6)Yv^bg;N7LeHS{zM_Yxd#ySkvN~ zeHg82+}<U$rg3|h(3-~WT|#SGTqCldx2DC>v^bg;*NE&CYg!yli=%0AG%c<X*}t-; z#nH4lnifaX;%HhNO^a(p_A}PBIGPqm)8f8YLMhg?IGPsMT*=Q^)8c4aTw}LwYg$|* zxNU1%T;sS?tZ8wL=C-YAagFJ=t!Z(M?6$3GaWpNCrp3{;xJGuTThrobS{zM_qiNh( zrgUo>H<t;mY205Xw5G-N|0yatnikjpr`Wcpal4t&n#LVxLTg$aO^Yiwux(9?D?+es zO^XNCw0K}mi=%0AG%c=t)H2buIGPqm)8c4a98HU(X>osL)|1w>IGPqm)8c4a98HU( zY234>XK0^jS{zM_2iCMWnifaX;t^|FJYr3YqiJz8Esmze(X=?47Dv<KXj&Xii=%0A zG%b#%#nH4lnifaX;%Hi2agIETro|QQ*tVv{(X=?47FXosbZc51O^c&xaYaE+x2DC> zv^bg;N7LeHS{zM_qiJz8Esmze(X=?47H4HFj;3+zoZe<li=%0A#YncTY1}|3q!*)U zaWswF=#*|vi=%0AMNqaKpTyBL?x&M&P2-k2p*1azrp3{;IGPrZTGQfDYg%0Y&tkNu z#iQ1=c+{E}k6P2>zA-tt2Ti*NO}ht8y9Z4R(X<dv3(>R?O$*VqkT@+w(?T>YMAJev zEkx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNz z(?T>YMAJevEkx5oR&hf#Eo2opMAJf6aYHmMMAJevEkx5oG%ZBaLNqNz(?T>YMAJev zEkx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNz z(?T>YMAJevEkx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNTP7BerkT@+w(?a63 z5KRlwv=B`T(X<dv3t7hw(X@~_Ekx5oG%ZBaLNqNz(?T>YMAJevEkx5oG%ZBaLNqNz z(?T>YMAJevEkx5oG%ZBaLNqNz(?T>YMAJevEhJ70(X<dv3(>R?O$*Vq5KRlwv=B`T z(X<dv3(>R?O$*Vq5KRlwv=B`T(X<dv3(>R?O$*Vq5KRlwv=B`T(X<dv3(>R?O$*Vq z5KRlwv=B`T(X<dv3(>R?O$*Vq5KRlwv=B`T(X<dv3(>R?O$*Vq5KRlww2(M0MAJev zEkx5oG%bueP7Ber5KRlww2(M0MAJevEkx5oG%ZBaLgKU#O$&+BLNqNz(?T>YMAJev zEkx5oG%ZBaLNqNz(?T>YMAP0U?OF`pC)G0Er8SoKNhOTUp#J+?b_&#gf6GpT`tNVq z`tNU{{`)(4pIVDh9@l&1apQk<ihOGPP4M4??*Z=y?*aAS-+Jc#p#J+?w*E_9sQ*$I z>c7;3ZIp8x<=jR&w^7b*lyjTZ%+I)-+oWbjmvftx!RT^sqnz6)=Qhf@jdE_IoZBep zHp;n8?M=_Cr5Rn$ZE9mimvbBC+@=;~+vVIwIk!>H?UZvn<=jp=w^PpTlyf`f+)g>S zQ_k&_b35hSPC2(z&h3<QJLTL?Ik!{J?UZvn<=jp=w^PpTlyf`f+)g>SQ_k&_b35hS zPC2(z&K;C<2j$#BId@Rb9h7qi<=jCzcTmn9lye8=+(9{aP|h8ca|h+zK{<C&&K;C< z2j$#BId@Rb9h7qi<=jCzcTmn9lye8=+(9{aP|n|_oWDyseS70_;F}qZZlAuH(P+Q> zW=5m^?mHQc_Pg(76jDy#qPQ6N7Dc1mr*Ba-x_$Z<MWg-hTNI7<yKhl6x_$Z<MWg-h zTNI6MpT1{sQRsf;dj_52a{3NIqs!?#1dT4I?+`S)oW4EK=yLitKq2MyeSdlz<@9}j zN~fH@@6Wc&>HGeSE~oGNGrF9<@6YIR`o2G-%UPvz>P!ul)9BTtDwTFb80WA4_FbhY z#po~HRf<m*g<e^$D)X9iRoPz9UmU6wpBNtmy|P_Zwh#Mb*t#`FDG!5YyGoomuV%0X z)U7c}(XBB;-5Mio<H>eVx5g-?6V$CSvb(_^(5u{4taDbe&RNAeXBF$5RjhMXvCdh= zI%gH@oK>uIR<X`m#X4sd>zq}rb5^m=S;abM73-W;taDZ=0&(s+a2%Wfb!&{$y#`(- zKic+6c$K_q+w0*~iaq>WuZmYG_OR`>@hZh0w!K1LrP#yvtJtrBUMH_&rL>Bb(kfO; zs}y@U-RtC4iaq=*e=Dg{>|y*N_#^O5@CNu}(BD$46nhwd3H%lC4$y75O0kFk>Xuxk z*u%D0+N%_M*!FsRm0}Oum7rT@m0}O0+g_F84Wrv$Rp7Q)rFg^mAHZ5O!<uasYqnL2 zEBsg5mEsE7YX54VYX8PZNYSk^vK{AFDRS^LULmhi<RDwULw{v;w@Q7)_HX&G>M6Fp z&R?beVq3Sy2)%Y%rCwt@#MZ4bvc2+9r5<E^8@6tZk-Z)JUTl96s8XMDy4PK+)U#~= zKDNINRVi{XCP051s#4@&{18Z=Qsm&0e}t3=u>TnQPq6E-KZ;HNQsm$_(7zNp*rtE6 zl3b<8!Ef`oq$))Yw!K<grO3gyS8J;jIoS5sr7A@Zw&75bgY8d&2SD8#qm<8px-~}j zXN4cs3`nP|mw7jlZwMU`dKUqAg9aaD)%}C=ol;nP_dSUtf$!tf>7RPiXwBIv&9QAS z>||!NQ<`Jj9PX6n=)@msj`4nQFKDiIN^{N$b<&7XCmslO(uhze9td^Ph)}n&2z48a z(Cb>h0nj<>q!FP`8WHNG5ur{R5$dE7q1SVEGWXfZ+-E0qpPkHob~5+b$=qkB=048r z+h9MalSY&>2o8ZqK%F$A^kGmZjmSO*W<i}aqLh=MP8yN@UGOwG2I@8zrRPB1#v)s{ zu?Th2i0~ZG=%f+ZI%!0xlSYKo*hOljlSY){m7JYY9^13nuV8y6XQ!0M>92u5055=x zpiUamUv<)mP$!KDy*j;9%42*3)JY?<y*j;9vmT>Yr*~@BW7KUd!A>cUQMa)Ob<&9N z4*ylN9=(%U&rT_i?F!O=75iP-zlMD$ztTw~dd9ter<BL2+gOCUjYW7bsFOxy>!cB( zZetO;C+^f7$LRj(8y>}#w8%Nq-k9U;lomNfx3L7i|54AVB^w{{do&{3)=48muaxeT z7TMNGBY|&+)L*4VN|6@X)=48mucGgi7TMNGBSM`tB8-!wlSX8R*g9!M_WQ7P(unMB z*g9!MwoV!m-ixi1Mr7-x5ur{R5$dE7p-vhR>ZB2&P8ty=K%F!qTPKYOKLk>DW-&XZ zMNao>^G?lTY<snNr)Dv>KZ^YzNWWtivr}5+zk0QKr?kknSDSZAi)>r5cS?(Fd$oC| zw8%ESQd(pi4y8r5-{w#!jmSO#>NXbHI%!0x+gOCUjV0KH7VScde3RygQmjS0P?%k4 z(Jr)T7h2>S3`c?lT9iPG5<KZ!^K>dPTI5^v7G+zDd~2T3{P@;9qqQhe=C<Km^ZZw9 zk#Eg2niJoeXWN|k);!zRBHx;4+qL%HdCt*V<h%2X)*|1XXS5di?mR!)4tj0bcjp<s zw(Ps}j9y#z-FeQ-TI9R)jMgIGooBQb`R+WUwa9np8LdUWJI`n>^4)nxYmx8HGg^y$ zcb?H&<h%1+3u}?@&NEtze0QG9Y%TKLdA6-ZzB|vhwa9npxwO_I-<@aMTI9R)Y+H+b zcb;u)k?+nkT8n&lp3z$5yYr0JBHx{7v=;g9JfpS9cjp<cMZP=FXf5*Hc}8oI@6Iz? zi+p#U(OTrY^Yl)%$am-2wifyBJlobH-<@aMTI9R)jMgIGooBQb`R+WUwI~r-i+p#U z(OQ&1ixOy20xj}wdg2N#@@;y4zvFG+rf1t)lt7Doo1UITixOy&Z`0E=XpwKzv+aK5 z+w}A#TIAdGY+H+bo1SfJk#EzpZ7uR`dQPzxCD0<@rf1t)<lFRYTZ?>~o^5N9Z_~4F zE%I%8wyj0JP0zNq$hYYktwp{~&uA_3ZF)v)k#Ey8{*h~q7Wp<k+twoArf1t)<lFRY zTZ?>~o>TT=)9=tC-=^nuYmsl$vu!Q%ZF;t?MG3UXx9Qoo7Wp<k+i-{$B@`E#No$dB z(=%F&e4C!pTIAdGmWA&FJ$u?M^)Pxa<J<I%p3C?)J)`F`zD>{Qxr}eqGkSL5+w_c{ z5%@MeqhovDre}0K@7wf@j@5UoeHtB~`!+ox5wdU7lPwO7jw*edp3(87Z__h6hV*TE zM#qi5P0#39(YNU>2i$ulbnUqJO6c0{p?2JRCEK;*-YcPN$GulV*N%IygsvU;UI|@0 z?!6MacHDa<bnUqJO6b~g@0HNC<KC-)d#?iSy%M^1+<PT-?YQ?!=-P4bmC&{0-YcPN z$GulV*RBTN)lj<{DZf)(yBca&gYRmnT@Ajgp>{R+u7=vx;JX@XSA*|rs9g=dtD$x^ z_^yW9)!@4tYFC5rYN%ZezN^7^HTbTE+STB@8fsUA?`o)B4Zf?Pb~X5}hT7HOyBca& zt3ALCL2a2%ZV@`(u2oHqx_LzCk*-$tHR|LRp-yfQ>f{!oPHqu;B&ua3swFC}WhAO) zB&ua3s%0dq)ox5bZ=J72;cL<MT2#Fjm99mfYf<J}G`SYltwnEZ#fE+>HjLioQX9lU zo!lZ@C$|W7a*NQ~SgXC7PS?pTLY>?a)JAl2i|`lytH?V*_vl(wr4~J@MM-MWkXkgP z7WJq_H)_SFpSM=jq7t>}LoLcs8-2>}k2ZihxkdKZl?(T#34d8F=MR;;@g2hZ*u!~0 z@yY$fC-*DfSPbqL+vkMdIefp^Htr_fJ7DiG+XFU{(hRnMDX<kxgBhNj!p?&QquA5? z#h%eSuJ0FnM)G0@?EUP3y+5cSy_R3Sh`j-N=kWbvRVY@Cp4r_`9Cg3O9p$3&#rT_` zcU<4E(ZqN+cn|nq@crNiz<a^p0e=sC%I_De#-~B=9DbPA`!KEd;oyLtd^k7=Hi+|w zgG1u~;oxafo>AEz4jP?;_aDal4@W+xlzRBDhyVKEVLe|TxK-2#Pk^5J)(1~w|7Yw5 z<zBD%JN<K{KZD%}e#HnI^{`Q|T$Tgh3X~Fhp8KeBF+K)90lNH;Di^1_7LTgr=Y*g4 zZ-c+k=<{gsUsa1ogD;T&Mer}dFY(vE#`YT8qd^OHE0_k`z-M{#Yv9+xZ-CG7*KcBX zV0VJu;Pa&SV837_((oNdDs9ATJAW25sLX!`Cx0fMl|rpQDIYBcpOlY;V&3=;;bZvj zF?{zJzI%*XKc@FI1drjn$MD@_fqnNFzIzPcJ%;Zd!*`G4yT|d}<M{4zeD^rMdmOJl zj*lM4Bah>e{rF-(-(>gWk^TDZqrrat*7yYIF@3+@;FN>dPm72B@`%&_Lgm~qk2vKE z;1^ZS{lQ;ie~G_-SvB1s*f0BoCem9#uV?NL(%5a_S9$VTer50M53C#egKvP(k@8LK zcAo4Yr4#H1pXaYV*e@6(-Wjz&;+;|Z`Qo-8ukFWc`{gy2D&n~434HMczIXy(Jb^Es zz!y*8izo2K6Zql@eDMUncmiKMfiFI#-!2EA(r=A!y$A5l0laen?;OB82k_2;z&+~# zJ~|M%XB|+E`W3C{0A4$w9G&hKbs%uhI)EP!;Ku{Bu}@RxPgCYklkca4ztNMQ4*t9F zNow~bwR@5$pXA9Wsoj&*?n!F*B(-~z+C53_o}_k9QoDoH<sfxANL>z6mxI*hAayxN zT@F&0gVg09bvZ~~4pNtc)a4*`IY?a&QkR3&<td+#89e0^GJ~g-OONm|@Cne2J*8Zn zZsmDO?-BZ}OW`|(4YchB+I9nNTUw|#tX>7WZ8wOAb3(W62C-q=ZM%WC-9X!JplvtM zwi^Ps?FQO*L*TaE5V&nO(6$?B+YQnjzrk(0fwtWcxNSECZrcrk+jc|Xw%tJ6ZlG;9 z1a8|6f!lUN;I`cmxNSECZrcrk+jc|Xw%ri8Z8rpN+YPks2HJK5ZM%WC-9X!JplvtM zwhzI^A$T|>9#X*}@nE#-9-=)Q5)Zbmx`${#hfv)^sO}+D_YkUkNIaa=GgjS0;-N?A z-hD_6j0ml|hs1!<s(VNb7_GX8sQV$+-L_Ttkm~NgT6GVp*0!y>hg55$Rrio;ZQH7Q zh&mpkj)zd)r&YV2;Az!PNSl0`Hu*Gd@@cinb4vHf^)zkrX|>4_*@sB~oZ8aUYK?xz zJ?m+`U+>ZTjUQ9`=iujaF!MQ>`5Zm#bM&ld;Px4~eMY&D1kWgU;}f9O;2Gua6l=mW zl>ZsZ|9O>UG59>a<MZ^0&(r=Jd1oW<Y{XxU_^T0rHBz2N{MAUQ8YxvH{%XWujrgk( ze>LK-M*P)?zZ&sZBmQc{Uyb;y5q~w}uSWdUh`$=~S0nyv#9xj0s}X-S;;%;h)rh|u z@mC}MYNWo6)VC3T{YCH<t#5xpinJ_rH24MSjM35H7nMFD{AD9Ne-WO)sArU-XGVfA z@rEyprIFyvVo8YazO1}tE3c8DS$)4JXcilb!Y_e;4L%G04fu8N8{mHjzX?8Xj70nf z^)KTu2z{s9ve0F2q0B9mIi->;1}T+9s4^JeDfFFLBSP1umAbT2msaZHTct*VG$l+^ z!n8PFREkTL7TZRbCoNVNRp<XpJ@hMjn^3=67Jdn|3Vj8I`HJ4)bl2`HdXMdIfUezF z^fsqFZ&Vrd9+e>#d=)0Y3X@-j$*;oXS7Gv5YW*y=eip@hmb{*YfoEahYdrHcp7|Qj ze2r(m#xq~znQthU<=`92MX2{1-zoeizx^h^{U*QtCcphAzx^h^_02oyf_C|IQTT6d z!&^JNwW~cV9roH`uO0T<VXqzb+T~3>BX1gi0ra|-@4-12bg2xBLPuy_%Gc-!?Rm=f zJY{>HvOQ1Po~LZjQ?_0`GZOUb8DY>XK6`^f@QB{f8+hk@Z!nJiGPYO$dV?#ZTm`)r z*Bjgb-!kg`dYj&F{4DrI|2FbvY%6VVqzilj90I@NH%Eqf!%3b$h3!?!-pFa}zr`NI z9_RT9>`BtkVpGnDXH313^Q2$E{%3FvTn9J6P4Hj963_qKcGPiKZ}eB361_t__eQPN zz0q>)JDq#<*Y%`t1V{<}e;mFSKz5LEdeE=u`vU*}LSHa~?RxbEu1jBV2FyuM`hp4n zHkjqFUMcMh=6H|y-}VLbJoy^9z+Zm=x^{iR1)lt0*j_>H3ts0}7qKsa-lN+muQ`{i z;5AaN^UgPT=7->qz&F7gJo#hnCGaNC-@^WT@z59ig!e3y@(<YmNO@MkRnl*RZ}GN& z#$Gq7RjWM20DZ&&z70X_sZ|?4<F`fr6HorR^No0X?~62$a)`frhTa!>hLryT{srh5 zzAs|s?2G&rPr8@&MLgH;i+HZx7x8}YzKG}AeG%{X?u$4Q>x;BuJLc(&{5R0b)))C3 z(jC?GMI6cWMf$)O`RlhpJRb4Ne4kpfYj+GA??;Yfzr>R#c-#NP_J6YUMNYaUL{9Om z)8H7G;~82=<ScKP0`s8P;rk-q-Q5@QI(%QGi2Z$jMO%u@k^U-NdDXoyLi>qaz^2Va zyqCK#LOY50|5N%RZ-7g@=N9-Adpv@#Bmc;6|2OIX#FPIAdj)$H`)AnJgucicb@6&m zU&QM<eUYF04bd{tqg7w@U-`+X$DqFGzxHpVUf1f2dYz{)`cCi;p7i+G7j@Ls7p-u; zqVK}??uEYSzvIbYcP`Q2z>e|Pck9UveKSKJ%+Loj>V1n!w~l2ZZfO}>PKI`pp@n2< z8<~h(MJD3*kcrrjnTS1@kzb6~oJ{0<;52CE$wbUjCSrav5i^rf-G!>7(OR96!Wcb9 zWTLLci=@0riqD@P34H#%5S8vnrTYV`bU!NHk4pCkR=@thD&3Dt_p87A8P~2KmF^Gh zk$zOVAC>M$rTZgR>3&qYKVp^cN2U8CR_XqTRk}Z7mF`ES`y*DfezjP??Yr1kw*H7! zx<6u-?vGfd`y*z!KVp^chpYYw<%F?*RJvcS(a%_=`_)>ER_XqTRk}Z7mF`!|F<Pbj z;jkZ-?nkBjQR#kEx*wJ9k6NYsQE8vmKN9$?ejywVz~KNK4#43691g(Y02~g$;Q$;C zz~KNK4#1(${MXyyZ~zVm;BWvA2jFl34hP_H01gM>Z~zVm;BWvA2jFmkk!}DE2jFl3 z4hP_H01gM>Z~zVm;BWvA2jFl34hP_H01gM>Z~zVm;BWvA2jFl34hP_H01gM>Z~zVm z;BWvAeXqet;Cl^(a5xBugK#(qhl6l92#14kI0%P>a5xBugK#(qhl6nF`w{d`I2?q- zK{y<Q!$CM4gu_8N9E8I`I2?q-K{y<Q!$CM4gu_8N9E8I`I2?q-K{y<Q!$CM4gu_8N z9E8I`I2?q-K{y<Q!$CM4gu_8N9E8I`I2?q-K{y<Q!$CM4gu@~9X9)cnLVt$PpCK`} z7!1MB5Gphz9+ZL>4WUItXweW_G=vrnp+!Sz(GXfRq}uscR-_@-&S+g4k}mZKJw^<v zrcQB>8$z9iP^Tf(X-M^Qx;1JDjT%CuhESv-6ln;J8j4mb20lXkbA-s}h-#-4BA+8f zK1Yarjs(_<BdWFk>b-VHRO@rXzr&uB_m2dxD#s&=XKcTYeHpw4TEUJe-Z1{O?11ne zu>X-atnjO!Ih`ot2vNilMG<;ld@9F?V}>J<r~RZ@b;@7y*B0y)b}M!o+jHt8iU<6L zuVQ<Kazv4UQ@Y9P2>24|CrJOF*rV9?!V&mC!nklmBeiNRFF3u3?a}#2<W)*#j~rnv zKEhahgt7RD#$u=cZ{GY**#8H61$!0yXV~`I5k}7=8a-`$^gN=`)3!&<BN{QqfIO(W zXsmSlZ-5>pk3`?Czxqa!2BF8_VfFMLp`)5%^>L?I&xh&b!|LNsze0+AG)(XI{Udr( z{o43hr_-;8>DR;b>tXe4r@sIWf#31l=-0#Q*Z!@?mtpnml<;rCah{<J^yOh%`!IcZ z7`BIL?ZdS8Vfytjt$jE`2_tXvPLCtQwDV#0U*)3ys~l<P!|K1bJt7aQ|LRHluWwOV z419}<P%V5}=w9ZVP>deUkE*39o&I~2{&<w0c$7A96z?CU4<41T{p4lvD(K#KOnquZ z==FhP%x90OJ=^}Wo;fD=jNb+S)~J%}S1R*jkflFnnU7@Ym02_=ORvn*E3@><EWI*| z7G=?*Y~cAwR{k9kdOng397ATAk7U(D8)SPvk_}!1Js-&ijwiE$=ObD5-;~hvk!;}D zGRu4<8+bmF4Ll#o297hcYBSEo^O0=e`A9bKd?XuqK9ZFZ8a*G$2A+>()vo-c=Ofv` z^O0<Dljl7j$p)T}WTg*&k7LxV^uf00BiX?7k!;}kNH(|)dOng3{u$fzku39(Ec1~p z^N}n{l0`|f%tx}!N3!U1mib7Q`A8OR$x2(43-gg|<iGGv&quP6zXZSJ(lQ^(M*bSx z^N}pto@G9gMd7o|N3!UAmib5)wa+pi$)fpL<|EmN=ObC>BU$Dn*~p8$)ANyR#Pg9X z^O3Cjl<LcTBpVqa-Sd$wV@EdP`AC)#BpdO3B+GmxtKR2dc|MZWNNV(aB&(6sw&x>R z^*Ez-AWOf?O6B|pk2P7ToNdoXvJu)6^O39+&s=#vl4U-URS&gIn?VD!XkZo%%rYOz zMm!(MMm!(MMm!(MG9Sq@AIUP7Wh0)CWEs)25zj}mjBDA5=OfvOqu(s^k*vma*TVCW ztaL!RGat!DJs-(3AIUNw$)c!P<|A1YHOqV?E3NR8o{wZv*DUjqEc1~pqhyx(NLK2h zT$qn!qgL9ilr|NN1m7<EVK5T;iTd3L$}@uI9H-A7r*9soZyrZ=kE1rni4%^~4v(WV z$B7e;)0U3YijLEMj?;3E(`Jt2>*IL&IJ$Hk<vfl$9Y;Bj<H6(j?l>BC9OXO?x5r`i zID8%_PB@N!9w$yXPMmO@S{#477BFy}IN>EU?IkqrB{c0NH0>oc?IkqrB{c0N>HKo= z5}NiBnl=g>qp&dw8>6r>3LB%aF$x=_urUf7qp&dw8>6r>3LB%aF$x=_urUf7qp&dw z8>6r>3LB%aF$x=_urUf7qp&dw8>6r>3LB%aF$x=_urUf7qp&dw8>eC8G;EyK2)i7d zmOd{E2eCb~J1wm?j)2EOM;fQ4+fMgBz0<_orvpcQr(xr?Mr1!T4SFBWX=$=?#R&7K zVg9sKOTUt8IsLzajyX@`k<)nObR<K{|KP9R0Z%$#JarmRosM`d>$GBQzlV~*;pxZ> z>2u(#{MC`?Y4vWSqr21U-Nqk-H+kkZ=$YVYV(-&L-KSyrbkyVT>8MBB)3lJ&w2;&4 zeNL(HZ#7;UW2C%W_84E~#`r2XCZ1Eln7Hi`j$oVHF|q1&mw!y-o&Rb@8WV3qzR->F zg>Fo|Ipt~4-}lF0X$&65sP&jsPH&US`ALs`W2&$5D(L!-QI|35GNv{&q7=`A##9&m zR&~+ys)g-$%RWOdI72TuLoYZ(FE~T{KSTRJL(4xy%RfWQKSRquL(4xy%RfWQKSRqu zLz_QCYd=GLa)$Wi4DI|3?feYw{0!~<3@!W&E&L2E{0uGp3@!W&Ej&jAk|P4i5rO0= zd5#DqC*~W1oS5$sdR~yDMmcJfBLc}0f#irlazr3GB9I&rNR9|37dQgR1&%;+fg_L{ z5lD^*Bu502BLc}0f#irlazr3GB9I&rNR9|3M+A~10?849<ix7q=?El81d<CJf#irl za)Bd|91%z^a0HSg0?7rAKyra2kQ@<6jtC@21d<~H$q|9%H2?M69D(ElM<6*OkQ@<6 zjtC@21d<~H$q|9%h(L1qI!6SOBLc}0f#irlazr3GB9L6f5lD^*BuCrG5rO0)jzDs( zh~{W5IU<l8Z6-$qlB4D1h(L0*pBxcLj#iW-0?849<cL6WL?AgLkX*zONR9|3M+A~1 z0?849<cL6Wtcd1ljX5Ha9Bnd31d<~H$q|9%h(K~gAUPtC91%#42qZ@Yk|P4iq1ZVh zkQ}<5BLc}0f#irla_XDr#1Tl22qZ@Yl2e;^x+9Pr5lBvbR^B86$q|9%h(K~gAUPtC z91%#42qZ^;$q|9%=ruVakQ{v{M+A~10?849<cL6WL?AgLkQ@<6jtC@21d<~H$*ISw zBt#%N^*h^M5zVRhskG{SM$hkZL?AgLklfq-kq9J51d<~H$q|9%h(N~at>g65arM*X zV4QwBPCp%|pN`W{$JI~$SI^(a)kDt-J%1lp-y9Ko{ywhWXWR4larH9eHPAD!arF+P z=kMd{9kxAxA6M`2uRMPrr_GPk=Es@8kE^xo8Cv+bTDYE6OLod%`3-8lwo}-izmKc+ zI^Fa4akXCCp1+T)-5NcAA7}nP9`XErTrJS)p1+T)1={xfeH?8ZM;ph{#&NYP)f$By zXZ}7;TN-EnK8|LN({jdXIpegPapv#iY8!s@Kk;VI-^bN9oQvo0<7ykWe{RIr<ILa3 z@%1?K_i_9?F8_)Fc~-w-{yxt9eH@>TGk>2Tx|krkm{4@F7);2g#zF9iYB3@II>ph& zMBou{B6taObTL76F%kH`045j{ClpsWeHwIJF+p50L0mCGTrojhF+p50L0mDRxI(`o zqL?6}m|(1&i2M)IzXLj=m{3Gv{9Diw#e^aXqoark@oaPyF+uz=A%>me_+f(hVIt!B z;6&s$_|Lp&9o#fB+D<UqP7pav5IIZ`IZO~aOek{D8;Bex#N=`?$;dUy2sNpezo-<i zJWLX6O)~OMqQ;X%o|9<oB#JtTmQJFglj!Fp$~lSRO`>&^#7mR3v`M0-N!r^aIyQ-d zO%i!d(z+&5m`Su{5>=TbE}Ep}OfoJ{GA>UtE>AKpPoe{pM4o4vahzqwah7M!^2|BX z&yjwP^mC;9cEXXsw-XB0mQq50BhCkoQS+?&=G9JYdsRQLcH+N!MJTVa)#$#T*XU{V z{439@Z=O}(JgdHWv8UgvohTQz6QjQo=UMg5v+A2?)i=-TP(I?wA+OQFZ~HE`zdq#E zew^;F4|%m8l|&<jZGU~p!*gCd+xEI>UfkOD%3GdQ-#n|nc_Pm|tG;>S&pfNXd9m&1 z-B0tZ`sUS^lp@B?3H^;YFWzkX>qDMZBi}GM68MHeAsiOqumFbzI4r<n0S*grSb)O< z92VfP0EY!QEWn}f@6$WsumFbzI4r<n0S*grSb)O<92VfP0EY!QEWlww<Lsi|W)2H* zSb)O<92VfP0EY!QEWlv_4hwKtfWrbD7T~Y|hXpt+z+nLn3vgI~!vY)@;IIIP1vo6g zVF3=O(X?qaZCV^I2h(WUw3xK*);Nu(O`~bkXxg+i&40C~O`~bk@H~yCP1B2~#kQaE zjA>e9nb9+*X_RdmWt&FXrct(Olx><B(=@$)8hx8a-=@*G>4;}c(_-6beVZ1;wmr9< zW~MMLZf$$UF-_l}M(d{0x@oj-n!Z1c;!UG?(<t6FiZ?CA^BX*4nuhslIG={?Y4NN# zqkGfn-ZZ*5jqXjOd(-ING&81YbZ;8nE5ddWwu`V`gzX}17h$^y+eO$e!gdk1i?CgU z?ILU!VY>+1Mc6LFb`iFVuw8`hB5W67y9nDw*e=3$5w?r4U4-o-Y!_j>2-`*2F2Z&Z zwu`V`gzX}17h$^y+eO$e!gdk1i?CgU?ILU!VY>+1Mc6LFb`iFVuw8`hB5W67y9nDw z*e=3$5w?r4U4-o-Y|ls^mV+7TgHZk2=pALV!QX3L=oR^*M_4Xzz9Nsv7H{W-9t~d+ zd$wQ4_Db?AinxqF0sjG9@iXirdWC&Nudt8k6|t?iiEZO&{G{0SlYfPs0@Gj{=zY1b zC{i+xg0J$t|HtPQ#WqH-GrXdBMyMFZ_)f6GPby+@%5Q-0R>~Z8pQG+`)P0V+ze@V6 zq`yk~tEA5pXU-F0&J$tI6JgGykn=>C^Td|(M33`CiSxvV^F)U8#Dw$2f%8Ot^Td1e zDBC=mHXkX7?fFO%^yn~86gN-&Hc#X>Ps}!tj?ELT%@e216P3*qmA!@s&r{~}l=(bm zK2Mp?Q|9xO`8;JlPnpkC=JS;KJY_ylna@+^^OX5KWj;@t&r{~}l=(bmK93ror_ARm z^LfgAo-&`O%;zcddCGjAGM}f+ufzQ7F#kGp?bkIDEeEe_Br;wX8?US6wr_xM8S&BU z_~>=!fUj$mar!CH>&CBZ9C6B7a0>Lw@arnA)4jU;y2cLM{x6l+HC`BhZe%v}I<ukI zHBvaG9Q<|RCCYq>GGC(1mnick%6y43U!u&HDDx$aq52hNzC@WXQRYjO`4VNm6mgj^ zQRYjO`I30{uUzI!l=%{6zC@WXQRYjO`4VNmM42y9=1Y|M5@o)m5p6lRLchL3zrI4h zzCypgLchL3zrI4hzM?jx=hbG6?$=k;T8!@3SLoMQ=+{^1*H`G*SLoMQ=+{^1*H`G* zSJbZbTeT~r`}GyIE2I1M75eoR>7n23etm_0eT9B~g?@d7etm_0eMOq4C+XK$=+{@I zY3G8gj38GTL9VJ^i@{aZ$mm(mRdoI;I)7DVbBgDWSM@gIyM@>A={0<M4WC}ar`Pc5 zHGFyvpI*bK*YN2ze0mL^Uc;x?@aZ*tdJUgm!>8Bq={0<M4WC}ar`Pc5HGFyvpI*bK z*YN2ze0mL^Uc;x?@aZ*tdYztrot}Q3o_<|kTMVw#)34LhuhY}7)6=ih)34LhuhY}7 z)6=ih)34LhuhY}7)6=ih)34LhuhY}7)6=ih)34LhuhY}7)6=ih)34LhuhY}7)6=ih z)8ByMH(>Y;czXlh-X#4^(%&TgP10{DJr&$gx)6`tkVkrCUzNvi;E@}u>4@wbq`&2K zy+P0G4aS3F<3_}*Pd8Y7y1|;#4Lo&2p3>XoDdTB=I|jNZ-jLUve%9|74@Rr>4S2YL z2XBZCr&~#GXnn@G#vA-!;Wy+*<InvJe!PJnZ^(~&v;1iMb>R{<U81H-)O3lOE>Y7Z zYPv*Cm#FEI+TOWfiJC4^(<N%UL`|2d=@K<vqNYpKbcvcSQPU;G+kS&<x<pNvsOb_l zU81H-)O3lOE>Y7ZYPv*Cm#FEITCCsWnl4e(C2G1vO_!+Y5;a|-rreSla7$)zlbYUC zO_ya`?Qc@ko2sd8tNl%S8TV-h+@~q@H^-awvYYg>o7Cbaz3irHp<mI<Zc_4_^s<|j z{3gBZCcW$?z3e8v>?UQtNttib%Wl%kZqmzc(LQg{K5tRxTa@`0Wxhq3Z&Bu3l=&8A zzC}yEMN7U#nQu|%Ta@`0Wxhq3Z&Bu3l=&8AzD1dDQRZ8e`4(lqMVW6==3A8c7G=Ig znQu|%Ta@`Hl=&x=`6ra*CzRx;r2mxkpOXI5x6}28#o(uU!(y<^NU_XFvCK%Z%t*1! zNU_XFq47{WFEdgs2OcSw87Y<-DV7;2mKiCQ87Y<-DV7;2mKiCQ87Y<-DV7;2mKiCQ z87Y<-DV7;2mKiCQ87Y<-DV7;2mKiCQ87Y<-DV7;2mKiCQ87Y<-DV7;2mKiBl=xZzV zwH3U%B5(EtEA+J$ytzVOTcNM5(AQSzYb*4%75dr=eQkxlwnAT9!OJW3wH5l>3Vm&b zzP5tLSLkai^tBcG+6sMbg}%0e_gCm^D|ml}zP3VNTcNM5(AQSzYb&&Y723cGeQkxl zwnAT9rKYRYbd{Q}Qqxsxx=Kw~sp%>;U8Sb0)O3}au2R!gYPw2ISE=bLHC?5qtJHLr znyymQRcg9QO;@SuDm7iDrmNI+m71<n(^YD^N=;X(=_)l{rKYRYbd{Q}Qqxsxx=Kw~ zsp-#X)jy+E|BP1sGg|e}Xw^TXRo`Z;ysdV-7~E#8ysdU?+v`QQ87pryR^Dc;yv<m7 zo3ZjXW94ne%G->Uw;3yMGgjVK9rXs)(daSaHg&wM+S&Ggs@sf}w;3yMGgjVKy_C*a zd7H8FHe=;&#>%&dXWk;7d5d`FE#jHCh-cm+o_ULS<}Koxw}@xnBA$7RcxFv+NCj(r z7hBVNWV5=s#(MjjX44Hy@prMcz^hwptU9i->bS;tu{FMnt?^xKjqhS>Dv96YojGf? zs5QQetp)xrw#Ij{wZLm!Ybvdu^v;~Mz~4C5#JqoX5!*X+*5n1JcxTR<+NAOSC+Ype zqq^>M-<dt2t!FbL6~ciuP8CIw6-7}LMUG1;_2F=MPHO0no5pPvH)(r)@6A2O$2BoX z{o!K^JO&|Tdei5gCdLxPb_B?fSjJFkG6vg8WXBFBNt<6PQQ}4jaFa_Sgb<q1Z11`6 z`^i7=vz|43_S*Yh?|RpFt-bbMUYS!wnZ>kM<`h*sMn4xTroA$!D6RTPugod(T&$S( zOl*<oV#Tyq<`h+%K98S^6;-EBdu2{h>>0f>r^s`$qBQTb`MFq;=VC>kixqh;R^+)@ zQEcm9V%vC+&&G4HA~Uf?+DMV-Vnv=B6jdWWvd8D-xmc0sVnv>d6`8XwN?ksWBa<R6 zs~GGi?KcIA%;grPI;Z=|_nQL6V1V?apjU<$rAFs_WloXjVnv>d6=|VGo{JR&ugod( zT&$=ztNZ1-STUgX1Fy^}GD}>Ps(m)EGcQWpPWw%PqT0Cs<&`-_wQ-{(hoaiJ(_WcV zl>UugnNwswxu~|_Gk9fAQ7yu0k7bHH7b|Ke#QA<MR^+)@k>_GXwHoK68J=zwH8<k_ z`b~kNT9eTcSCQvpMfrwmoabUio{JTkwJxf~`N%nr^vawfGuTD7KBql1T$J1R`n@uz zD7P_sWloXjVnv>d6>**-&Qs)BNl|T9XW&^$Q7zYLM|4FTsmODaqFSQvi|1lR=EI9> z-9FMQbBb!?PJ3leQLS}Px<sE_qR%bS=a%SmOZ2%V`rMLQ(3NzFKDVTn;<QI+B`MtK z(OHQ;w?v;?qR%bS=a%SmOZ2%V`rHzIZizm(M4wxt&n-z&Ivag%i9WYPpIf5OEz#$e z=yOZ-xh4AC5`AuoKDR`lTcXb`(dRC4cT3#Y5?{(%(w8en(@T6WWhwQ3mAI7pFnBAt zS+&C+*}{*K{(W!@zy5FF$G|@!|Kp_ZB>ktP{|xK`J;T4mmqM3PyGic>{r2%vYA@)K z=~C*Sz|VtU03QPnfP<i)H81g{(52L2(nr9f;0WmXVfOD9`pFmjcMHEl`Wetq{n)=- z=qC&8-!1g4IQw@C{q%Dwbq4hO?NaJR(BqM%)C5PqO!^hl{!-{t>Q&OOapbp1pC#?q zx0E_Z`rD*E|ID7?La%UOPjI1oF7^Z$di@N0f(!kGWhrItEv5b)^q74q<%oSrUt%%( zOQB0CNAFAeMwZdDD@*Ar(C;%YrE5UXtFXhk(6cM-GcH_3y3S?sH5>LESK2FWmeTJ4 z-vu^+tzaA24t9W@;734zDRhZ1g)XK4D`);F`RFQ*u4pMsw3H>j6uP7@ljvW3DRhZ% z>9FIua_;tj`BLZ-UkY8y_)DQn`XZLDnJ<MdW!^*DUkY8~OQB1dTgYi9=Y6F8rO+k5 z6uOjYCH(>N{iV<)z7)EoFJd|UA@Vy&f0(qt6uOkTmGo`kI&eL>0o({~0%=ox3y6K# z_1~@B#UJqNACmqf(tk{v_LZT1WoTa++E<45#g{^tGN0hcF8=jN(sz;mbJCw8{b|yF zLHaLA{}t(9bNGLf^BM5Jf%kxZ3ctjcLbd06>QB<z^BweB<uY+m87I;H=FTS~(k|zu zy?Qyub7DLv=ExY&iSe8m&x!GzSl=wVqT|g^oU%4zJSR?B8?nC5=U?qPamt*;DRUC1 z%t@RwCvnQ0#3_4DoU-S{DSJ+g=frqUoU-S{DSJ+g=fr#!D#ml-lszZrt5C7NhUBz8 zC+4eAamt<(<2f;&6Q}Gsamt<(r|da#%AOPJyL?7_PR!SdV>~BL*>mEQJtt0$f%cp@ zl?UxPamvpzV>~CubK;aeC&qK)lszZLbK;aeCr;UO;*>on#&hD7Jtt1tbK;aeCr;UO zVmv2K*>mEQJtyYt#4(-|>$`l;x97yENzk4Xr~J%4#&cpkC&qK)lszZLb7DLv#&cpk zC&qJPJSWC;V!los<2iBKo)f3Nc0I;(;&d%(drq9T=frqUoW6;)JtxL<;<P;{=Ig{U zUnh?FI&qw~=fr7yPOR_p8SOc7+MW~RIWe9Sr|mg$8eQQzF`g6SIWe9S<2f;&6XQ9t zzQU)g!gFFgC&qJPJSWC;Vmv3tbK;CWC&qJPJSWcBbK;CWC(hV&Vmv3tbK;CWC(hV& zVmv3tb7DLv&e(I}j6EmL*mGh$C(hV&;*32f&e(HeeTC0x&x!RFJ|k@k&xtcz$g$_d z8GBBgvFF4Ydrr(3$>I#{3(tx1oEXoE@thdXiSeA6uM@`^drq9O=fr%kEY8?-;*32f z&e(I}j6Emj>%=i%Cyw<MKIhnTVts|rXwQlD6+YvA!ng38xA2^|<RqiY@%s^P;UsV2 zId9=PZ{ay_sRl>W37(VSISEH5cus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPe zg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=n zBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeLY$M}ISFx2g6AZ7PJ-tocus=nBzR7O z=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=On~A37(VSISHPV z;5iANli)cCo|E7?37(VSISHPV;5iANli)cCo|E7?37(VSIZ4`{li)cCo|E7?32{z> z=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-to zcus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=nBzR7O=OlPe zg6AZ7PJ-tocus=nBzR7O=OlPeg6AZ7PJ-tocus=nB*Zxho|E7?37(VSISHPV;5iAN zli)cCo|E7?37(VSISHPV;5iANli)cCo|E7?37(VSISHPV;5iANli)cCo|E7?37%8I zb1HaF1<$GAITf6wg6CB5oC=;(vF8LS9l1xS86Kg&h%S6DX+@h#zaNx)E3H^jxVbVY z{IB3g!QThv&&t<Vh=m^m|AhRHlfIMmpOXGF%IzZkY5t|J5bIz13bAlEsIL$!t*;OZ z^%Y{_K92k+@blmoz{kJ?;2`)V${GeWccK3t0rgg((j(y0px)q9j-KNP^%Y{_SHNe$ ze+D(XrF=c*5o+z0@T=hS;A!sS45;;o`c-R9h58DyaDrpB#!P9w(<;<gh=rQd5WdFM zev9;3()tRq^3Rdh`$tOaS)@=a^M!i<NT}80Ld|jrHOnE?oS0B6_JvxpFVxJWP|v=F zKLa&Osq_U<Zxkwh5xfNIDNK-(^BDED7NNdEEYz&NP%Aown%fs@MTc+|={nM@N#8_T zUm@17`U<g7Um+IiE5t&5g;=Ps5DVME4zLsa2&mp)zupci22fgGAr_)Hd6CQ2SYC+U z<V8BO^sHl~WaB4%9(j?|pY-waA{{R;a#~*@4pQ<W{pzm}3-uLZ;k!xeE5u5_hqS&z zthByDEWCvreT7)*_mOTPt*;O(M_(Zpet@*TLag)$Nw<^!5a|xmA12*N`c~4nf$PBa z;0ACbxC!Lm{k>3~c?&sPN&f-qKP3G}r2m-o$3WVhyvYBe-N}ob{xgpJ1nDl)pCo-3 z=|3m^Dbln`d6DlC4*eBkrGL$#zVxN^XTbjk>MO*`(N~Ct`U-K7wigLC<`OFY5i0%> z+F{c6BB8z_CDfQpXfFy<zimuwZbx_vct3auJPMuxHG<GFKhZS@n$Z<%J(Y4aKk4)s zX^l{neu4ChPHQcbj?o;N)1Om1Lx~wm%ur$`?GiIuMP_t~8LbU7y2K16W+*X3i5W`F zP+~^&oRdL@5;L0HRGJbql$fE!Wt6y#5|>fpGD=)ViOVQ)86_^G#ATGYj1reo;xbBH zMv2QPaTz5pqr_#DxQr5)QDT++{QjWId;AH12V4(sNUc`RX5Ckn{Ctn_qu}p@TU3u# z_<0q6UZoh)$Jo!S6hk^~Kd({@X|$hLDTZ`ekAS;D%}6V!57dmb(oazCmpH>Pr5^^5 zfabhPE^T}o)OS3TJ`R2v{0jIC_|Kr;hte_Qpx(b$`m5mcpk2C3@uAT!U4=_m$)$Zg z6O{5Y=~qZ==2`i#lKvKFcpdyUs5xi-`mf;kK|QTd&LnsqoC1Fe{yQa3gEQbI(5_pB z>sH~qRk&`I{8nd^(;Dl*n?Sp374BLkcXe9d)f8$sWTAEr720X5aM~)Iwn{OcbL_NL z{@%OrHjZ59TF@8hjoJrUh!W(qVqPBW($PR#dt560m*Cyte+U1bBOj2?tMmmtV=p=S zN}<v@a`e`)(!0UEp!@Nv^nQ-~e>l=Du`2zK;Qs<22M==%deWp{_3oRHT28yKtkV3p z^QjBXZ#(^O90T*3-}dpp0DlSoin275ulyw#h{43D{!l#7gH)*p(J|^lbXDp*bRPPS zs^A{d+xc||_)z7Fe%(pFBbKVbu}YO@!j1m`o&-^;o>@3w&%cEFijnZgz862C5ZcYE z{G3AQ9=u9(=04uNc2!`PtJ3_rbMzcac-`n{3PP=N5$atVp?kwBJx4IEBIoU-e-qR@ zddk0<eC<Z9^l$l2)f+nfZqnL~TIu(Y)^5~FH-Wd1(@gq(q+3Y8pL8qf50Gvn{Xx?0 zq(4MjJGbg=A12*N`c~4naaHR;eN|h>=!@UNjo>D5Gq?r(1MrVPd`Uf>kJnc>g^mHL z6k{7d1^xv{ZPJTXW!#HZ(Ti32Tlz}77ptNdtD+aH%51YLGWUZ!K<&P&-ph^};;12x z8m8>1Vakpg;;12x8sey7%B?BXsC+``2sy-2)eDkuM-6e*5JwGh)DTAvanuk;4HX-^ zTsvxrqlSu&owlQf8gaW6J8GyAx6zIoYQ$}{qlP$Ys1di1JPz7XL&d#DJ8GyAx6y4a z#8E@Vy-wRvLyfqNcGM6@4RO>EM-6e*5JwGDcGOUDugkTghB#`dv9`}(M-4T;Ho6su zIBKZ))@eIxh@*yzZ=H6#4i(=T?Wmzf(?&aLs1dZ$jvC^qA&wg2s3DFT;;12x8fv7h z^Q7&lA&wg2s3DFT;;12x8sexSjvC^qA&wg2s3DFT;;5l^7E&$Xs3DFT;;12x8sexS zjvC^qA&wfR?Wm!?GHSG=hWe)Hq|lBU;;5m&BdW9=HB8%4LmV|s+fhS(N7QIX4byhi zFl|Q-anvwvM-9_<)G%#F4gKt1Xh#ik)DTAvanuk;4RO>EM-BDF&_~))LmV|s+fhS3 z6L(p5)DTAvHLI#tpm{7IjvC^qA&wg2s3DFT;;12x8U}XMFtDSBfgLr(QNzHF8sexS zjvC^qp`I-|AC=;$A&wg2s3DFT;;12x8sexSjvC^qA&wg2s3DFT;;12x8sexSjvC^q zA&wg2s3DFT>KUc3LC+|ScGM6@4K=>dUEru8jvC^qq2@ZAZ$}L^*Wt7sHPraRId;@g z;|r(lsG;UMoVKHe8eceVM-4T;aN3R<YOcd+J8FodhMMbe+Kw7(uESSlM-4UCVYH)$ zIBKZ54yWy?p+*))J8Gzz4I{pUqlP$Yh@*x$YKWtT8XfpZJ8FodhB#`7qlP$Yh@*x$ zYKWtTIBJNah8a6*=)GTq>XgS>)hUmvs#6{-Rcp4vsGT5$nk5$gj>@V|`Ch71zL#pv z3HX?Q0zVIGC4+Jv0}p_M;1Fjx2p$5z2tL7iUgMav;5ksM3v`}0z@LE^z>7wWi;Y@= zDO?4v2Hh`IE2=lX-{((jZd~|5P|x6$_FPJJ+H)z@+6_T>k#-$bdoKy0XH=@wpHU4~ z>zl(yeWPBuonLqGYY*wa1NU&ALmcx2zaAy+If`nn#xd$UNJ6c(5PrS#DWRRYS|bbp zI?Z{0eP2tYV=yrWDbRguwMNrAKKS2VqSmfB{h-el^pf7iF*$HIIelP1H~{VkkGXV> zVtj_@z*C^Up`;wov{dVj7?-GTC<$L6|3&g&qQtL}&%FfSApHvXD)<AAaSu|hwH!X% z95@fY#jo6*-g(h6UVR~S)Lrd07(&lxRBI%q>(NNc={vx!;2(m241Ns!IQXaFCqU0X zRBL|E_-PRKGO(BVKg?m~{{sJS@V|rq1N>`n8`pC`xC8v0t|ub)h=@HRVvmT}BO>;Q zh&>`=kBHbKBKC-gJtAU{h}c6TbDc9{G#3$jM8qBuu}4Jg5fOVt#2yi`M?~xq5qm_$ z9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg5oylN*WlPABKC-gJtAU{h}a_{_K1i* zB4Uq-*drqLh=@HRVvmT}BO>;Qh&>`=kBHbKBKC-gJtAU{h}a_{_K1i*B4Uq-*drqL zh=@HRVvmT}BO>;Qh&>`=kBHbKBKC-gJtAU{h}a_{_K1i*B4Uq-*drqLh=@HRVvmT} zBO>;Qh&>`=kBHbKBKC-gJtAU{h}a_{_K1i*B4Q8irDL5Cdql(@5wS-^>=6-rM8qBu zu}4Jg5fOVt#2yi`M?~xq5qm_$9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg5fOVt z#2yi`M?~xq5qm_$9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg5fOVt#2yi`M?~xq z5qm_$9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg5fOVt#2yi`M?~xq5qm_$9ucue zMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg5fOVt#2yi`M?~xq5qm_$9ucueMC=g}dql(@ z5wS-^>=6-rM8qBuu}4Jg5fOVt#2yi`M?~xq5qm_$9ucueMC=g}dql(@5wS-^>=6-r zM8qBuu}4Jg5fOVt#2yi`M?~xq5qm_$9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}4Jg z5fOVt#2yi`M?~xq5qm_$9ucueMC=g}dql(@5wS-^>=6-rM8qBuu}7BJBTMX&CHBY? zdt`|{vcw))Vvj7bN0!(lOYD&)_Q>i<=wOi5lTf2$kF1`A8XbFNQ;t2dDaRh!lw*%< z%CSc_<=7*urvN_2u}4-j|3=3iSz?c@X78PL?2%2mkIoW%WQjep#2(qSV~?z66tyD- zu}7BJBTMX&CHBas9eZSnJ+j0eSz?bYu}7BJBTMX&CHBY?dt`|{vcw+Qv}2EK+ObDA z?bsumcI=VWyrs)^?2*;%rPGc*vcw))Vvj7bN0!(lOYD&)_Q(=@WYfRCFUKBP?Vn|I z@0=y}$ZDqsr`>yIi9NE!9$8|KEU`yc@7Ven#~#_hu}3y=?2!!|dt?L09@)ULM^-ba zx<X=)EU`yct5u!v*dt5qktO!X5_@EcJ+hiv^)Ze;vRbX`QXG3^i9ND`V~=d$*drS_ z_Q(c~J+j0eSz?c@){goNjy<vgcSr1zCHBY?dt`|{vcw))Vvj7bN0!(ltEZ4UL&mX3 zHsjbMn{n)s%{cbRYKGtF*dv>9?2#q*$P#;GHHYsU#~xW?k1VlAHsjbMn{n)s%{cbR zW*mEDGmbs78OI*kjAM_iW|FnjiS#fj+^#WNgES!YJ{rNtQ`$8^sQm(hj|YB!^l{DS zPXu==e~-{JQFp4}G5#fZH~1Ny?N0SR&Ud+YN@Mz0`fqg3JJlZz3Lj9^dZ+i-5IzV# z1UlxtlfLOr^-VrT`=JQ?K<$U3wDv<0y58?p-(>t8_>bWK%dh{7^yf)yKNKBv0DPSM zLDEA;-IuOY_hr<&!JtcT4csr(YF?qAmUhWQjM~pm=s3SiPGa;Zrb`|&DqIhG{L;lU z%`SO}kMT3jF7Ib2^fS#ad5F=^G`sMME}j^6$t#@Wr-WTRAME0JUl&jMx_HLdC9lx0 z@(QD$+;zz-jDA|zg;#Xp6<zWHT@^mig%5P`l&veRRn5xRs%Bwgq&;>8q3<Q|Ok)?% z*SdH<)g^uE%+jaPPouh|Potkhb@2qMOZs%$Po27?PotkSbxEH_tF8;xb)mX0RQE~0 zoe_LeJPTF2QSX}vcS&8NLO)l!OX`{w`aE|jsxfMnj?mh^EB#s0_p2W7LcMpTyUFh% z-%7qK-EUMa>ujoJqqY54sXO$1>aS9t06&@fjM8_3e-3^Me94$@@-gXV@VCh+gKrsw zTb&<#1pGAk7skwI{cGkQNdKeoZk6~M;U_?scsC{9opOnHr(ELQDVKP+N;EcuF7a-y zZ8W;XyH%p`BjBgOzc5nb-73-Pe-z#W!}q}OJurL^4BrdG_rmbK^7$*ly^7){h5Cwx zP%8$7$H7tX1o#}kj*<2=mwVGMkbVjLI`}fjyb5YXpN{z<c+E&H+)FLo8*KOSiqM?@ z0Qev|yGZB2C&+n<w7z1YBgb5>zpEhBS1g35Ipzh@T#3J{pnSKcdlkzW^^F4I4@heb zq0&>}PeDH`zc+Z3V-~<ga!Ozse9P$XDhNZc2Goi{<!G&-Q0sPtS{W#OCpkAO&7NOE z>-n!04^Ikz2wpS73A=YGy@Q<Z8g(|~&BFUskE6oKsQOZlYRc#*nD?pwHNH&FPe8lG zeX4QgtHzBFg1f}ted<q*$2i8de4kkLF@DN<pSbm}`T~;B^?RRsN#jc#Ls??d$G>G% ze>W(68@R?t@{Dquu6a+eP1h{+J3&IvOKeO3RA=7icX@<*OIPSPVVmDF3%2Q+b-b?G z=)Pl{--Qu+c63|tDEK7kd4_HD?AwB`^55sdGyLkFeVgAh6KaL3@SEg&ZN@ggZz9xN zy28t(e*qRbgJ+w!1u<xc+?J{GubFDlvkKej)3?!&f7b6$1)o)PWV9MTtGLKGZB$fa z>;-p&ePBO00PY8$15bfp1HS>j0=^1bIiFPoVw?l#!NjOhhH(YxKH{^wPUB6&?W&Rc zh3?n3OGSgiqoC)JwsXzfr6uRT3|dRuxr^<pBc)Y4I#RV`ypNm*eWc>FQK9?m?ScEa z?ZFT@0(v#hcEx7ScRaQorEMo3+fF>TT}tyY=54!_=Cm2uuD0y7dDyOa%xSN&*)9bd z&Bk_rPg^(xzD2&-*q+INH7?O_{0ZMi+MH}xJJlJ`>JFIM0W&)^R-FuXXq0N40<GH} z=yr!jtj_lniXG^7heof?`6Xx#?|`Kpu(U&?QT>XNcQ6j!p>e2ltm+-8dWXiOPVWWx zf!6sBjZBT!`3{Xwo&IOgGif`ddFMP2TJ1Z;m`nc#XtnR~`-Z|FlJhgtS3x_^4vkEW z*8GmZn%{xucPNfkT4PoHs!^(OrP|{TY03C@ay)ysLu1v+pd0qOVXqrqb)&0p#t7Z$ zsvBK(qpNOKK6In2ZgkZRW8E;;jjp=oSA#(}y6Q$(-RP<tU3Kd#QYu#t=3}g@Zgka+ zuDa1xH@fOZSKa8U8(npyt8R4Fjjp=URX4ioMpxbFsvBK(qpNOtoy)SWy3tj)eD8jx zt*dTy)lFOLMpxbFst0CzV5SFM^`NUBbk&0&_ModCbk&2ddeBu5y6Qn!J$PdeEcL)r z54!3>S3T&e2VM1`s~&XKgRXkeRgdO6CW9XQvj<)EpsOBq)q}2j@X{W1)q}2j#M_|$ zWnJ~4s~%jm2VM1`s~*jM_{`Q-k7hiK)>RL>>OogM=&A=@^`NUBbk&2ddeBu5y6VAE zd(hS2h~X>2--z2Q!QYYoJJJsjy*xmF`T)J>14I-LNG(@_2c#CGBZLQt5FXHfCxZv{ z-^t)X{r7%htx+-0gz&gr>p{(2Iqm4;LHg(i>7yUiOqBkmQgpmZF>d$q^wbY3y3p~8 zE_}>+@~6O`8s)}D_rDJ+LeP=)=?^M4aN0flLsG~Up=02O=p`SLDxCf)=w9+6slquI z!5c;?!RToDq2Ng$quO+O9CQ!(km}AkUjx4Zy4QP1wdQ>HdJn0NoYs3-!Wc}9^mh-@ z-#w%{a*p1^60Q~Y!hbLP_o}@shv>0at={RUb>?28$6m4O^a;?>W3L!?j-$t3qQ_o; zl}qWbgN`11{Z%fZqsLy=g3-}qFLm8ZUH1|__7Xkz5<T|HF?1d@(2E9o1NY0lfuqM> zd4<vS+lw}O(MB)PW3ODozj7r+kG({Xy+n__M322hkG-m8pVR$9ujT-ajvjl79(w~v zkG+AT$6kMxOIQXSJ@yhk_7XkzqRCz~*-P};OZ3=F^w>-E*eiwT3kWD=Ckok#LUy8% zohW1{anw!}vJ-{ulvn6qC}bxJ*@;4SqL7^^WM^Q7>_j0uQOHiUZvV>)*@;4SqL7^^ zWG4#Qi9&X&73)Y8vJ-{uL?Js-$W9dEohA1K-dR$p=;(^@u$*L<RB^v>l=K+*Resg` zjLP>4+g<5zk=EB-m3GV8B@H<39(fnexeK@4rMq^H``=yunyb*g@Gi|E`)vA}t59Ea z4ZN?Vu0+Z)x<}rns7%L5MY<lTNJlC{bIwkF-A&s4@-EFdIp4kWF3meRZ7<yw43ORr zx~Jaduel2KHCLh4z6*EVrI{$F-FNSz_t{0ivrGMsbM!S=;j5%yBkg&@T~eK|$`Q^k zW}$aUb-vo4f_B|qiUWOSd+#nqgHGG8c4=11Y5VXl`m9~dKkveOcQOCG3kTjM?Wrtj zPh}}e)W7iIT{!SA>CnI4tY06d)jv$De;9{+7>9fqhkTf}`7mwsVZ8BST=8KX?_s<y zhbnTYB8Mt+s3M0da;PFFe;5pMilwgzt%@9~$f1gy${kgXRgps#IaHBD6**LqLlrqx zkwX<ZRFOj!IaHBD6**LqLlrqxkwX<ZRFOj!IaHBD6}iBw$kD6jP(=<^<WNNpRpd}b zPIHnz(yGXziX5uQp^6-;$f1fHs>q>=9ID8niX5uQp^BV(+X<b=s>q>=9ID8niX5uQ zp^6-RaSm1FP(=<^<WNNpRpd}b4prn(MGjTuP(=<^<WNNpRpd}b4prn(MGjTuP{kvt z;t^Ew2&#AlRXl<!9zhk4po&LO#UrTV5mfOAs@TmLc5{Z^oMAU-*v%PsbB5iVVK-;k z%^7xchTWXOyT^_O-aS^x8TN37J)A+$ex1)56uqiO_Hc$hoM8`V=;I81oS}~%t&bk9 zj~=a$9<7fatxqbQ4EpHN`uv?qrQM_T(WCXzqxI3F_0gmCDdP1p?$P>mZASNKeY!Rw z*Vf0i_0gmC=?b0i`H(((v_5*YK6<o1dbB=zv_5*YzQ8?NA3a(hJz5_<S|2@HA3a(h zJz5_<TAx&;s}%!2{|`vJN9&_U>!U~OqettbN9&_U>!U~OqettbN9$8Otbfs?_0gmC z(WCXzqxI3F^@(AXLXXx*kJcx)^&KC2w7v9bd+E{k(xdIAN83w}wwE4lFFo2`dbGXt zXnX0=_R^#6rAOOKkG7W{Z7)6AUV5~>^k{qO(e|pns9buqz4T~%>CyJmqwS?f+e?qO zmmX~|J=$J+w7v9b{V1d#h4iD4eiYJ=Li$liKMLtbA^j+%ABFUzkbV@>k3#xUNIwec zM<M+vq#uR!qmX_S(vL#=QAj@u=|>^`D5M{S^rMh|6w;4E`cX(f3h757{V1d#h4iD4 zeiYJ=Li$liKMLtbA^j+%ABFT2^Yo*TeiYJ=Li$liKMLtbA^j+%ABFUzkbV@>k3#xU zNIwecM<M+vq#uR!qmX_SvJZvqLm~T6$UYRZ4~6VQA^T9sJ`}PKh3rEi`%uU}6tWM6 z>_Z{@P{=+MvJZvqLm~T6$UYRZ4~6VQA^T9sJ`}PKh3rEi`%uU}6tWM6>_Z{!0UfXh zbTEKI22jWV3K>8l11Mwwg$$sO0TeQTLIzOC016pEAp<C60EG;okO34jfI<dP2s=#& z>@+Rhu2wmKLIzOC016pEAp<C60EG;okO34jfI<dP$N&l%Kp_JtWB`Q>ppXF+GJrw` zP{;rZ89*TeC}aSI44{w!6f%H922jWV3K>8l11Mwwg$$sO0TeQTLIzOC016pEAp<C6 z0EIkCdwi7k_$Z$Ls5qGn9>wz?rPVx2t9ca9e-zJu6wiMY&)?6i-F{~6_NPzlzx$b~ z*iZbhpIN*8%-Zc&zpr1_;~PC|w_m-y(X)2@)u$UhYqy_%d_Vp8e){qK^x*rYF8?|U zde&~g`eLK!6ZSJ}x1U+N{nC|legnKo_<4U3J@~x8h#q{NUq8>UU!aYAfj065l>Y^k z@13|u1MkEwlny6_M?udOKPE*PUnb`#pl9|T(;1YnGZ;Nr{FpQ|A@p4FW73I_@l4BO zQj34}T=8SljDPi9@ncepb6x~JSNxbXWAt3{W2$SR>UU7+x#GuEr#fD3XHpT8=ZX*L znuWCY18VPsN_(#O0PX#N>Sa`E&lMkFKIDL|S;y;|jeqU)1fEqp!0g8X<~k1G0|#{F z`j@WR=(*wpn%^*b9CSd}?0nA^A5h&HJ$rUQahK6!q64Zy=X=zAAn=IefLy|7@Vwap zxrEVk#RqVS1GvNi)tAn!`Z9XP>~Ze_B&;>cANB}+ZI8<noc2ihak+uh9*sUOZTr_B zg4c}R14x(!x0AC2^wZVHY2A<0ejlf`KCafPvq}H{?|IN~6h5wYYV_#!aoXVHw86)j zwRoIai^tUl^<T9b9jVsm^vy~SqP;=1H%NUA;?INl^C0y#h(GH~mb$7z{CN<S4x-XQ z>SYjr9>kvqsf$7Uc@Tdd#GeP@e^C7EJn%n=KM&&1gZT3x{yYftgD^jcKM&&1gZT51 z9AhvT@|To^9`y`q&-oUiX9<Q>7o)<X<aqu3kZQy^R{IbxF@#GD`5S1;@mO_8I}$j3 zj`X)d*Vm9nDE`;KaOQsnzsIlN2Y&#%PKVMzB0UNI8~-{_`p2MWlZMi7fd9^Uen!5& zey02jq`gjlDD8FfL+Q)ln;iKcq<_JG|39wt3OT<d$7{TY<d#0qJn8Ekvp{+gEE$8q zXA3-+J`^kitNkyztMMM6E%+ZC=_egSfuF_>sV5SL@?Ia)L(bpw>)(_1v;HC6bx30? zpTQnGq_LILcG@A0t(>;s4ry%Vv|V=y*B#Q>$~k_rKP2Z+E$Ay}PJa>foY+wCCD5~c zL;A{@@oA1c27ZMc&-)DpCqO&-kVa=dv;BNXqcf-Vtuvv$Vkmfl^w+qrZ-6iJUytR7 z<QqPl$MQpR52x)OL;A9r(;mGJ(GrLBWi#iX2Wi_Zp#il}^X8TLLu#SMpYbm{&rt9m zAoZwkn)wXwQ-(Cw^O@bV3~8+AwELGKjrE+qK{|0M`huDMr7xHntDH}J8B(kFukJO5 z)b^eBv+^N&oFRIgA-REb>;^;nf|<|Y9%@KqL+7tCGa4Z}{Z7zcaZv4ULg@L=gK{&Y z$L|N_X-<1q=V03NX$Pfqquuo&WA=m6r;br?Zp?!B_*d!5Ii9OI$awr9Bk_Zb!4ER- zKFBEhAmi(U;@ro3&g3BD;)9Hc4>A@$$oTglBj1C%I~~br_aLL)gL1ry;E){87#Y>8 z8izH{b4Y!w)1J>fMBjQyeXG;%TMrRS9FhkrE%#Ht+|TG9_7FYnA$r(D>S3LKoFhG^ zJ46qANIk6oTL51m-#zRhMP)w2*GRjcJw()Th~D;)6y;;w=N=-)I7DQ1NVTam6JH%t zojSb+bT9lx?(vJ<;}^NwFLJeCL<3($1MFNMJV8vt{`E>bcJlu96GGp$cd;K8`mWi_ zUid##zbAZ19?rh@LbrGJwHLaLv#-6-G1U|B!0z@+yCpsW8&9ZraN0e!ce*#e;&SP) zpHRDV+P(G@V!*$;b+GHb&@IBd-YcIwVyAnht>Gt#L!Oi}uLMs@lg26V4bU_2PfD4_ zUmE3m#+{(PWUlmHP~VGF`X9iP;6H<&hkueD?MYpm(?0{Rf}Ra{Qff4Me)UOd(wGGu z6Fv$5PfD52|4rdj#K2Dx13yK?`xFuHQ$+hu5$!)kRR0uF{ZnY*DQf>q-Vq=ertcf3 zHizlshN;bAYIB&{9Hushsm)=c;$doYnA#l1!-uKOVSIa-+8oBKhpEkBYIB%4d6?Q9 zrZ$JE&0%VDnA#ksHixOrVQO=j+8m}fhpEkBTyB`!9Hushsm)>d8KyResm;UG=3#2{ zFtvGD4s#_qOk{GH+B{5c9;P-AQ=5n7Cpw1OJS;zP+O>IDeqwZO9wt6HOl=;fHV;#q zhvhB))wOw;+B{5c9;P-AQ=5mm+QVG!VQTX*wRwcPI6^HPp%#uX9y!7o@CZ(F1pbdO zraJ=jM_~R4%pZaIBQSpi=8wSn5ja1>IPM6JafH#_5ncaea73IB3jI|52&1_px+CYi z?HqyaBd~o$e&U?O$e85_W0oV_(Gl+G2>c)M9#6qheEuj7e-wv5io+k}UXF_YNgd-E z`=iXh9>vd(;^#;4^P~9rQSt9%PJ*6AKgwMk<@%4JmZRw8C|7<IpFfJvAI0a7;`2wj z@}s!@QQZD0K7SOSAA$c7_#c7)5%?d0{}HZv1pY_he+2$V;C}@EN8o=1{zu?{1pY_h ze+2$VxbhMBAL05(;C}@EN8o>iyBLB05%?d0{}K2f;Vwqte+2$V;C}@EN8o=1{@Keu zU>AF#d)TMp|7q@pUF?-({@I~k=>GI+_<tJy*>_$!=KpE<f13XEY3_nO<(2<Uezog9 z&6TrLywX?6@$-nM;s0r_oE_l<_JS9BEde{f2gl(582lfD|6}lf4E~RC&Bx&X82lfD z|6}lf4E~S7|1tPK2LH$4{}}uqga2b(`7!uE2LH$4{}}uqga2dP#WDCl2LH$4{}}uq z<1UWD|1tPK2LH$4{}}uqga6~`|2X<T4*$pD|2X$@9Q_}M|KsTYIQ$=n|Ksp~9Q_}M z|Ksp~9RAsBK46!5q4__K{@Gt%X~#Lo;r}@LKMw!wG9MgA|Hsk)ari%u{=ZBw@MY#1 zzRVqcnLGLlW4*61*82)0xUVpR^A7KWfp>VH6nZ@M4C5*Gc~{!wsb?5ZdAIjHN_#xz z-QJB~^}ncPc6(Ra@xwEWr@ZI8(~g<F=ey7B@f3T$3q78CChhT*_k1^cJmo##eWb@z z-t*n)@s#&`ciQ7A@A>W=kEht@UFh)?`@9Q1p7K8LMvtew!@JSrsb|t2PkC>5|JUOw z@9^#%kEguDyVD*|d53qWJ)ZJD?@oI>^-S91Dev&EvoW6X4)0ETJmnqUjUG>Vhj$<8 z@s#&=ciQ7A@9pkmJf8C2?*7%|DevuW>?OzJDfV+0dOXE`?m~~J*w0<)@f7>H3mtKJ zCwJ#~JoOCYDevd*w8vBI=PvYk%KN!H$8qm7jHjLnJf8Bd?#>wnJ)UA$ccI5q?CLJ` zc#2)!g&t2m!+454+?Dot%6qu`zaCF{4|k_Mp7I{<PJ2A%J=~r4c*=XY`(GYUc@K9Z zN?<(o4C5*9;coUkp7I{<{+Gv7?BOn?9vM%ud%JQxo?`cQp~q9~-Y)cbirw3V9#46% zc9qC@ioM#EuJ)N3PkFC)9nW~md$l|5Sk8O38$F)#UhO`Q$5ZUpF7$Zn8OBrItKDgj zr@UADU@%I%8>QWi((XnXRgDtuj54YkCFU8G+OGtoQoGTy>!`G9^r%YT%h5IH%Qv8P zsJBRcesnlWL^R4cYm~T1&(Qo|smSQKXjGapIxZSj%^E$<8WqD%I~E!x78(_AN)rW* z5(SMi&KlL7I^WUkDC4YW<u;!Ro|W4O(}#3D&oa(>Htm&)&uX0Y`@*lQ9-q}X%Q-(0 zGtc6v&!#UrEsxTF<x$25eWc>y`-T33&9fS3>0gRSedL(WrdZVJJoyFi1@J}kU*ZfN zXFaQ4!)J5U^sMwbA++~CD}6fc9`IT9AU?(s)U)a@oOZnQtYUAKrP^0niol)z4bonP z_^jg9(cpyc{eGd>XP(e~4GO)&;e_r>Y2BCdbzjE6c7EX5&J)abo?y1~gzn2ndY<z{ zV2?e)I<6CeXE;yL_nqMSPjKZYxaJdF?Fp{)gsxEM*R>fvWBHtFVN!S$^f>o9W{;m^ zl<^#+jOUmieolPqS4JGqG2(cRIpOCRcRa_q<2h!1PtvEHq)$1?EZIrjulDm`{_tel zGhHWj@6Pf3;YsEXPtyCGr1v?={NYL6uYT428to@1b*D~yv~^N<YP4gV)IB=wR)3QD z!;{P(o@D;;r0!k6G9Pl1_Ipx1ILE7=PKt*s!71XCQ}kS?=($c2pPZtOPT|$3aNtwK zB&Uc;P7#xwA|^S7)1D$GIYmrziW)gZjhw>qPT_T@sF71dAg72xP7#5eqK-}xdz>Qn zIE9~_!cR^SdyL5+_Gre+>Kc<jD9x3RsYf1E+ADCz)E7HF23jX$+|d|wn`62!{a3xP zkNGy}Ipi@pjQ{Id%`tl6G4;Ye=KG*m<c`UIeB_TvPlDRtK{=jz9#b!D^g5O?TJD%? z++}Hh2cheIOg*xX@eJpfdSs*acM!UDkEvgF`ZDN#c})GXbF{yM@E4rt3OT<d{VT5L z8aPk>0(gV7Es`!7<%c@I{Lr`z^s1pT`JwS1pH2O;k95C0MjISc@9Z3}BN~%i`p6z~ z{ucau@FCE>^O(HUN4n*X(Q?O7%b48N`F-T?CEZVYAL#+okAm*6$K<^}{tKMhZzGPW zr#3#xukNYG)Kfdh&N+tTjj4}zn%dW#r_-ME9FvFpY@TTulao8`InOb?aZFBPPM!zd z%Er{noa0&0F{#&RHy$IF9+S8D3?5OA$zhy!8yxfg4njwCW9li))xVNPKhnJO(TsX+ z{|lX{*LGU_I|$u=$Cw2j(^DALxcYAY>sO#htYhkTe5B_>$JF~c?U|o3^+8U1E_6%| z;OqCBP-AicqkHo)@9!WCz$zb!8;q&N>A&i~jCSBL^<u`A9O>sfW9rR~t9)PH-$Ce^ zr7`t!KGF_9rk>7euc{hTf43(Xr#By`Hy@`rAE!4Tr#By`Hy@`rAE!4TS4+7PjMJNs z)0>aen~&3*kJFov6IG7Wn~&3*kJFov)0>aen~&3*kJFov)0>aen~&3*kJFov)0>ae zn~&3*kJFov)0>aen~&3*kJFov)0>aen~&3*=kcOEE|kZG@{9}ej0p0K2=a2F(IAhn z=5e7sV}X47EcqS_<QWU(#mu16j`#9%9_KsW%cmXx=f#!t9slQv|MTL_IgbDH#Q%A5 z=$tn|NB(*Fj7xXqpC|Iq%VC^$<ew+z&&yjTmHq|)a^#;U^3RKZpY2zi!I6KS$Ujfy zpC|Iq6Zz+f{PU`jJwYBn$*V?mJaK*=Kgkp4=ZW+4#QAyR{5)}fo;W|RIA2#nFOye2 z`V5{&&8s$@wtM7Nr%u~F@~T;<?H+l0pS)_>Iga!5#QAyR{5)}fp4dK5Y@a8#&lA<> z<rqHRQGFiA$V(?aj~yc~wK(lr*}UAq=omgPA8`6L&=Gr{h&@llo+o0@6S3#j_I;#B zZFwU0yxzEXj^p-x;21AY^qvnqcFRkXF3}M}UhmyIN4rZ1-ACk!=kvt#dHT@2)a&Ez z33+MQX?sFmN_IN&zlh}XMDlsHcjrX@RqfsBENMsYd3xJC(R-fgJx}zWr@iOZ3j8n6 zfaTQ|oObk{Cwl*?TEuAZRkaAA^n69=$mFYP#X>1W`BI3{E5W`hu7oPZ=+#pN+Ejry zRiI53Xj28+RDm{CpiLEMQw7>ofi_j3O%>F$PXq<pRDm{CpiLEMQw7>ofi_j3O%-TU z1=>`BHdUZa6=+ih+EjryRbVt-piLEMQw7>ofi_j3O%-TU1=>`BHdSCWU7$@BXj28+ zRDscSfi_j3O%-TU1=>`BHdUZa6=+ih+EjryRiI53Xj28+RDm{CpiLEMQw7>ofi_j3 zO%-TU1=>`BHdUZa6=+ih+EjryRiI53Xj28+RDm{CpiLEMQw7>ofi_j3O%-TU1=>`B zHdUZa6=+ih+EjryRiI53Xj28+RDm{CpiLEMQw2uB1=>`BHdUZa6=+ih+EjryRiI53 zXj28+RDm{CpiLEMQw7>ofi_j3O%-TU1=>`BHdUZa6=+ih+EjryRiI53Xj28+RDm{C zpiLEMQw7>ofi_j3O%-TU1tQo2ZK^<<D$u41w5bAZsz946(54EssRC`PK%07=HuXGh z>UrAK^R%hwX;aVBrk<xwJx`l@o;LM7ZR&a2)M?t$X=cDq(`ruBYEIK?PAgKK3{Io! z(~3fsMqQ_Q4sx33Ag9sKX_Rvs&74Lpr_sr2baGlTnE&gD>om1|TJe?BFM{^Z)7158 zMwh2m*Z$R`%hQb1PE)6+;r}$upN8|(uzeaPPs8MCcss4AV=_2HEOCY?;tWy58KQ_Y zx}z)l)iVQU)b51D31^59&Pe~x@p%7?G;j2H|BO^>^mzY_^l9`s>x{0==&}D9qJ=X= z3ulNH&gcr2Pt0(JnBfdpdxon$L*#IV$l(QQ;RWU)U*MP*IOavhj4v`~e36mhi;M(c z;`o<1{w0ooiQ~V<@n7TkuW|g>IDSI;6TyV?g^X<{(nrP8gyt6Z2>mQ|f)TFv6DHqp z;!MDmc5Nl+hI4dIok8a``gyKa@srMhUNJR6jVns^nW<~_Z|V``zMxl3O;Gz2nw|C; z%;$vW8Jsq&6PizO+T2bsZk=G<I>ESgLNg5h)!a^Kl&v%jOlYj_v}c(n7;#UagqL;i zgTc$XcOiH8vc_MNO8dTE)+k%3`_iwvFQXOvGFSd`;Bn&18U+}w*q3!Z#w9Q|s`OEz z$DS`!;wxfxGI)i#f>)R;ctvbD$M0voA|8aga^u^C-=x;QNv(a8TKguo_A2LjmGivH zd0yo_uX3JOInS${=T*-08s~Y9^Ss7+UgJDxY3XNa<7a8(XVu0hgR?wkIICJ#n$~@m zrwnIl*Jo+hXKB}GY1e0I*Jo+hXKB%AY0+nS%5YZo=<|FX^nB4-Mn-2D6P@KL!&%j- z^F3xcOItomTRy8gbxvYrq<@x?{#k1FEOmO8)_oQ~d|j=3GI(9B+vuk}ud8(%eJ`)8 zbsPPZ=XJGiqo4A;uGVeb3;KPQ*VVd>e#-W`TDQ@6@w#+qv=_aO3%$;jzpj?<oU5Rp z^1QB=ZuFJEu9j}hf_}>Lx?1;SaE{h@j@EZhE%!6QIcoMCwQ!EscTUQjP`<DKoRq0F zZS5Rw?VQHlSCr!xcTRfHF~P&+<VZh4`cd#W|Fz!ENy9#mS5usml6?lRsW>M+JME_r z=V*iHxa)JY!E@aEIo$Xh44lJ_&%whv+U7afI7dr7$Gx9pzU!RoOXp<P>zr!M>9xXd zb6?-4ly7tXZ*zuk^WX2_X5YchzC(?FhZ_Gbdj2kY{w{j{E_(hR$A6FGzsK?4<M{7$ z{P#Kj`yBs$j{gzI|A^y%#PL7k_(_hR<am8^RGdt5{CVZyADmadP;NOW^nAj3)tYnM zcFwEToZ}gQ^SH`+?(saXa$aZCkvg04J|C~T(~+vXQDF{zjQk;R1oS-ZdHm<RTDt%1 zzVf_kaF5X64?M5gMIXa8sP3G$V$ZAYoVISy)9%jG?#`>eobPsbo_BlC^KS2X)t&RL z+w;8JdtNoD65;2(YS3wW>3Mmnj>Or13}Ziru^+?Ok6~;I#-?Cw3dW{jYzoGvU~CG; zreJIe#-?Cw3dW{jYzoGvU~CG;reJIe#-?~1dWyH9rvfuJ#mx2;j7`DV6pT&5*c6OS z!PpdxO~Kd{j7`DV6pT&5*c6OS!PpdxO~Kd{j7`DV6pT&5*c6OS!PpdxO~Kd{j7`DV zPhjjPF!mD|`w5J_LH--$zd`;R<g+h)z`pPS`@#$T#_Kfk-Zb&vv=~;7MnIE7zkfQN zewws<u4#>ij3+?9tuvjr8%(P%CWP*Pr&(t^jsH)pMtsbxphsuZY4^O-M3U1)lG8+z z(;8Fgzr>T%thAq|znG?nnx==EW-ab?@RZM>+A(^De40LMnm%hf@am~))r)_9iL`r- z>EJB*9nfRIX?l%mMe_dDK0mGK-RKp()2y_gW~Kc!EA6LQX+O<M`)Spej$xJWG^>24 z@u+FsX<AXYfAtu1TJ>doCpjK(UZ8d^P&*fx`@JB=DqoR<(Q~#Jq+X{zSARiKgphi? zKz&_cPV$13>3q-CUkDr(T#yc(cFc1@+B15d@PhQ_^kvdT(o0|rI@-CwT>S-R=PpQf z&hcFR1u5-HFoQy7P{<4lnL!~lC}akO%xJtk8O)%N85A;;wnAo5$P5aZK_N3JWCn%I zppY39GQ(J728GO^kQo#*gF<Fd$P5aZK_N3JWCn%IppY39GJ`^9P{<4lnL!~lC}akO z%%G4N6f#3xF@r*8H2TwBFrt}3Au}js28GO^kQo#*gF<Fd$P5aZK_N3JWCn%IppY39 zGJ`^9P{<4lnL!~lC}akO%rJVoh(a!+kc%kfA_}>PLN20^izwtG3b}|vE~1c&DC8mv zxrjn8qL7Oy<RS{Wh(a!+kc%kfA_}>PLM{>&UnDBNNK|}LzH}wHh(a!+kc%kfA_}>P zLM{;pULp>>L>zbtXTC%&U&4(q5eHtP)-DkTUJ7=L?MuXgmxu!|5eHr(4!lGhc!@ah z5;b)RcfCY?UBX>2;jWi(*GtsbCEbh9={WEbao{E5z)RG~B|P>Lao{E5z)N`SCEWEA zao{E5z{|vemx%)}69-;SzoBzp=3Xun2VN!)yi6Q;nK<w=ao}a*z{?ut>3HJ6%fx|~ zi32Zl7niyI%Yoy-%fvsIx$?`zftQH`FKfi-UmXWt=E^S<2VN!)ysS~4@`(fAq=mjo z3w@JT^Cqq4O<KpBw2n7v1#i*{-b9UWqQ?JV?c&c--Oo|o&(YG)(bCW1=jZS<ODVII zGK&w);sdkzz$`v6ix14=1GD(REIu%c56t2Nv-rR)J}`?yX7Pbpd|(zIn8gQX@qt<H zXqG#g#Rq2bfmwWD7Ddf+r?cGYEIu%c56t2Nv-rR)49vp7EIu%c56q&jS#&jv56t2N zv-rR)x|+oYX7Pbpd|(zIn8gQX@qt-<U=|;k#Rq0l-7G#Zix14g{4C7R;sdkzz$`v6 zix14=16Qe$tJKI<eBdfRa1|f8iVs}H2d?4+SMh<X)ZJC;?kYZT6(6{Y4_w6uuHpk% z@qw%Oz*T(UDn4)(AGnGST*U{j;saOlfvfnyReazoK5!KuxQY+Vp^!NgGKWIuP{<q# znL{CSC}a+W%%PAu6f%cG=1|BS3YkM8b0}mEh0LLlITSL7LgrA&91593A#*5X4u#C2 zkU11GheGC1$Q%lpLm_i0WDbSQp^!NgGKWIuP{<q#nL{CSC}a+W%%PAu6f%cG=1|BS z3YkM8b0}mEh0LLlITSL7LgrA&91593A#*5X4u#C2kU11GheGC1$Q%l}hC;5PkZUO9 z8Vb3FLaw2ZYbfLz3b}?tuAz`?DC8OnxrRcnp^$4R<QfXOhC;5PkZUO98Vb3FLaw2Z zYbfLz3b}?tuAz`?DC8OnxrRcnp^$kLGLJ&$QOG<BnMWb>C}bXm%%hNb6f%!O=26Hz z3YkYC^C)B<h0LRnc@#2_LgrD(JPMgdA@e9?9)--Kka-j`k3!~A$UF*}M<MelWFCdg zqmX$NGLJ&$QOG<BnMWb>C}bXm%%hNb6f%!O=26Hz3YkYC^C)B<h0LRnc@#2_LgrD( zJPMgdA@e9?9)--Kka-j`k3!~A$UF*}M<Lfy$aNHQ9fe#+A=gpJbrf<Pg<MA=*HOrI z6mlJfTt^|-QOI=^avg<SM<Lfy$aNHQ9fe#+A=gpJbrf<Pg<MA=*HOrI6mlJfTt^|- zQOI=^vVcMsP{;xbSwJBRC}aVJETE7D6taLq7Es6n3RyrQ3n*j(g)E?u1r)M?LKaZS z0t#6`AqyyE0fj7}kOdU7fI=2f$N~ykKp_h#WC4XNppXR=vVcMsP{;xbSwJBRC}aVJ zETE7D6taLq7Es6n3RyrQ3n*j(g)E?u1r)M?LKaZS0t#6`AqyyE0fj7}kOdU7fI=2f z$N~ykKp_h#<OT}4fkJMekQ*rE1`4@>LT;dt8z|%k3b}zoZlI7GDC7nTxq(7%ppY9V z<OT}4fkJMekQ*rE1`4@>LT;dt8z|%k3b}zoZlI7GDC7nTxq(6!rI0<rq7))j<atHt zwHb?2#r?uj(qo|KrWVuw^2wqi(h1?WNPDg0A~D{gG~l#n{1($52`;7`=`HH6jlTf> zJ)A}2y+yu!vM9~?U;ZA>qLia6k#dap_;|&-Dp4xZ^+-iJQW39nJX^RJ>?Z9w!$n2E z&iBmWqGDjD9XBp25_Z~OK3OD&T-38Lm*wx_EGjm3zGoK~6(KwAImSh?ZS-8tqGDsC z*L*H2LU!6~J{P4raV6FHsvIpZN_D<YuMb(2>WrR^TvQb8GkZ>QQL(hsjz||3Q9JFp zbW!ysR79<R5uGk7u6EjUm5b7z%98d}mZEF@i>P&xsC7{~^shJT*CMUHNUJZZZB7P7 z)r-+37HOMB+GbIFI>+<0MV;Arv#^9JN~ofQDoUuLgepp?q9lJ93`!cOToGCoB~(#D z6(yBBsvMuWgepp?qJ%0+sG@`_N~og5_i#$6qJ%0+sG@`_N~ofQDoUuLgepp?qJ%0+ zsG@`_N~og52%>~4N~ofQDoUuLgepp?qJ%0+sG@`_N~ofQDoUuLgepp?qJ%0+sG@`_ zN~ofQDoUuLgepp?qJ%0+sG@`_N~ofQDoUuLgepp?qJ%0+sG@`_N~ofQDoUuLgepp? zqJ%0+sG@`_N~ofQDoUtg2~{kiiX~LBgesO$#S*GmLKRD>VhL3&p^7C`QAQPIR8dA1 zWmHi{6=hUWMipgLQAQPIR8dA1WmHi{6=hUWMipgLQAQPIR8dA1WmHi{6=hUWMipgL zQAQPIR8dA1WmHi{6=hUWMipgLQAQPIR8dA1WmHi{6=hUWMipgLQAQPIR8dA1WmHi{ z6=hUWMipgLQAQPIR8dA1WmHi{6=hUWMipgLQAQPIR8dA1WmHi{6=hUWMipgLQAQPI zR8dA1WmHi{6=hUWMipgLQAQPIR8dA1WmHi{6=hTrqly?+#Hb=h6)~!aQALa@VpI{M ziWpVIsKWa`?+Gfb>h$i<N;88|K^qmcQ9&COv{7MZqatlg>PWw_UXemZh3Dia6;^dt zbR|BIS9MnCNh|4p<#?~^tfajvuafqhc|}+2BfYA#lJ*L{iWKF4c~xgc>p+Zl<BIxX zm*Q2O6)DX*o*}HHy<(>#)fv62v%;#*iu!0D=~bPTv{!Xj(r<FSS9Mm>Ue#HVN_`%$ z^s7jvPJ2~nMeG^9s<Wb%Ax5w2tZ03R(_YnCVO3{^Rh<=9b$X|4{R=%;q-ULtRh<=P zdn&Bztgx!Hg2F4R5g*^f+5Q&vs?Lg@`x|?GwNk#%^Dya0K+hCa)C!#6N7}18E6f^I z)C!y)Am6JxD{2cyuj;G>Ue#IA8WN*dbykR2D!hMD2^=|8Sk+lk45RzidJ^Y2il_u$ z)mc#s^4Ywqv!WK{v{!XjSk+lk%;SG~RcA%dzl@%btpq4Ut;}ces?JK_XF3(;tSV}2 zK7&_vR@By<_H1p1Rh<>uT}8SzS6<aw(JB<<4?#3TORUfmE40LlTB6V6ncfPkIxDQ| ztgx!H!m7>+Gh-FCQy)3UkzUnVVV10-cIvdBTvrsE`Ch!Lv!d9{=vAE+twJ$+RcD1b ztwNkup<P$hu5||5bw%yk>6IMmRh<>ucttHy_r<Et3Uh81wR9ipRh<>Jcc&d2RMcAc zWW4wKpis|@go-FKDYdl;p=R=hKP3Hcr1g%5a^3*75<_XN#1QHojZDg`I)!>iL%0mo zI~q!BHJb1qA1QtMNWF<H)S3;U)@%s1W<#ho8$zww5NgeaQ1566wN68*cQl08K)s`p zNr?@k-a8hq@{zhzr}d78@Mh&?xVsE@m*MU*e)B7nu_Fl&S4M^Ul7>)kehEj((K=71 zwTeI}UlPifgo<*7`jUoFE7OEpD=1XdCsfoY{1=XwJ1MP|pF*wt6l&$CP#P0z<)=_< z2!;Q~zqImGX}$R+)SF*ItsWH0k%U_RDU>4#wf<8$18V)J(pvv1lotv0=9lmn{8#Hg zmDU?WLcRGV{1s`fDpY!&wBGzudV#dw{8CzPeq}P$W`^3#P@5Tfk^V((W>lLxn^&(1 z?{&Fey(Zjl4pqxOvWFbK`K7ep{1WQTFQHsWs5ifaavq`H{1VD}gnIK!s1>0?z4;}S z^9bcRLcRGV)QV7{zN8`4n_oh``6YakW4;9H%`fE~1@$EjrS;~Q@GInKji}OEBPx{t z2=(TdQ2ry-n_oh$Di_Lag!+<(P;MiX+X%I4RH#vaP@@2$Mgc;N0)%oKA=;4J_-tAU zEYzD{LiAv_Q5p@%ZOofikP7wYmr!qh3FS7z{{X2+uWD0{X5fW-^Ghh75o*LF)JjpI z-ux2EXM}Q<jGBS}<+W|XD(Bl(g!+<(F!J$o6{oeHQ@D~}?FkvVigQ-+tKR%l`X=(< zPQJdRp`70&t+k^{>&>ssGPS!2VXYBAS>_l|Y5U1C$9O`G2Zb6B3N;cGYVC*6ezHt@ zGiE`J`;@kOEOX>0)c8)g7u1+eX{`wnYSbpwcui>cSmp>ysBxB1;~k;KR+(jvk%Ssa z3GE)s91#h%_Csj*SdL39_c{cn?FP$ngXQ?Za>WnM(W-3WMW<;C%e~f4>4)S(%TfDs zbiN#gFGt(UQT1|Ey<9z`|7#sCM}f=L>pA@*sI_)VYpt?SZ*mF0Nxs(FDg7F0jk1-t zDwd;%<tSmfYG3)(=5mek^(%F^Tz$c0#`{)Z5!&0UR2N2jdzJ3UXos&-jR<k?Dtx;N z-}WBU&bMz@iFu=ay9(c~QcH1K&q;-PLMpU}d*^7QJ={A-8}<Av<DH|8cJeB&wu-B* z!q2Pl^N_j-sfCbQ2vrM{nb5OOLd_>+Lii6oo20aTCWQGA=0lhdVLpWU5Y9t54?TmV z<2A+?a!y^p)2~R+p<@Z*56IEjS!uH!!gdJTp=XMelNi;qj4MF5rjR=d;Xm|KuuL`l zSHpib{8z)jcf<6r=D!;LtKq*I{;T1?8vd){zZ(9l;lCRGtKq*I{;T1?8vd){zZ(9l z;lCRGtKq*I{=F0BWX5|?8r`2(!~Y7+WesLlXf8{rnZr?`_H-6LEgh~<?`gE7uFwqO z{X%^KM)(r=b?{}5{}-Kkh1!mPb?>%<x?7<Zq#VtC>HM1eGHxg5Z+wjE)W2%gqEM?A zg-?L?{1uuZ)Rm|mIbSPqgjz!-%!36`yG1L<e!fC;hCbfC`3lv$(e=JU_3pImeMNAV zw4Ho~>fQOS_Z9MLr>DT5g6@S^;MFTMf9SOP&=s0LG`<B|6)QB0s2m)4h2|2SwhyoH zjH1xYukgI0(EP8^jH0ent#u-kh5s!4XNfVgaG2HjWipe6N$+Lmw3*Cme4#XaX5lkS z|LA?k^eg;iVZ%F%ImZlmH!-8{+B=9D-3MhAuNt*}QW%52qb&E5<u2GqOgV~0g}&w* zuDOP5u0b<3Xr>0u)S#IfG*g3SYS2s#nyFE{))_RbJSwziYPjDT?zDzGt>I2<xW^jq zu?Ef51lCLqnyEoEHE5;=&D6j{4VtNejT$sl11B|TrbaW&Dm}1fYGA1b&D6kE4VtNe zu^Kc}18+5GrUv$E&`b>+)}WahG*hGg-RHDsYT&j8&D5Zo8Z=XbW@>Pf8Z=V_&oyYK z2DWR^Obwc;K{GXIrUuQ_!2e44UkU#!;cz7!u0%5{VR9u*u7t^zXl5mRu7uB(aJ3Sa zR>IFp*jNb<D`8+I_r8+5UdjEg<i1v-nU!c}C7M~u9j)YER&p0Bb^ZGO9hzCmHNTDC z_HADEmw6j^_cre9H*_yoGQXjF5$Xz!T49x`<sNIf$67R2i^givSS=c>MPs#StQL*c zqOn>uR*S}J;indUYGI}pW@=%k7EWs6q!x|UqOn>uR*S}J(O4}StA)2(G*%0HwP>ss z4r|d^EgGvuW3{kai^gi<wib=m!f-7btA*!UG*%1SwP>ss&TG+FEgGvuW3_0kmb$1# zW3_0k7LC=Sv07@R7LC<XFSTf_mfERBW3|*mEgGw(E^5(OE%mhu{#U{ODmYvPhpW)o zDwtdaldE8I6&hOwpR3?=6<n=?rB(2=3N}{3!zvh9#l5fMu2*rttGKUKXlxZ4TZP6} zaYw7TmsQ-wDm1nVjjiIE>$v7RuDK4))S;O=G*gFW>d;IbnyEuGb!esz&D5cpI_|fQ z`>o?n>$uZ8?zE13tm7W*&`cegsY5e$Xr>O$)S;O=c&I}&b+A!~X6oRi4$ah|nL0F6 z2TOHmrVg&^&`ceS)uEX>c&kG*b+A{5X6oRu4$ah|nL0F6hi2;FwhqnIp_w`~Q-@~i zV7Ly=)WLHdnyG{BIy6&<X6n#P9h#{_Gj;I48va+q|7ti~4Tr1I%xaii4U?;3ay6RK zKJD^~)$q9*u2#d+YWP_V8>``AH4Log-dA(itGVCR+}CO}vl`8;Ml-9qqt)EYYVKk+ znpurzR&&iaX+$%cxk)1$p%ijOctX9*O;U)_(c?{?9Tj>Eb(3mb$EcQ#Iq(UN83XlB zf%5fEfzUGyH>r+{=gDz2ev@j%c**%<+ZYPpF7}Kbzr0=ST@m`ow~IHU)--0`F0S;e zxH3LT&KdA^@G@8gwO&!jXuV?Q?P9=K4LTZmJ9W24C64MFA08*JQG3yQ8fq^_k6hNk z!y5769P_Y-n0$@eh;x2Ij>ku9bX7WouFANboCm>OU=B1(Yt#z#U)8VEW28MMTBBBA z)OvWKd0Qj>`^XnbQ@WJzeDk?RYBy>ns?g)jHDc9hR@X?+LbzQ6w`)-E8g#oxiq)A_ z`;(cqD0VH1U5jGZqS&=4b}fosi(=QJ*tIBjEs9->V%MVBwJ3Hiid~Ch*P__9D0VH1 zU5jGZqS&=4b}fosi(=QJ*tIBjEs9->V%MVBwJ3HiihT#y^A67Z4$k}z&iM}h`%e1V zchb+klh*o9>iTBg>6OgQx>KR9-{^k!UCO^-s2ND%E#l-|srQ50X;kSClfD(yPNT}v zPNTy0;0DcUyvsX{3jYcGJop7rE9#Yh02~ChE3tAswtZL1_4uyTIq*B+8{p5t3!v5Y zuCx{VF7GrdTm`NMwJWi5ZUJxe^?0XI;d=0+9HW_7<!B~W=uz{#ywj*qZv+VS)K&PG zKGHjl3b%rP2>vnnG4SKypMswNKMDRh_-XLh=h^1-XYL0zZ?E*{RLXC`|8K$nZ^8d> z!GAsc*Ta83{MW;Oy?1)e)Wd(hcY0OY{MW;OJ^a_F%zr)n*Ta8(%KX=Rr&po*uZRD7 z@ARs)`LBondibx0|N4~quTPo(`jq*vPnrMvl=-iR|9beZhyQx-^s0RGUl0HF-sx3o z^Is4D^=b28pEm#X@Lv!A_3&R0|Ml=+5C8S>Ul0HF8S`K7onD3JzdmFB>oexRK4bpt zGv>eEJG~0ce|^UM*JsRsJ^a^ur&p!T|GVM;-SGcz_<uM2H^6@b{5QaV1N=9@e*^qC zz<&e$H^6@b{5QaV1N=9@e*^qCz<&e$H^6@b{5QaV1N=9@e*^qCz<&e$H^6@b{5QaV z1N=9@e*^qCz<&e$H^6@b{5QaV1N=9@e*^qCz<&e$H^6@b{5QaV1N=9@e*^qCz<&e$ zH^6@b{5QaV1N=9@e*^qCz<&e$H^6@b{5QaV1N^@S{@(-t?}7jKz<(qBH^P4-{5QgX zBm6hQe<S=i!ha+DH^P4-{5QgXBm6hQe<S=i!ha+DH^P4-{5QgXBm6hQe<S=i!ha+D zH^P4-{5QgXBm6hQe<S=i!ha+DH^P4-{5QgXBm6hQe<S=i!ha+DH^P4-{5QgXBm6hQ ze<S=i!ha+DH^P4-{5QgXBm6hQe<S=i!ha+DH^P4-{5Qh?d*T1R@c&-;e=qzu!G9C{ zH^F}s{5QdW6Z|*9e-r#S!G9C{H^F}s{5QdW6Z|*9e-r#S!G9C{H^F}s{5QdW6Z|*9 ze-r#S!G9C{H^F}s{5QdW6Z|*9e-r#S!G9C{H^F}s{5QdW6Z|*9e-r#S!G9C{H^F}s z{5QdW6Z|*9e-r#S!G9C{H^F}s{5QdW6Z|*9e-r#S!G9C{H^F}s{NDoqx4{1`@P7;Z zH^YB3{5QjYGyFHhe>40y!+$gUH^YB3{5QjYGyFHhe>40y!+$gUH^YB3{5QjYGyFHh ze>40y!+$gUH^YB3{5QjYGyFHhe>40y!+$gUH^YB3{5QjYGyFHhe>40y!+$gUH^YB3 z{5QjYGyFHhe>40y!+$gUH^YB3{5QjYGyFHhe>40y!+$gUH^YB3{5QjYGyJ~~{@(}x z?}PvM!G8<<x4?f3{I|e=3;eghe+&G#z<&$;x4?f3{I|e=3;eghe+&G#z<&$;x4?f3 z{I|e=3;eghe+&G#z<&$;x4?f3{I|e=3;eghe+&G#z<&$;x4?f3{I|e=3;eghe+&G# zz<&$;x4?f3{I|e=3;eghe+&G#z<&$;x4?f3{I|e=3;eghe+&G#z<&$;x4?f3{I|gW z`{Don@c(}Je?R=U!hb9Lx59rb{I|k?EBv>@e=GdA!hb9Lx59rb{I|k?EBv>@e=GdA z!hb9Lx59rb{I|k?EBv>@e=GdA!hb9Lx59rb{I|k?EBv>@e=GdA!hb9Lx59rb{I|k? zEBv>@e=GdA!hb9Lx59rb{I|k?EBv>@e=GdA!hb9Lx59rb{I|k?EBv>@e=GdA!hb9L zx59rb{C@!cKLGz9fd3D`e;fR_!G9b4x50lK{I|h>8~nGye;fR_!G9b4x50lK{I|h> z8~nGye;fR_!G9b4x50lK{I|h>8~nGye;fR_!G9b4x50lK{I|h>8~nGye;fR_!G9b4 zx50lK{I|h>8~nGye;fR_!G9b4x50lK{I|h>8~nGye;fR_!G9b4x50lK{I|h>8~nGy ze;fR_!G9b4x557h;s1m1|3UcwApEz(e>?oQ!+$&cx5Ixs{I|n@JN&o9e>?oQ!+$&c zx5Ixs{I|n@JN&o9e>?oQ!+$&cx5Ixs{I|n@JN&o9e>?oQ!+$&cx5Ixs{I|n@JN&o9 ze>?oQ!+$&cx5Ixs{I|n@JN&o9e>?oQ!+$&cx5Ixs{I|n@JN&o9e>?oQ!+$&cx5Ixs z{I|n@JN&o9e>?oQ!+$&ce+d3R1pgm`{|~``2mE)ye+T?`z<&q)cffxK{CB{A2mE)y ze+T?`z<&q)cffxK{CB{A2mE)ye+T?`z<&q)cffxK{CB{A2mE)ye+T?`z<&q)cffxK z{CB{A2mE)ye+T?`z<&q)cffxK{CB{A2mE)ye+T?`z<&q)cffxK{CB{A2mE)ye+T?` zz<&q)cffxK{CB{A2mE)ye+T?`!2gHg|HJVAVfg<r{CC2CC;WH9e<%ER!ha|Hcfx-s z{CC2CC;WH9e<%ER!ha|Hcfx-s{CC2CC;WH9e<%ER!ha|Hcfx-s{CC2CC;WH9e<%ER z!ha|Hcfx-s{CC2CC;WH9e<%ER!ha|Hcfx-s{CC2CC;WH9e<%ER!ha|Hcfx-s{CC2C zC;WH9e<%ER!ha|Hcfx-s{CC2CC;WH9e<%Fk3jepl|E=(UEBt>X)t367ncMjFHh#S= z<&pJmsk=aruWw6z3jDO@i*8GKF7CGUZ-X10lfE6?2$sqB{M&7mavP=Gmier5ew(t^ zQPw)jT1Q#yC~IBHWv!#Ebt#v%j<VKK*1ELIT1Q#y(k^RV+GVXvyR3DTwT`mZQP%HJ z)_TfXPg(0JYdvMHr>ymqwVtxpQ`UOQT2EQ)DQi7tt*5N@l(n9+)>GDc%KBZ(+CW(w zC~E^{ZJ?|Tl(m7fHc-|E%Gy9#8z^f7Wo@9W4V1NkvNllG2Flt%S-(eFw^P>blyy60 z-A-AzQ`YU2bvtF<PFc58*6oyaJ7wKYS+`Tx?UZ#pW!+9$w^P=CqO6USwUM$mQr1Sw z+DKU&DQhETZKSM?l(mtvHd59`%GyX-8!2leWo@Lajg<ADDQgpDZKAAAl(mVnHc{3l z%GyL(n<#4&Wo@FYO_a5XvNloHCd%4GS(_+p6J`Au%Gyj>n<;BEWo@Rc&6Ks7vNluJ zX3E-3S(_<qGi7b2tj(0QnX)!h)@I7uOj&oN{Cwe#l%Fr$k$SJRdWU>$Lijt<@E!6o z<L{Dhh2N3-J<xOJcciqASGXJ81NMP?LA}MJU%x^6WzZ{#?vO9({PHE^Z9Yc6WTdS0 z?~<mJ^nW7#U-*}wCfp&vGWw~(9r7!o{L1JjAb0Qt<c^@r$7kLJ)`JaTBiIBsgDqey z*ao(P9bhN8&F7b28TGwE;pcSaTd18a)Xo-aXA8Bnh1%Id?QEfTwop4;sGTj;&K7EC z3$?R_+Sx+wY@v3xP&-?wovqZ)R%&M}wX>Dl*-GtfrFOPbJ6qM)T*+*ucDANmJ6oxp zt<=uelxt^e%C)mK<=WY*zQ)J9cDANmJ6r$%>b^WYsv_<FR@F^s2b4_^ut5-I3AeLH zQOpV<X`0Y9U<@%$(n&gW`sQ|Z*jxd{-Qd0}DDLaH&FHv{=(x?ejXG}Qg5v0>j{7!l z^Ly*u>Q2;|`R1AD`TqF9legZx)u}pf)w#E->vkI|It_?U1ESM_=rkZY4Tw$yqSL@s zbQ%zy2BxCZz*KY^5S<1@rvcGvKy(@qod!gw0nuqdbQ*+;PJ>X<X%H$p4MIhyL8$07 z2o;?Mp`z0uRCF4IicW)2(P<DWIt_F;AzVbK0nuqdbQ%$zMntC((P>0<8WEjFM5hta zX+(4y5uHXvrxDR<M06Svokm2b5z%QxbQ%#IKceGDbo_{pAJOq6I(|gQkLdUj9Y3Pu zM|Av%jvvwSBRYOW$B*dv5gk9G<41J-h>jo8@gq8ZM8}Wl_z@jHqT@$&{D_Vp(eWcX zeniKQ==c#GKceGDbo_{pAJOq6I(|gQkLdUj9Y3PuM|Av%jvvwSBRYOW$B*dv5gk9G z<41J-h>jo834pf%cng5H0C)?4w*YtxfVTj63xKx(cng5H0C)?4w*YtxfVTj63xKx( zcng5H0C)?4w*YtxfVTj63xKx(cng5H0C)?4w*YtxfVTj63xKx(cng5H0C)?4w*Ytx zfVTj63xKx(cng5H0C)?4w*YtxfVTj63xKx(cng5H0C;NxZ%yE>3A{Cdw<hq`1m2p! zTN8L|0&h*=tqHs}fwv~`)&$;~z*`e|YXWag;H?R~HG#J#@YV$0n!sBVcxwW0P2jBw zyfuNhCh*n--kQK$6L@O^Z%yE>3A{Cdw<hq`1m2p!TN8L|0&h*=tqHs}fwv~`)&$;~ zz*`e|YXWag;H?R~HG#JvcngBJAb1Ocw;*^6g0~=e3xc;GcngBJAb1Ocw;*^6g0~=e z3xc;GcngBJAb1Ocw;*^6g0~=e3xc;GcngBJAb1Ocw;*^6g0~=e3xc;GcngBJAb1Oc zw;*^6g0~=e3xc;GcngBJAb1Ocw;*^6g0~=e3xc;GcngBJAb1Ocw-9&>fwvHN3xT%~ zcng8I5O@oLw-9&>fwvHN3xT%~cng8I5O@oLw-9&>fwvHN3xT%~cng8I5O@oLw-9&> zfwvHN3xT%~cng8I5O@oLw-9&>fwvHN3xT%~cng8I5O@oLw-9&>fwvHN3xT%~cng8I z5O@oLw-9&>fwvHN3xT&4M!m5_tT2|t9%gis-wb;=YzyoI%Cc$d95x-c412zV{Xtn? ztoHK~*p;xY%8EX9RGbcfE7@isHUqKQP>9VyY&H~Pv!M{14TabY#AYBiGlkg96k;<| zh|NH324XW;h|OFfHUqI4h|NMFwg9mOh%G>D0b&afTY%UC#1<g70I>y#EkJAmVha#k zfY<`W79h3&u?2`NKx_qKD-c_O*b2l}AhrUr6^N}sYz1N~5L<!R3dB|*wgRygh^;_u z1!5}@TY=aH#5N$d0kI8;Z9r@TVjB?KfY=7aHXybEu?>iAKx_kI8xY%o*apNlAhrQ9 zOg-*J5vCre>{NQT2~%%UmcFx*EWKk+R=vR#rrxBiS`Qzlex&Tfu#YH9{empDe9DJf zzOpOS9%}i@?gKj+c3;?Ou+w1=fSn0D8+Hz?54MPGCo$7Wb~&tSZ=GPK6U=minNBd% zNzBkuVn$ij-a5fdCz$CZW@ryFqwGr9R%O9VCo!Y^tz^4_*bT&P#G)IB-9YRHVmA=G zf!GbiZXk98u^WipK<ox$HxRoi7L-2_yMfpZ#2z5_0I>&%JwWUMVh<2|fY<}X9w7Dr zu?L7fK<oix4-k8R*aO5KAoc*U2Z%8s#()?DVho5eAjW_g17Zw_F(Af(7z1Jqh%q3> zfEWW}42Urx#()?DVjPHZAjW|h2VxwEaUjNl7zbh;h;bmsffxs39Efos#(@|IVjPHZ zAjW}cQwu*q*wn(6onp}EG1ShLzaO<y8*^(mX4P!Ws@X;jt-iC3I@tNJ3t$)G{KYgD zvyCR$CD<Q?zZ8B5{xTXH+Qtg_Y9)qEeM%v2hCLqk1lTRGC&H>#LpEmZY|Pr(n6<Mp zYiDED&c>{rZCnAYD`Dx)Q@ZLZSo#)F@~?rt7M8xTllEK>djl-Jmr8qXg1rOwPT0F( ze+~N^*n42r%5d9IYu;=_tw6I4wTj**1yD752<JQuU%fwK(_K~Ve-yr2*JK-y!&j@D zY~v5`)fy+;pkE##t5(|BhI(tvHs~Eyvh<EBS@qVKZO}WaWa*7&vh+I<WM6|-D|2k) z4Oq24$2Qa(0XAkSZR2hDyJ6pf{Z^Hkd0^H1RyNCrRV%4%rrr#-F`sEO^=7DzSxuXb zgs;~2*laX>wNlPz<6tMMD6`41GqGnj>>OAhY!Pe;>|Eq?2<)M-)ru>I_QufOFh^=L zv^Ta;<xGl7=a8Z*yIkc%x~lxcRC)LjDj(hkf0a5)dP_&eB(;ZBSNUG}li^Q+zc2i0 zu=`=pbol$jKLGv=_%q?pf<GI60sJ}eE%-k8h472u7sD@wEr&f2b}sCpuvM_AJ7#)q zu@HOIdNW%z!dI)FY_SNwTKi;U?$;LRcbEaT1^ON4fo-u2XR6guwpamQ&D`3e8NQlL zwM7ej^h(hR9~=sBC|nLZz+@+EH*61V3^q=-&&Z%>l0G8`R?TSj;i<I`3e(3k)qXq& z_VHZ!YOSmTEpnhm4$gF-MGmycffhN?A_rRJK#Lq`kz*(=atzfr97Ac5V<;_hphb?M zIB^W6MUJ63aSWwJj-j;3ffhM5|Ddi?TI4{B9J;?NUuls;_jhHL7CHC?!GRVz&>{y~ z<UorYXpsXga-c<yp|r>`lomPABF9i#<UorYLurv?C@pfJMUJ7g$T5@_InW}<P+H^| zN{bvrX^~?nEpiN{MUJ7g$T5@_InW{pTI4{B9B7dPEpq6YM3rA@kpnGqphXTo<8Yuw z4nFB{phXTo?{J_+4z$RD7CF!&2U_GniyUZ?11)l(MGmycF_acL&>{z)l{nBM2U_Gn ziyVAz;uuPc97Ac511)lx(jo_1<ls{k2U_GniyUZ?11)l(MGih^aiB#GQ(ELeiyWr3 z$bl9)OlgtBlomNmX_3Q}7CB65k;9Z0IW+G?C82pIWtA2=^!!G=L5mz{kpnGqphXU} z$bl9)&>{y~<UorYXpsXga-c;HSN(`qlBztRMYJvwzS1IxMnG!6(jte(LCRNJ<Oroj z4vmJCue8XaF_H3>7CAIBQohn6hsH<BS6bxIC`tKBiyRs&DPL)kLn9{TD=l(_(jrGF zEpmj?B1b4Ka)i<%M<^|Fgwi4hTI4{B9DMfVK#LrF0_8x99DEMtK#Lq`kpnGqphXU} z$bl9)LTQl$Epmj?A_rRJ2&F|1w8#<YmCzywpIAB2A_rRJ2&F}iP+H^&rA3ZVTI2|& zMUGHflz<i`phXE%n44*jnq5yoVG_`y1hgmtElN-XZx#|-B%wtTXG&<1gceC?k%SgW zXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4 zgceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{# zNobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC? zk%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT z7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgW zXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpzKok%SgWXpw{# zNobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC? zk%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?k%SgWXpw{#NobLT z7D;H4gceC?k%SgWXpw{#NobLT7D;H4gceC?Q4(5|gcc>CMM-E;5(<-q7A2uYNoY|L zT68*HbtH}1a+IZWl+7hO0F4?j=(kJAS0lIq<TF4@<&dvNa05`P0Vve~lxn~zqfy=f z=~WBaI-EHlb^+`{*u|vw1Eg2VF2SB4{H5?i@RtEmJxLDWGo1l^rZa%gbO!L5&H(9{ zDjWTdE!oYm$HUUM4AGt~u=FiM<g4-B0Ca2sIyL|u8-R`tK*t84V*{jP>ijEF{vEJa z!Cnn}4eYhB*Wt|TVQ+xF5%wln`hFqGN4+OLKsu)EuVH@!dk^eyQM-F#>9-r`ocm!P zKxud4oQHt*F#O-cr|%u2GarSoMtK9IW@?Wb<qeRUDPN8921w16uSR(Tq-M&07WO&V z7hqq6rSA`<qx6kcWa;|@$*L!-0a7#Nt5MznshRTMg8w#rHOd<xHB<Y)RV61iQ#K1W zA6AX>21w259H`j<shRTCyE6l%X3AHiya7@(<&VWx<6zY@%mAsGvQt&NV*A0)#L?NX zb6|b2MX)8XYE8%h=^M2W(l=!fg{=k(+9&B7wNmKY0O^}TUkEI<mSceZpe%hy4PDFW zS5V2CuxeB|0QDOn^`rWd`q8zdev}*3Z-CTK?OCOclKPQP>PJUO{gm&8KN<cM`1``2 z2D=~jOozWe`~%?6fIk!dEcmnG7r>tb--7RhUkJYlelh$~_~oz%!p?;~6t)Vs2DV<+ zozzd2R*eb=Nd1(rMuh{Ue#%#)!U3q?0I8qaquz@dAoWwedOK!-)KB^9No;`BPx<Oe zY=G2H`RYk*fYeX<>Pc*X)KB?pR5(EDr+k;g4lvmX+YQ?T8-tC*s#QG$)DjP%xu*=+ z9A!z<l+85`Fx0<EEN(D(hEYMO>oa3GZMi--GK~e=7sga0pndwjne;gsMxG({evdKC zI9B^K_B2k{ewH!HctiWS#suSY?dKT<Y`FIG$v;^8!;Behwf09C*=a0#``oBD=qs?y zY$|VpovVFj(7RjO=f*HTP5Z)Fz>BnR4ml^o*o(e3#?8lLOyFm0KhxO3Z_$31vA+mv zKi4Q1sq^!U7IC}YpKr97!?i!mSZc1;{s@EKxYj<sE6?|?iFQSk(G8IfZ$~&8_O{!7 z>l4wg?xgoRZ=vP0W|Q&F^3J#Iu2{rdZ72HdL^v6>;{}1fNZcC?$ECMXoxdc~)gKEd zDrI{l-VsT7XM2Bkg14$a8td@-ENfXLAt?v1ufS)ilT;?Nk;&jCQRxkPlZkLgq&J*c z?X^2oMF__`yuIP|-c=EABGMI=$w-3A9*uk3BZ*{~(w_cARCYw$RUu_Tx;#UQ-knVL z9XMys+O=y7!n(NaRI!3~yLZmNJC<DE7wL$|XjhynP|%(1jWtH8wvzDsQRifL#9P@H zZl^>andMDYwy?k&nm5iWuoGQ#Vs0LCPGfy_?V@1qY&z4ZF``Bn{UzydgAp-03@`nK zX**0_yI~uB#(E=xv$|=o*SL=U3JuHf(H||shUYV8(cbxVwoSRkC?791Qk$;pgH`!O z$%`8W)Pefwc%04%5=NXlj@M{R=f8xGbs7DXN0_dzq@(RPyMt_k&YVqu|2_w=QAO8B zDF;;-RZ7cPhO4D6gW6kwBdMHHC7C^>B&lOV%Na#!6*EaHO_&fOMlVXbn)ca7C)uB} zplYbfq0U-Q=_<OyOV_Ges@#$&nag_=tf=}XXn&ZFE0%hwHi~~q`9z706qAzb@JoLF zIqNENN#gxLV-Edai~j|L@KbhGv{l3k5cgg>^S`|=Np)3GR{SW|yQn6rR;tV@&t8g- zin$wGi8H-aPsNoR=Omq{n5v|F!gQXy_0wxqoBMfW3kkvcm*pPJs{nQFqB_QYQVvOb z8tJ2vYNOUzL@l%yZCvF^|F|-S@sCal^gsD0u4kMHX0i<CVVNw8WwRWX%ko%08^(sS z5o`~(CmYE|vC(V{8_V`$<Jfq%H=DpFvPo<o=4F%F6h`mvF?w^1O=tVF1K12Ulg(nY zSpl2FEaqc{tcVq}5=N_BSvjj<2eO0MTy`)!gw11zvPxFPs#y)IWp!*mTfpksLe{_< znV&6U0k)Vmu_Y|Xma-6A#+I`c>@e2M4reWFC2M6zur?NEt5`ehU=h~Ix>z@hvL3dY z#aJ(kGn@4>hb5R~N!HKSu(fO*ThBJIBiT{xXm$)cmL117vQ2C=JD#1uwy+b~N$g~H z3OkjZ#!hEvumQG}oypE(XR~wIx$Hc4KD&Tj$Sz_RvrE{e>@s#a+s3xDE7+B62fK=0 z&8}hBvg_FO>;`rtyNTV*Zeh2wU$NWR?d%SAC%cQ$Z(_5%**)yH>|S;syPrM4e#ahU zI~n~tCHp;lggwe0V~?{Z*dN#**^}%k_B4BjJ<I;Yo@39m7ucWKi|i%#GJA#5FO0F* z*z4>K_9olK-ePaF-RvFqSN1NWUs7T3vk%yZ>~HKN_A&c}(XUgm&)FC3OZFA}ntj9m z&c0>evG3Ur>__$wZg9pqr|&N08QjA&c^1#+IXsu=@q9jv59cHJ9(+$el8@q}`4~Qy z@5RUQ@qBMSfluU<_&(grC-W(MUp|#j<NNXHe1CobpTTGHS$sAx;B&aeeY}tt@nT-W zOL-YD=N0@weh{C_59WvPdHhgb$*XuZui>@4j?d={cs*ap8+ar4^F=(s7xN~*ga`Rj z9^%XRa=wBe#+&)!oIZQ!t^5ey#>0FSZ|5C6!aI2v@8(h7!&mbd@8xlB^FHqI1eZL? z`}rEamapUM`38O@KZ+mCkKxDi<M>9tiErk|^Aq?Mej-1KpUh9;r}ESI>HG{nz_;=< z`C0sIehxpEpU2PV7w`-DMf_rZ3BQzI#xLjF_;!8;zmo6ZSMjU)HT+tB9lxI6z;EO? z@tgTA{8s)eejC4?-$B1ee;55O^>6sy{2u!4*L(SW{C@rb{~dpj@1)=Re3*VY^AY|i ze~dp)zZCfg`sKnW`BVI9{tSPX|A{}xpXV>|KhrPxy+pri_X_<o++X->{B`~Y{eIdm z`dzKJ`EL59p}+EX`Fs3-`1||={vrPx|A>FgKjEM9&-my33;relioPlR8~%6xE&q;x z&wt=Q(s!F0g3;Gk3i>9$4B??~U&|8NB8R?9D^JjOCeb$wjSzc?J;g{dioR82jQW-b zF;0vZdy5J59{wbH7u+i*iz#AXF;z?x`-$mde{q1AA!dqMVzwv{bA%;)qEHlxVo@SW zMVTlU72-f~keDkD7Key=;!sg3szkM@5w)UD%ohtpy;w*qBpZcaED`~+STu<xA}E%M zkXR;`(<+j~Xm!WoqD8C}t>OsLMymr>iFVN;BJ|077kw@s6+L3Lh>2bir_XEpXqGb} zq)3W>u|}*F>%@AoK^!TL5=V<;#IfQyu~BRio5k_s1hGY&C{7Y5i&Mm@;xuu(ID?+W zw~8~xS>kMQjyPAGC(aiahzrF<;$m@$xKvywE*IOxc5#KcQtS{{iL1pm;#zT?xL({K zZWK3(o5d~SR`Dxwo48%vA?_4+iC>G~h`Yr-;<w^nai6$fJRp829uzypL*ilad+~^P zR6Hgg7f*;kh(C%a#Z%&G@r-y@{7F0~o)<5OKZ_T|OX6kmig;E0MZ6|n7jK9+#V+xd zcw6ii?})#Ocg1_+f5iLZ1M#8woA^k4EItvRiqFL7;tTPm_)2^&z7c;H--_?V_u>cf zqxgqun9Ss+FikVV^q84umYHqln7L-2nQsm=hnpkJJ<L7Lk>)6Kv^mBcYwl%^Gsl~I zn-k25<|K0;(`!yPr<nViQ_X4Se&%#@fAauyhB?!mWzIGW%sHlI`piPJ$SgKX%u=(= zEH^961I>fXx#q#<A?7^uP_xpkGONuRv(~IL=bH=6dUK)KU^be5bCDS^7n@Dy5;JHn zHAChybGf;~Jj`r14>w!Pm1e7XgxO|>%~fW**<nV^PP5DGHlt>bx!R1Gy=L6B%|6pH z6Q(qiX1}?{Tx+f~*P9#6Bh91Cqs?Q?W6k5tjpinEvw6IEg1N;!(LBjK**wKO)jZ8S z-8{n_Ft?g#nrE43o9CG4n&+A4n-`cDnirWDo0piEnwOcEo7>Fo<`w3Z<__~J^J?=N z^IG#d^Lq1!%!0U`?1*%l@%~tBMBI+6=k#d2D-lWdC*m2B9&jU`$h!7exHrq`x08{M zRk56SfA1>w@ZS~pbl5Q(E@bva67&e2%ntXGC+Xon7bi=~DHh4@3nwD+Sfn$V0}lg} ztgw3KkIL0%U#wqd&_jD~*wekfuR9X=P##gcBSXf*vfJDcN!Y@UM?`Y1ossNLM2J7T zKVf!8*F=PjuFH^-HIaBmM2%j|cr+fNykd4dTSj`L@XT05%8ZE9AC6^Gy}|@bRf$M7 zvf$z5&+qFQ^#$Qr@_)a#=dMbG+gC@Ds$SWv$VGAU^j_34m&PU0XnQo#-rt*pv4x}& zN+hEz5ndDdZ=MITRqhdmC{~3NIoJYH4v>;GGK;7nuA*wHC?b40ZumOvwQ*AgDN7L_ zqoY~v{Rs+8`+BiH5~r-~)sZ-5n<*n0b!Bx#X;3DkGCMt*%(W98oy4o6Ilqerg0V=i z4F+;zc2~4L9HZ8h*&mNuR%K1j;J_~1UPH}7)!ow(p%_q;prBJe{W6!rD*F-fL_Wfs z+R6yG_a`H~w_mlNFmcw`J<M$xsupUznAslbpfZGWsd=QX%A+Fo5*z(g!^p678C{m> zJZc6Bx;T^E?r^Nrh15G`UaDOvvOJZDlcy5p$gOna*cpqg^HhTA?8+2F8I=j*lULSe zRqG;oYTb)#-HU4nFV3z_U6oUhb5wK7smFQB9bP|puG-G8Po1x}L~XL$vk35>fC~|D zAp(P!c>-?v0;wyrdi!I^XkTo-CxBM!3AvYs+)G1)mu81j*LV)Y!HlKdb|RkB6-o3` zw_X*K9u<l;5l`5?HavK(hh`M00cM9&S!9GkOE{9%o}z`KuP%<b7e@v!&Zbbh&CL^a z&xyL{=>DU4&yJ?f5fP#x?$Thpr`zu7_F$GC+bxfsy2{fLT@&qycx;!1e)qzD_rm_c z3$y#vSFFcD(j6ipdwptmW@T@<Jz>W)!*1%Sq#ma@L6TW%cTw|Nof&phPc<^}w7Wpn zgFw0MgT7haVJFRYGM?)8FlFv(cas`7=}2uQ>~PZ4LGa;ZX00v<O|Pi(cxv5pM3Cgv zQf6VIKAy~pq}|L~UAl-aU9FpO1WE4v!4l<m4f>w>Zq8ju%=xOUU1aj+L%Cg)ysjY| zxeErb&g~xb%>|?}X1CI>dc;RHYY$CcQPat-ck7{cWky{;ryh{*S#Wdf-IBX!x&E;F zbOqJ!EZS1CANABPRNFk-le=)R47ojneqO_nLg%gi$%c}uc#PV8wB6I_1~G;tuW{&k zu^}6tMwiYQ60=be6(i$mbQ@jFO%}Pi#ogQ%4ap!cK4inQ$jvS81}7fwvt=@2(_K>3 z#=Ar$-jx~9O~BSoAm9edMv@=s?vHnc6aBrhaDOu29=gQ?wcL()mbfLM8w;sz-jX5U zd5Iw#o+WNc5^fT7uaZb|gM)PD(k)Nj?^J)y3hHj3Me}n|YERI;Tq4O^Is_muIb_4L z)IBeW#9XSHOp=Tz<o5Y~B$**y_kLaXkX!eDBpD&4J{dHR81Kpt4Xt^8|In?>knRxu zZkoG%um-tnhxmsL@z)Rf*@vZM!PD%rvH?j>bGk|9Y)HGFX18V=kYv!4N|*F>#%wBO z4(=lEwNOsDnU(H6Br~i<)0J=>%XO6ueqL}0e|dDXr3WhA`3K9I;exw#Wq_vabkR)3 zl*c6@0|e&{<tZa+$8DLPsw}p0Dr0@!VYu00y7$wwNi>{S+b5$`G3?H&O}fYP>q(5T zOLfS+06m?NVqr&KXvhUQf!;`$d*U8Zy5uK#8Nf5EBFS*Z{4muJ4;w`l#hDt@Y^FnW zlMK^+i@Yoas_-%z!hL;V>cqXPI>OxF&lmOc6;bLuC<R{}6-&D9j9|2@H!PNh`!jWk z#NzI#sHVTgGU`HAR*X=2rb?yq9<EPL?aK+LNy&>0saX~k$fZiv3*M-H_ju}3?99OR z<|;Zpqf1pXqazYahBI|u^h8WmR0nCIIvF-qW*MtdJ$g!~>bV82s^;;2zAl<+>pJ#D zMWWmG&|`aV*q4DsB&n*p2%?Xw*-n2{q6}NbAW!9-D%tR#l{8Oh>jqFC#6}J-hMOJk zj7I6Bg2JMdSK>>1g=uf_XmP&2qNGGIo8Br_Tj}%5(q4Jmt4MjJR@zHrm8P(K#U-hu zzH~0WbS}PhE`{k_3e&k1rgE_gi&ME2rt>Q-P47$RT$s+eFr9NzI_IKv&PC~*i_%;a zr7?=q7)5D}qBKTP8lxzUQJlsoPGc0OF^ba|#c7N|UW(Hg#c7P<G)8e6qcmNr()1Oj z=_^XpSCpo&C`*^BEM1nebXm&MWhqPNT$awcES+;%I_I)<&gJQx%hUBOPh*s)G0M{z z<!OxaG)8$EqdbjKp2nz1V^pLuD$*DgX^e_AMnxK<B8^dz#wZ)iC51sfMYkcJl{)IP zQW!ofh2gVO7(Oe7;j>a0J}ZUcvr_f&S!oPk8e_1`C8>Q@MY(6W`&hizO_t;4;#rF% zdwEI@ve%|O&tdL~>)nJtHl$IzC(^4D)P}mhRh~aM{G~xeHf9GfBLEMhvtff%2CKp{ zlJ6d&$La)qG)E&dck)49<Q}4Ya9#S4y2{fL?xL|uwwur?o>erl5lLo6VzDSGo6Myl zBt6adt4Ruv8i6Jw9vbl4@eT}VF=L@HFm%KgK(h59dQ~i|lOC1S+(C9cHRjBqUGa9e zvgH-UKAOOgN||f>6E^mi`U-UyB2PD|QeRO?VRrhn(G1-7vemRiFHKQ&WYY)&a~j=w z7!|v|%vIAK+RwwxN9q6#l9SPJtRvdlnNfrLMPI`1=x<ME%YM3ts1j#j#GOeW$Eu-v zRy5v8Bl+Zda*}FX7R#lZNWxx+sgf*pH&=5e+3GnOUY?#k!A54x-awC^5p}bvtFnAL zEzEDH51xtZc0639p2K@a;K5_aLrLxse~%%4Lfsl8d1}@tMQ@&(?-|@60#X~fH2;(G z^T9@X3$&!R^3;SyYD1N@(6TI@qU5w)sJDxBTCCF&otEmfOsC~Kt<Y(uPODT}S6f$y zw63mNr!^|Ie09LJ3ahZ+DyjiKrNE~YIKJAVT6I39$j4XfmX}hT@2e|z^DC??qP8xh zVS8N^H-3r*YCvgmMnDZ)Gi=xpY`?N7Ns*hQRpjPKX_47&+pAT`Rz+g=TAWp^&nmWD zyy6nK{KX}DyHszNy7ee7b?Z@Fic(m`rQnfL@JK1@M=5xu6g*N29w`Nnl!8Y}U7m_d zQ+VKo_JbFzxJ=`hY5X#cU#9WPG=7=JFVpyC8ox~AmudVmjbEnm%QSwO#xK+OWg5R+ z<Ckmva*bcE@yj)SxyCQo_~jb6T;rB&+;WXuu5rsXZn?%S*SHlLr$Xb<Bd}^;#T6Q- zLgQ5EdRAz>3XNBx@hUW4g~qGUcoiD2LgQ8HdRFRsR%-l8jbEwpD>Z(l#;?@)l^VZN z<5z0@N{wHs@hdfcrN*z+_*ELeO5;~){3?xKrSYpYewD_r()d*xze?j*Y5XdUulr+h zmBz2q_*Hee;I-OPkDxBc)w>x(wC24Ar^})w>x(wC24Ar^})w>x(wC24Aq*0 zYF&ovT8&*dh+U(3(7I8qb)&dOm!U?Np+=X%)f20@Mwg*Rm!U?Np+=XXMwg*Rm!U?N zp+@siqvKGk@oO~)wVH!kjbE$rYc+nY=Ac&N*J}J)jbE$rYc+nY#;?`*wHm)x<7@pa zuG9E+8oy5C*J=DZjbEqn>ok6y#;?=(bsE1;<GXro71wF}I*sq@xmBX|utck7iF@Cm z{Vslq<>HrEE`EvS;+I%1eu?Gcmsl=-iRI##ST25v<<`GM>urhU;+I%%{Y!i<|0O=R z{v|%Q{v|%(`)b{ON-2(0isO`~j-!0Ejq*{7@==QNQHt_Wit<s4@==QNQHt_W3VtX> z`6xyCe6?;rrBvg){gk#fzS~b}TjRU^l(sd#+fQj*<GcNowl%)nPib4@yZw~5HGVbt z@zuJ0lu})Pw~x}cuD{zyX<OI7x=i&o+AdG^HS#O;zRDr{bUEC<O6TZuRBIlpbvfLA z>Z^78DW$p`Za<}MT@JUO(zY&#+fQj*$HDEVw5{Xd_EXx{ad7)7ZEJkDpZaRueoCp1 zgWFGOTgSoer?jo(;PzA6)^TwADQ)XGxc!v2b^YCbO53{rZa<}MU4OTq`fA;NN~x}| z+b?Nb*VpZrw5{vw_RB&`-xrFkx{-s^R~UrQ!{VwT`!MWK`}Q0>Pd&%{>@>`0seQxn zV4BX!-AreakE5yU-Gk{%N2H!@)p0dQ*dw)L6@B_h`$E+}-_ux~MGx`?ee{uOrh29= zu%$=spoBj7q9hAX_vB=%r(C4;fUAzvv#mOg$6Dp!iB_GU9%d=!ot^4-ROl<TvX-VM zT9ewz=u%B~_>dWVwVmTm-zzsu&E6|##L!85wS(1r+z+Na?uSyjLq3qoPJbBXaVPIH z^^Cp8ou$vzQ}a24)A781VYZqv!-SBjygX%6!$&>fwMtS0S*rvs!zy-rGNq}Wty@TS zr9mrqg^@+?QH(J%|3SY`jg`E4v}kM${c*J=t|M#G7Wvxd7{x}Z!D8WLoUR>VaDSlD zYm8YE@Ox>+utE9Izo9mLU7YRy#dZ7Rx++|ejnd@O`NNDoXeIJkV?6ffPzgM=x;T$Y zG2GbG7;Wrj?5&Sv(8}i=BcDn;!Wc>A9!F)E7>jm=jay;wg1t9J4ae9C`xxv~F}pox zJP-Sdt&YA4`yT8ks-YR*!2YN#Go=b?4(y(=<E6BGYzpiQ*g~>}>_FH<Ve7~iu}0V> zuq%=(w^rCr*f{u6>!caganNSMA8NC(M_F3NMO^(dO~uNMn6hd$po-mpnhwT!Fy?9> zLoTl6w7T1aJcp*L6-_esqos|d#vw)>ElgZywCbxC>vV%ouh;1#I{iqeU$`k7q0=!s zEp*!;TR`;~VPp)h(V^`OV<G)FXx+M6xxS13bK`CLFN}BSze!sK)Rtx$XBY#<R^v?L zed9ypBjXd}Gvf<dS=dEu1h=y*Xcg8=v?6Ifoy}<7&vmHXcq8YZtrSw{P+Oz6PM&+t z&({(iPe%luGm_33O=mJ%bF_(~%c;kZPuDXl6RxInM$tKAsI~7!t#>@N><M&)5%Ot3 zpeyJMx`M9LSCIGbt{7&RY9$l%vdMG>XH(fU3LvesRQamDOQle61B@c1DaQWvO&Ed} zlLqK}F;v<}Z&q=mo|ljf@M5x!3Z0Iqf68;)gE6|5wnt&FF?yM`Y4j3L_KYnHwtSt> zGWoVmqpNAxJkFRe*UI)}%p4~8_zc4edva!aX!+@;GS19xL95A{HDu3(D<&N`!I+Ex z0a}MFX$7oWsjXHZAEN$SlZRwxj(X;!7ubM!`uu?lJm0^ZmC^g@w$qnv+qB0@Ym?Yu zZQ`r83ANsFMESHAzx^hB%Vl3|eRsb*|J?k7m7gvu%b-F9QAQCmJtKK2=o@Q|_RvPo z$YHczJxZTu#AkWy<Lw2$nbra7fEYO?b-=qc+8gl(leA)oKK1biBZ)P%f?ay6E3HXm z^Q|(g)JM;{>R<ENe7ePy`icvSN-9b!nyr@q5VY7TbU|nSZ=kBAc-dfnp|$byp_ODA zhS+$zVQoB_Z`^1+eCKCNM&EnVmbL@NEIfQp!<nm3JoEFCr7KF_f3~!Bm48W7v9t5Y z&WR^K_S%exdvd-id-;OonO~iC^R0KTIq%c!7Pi;UFIb&>)}n@)r+u_y;i=DO?jAqu zn@!iRc=^ey&u9Ox|Mk;vc=U{k7sVR0&Ykr7s~>mmO6;uPd0E@sU2ok#=F0YU(NRa< zG@A=*L|1Nn*xLA@RqV;4_K=a0$(XsnwVyRLwQaF2d+B!s>~`5#Kogiz`m`mkUQDp^ zh>?jS8T*IHY3Y2C+S<pQWzDem-?rbjsavM#3)>U1pIkR5Ddp_;Z~^VN#;A*?sVwrW zoD`_YvWBVs!_|IMaL@0oNoso!liI|Nk=7o*;Y2H%fM;p2(nl|Z{In^FO^g|ff8X%f z)Z^aRoj-Nw>*Ig5oo~N-)9k#>qc@i2Y#KJ}*5<L{J73E9@uXKjJ@d@lmyJL4>JQGk zzr5ns{?g=u7X)^f-E-l|Pn=sd@z%Q^-FEu&H@|(M!>lS<#GWm>V(er7>!&~Z*{&^( zyxhNe-6P)z-~8ZB&xx<?IPbF7ybIpC>i!2do$yfGt~YMl|D{K-YUp_7(Y+79@5YL( z7s{t>FFJM3DHnZkZ|3BAHI><!cSa^my?4QLHOGCh{prW~9iyIJ`skP|k8An+`a@PG zjy`GZWwWZUcp~`J(FcDu<G|kp&M~g)&Dp&2@2$Zv_PTxf@V<w>`|_r9w+$Qn`om+6 zYkPe7%=i}b(+|E_H~ra+-+xqmc<bHug`X8s?|Yd3X6xQJ|ABGmzH$1Jy6UfA?Ol-X z+wn8Xtc|A*=?Tr1zR}i5MRC^1yyY~H8%e~y)#1K~Z<MvC+MPKvx28X_DjZ*<)=j12 zku%b>v^%^u8SzcD_Ex9mjvVFg@K)1Ib+nVFYSrAWZy#%-8^1C7u<F1v)qi!jEux-E zQ+BCtt2T>L-S+=PD>ktyLs?PXbrTzISesZ5S1szgCvMyE$0xnF<Q#qSjVJbha>t@i z-*|BN16|?!uI!lb%5VSvM9~e$TPLqL=CoH=zgBwL@CTm#aNTEXuRg|}`@q?^<loc% zMeLj>?preZh6M+IdDjaokJy`E{_ULAlXiT4<waMIe~j-swsFbZ!`ePPbiy%r=f6Jh z(L3Ha@xCKA^!N(Ig&Rj+JKy`1Pv$S5{p7mhqI37WaL>D6@1Ar0U*Fz&%4stmI(71i zo%d~CvE1%|VD9z%ow)Le5u@i`e*F7OAIynA{NwKzzIJ!!9_LRv>eWN0KRapNhnM>v z|Mag@#=ZLR?enWI8h^yLNn3Zfe)-W+pB{a~DmL(?Klk-#rYyVm+$V27Y0b?a-IM?M zZvV^MzU$t0^QePvKk>oeYQ=cf+W69csTiv5xQdZwd8l`DW>}SK-#+GnR=HKWt$17E zmefm0zt9c0tqg4{)s;{Y)lw%;`*#w;Jfvxm5Neq_##v)i?OTk@BMG5~RzBM|yQFAn z{OO(~NT(OB{`>nYcGgVroxJ|Sndd#Q=|=YF35`$QddiCU8(9Zj+4|TMXODc>T$2Cs z{ONOy@>_R5e)gh^o}aR6^w;ysCI|X_$9;Ng`H8o`_ul!&k57lrTQv1g*G*rv;pSh5 zD?gv{)Vq(r-1^#YXP$7#otNJE@~-9oxc`pdAM@qYd6#{1{*N=CJ7~$?dzVlDdfq~7 zlleXUMRY%oEZJ|@Z3TPnI&Am>C)_yY-Ikw~*g9fJH~x2b(K4%q2%#QYP)$oyrMJkt zxPKLW)Zty6pbu>l>wnr~y_NlH*%*Dmf;(!3RZ>VF@TIz@+N96L-8<^WyZ*xxXc5RA zIcD&dn&_8F@1h6|CljlE2U!(v5et{nian{`xa*e@Z#d~ycNVGNmm7LZnv>`Yd-rcY zz^b6q6{w5F$m!|aQx&bYJLqYF?qcd;fj%v%m|dtI75=yGe}C7+JKC<N6%)(G9@Y7H z>z4c%KE3f8XY(1Sp8O3hU>@zcXZG=v&)xm(C5s!s`_q*BUis{{l7l7<Je2+2_ah6M zA2|7_gAbZE>8sCI9C^tX?9NAWeskPY_fJoTyQcel-uZB2c*XiF-yAz)&(YIw-Q_Pn zcIJF<&E2nePVGG9$-q7P{J#9LrCWC;z8crr-*b0;_tWL;^RKtAeRWpbg3DgKeiXZI z^NZzYS`{PD69*o3=+e7xdUMX+oBwj{9!n14x4$^^hsk$c({knT-0Q9$v3OW<f&ZGP z9&erY{Au@o9zW`Ud)dnGeJ3`*aM}r7D^41>?SYa{-k*8p<af7zwfUWE|JJsy^~ko( zKP=B(S()FywC&H;lRtL5|0eb-G4&#D13&M|eX5Ij&8XF7MLfA}TNbb_-_)|qvBFwr z?Vq~4b2etezl>jdpR{JA&*avm@&Ec<iuF&Q6R4B-rmkgqX4pDn`^s&HTZdW8t<ack zYSmqlIbG3CdV1Z*%G3|T=BR;po*TdA#?#MO{m|3L^*p~|uUSWqE3l@G*;hZk&{|;o zr;jgdB=)JC)Lo2(XN)x}g-cH>`cX=V|57A<D${*TdW^E@F>2eCEoko4+J7O4eeK~Q z2%vf(rpKcxmUqV_Yl3gQwHNK6w!a6Od{K2;;7Z&g52M!#MxFo6>TCDwdu;o>bN0!a z{cy>}JGM{y>}P7C^zT&hyVuTnsq%_&^w!bWu6xvaWb52xs=l6j*Y3@7EUo1qamn_# zzTG(IgWo+-^KRnJ#dn29csIw-J8SnvO+9y%`Tla%><>;e9-SC?*I#(g3EwQb|J$xm z<ddHJH$2f%K7PcAb+v1JpKsY9hPUm!edVQJo!Yr()82>e+WgLa^;eZId*}7awZm2n zw>D*rr01CLwElIz{YlsQCq|FyT^zJe8aICP&q{3le=9#UQX$bQE36pQRa$Fcm8W&} znV;&aYIGFjtdaSC(r<dhm$WgNl&NR2%#ohu_9}Ys!AC7ZotQIfw4WBesW*nbG||<i z#1G@CL#(;#c<#s&ym%dsdFyMuDXpCC^{Z87((X)pgJjmy>kr;#;aHSj&88<g@0vp2 zUU-T(b4Lwds`926rrp=EWeQpR3*>)0JUXXl+qu!BLix|UI_C7kl2aOsZfNyxZ+vma ziRCX|aMhuojH}O`GAS6?S$*{rr(U=0<M;y4wHc!;Cbb_lb<eGLU-0x3StUm&-k!hw zhxLCrW#PMr_RTJ7KlbM1o__e2m%jV@@$XN5>BSw}Ud{gOiqDSx_{shUzm6>Km~!cT zN8fwXZyNr%qIQ`7*~1$BDYe^7YIiIRcd3Jo#5m_CLzB$otQvJ_AM;>~ZdcpNwv}ut zN{xGDdl3yy)a_;VFS>4ZusU@RJs7!o-CTYuoumUPN%^<ZspRmVJO~M^KXo|`?vynD zXM?+WsjDphPiqv8hJPxHKdDh9VeO-M+1I4jaoj(bnms((?k%E<3P!^`a|AV{!Fh?j zO*|tS^qBawK91qtqQ6c#@r|y^(PQ8G(?`b)dur!}e{R3;lRM9Trzf#$*Oz~qTmI3t z2WK5|&-{&>jySxb{)>s;Z-<|q{mjI1%WB46ank4Coqp-fU+0&+J~!{inseG}XI%W_ z#dhOCzbfwP8?ogd%@r-pZ^t&>P;ykH;fD))?;L$j{Us;$jky2dU#&X#j#uw^{+0JG zt=xLdk4N9V<E#(jpG`UCk+r4%Q<`&w8NTOc?tjaYU8iqYedpSm`|@u3V&$}1Z$Ej8 zbx4D_Wc?FYFS<E*=O3qL=014NrmsePde7FoF23lcmboK8eV=>wn(m)_vR!n_b6Z~j zpA|*@2Y+yO^sO3ukr8|Pg%e&`cJ)P9&-+8;<nFDXwcWGpJ<rOD$s-H@=G!E?NEX{U zW7ft${C_KZe8evVeQ=&?Tc0&zNQ=nxsd<KR=~<&};maFx*d(1BY(BZZVKhN8xINn1 zckn{fNB6(o-<HoCGw<lvFWKoiY1VfyO)}O@{C;Rlqw$})@wn9w*IxDF_^GYarUZ}9 z8GiYagS_{Z%&fZnqxr9FuHP|f;k}=K@i)Qm+u2u=_;KdgJHCJL$@cM~+{Wq&yEDH( z=EWD+6uvpL{);o7zvH&Hw{L#-)!^6n?mp_?7ZQimRDJdS>i?-On0onL1qVNJ!!wt@ z_P{MQ=brQIvC|elc;mSrkDARgXNnIG{pg_|wk!GB#Eu*+yvMhR#b{Tz3VqlA$+X!& znXDTUm6h~(k(y+r804lmG8u&+eI&y-oNj5fs?RF%m6p(lz0JP}!PDm-^T&5P-)f4! zx5w4L>-zG-qj&t~Z@&z|%9~ERYu1VbW8$sH+sr$ky1BXd@Xx+_=E>7u-M;jM_2Y-n z^Ih(nKJD+*XL+-KJ1qH${r$zImu^4l%dbZm4~}BHwl?1N-kCQpm^kIk$>ZL<@#vRc z{`A-*Zmqlf)>AI+sJi^D)}r&S8FM`!^V<XG-gNmDAMKjA_x!-#w_VWm!#VFX#Qisk Lc}I)-BE$H9jZEy0 diff --git a/src/UI/Content/fonts/opensans-light.woff b/src/UI/Content/fonts/opensans-light.woff deleted file mode 100644 index 99f335326a5b23d67abcc13b75ab12c63be79e24..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22656 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 diff --git a/src/UI/Content/fonts/opensans-regular.eot b/src/UI/Content/fonts/opensans-regular.eot deleted file mode 100644 index 091cd51b9e36cc56abf245fa7f3e894873fd9e06..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19216 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 diff --git a/src/UI/Content/fonts/opensans-regular.ttf b/src/UI/Content/fonts/opensans-regular.ttf deleted file mode 100644 index db433349b7047f72f40072630c1bc110620bf09e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 217360 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 !}#gv~NV!5|5YF|kE)J0y^zWbk4>=%|6F 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�Ff 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 diff --git a/src/UI/Content/fonts/opensans-regular.woff b/src/UI/Content/fonts/opensans-regular.woff deleted file mode 100644 index 55b25f867099eb26c436b1c9fecff14a51290d46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21956 zcmYg%V~{946YlD)ZJTFp>#S|twr$(CZQHhO+uptVe)s;ksd_5uXS&l}>8Y7aGI5a+ z76t$W_$^9<0Pz3S(UHIM|Cs-z|Gx+eD@p+X09yWX*#AMZ(hWyOftKNy3;mU~exaMk z-2-c2rDyxgo&Lh~7xj^+n`s8lj`#ooz_PzOYQM;blR_^vwl%T(<thOHK*|6Bz#6^v znh{L&9DZ%VReyE<iv}J51%SxJ(#`mn`|XRl2LM1$v9hnK$<#>C5CDLO{8xwZ7c6fq z$m^!R<S(cBE93ow7`6rE-PFp_^_TMm004sf^`)BXq5@=LWN!@sz!wPs0E-L&Km-Dv z3<qIpWAJM$o%<Va^?z_qF_5&<bN!7)_P?0%{{xT~pt!Z3mC-Nv%L5$%0DwYlhuZhs z+Bi4@04ON_+Wq<hy!UrKTe7t``qfdK{Iz@i<sbxFk_&&^{~On_FdO>cHozD&eZ>Fj zt5sdsOaooxJA=&L-tpc(j)<jHKR-F#e~dbYq6VL%eS?Ej!^4x~{ey$|zJ6z2zybf5 zfPn)*;Th36Ksgx!{va>cj{P9%neLq$h0E&dX2QiA-05zC7W!%e)U?3n{5tuse_dT; zUtJ7PVNiHg;6r-<H!5f(-$o$#dVn*I;Gf9g$Ue!DRBQSzIe58P`B(*6`C55fIdVDG zhaX<mBcR-H{8C80KM;Q~av>#B!+&Ca@ZNdveD-#Je0Dy)zH~1Fq~KV1R9|$51HQo^ z;F$875B@}2LfaZeO7i(bXaUYrQdL@BVrOb@a&>xs0{Q>zC(PI1M~s)BXQ;QohYS}V z|66W;j+w5$&e87v4jL>xOj>MwjGC;x%+l=q3>z&yO;c@sjhn5#&C~7u4d}<;N06tt zhY%+>M_*@m2N4Esb(OW1U^1!EX*yP4G@(+h-DG_A?u5l+z1?oK5q;(1e6uwon;4(& zmZz`B&my3PDlkG7tPdLeXkU9_Z;q~fX)ZYpT<2hc5udJJg2!XOf4JsQB7I}XHfsBJ zJ|w}HHqLm0q3)jU(eX|ud6~fxr8l10)&PgYtv%XnTM0=H15hI25&b*PNP6%lw+36F z#1d*Pb=gLIfK<59gwRMB=-)knckqY22=EY#B%dbZKP^A%KRxd)h~$KKHgLddzQF&S z7?~J0jZ*t&XJ$v}67mA_;^K1hpa7(}@qYf7Lnoyfg$-fkZ4^*wbV7x3x+Q`N>&t%K z6vy~xJ_<{#u~nx9eP~9?Q+`5H1NZ$zf#R&Ysz}kF6f%2y_M>%ypAhVI4R+F%$F3wP ze8weki=7wm>z8dB^N(Bn*$<Dat>tMMUY_^7H?M+jt%ol2BhIF#x2s{+3yTUJpQj_V z7Zo1b6jWYH)ax;stqv#$w~y(`$HhZIXHQEeMtwVF#9jeBVMs!qh(g)|_cr3splTJG zw|Z3nP>@|Rg~X?JKSK~gWZ6L(M_^d~yrTp-Dkbx>YU(y>mQ~QASfKCw!qbO<<l!g0 zY-kY8v?rp5Y2+xnDF2K6kh1V{6rROwX9M0rOg9E*wWOtkG5au*1clbjeiW+lg+N+s z`-WQ7uctx%H8b%D&Zh#{%f`kV_G+v|37FKlY`MB}2nYiubXI~Ph@|OhA$fNc-kXg} za?NQcL$Y_Z0nxzjyHLE59szHR<U%lsnQ!ghEIa`*cOEK*#p7$I?BkC_Y+<No<U6e3 zjX0H`ZQ*=C2IblvI(2`gKiw5t?wOeOI3vsy=62{JUoKrl6qXodZJM6vL|c|32BF^_ zDJQUt*<iXMTHUzN&DtAQ{UTKYFv@wP?FfXg)?zM8b`L8ooIfSTqEPVx&>b{$w%aO~ z;D1h=9RET&A8OJy)nogj#2+uoKHTw`xeH7+d63t&Of^RnD404H#zf|b%yRahy$4|u zS*5yNv@&E*MKmS#u0mFp&#@!WsSC!>UK9gQ!?X=HBN%TN=TL$nAwM+eOX;JBA4u?G zcgl1?q4VL-Jsmm1KQocTq5Z{loRqn)$rVA?ACTr+kW`H;p(CSQ*($34OlW4I0cK6D z0D{7U>(m`$JrRGkJ1ruSrCOU$x$Ns$857v+bnV5h?<G2T`zx(B7vppSu?dp}o83Y7 zkCUbpfD>EP3ww5zKtQ4fhY{N^Es1-}a>Dq6uLc%U3<a4+;g%Pis3{@14=~V@fU@x@ z62#P$2;aOlHVNi4=<Xltk0tSDE>mj~OkUeS@Uki>6~xaf)@qz5<tcfTLK9CUi!kAE z*)k5R?LvuWy(~3o*c_2W;d02|&SKfPp|{Fq7(q>%*;y#+7$f`+pvCCoE(Gr2BS4Jc z2mYF8YcbZ*f9a_a=XrdF|E$*9#AqzB-qb67a;>442&c!5Ig5Ag;bZlVV04-K&6#gf zJA7dq2Rl?&T??oDqLGSD>0DMQLM<Kj*Osw{F>exfGEEXKl9}BcYUFA5mXyVac`Zo7 zbDH2Di>6Z9HWtuOTgvrFM6dItbs&cm`KkXw5c9|K#W~B3VtSm&(=RiwM*>EEw$7yW z)eA&6jCfHFT{6kd5z9~hlOkOz&2s{+vkZTNHqH)2FfzASG+GKAS6fI<u=S^bcN@dV z*u4~rmT@5Cuh9a2aidL>l{TLffJ#z!ZT{hT`u6jVGdMN&rk4xC#ZC#LVZr*Tdd(LI zxVM;%8t{e1${1PRo$#BGvP{?<{5Nb*M-f9wM?x8BgV3F{Zx9%B5PI(xea(IO1#6q` zD{!?Rhn(|yHu#L<^tV@nr1^*92aMu{fa1TG8Xy2L05||701N=w&kq2cYi5uqn69GG z`)i^~^Gm$c4%JvZ2>6(>f~lH1c-|0ttdM&`{-bYbs4pO)l=>-nC<1T@b}(TG_+NZN z!hFIJ3ZJ2YgG06_E-1s0pO@_oDHigViTX7Xo9)S!4bKO7eynS)k)=-p!8kx8P)o%s zQfw0@y-FgEJRR8+d}I_9qjc(0I?@Ttz98eAnac_52<}A@2QPNm4yoJ1!oB^wZYUk@ zwQ6$N)D4)LJua8?6l52ONAiE6xqsCqo+m6q$tiM06c5Qu<e>x$>Dq2QqnT(Wsb(a& z7h!BP7wtVc#w77lrtu)XWF+S`(kDXTUl=D*LgHtK`Fxb*JF^<frZ}?J^_fiBqn6Aa zh>629cMs|8?dZ8^S3#mOhB?e4NlDkL_8r}|c67Ox$p{kG7Fkd0Gd|Ajco~;7o{YE6 ziZF4F-oibT`}->^2?PMPBpKsvVSUDPIrbESSNEwBckWv<v5yq``>nvJD(^RLl6QFi z_FSQVOYmO#AFRHGpF=v=d5U3l1SGWA-Pjtx&D&>c)s+HOV17q8wAV&B5^QkhBprCo zd+=1JhFqxGoN>oo4WaPO1-+EBXX)~fk;60z8vuLPurv3K*<rA7-w2NmWh{ASR4R7m z-XInJgAxeNbD<+3V%$dEAv>L{1+=s51g4vgziUE17FA96lIx*X9iBN!2+O4#BMo(q z;U$^6kQwB0z`C57nt1uRge^%uW+p?VN8`D#o!Vo<qAU5>z5S%XbMbl=zx|vz(N6O* zl(}w~(r8Is{*%*%_^v;DC}(gtM&jfAeUMIOCSi`sGw*g2AJjn|a{hJ=wPB&Qld0!0 zoCu;(#~!A5$<2mC%LKz8DsphWg{b~;5{tPOhebCdyah}h8@9rxK3r1RXjW!sfoSik zn3Gr*qOxI`XBA1gM4~PP&*q9Ub}DYlyk9bk7aBu5VtHQ(-buCUiviA_{GEO>Bh_(p zV}9Ako6OT`u`*Ef`W0o7Vx`^UzJ#i&p)Mz<tSl!NK4YcXp?}c6M5%39xwpW~ltAR< z)C!bxQ~hy#d}Xm>vuWMhezK(U-FR}8@}k+}_Azn6+FdZ_BF^8r7m{E0VttqL`XCC$ z_C6=z*Q^XC4$p;=X2qc40c}<d62d{dQ*wdvs1~)o9Ng@Q>Qpor8F@%0QQyv#I+>R* zi40t7%nrp*MM7;ITrrp{t=c!9o)7~wXzfr4M7aU~hkMov!KMF=hfqIe#g{~ANBMsA zlRgFpTzdO+Afj^Ru&;`xfi-#g<z{t%YsDE?s~)9>_Vj#ns|wTP`ztDqcS-Xbt)=*5 z)3588d(&j_wnKw;Z)?lnfKx?zN9}fdRgZ`adHK@YXlpP)VD(bECQ@Ig#z+S{mF^WO zu-1^#HqMgKG6bOrTZUM}PP>jTJS|o#P#=fbT~Bd38i94QK;#ZEE0hU)A6mFdrM-@P zd{9*?*)s>ARXsDCNxs|dX&OzrN~_&#iZniI>2<)1Ol?|br=gSe;?~S)JDRSBhJm)k zFD<UGG%vH|t#egSP$qjQN1~G_B<Pf-X1m+*svON(9I0VPwPS1Kw(Z8JA1~W8%4p&N z7XN`QKo@vog9Tp14K~6(O1*+5oegFgDNDKrkh7QpU-8jenrMd@Rs6Mt!>yupNZZ1k zM`o;CvXd~G`qSy=`|88TFaqgzU$SrbFBzV=zDUSEM5Qq~jWh=n25r(ZPDr)G*(N8x z3N^6upq+X4%2`<{;)MTbY5(6uX5#$!sAJsAv^4`zPUGjeBUdP_uEeOJjAZ$f2TOoz z58Rs|w}{o6R|rR}ve-BLm_=Ptx#UTD0JbHH(Z#V5fv7s7FNXt;3mZ<62A$%l0rr5s zZAi}`<hZh4n+JY>SJ>~wBd8sVr;aO>S58l(0njw-`k8;cK%ex0Rlc#(HN{{4=3wz8 zn#nPrp`ZSv)~Bkg)eb5DbXFj=I$Unn+ge4_2SBF}%c*ph2SDt&iUtw&Oq?~uYQ~mk zo7Ba!`ys_<S3O!<m+N^r{Onnv<{?CFhf1%y{LSMqJ^qwQ%(YG)%kPO17$*#+81&uS zG<>2}KlS)@KRsOKSpUr5Dos_dJ6)K&EXC5QqIlrW3@qF-x?*|+R~eJ7p{+)23{WO% z0k_5g;-3KE!k8n_l_4nWRYfaA5Gq5lfr>f9M7p}Vscv1-pB!V=o(}QYTbGPye~j^U z+G3cf(G6@H-!NrTEI2T|E-Ka*(S!nI(yKNSQl?(5vLzI&%o6XXZu6U+gX>Sz>7T8} za3T!3hiV6H>XKbr(=_FQ&+PCP=?*c*bThf{ezfp@o+Qt#Lfn6eci#2}qv2LpUeDT` zUrT@`*Wd8!n<$1y!~9tv287mLWS%92RTDyx6A9NgO_R=oEh$i$JzFd+CVhr14##w; z$t@0>m)9lq5$q%7EETkDvAeEK5NFBQ#PumaK<mn)<CC<p@zV3b`H25=86Fb}(ZY>B zsm-IU+<w?jHwj+hmZLxI_>;4}A56m;TWY^p)BT!MveaB@nHkMYxk8)%s%U+ehzr+6 zH1<a}E~6I@EL&AJgfBlx1t}keBA=^ioz7;@18hlnPCqZ756cbtO4AD=;4;n$TC;ox z#`D4V>4;R*jb2d{_Nu2p*;LVrQI&_r;KOe1@4JuyM|WMlx2v3Ku_3LWHv~RfU8xrS zn5Y&@(v(=+D;vn*KhfF^zX)gk*(XTI9wIHIHG8%W7&m#taKJLjYvehLq<?8#^Y0p! z#w)9|P~p6o4L>-_+?PXuZ-@4$w&#u<FE=LDjLSA27CKkTnF+Bn&(?<AS)$8n;=Ytn zujhQ!vMs)3%au+7s)VU#>gCcFqNhlUn9wZd4$qM5nu>qu!&jl(`;7@g^}FIYN6f%T z5znC{vdI42=}lw$o`tm{z6_8kaK2zXZPjpW6>gMN|2&U5Ju`k=4=pcMfG&YdEXS6; zGIx%&&kt4CFL>Da+`@E3eDu7$S5%lwpds@*>t+%r^sY$emI26BKs;~}>Jn3&!T2GE z%jN51(8)4FC(lLO0b#7$W@W=I-9cbgTx$Yc0NFvN&>ZCWujlgkIhN2*Er*kqE-BJ& zME+CO=*!H`%-*C-&8`+`U*+M?f^vHYiF@?`r0v}Cp~O{>cOW4aOCqAjgi!7^I6&^h zy}K_w?DC;ecgPL)x|;#beE3Y*nQ}M!YG0?esnH1Bn9`zu7)okTzB`G&cK_s?`SwY3 zI30g8a7lUMW;+^Uy;<gIGhI_4T7ChyERNx3cfWPe;^5+SoumrtK?2|%b7qtKdyXv{ z##my?#K*E*vl0uw*@OW-XdD6f94>f?L_8HVBcHk!*@jL$eF1h$tsH}G{px^FyPBW7 zHxYMh8ql^l+lsL@KJ5LYQ$_PL+t|{>|H7m0awTT0(RLKX9Ak~G+8vC_y11@9>|gzk z>wSoa=M|uYwWhTBJX(#WP2<(<`sr84a9NMHCp>Y7vg&|T3ic1Sq;H+pl#aPQF_at5 zqu+E!KN#ec2anM~y0IkBN3B!Uo=xyMN~ty((ozsoG(mf&Vu__<d6Bl1c`Ukq?Qhg| zJPXa-A=_1hZk#ht#wXcZOQU4n{)P|8j`ttKWn5W@&=KrVD#;uaSZw(|^Npoc2^;c! zGA!@HU_84kQBNxzjp=AnrnDd#dm4AG=_Ljr=Q!J2k*4s23IABxq(4#U0VcRWieur= z_VE$Jx7k82EYL-qXSwI*&#F#M`WqR2g9f%O`Qo`!bZSe8vaZ<7ES~^;D0}9bk(4X+ zFQP1>{uI~SPU_$cs9@~UdiZ@X%2JK$a-Dt;&4Q&tWkUKrx=<lv3yh}w>0v4kYw_`k zqODkVs=JslY=9yr_1E!n67E!)3pK`qAFRBd7f3cYCc}<Av#D#xO4T)~JO!SYhl`g( z1<_Y$>~4#NfD6HGYe%B(P`A_FDpc5e7X-$%{*}Z-3#2o%+Kod5iPnsbd%#%+kdUp> ze;ZJ>)5O@Bs2eEu0zoFxvJ#nu&+A!SiNpc*0tdvdSkz|3pkyFLF5|T(TcENHLHdND zTwqQ5Y`I}2P9a4=t?^iOi<Ot`w^q|tY_d+9TRJMv=gchDdVV~kaMnXQb}M6gc<zui zMo4{vQ}i4$UN}8pPF!eIp-H~bDhX7SH`VE`^F)NRcYXldPwZ=<NBkx{;Pe;LSywA` z>Yy%1eaj&`5o3n1tJ|DoyQZD#HACGry7Y;5Ls;)d`0}B9dj$1o%jsyv(2oSC4VhxI z`KfK__~3)tA`)}PVC%&?6EoMTm;ThsMD;a-+g{uX<Ow`^h@~VIhDMb+cwnD-+zBNQ z;dLxTN%oe2PU7%nHKjb1Ht5Di2jH;!CW#Ak1Dh$+@EbU`L1RiY@df5G45J%}2M8Ac zVrUle2mE1|2(aA=Ad0M=?MPrB1K411h<e~tr`6b!yzC!mol)yV0TQ!blHUyZvd@U) zz*gw7`U&G?`<SW0`-bu{-KqOi)6NuhdEVhuuvJWx{vc?obQ*EKB$2K3XiT%Z3{`(4 z3bk6=SIi=768Yx|TAi&5fT@86Zk$h2-uFe9Za_cQ!fbECFEO+E$8T%!eTSvHV-M3K z;mQ>S&CA=sMvVH#z@{$alG2&fM~gt)f5az>GFq|nn(_|XbQlgKou7(!O&GXD7`tB8 z&C4vdBr}hR-0u9Pjz)aM*o7e<KN7cr7UNjAVO5+3A*c82PT{F`dB>;Cx6dIiqcV8v z1g=i#5+yTK;aWP*UyiCu!Y9b7NgkWXlqpr<@8sE|f4hk+JRO1O41ONEBaQ|x`iLkW zUpHaTSq%)DnX)M?2Ijh0`K!Q%n!D5~Xt=gSls!;gtcY%TzhXg`4TXfl7o|H)FKs7i zwl<m?Kr!5wCf+g5j$E^vZ-~?p;UIj;{yvhdrcCIFYu5B8W9fmb$qc@Ymo3*ZOF+Mf zoxHNO*bHFJ_G<{nj*Y>S<&YlN?S@bH&e5aZv(7Vw^Fb<1#VS@{5TwGwtvuH|*6+$J zAT!FPm2-gCG|%I13(DpjaoTjZuLKN>){q_7{u2kv6^3G&YZM=WeNqHTZj@a6YA9D= zInV3WLBctvsum83lc7&3^!?qEA!7I}noNoV)dPWO9i31dGG#{<)7%;dMO*yHHrDj< zo}GPk7W}M*(%%3%DFixze$q6KxWVYZcl_CS*DVn3d2ZbAk?xh(AQ@;a<IjhRsW!WJ z5qnK-84TXLL&Te|`Aed&2q-nt!&S8yGIityL0+jHZ`!9FQBF>VUu&}5>`lavu(`=% zJTOYk2{^d=0>Eslu=-A+uYkG|NWBb$h{e_)YaC*)*PabxZ7Kk=aVUp0!b1;0gT3zj zHVp<*X^^q(=&|^IWPK-8ojH%I2elGrK)t7o5@%Up3k=5c7|r^Dw57Ks$|kb(I5z8# zVgs_@vs?18p^`6GT;3-mbUQ`u;Sfy_t4lz9R!shSEE<i9Oy%UL1!wb_M|(kfYfSNQ zp0w^Adz49L7ZB;WgrN6?F4qt5!3njx9c-EEf*$0}I`aVsK8cJk(B<srml%c1yp2TG zLh}i7j?<HPCK_uU@el(8cC4deBHLNG?|ZyG-}eVT|6)0BbQActNA~Dj)1~w_){LBS zPj;b4a~js%hKlVD482-%H~A_-$>9=(l{11VVa%EQ$<@UJ2vM#VUi*vTf$qiFg(QZ% zYpYI%>mn|Pw>h{%8o|w2y}!Qi8b;W+Qhs0oN&eYYxKyem6a#ONEcAh+60ic4H39qP zVb+2x(w=K^2e?Z&`qTR!cAPpEu!lDPAuR(B8R9LTSi#%x>#4nXgDaeBu~b7-4v-l8 z$(3Eb4wzvNbc9Dr0g-<~N6K2KGH1q=lVS@|r~*TwhB<!<l;0&NQYVy{1l}ry?vP}h zw%!K}SOh=owcb}kSJw3znwY7#y6m5N-}2c0^oNGCI)U?Y_48t9VM>G6>9Rj6H!;de zu!jL+X$lhT6Ql&WpjXb8kXj&-4*|X`4=6Y{1`2}AK>NccV^7=^#MDn|A`z~YbRQvj zDLII(_w$;~OojD0*4o~Mqf-67%j|qt6Y>T2LbWX`tK@Yc(|Ue*qUBQ5l+N4r5_-Rx zeRJjd)K%ZqWx}=WF(|NhG=kh=0*DC<gc)8c)ay@&&QmTgFbK5l7x9`fT&YZ74A0Rq zHqVhfl~ilVW7c&k5bb~6s*B4g4wEXSN~Aw$PG3P@5)#(0`kucuY?(kTz!DW6b*Ob| z@J!iRz0~#cjCFGtg89C!r*m)!+Fq0QdfQPFJxf#z`)PYK%3QSSi<()5$^aTI4<W15 znhKKe;p@V4X?uL+&_0ld=N#f)K4NqwuimKe*AT`_XT8xOB$8LKUEd~khGeN!Grd){ zqp!EZU=lNYDctQY$VKMxcL5K85k#!rOWExEmO&ZM$DFl8e`YAU`SN1RQu?dK3dhcy zYSF+#qh-mbAY1#~bNzX985EC-_v5vApk{6j=7FWBVINqXq%RMs)7%hm7ebZI4-5=n z6xKHZ8(?J<P*k^+{xbjoDbi<3EL5X-@c^sxi=GQXhGTR+At<k{&<`7?ORPQEy?}1( zc$8k=zIXI45UmW;6B?CbN02jUWWF#LWL1P?Ur0oCQu~-gygI%D=0U(j3{|Ugu|}_( zaE-?ZG!s5NRP>OpWbPgt9W-2NN<Z7!iLLE@_c$E0QW1q=t_9P(C`@THI7-Q7WT3O@ z5<QJ-!oa>4U$?BeT>c>ta+PucUp=s24k>N&{!YyqrelqMZ~C6o8iI!mJ(RYeSpk37 zi&=s4_J;g!G5A}@U<~>CuFjy9pq@nty|PC<N|4rc;LCz@%ie44DnO{FZ~rai)ndX2 zDbcMyEal;rtWR?UWHB`SFYdf35W8VQpgIYH(t)L?F7mfNu*NVfJnhyZUGgS0`w@D9 zUsis`b<!xaF$H6=B$LGb6Xd=(D>SZHOPvA~!tl~2iaEk&FBIh<je68ARbf|MoH@(0 z@8G+aE`6zy{0T7i42g`zkk%NtoZ*f4EbYcz-i!6z!zR8Mm~0H6dAN8+>Z!e&BC<Y1 zzSJcGu{w4AM${00ZFt=f|280K%c)OF_h#JZOkz51N!{Mx;;qc|gcZ4Q!z&grYL|*} z9ryDh*3hTZ!;<o!LyyzLP?sIU;3=PK?yq$u4cv=155{XN#P+Ei1^ODL>24$f4Wq4L zx%}6`M}7go>n~jc%0F0xq++ES_(BJcm{IK@<ppEb-1BSH#!(om-~dpthLo`Xuo0{* zz3AmQ3F8Qp_6zadP>N>9N#E}?_w1t&!A84he!d1wkbXA#Ptues<fmS9tBn=5hSM>v z;XC@h16NAE##5kJnoYM;wIp`lo;Mg*XuO~62$2^2h9%ni#Jvqcd<u){rTO7%(_;VV zI&9|P-R*sj?jdjRX;N#5jV6t*Zd}qR|FrBhFL!_bVZZE^*r?D+H5@2Y4XJ8X3U_ED z@`G{xd3d&fG}`WO*W{TU1!6i!v?T+VHl$1a<50h$&kCfgDzapbAGa=8HN=1JN*9)> z9wQQZ3D4%GhW>pW_m)=CN&>c3nEm}S@$>!NVA1eMsM3*YRk6|9X8m+5Wue_?whCUO z%=z)uH{#R5d=Ul5%{rsmb9-kJWvlMXql_G}4i>M^Bu!nU8+34xxxSyf5C+mmiZn4B zsXC_?N}Fz(404&=Y0|yrYZi{*?&tCFJ<vV$xx@3xQ+Tqo`gOt!v)O*sRs}k5mj=@^ zNT_@O^gNJqAk(eV&N3zS)9o^_tb)8v!uM$jA0I1Ke!A+&K}n#?Qnr-`m^ACVfc&<f ztHdNPMtP|2rVvVwFfC7xMHxE$`R1L#c{vEkS0A-i>Gh(;wI4S=xwjPz3xvYGAS*ia zcs`3z^Q1^J<<%Xch;++<l>*Z^#`jjrn2vm!c=lIxiNxu!p=xzJAu{o`P%))gV#T#! z=ad$<eEkL7TwW1FI7#kkbQGW4A7eOKh})l9Dw>7cKf0Cf4s#8g2&DxQiV+VUZjDGt zqh@gT*`7dUy*yT2aopOSn8j?<ZF-QxzmxrH=8s_8rNdp-w=*H9!@slK7P!u#@8NH| zx8nvz21wwo51G%;N8js7&4$bUH8HGdNiXABe~h2EwtI|fL(fPP^B5d5#XnyfCd-vv zkIT<X1u7%z6Q4MsMy}4h+CR}(G?1{_ob;<-cI$fYlNvo-z@F=HJY`s;?E>RSFdQi8 zHX-YKk@Th1sMTzy^k(q#8zc(b=yk%4I`yi?LZMT7Q*arS?N?=geBA4rm5auB*bXk$ z?9JyKMtGpQfYr0NdvbRQY`l=~CGI<rK#Zt-U|Nkp;H;C+$I&w;-Rp=;$%LuIhC}9r z6v__y3dS2;`K+wG_=P8&uhfl^ZAsX=W^+_zV{_mfb~J`8?>9M+1JwUuD_1Gk$hYa^ z6vyWh<+2dQ@*p3ZTQ&$)%0ZM;xHnDeU3IuzZ8Vq(89PunKlub&v|8wo3=+f*p`?=n zNJ){yF#!I4HY}BcM61{tLk%{PCWs3ORb^o&p%X$W)5r^!(n%^)4T;X<7NL_;RgYqa zie&3=-7Ym)UT<o3C=z!ZkaI;e!BUg-A6QoETCgSXYN=tt`8H7iG4(0(ulgH`3nN;F zg(B|hLdy?Bo$$$Um9p*DWlW*M1H`QF=TXZa;5c^-4V`J}+9ha`Rv_H}?Z|9K4PtHK zgYkS}RTT^qOt54l854CDJ`@nmVsLm^U#Dnjt}NHWE*W2E1U&FAzbt5o?&hDS(RAI7 znngW(pv?)EwkKgW3W$b9|5_f2pA0-g*=@?Z<b$pk1F}%yyyR#a1KBrcFpGpkTsfXU zk4#?b+909x6e-U3mWf{~$QirfK~6+dy1W!sRfs1UwW|_^i_@r-H~^3^&M!8m!RHwH zlSkk^R-1qNjc85$arc~VlRezN!CG6lH?>9E^>|)E+oLEUzb11z|B(6#u_;gb5xe#E zYNJA{)tSHg{-rzf*Qj%=lsxuw-afxnyuOr*0#`aBQrN?8)>Ix#$&S2xY!!`}L@Y|2 za>K)RIhve76jzhsRZd#NezkplNLne^dS2(}Y2a#$*-&9C9Ea2LYdi!_7bj>lo!{t^ z@fmN-Z{b2@rpVjlc=+R_=C3z|?o`TI(I9#;2v}5<Q<~=3^(tElJpuh(UW>X=?GGvU zU)b}X6ksJTQ|W9(_^}MFsBR`+O*{nAopqScWEB;gZMK($jUAuoB}=Ete)THv_e$Ut zd7Dn3ul^uP2?B`ae>CgG)yFMxw<QG;I&y|#?ts!r<KU~0=1sx^lc-g4F-*>DJgJ0V z%v0@rgl~p{o`xzj&ePUyN;b>N8cH}X_5n^AkvTWuOAaI_q|>2?O^zIu3qY6ro$)L8 z%fp?`Y^QV(id*DDYCef;kWv}VZD!lHKhh0@$<jZ^13f%TH@ZXqMhPyKnL3Xv2R`0S zUlS3=JZtWc;V$KN$HVK_8S2e$tv<VR(m`K%H%+DvWBb-cSsG4%Z(m#ibtT(fA})_3 z`0WeNgQkj?%c&D1?93}kEn;#BbQ!{X|I$yEuRcuqs{{!+mnEO|<N6>xuN6@Z9OEY| zAJN+#Fs^QBw4lO`WUX!pP(JN*bz!^^O^mnyBuv3gVKDRO&(X}*{i_B!<Ye9LSC%VT zP-S)wk*;AavlLcq!C4mtp*{lXHZG4M!k`ZoBo1de*v?(2T~;0`^#U{LbP1+}K=CJf zPXxKfRePBAJwO|CBT<gK=K`{j_#(xuoI7)4W*OgWr;Yb$Jz88!F{yi}+nx@_YgICq zHKXKZ>kS@gWb8RR9`O?0=n#=*i$Ua~)8TT(&ETLRm^-68C8#)2tV6Zs{_EiwefhwA zJchg|4__n|t4bw?YB)NdpG8MCZj{w@T&_;4&DO`W#i~~?UNqI5S>@BxTfEO@b7h1! z*8hyAgN2$tl2j{KS*fze&szpfy`{01AN(l=3blI=l>HZDCec^z@K0!e;WB3B{5m^d zt6O`e;kf<yWuM4uKWiNp*-sGlU`2G@!{wI&aR%F(ZYQN9qaz@m!IrK!kUMSfr*jz% zP^f%J!U7R@;Q2cn%Ij->W@$N^>pJNraqyUR3wQ$X&Pzl2Bis&K?|j5heW8n)cMCT+ zCs3S<H_PP9@5-k2Jyyn)Qah4{EOAM(OmBM6OqmCW8<Fx8xcK%>A3L*G{@#rt-zm@= zi=!`enmE{G&W<h9CXzGay?R)Pvx;RUYN|Q16*3pAlxQOhONfxw!I(S1^B8Ph)}h&F zR^bd+4vuAzR3qo*tNGF50Ur0mp+80rUc22@y7i@QedMB5zuJNS#Mtg0o5{m=QNNh; zQN7AZ7j*Xr3u7ERf{t_{DLW>ZDC?Vj8DFFgZOG$U3aD=zh;Fib2qJOnRp+oi0Zn48 zE?ui@daB_IBopRw@sx;cl4gyLeK6u<yjcT;KKi?7ew*{xXi*wew)^qt-54XO6p7IG z#Hxq9YP${5hwWg2p_?ssN2?v2E`uwTbk1`jbtjqRc&np2VEZhgZt*aBijWFwA!-4B zZhfwAJwqJD@m8e^BM}6vD_#$>1-lUunU$?yf_Z>ln~(PPDO$S}j59`B(u?KxDhkO= z;v=TuwDXk4kD#Zzr)F-gaXcqGyYTjni3C9n9_E=7DK?l}`Wy&ieu77q6w?u$M;1Rk zx3Y%8!iiGZlACh~tWZ$5e*McebN9<Wu3vt<Z{&(y?UBlnSB|uwV2HR(S#UsXw8Gpg z2%1bKCbu722?{%!`Q^VPGq0UwoAOP4%0J*`-Z?muuB)jM6qMYo-cn0#39f7V+LZMT z71^sD4?`-+HLpA}t~UdQmfZnZ99LUHThm6}5-L3GE84*oqTvy*4ZR=neCm-(RVyPL zlY$qu_hDk~=qi#sx$8&ugcW5jvzP&|+AplROcpBbI}3jFDbM_S18{B?BR9Z(_)a4i z9M<aWCN$MX0URmwtsO2iIXquKuMH?lUfJ5i)sr9kkMYVG$tGWpR=C%;yPbnM?5ew& zo6fmeXe3~dcjQC9(9{bFm(Pj_{KRP}TGaK&#~u$^@hKB7;qd+u(sPt<{Bs#O$#^Kz zI22l3j(4qp9)^|SG!)g_ZEc@c-6Di(aW$1Vd@dp}HkxHJ>XJ0w&G+m3l(;>VVZ6Q1 zCxc@8hH!7a#s*7DEPd?G8?!oy-$?+!3c0bsFyT!N=tI@3#6~K@($6hHJ5D3#(4q6? zYE{YW*Q1t+De2XtlyRD&+R;fOQ`*M}2$MYqe@r8E%HhQwEiD@pwO2*NZ!^6t*-%$G zTZueO&^*vwd|CAkU}G%~?SV5?SYy5doo@Ld2<2G#%Xu<F@nyk5mavaXOVPp94)ZxU zz3gELHXz32nLfuKVR}aG)-yeN<}nEXjS8YmE%2$q)Pf=hfl?^t$naxk%SNR)PKH`g zH_cmGL}sm2GTcFyO2y1Gdp2lo#x0T{BN1!H%4$mu<)+3TKweV=2eKq3z#gfqT5Ziw zBZxkU&|b3X4W^3=n2(CEe%{7(a;`p{-|~;F@N&BK<x-Yry5hCEj-#x<%-(&^7n2h{ zX0+3HwN9ff@J?mpcC5Yz`6m|e)VnMVPMXT9JYGx}s<n)-bIpi8B5zEoGd^z<|6Q0u z*j%2#IAZ&VZ8wNR1JrXs)ep8tE>?rmTY75}`~{SZlIH&UUTpmQY*ggUzL1fcnvs!u zdQfoKXdIgwo1TX75qAomd$EStO62xV1Y=KLxhNd2EF{iYKRtYa0>P<AcM{|9ry_-# zQ*%sim5f<J8xiWPvls5$?36_}ONke#u3ZDt8oFm;FsTbTdnjxKxL0S>k%p2e3<{ji z>Tuoe2XxqorttnKpxt(NHtra~TJ8x6>89=4G3MlL?{)5G^v1L9a>1K+wZiM@^uhbM zWS(xYS-8%s%4r_@xqTbT;hcP|Bf96dJozY5y%IsY&0X-(Px)?yE4hK&AsthuZ;C7X z#OUROI0D<x`j-i65Q2P0+Ht^)88P8k8irWjLJU}*1MQ|V9+*J^lY0i^vX7=$Mvi!W zMqje-$!^b_xT+6Ii^bx$Fp*>-5;Kvh+?1Q1B*o5YKW?z7MX$fOT6gegTH^>yo6ei% z!q(E!i)Do3smoqb?Bdjp7@u1lPPGg`IAfuFDN}nFS{SD$-aVng+r=J$b4hiuY+QsL zmrmku&Wyg3D+R9VSa-izk|pci{?pc)baMfiHr~o3^CeOpq-Q?T9XU-dQR;yIsDlx| z_Fo4P`H@@W^c;yAYu6YK0p8v6lesRBxNoxv-$G;8YPo7fy)jJ0TemFmdlzY#`687I zDrtfqJ`?{r(2BnNdQ56DRG{Y_aVq7ACqH>o`aJgIa0G~zQrB6-ijeV)LiXq%iQUnn zkqKdFbOu1Y)18y&Yp947{ch)nEsLL$(Kt>xFn1?yc^0xbrTsWdtgHs#toNQk*qsg{ zzE6ruH{Gusr0eg&Gms6gSU)#Mx;B`{1bcUv-dwPa5D=A0F^7JBlKMgK*o8?9q4hC7 z{DQ8T&HyM>?7uo%b-TNd<00e4GSoh<94*Fuc>z$A%F2thR5>h9d*NjZ#A#R|wVMgF zOYO4V*zPP&n=u7sYp2D<9-p5NiwH|DIa1<d_P1B`@=2179P})z2xu4csqvX??DbRg zi;DX*Jv}xWbo#7nxa&@os@F7><2Y+w6a6qVRS?x{Au+M@cC4V0r&%ji9_rJ*V^GD3 zRX*QFstZz(N6g%%bg}w}Zi3Wdn0Z3wMxjp-lxJ2$>K2(vs#WExF7et&(M<8s7z~AG z2S_4mUTw8$VLmH)QpG7Q8$O0F!59|Dn(xi_%LWDG?apcPvt6cZvm_dAHWRoG##f%s zMtzyqoGMb6D^&(9k~6u>ZVlHPJ#qq_Gu#!?+dXlxO}=)>+^y|bhlej3S5T72-j|I% z4P6g+nnl-t+QeB$v9N!C)j49uOoC9WVo9PNrdW+?X5)@KM?4qp$7qUKS{++Yv=#>! zb|(X^<)&Sx30so<_L$;}DsD!dF>=}d6qIJy!O*PB%7RaPDyavxW_z_Gx(B7gh7%*N zDI#qUA5tKSAre`v&wu+%ku^e<A*hf&GX<0LeD)}fl9Tgs!7FO9jDPu475Pr(a;+aN zW<oA%qbUU@#zT{f06%|G+O89uUpY*KynIT8oCTTqtZZK<@p7%zZ$d7g1|vc3jY=_3 zht;SnUKs5XuvVE*n)t*o-8f`jDKD6w@ZgmXykb*%*JQoby(93Ijm}2VldXkC&(F=j z#6Y`X!$Y?PW${Q_^h)Vw!@;hL58iKvqpF`yl`BZ0lSl#2a{+H15+}o%SW}6J?H18- zAA-3YUmXK$--yei*?gtddxH(uL-h8%=qtp*L56>O72Z-;L1Oc89cxU(bKZ%DSG0ck znw`E>bFJO8cLeX@xcS-_Fv81{n$dZ2JBLGy!`1je@tVfyP~2&Z@tUxu=L3XUuH@hw zD#~QO7t<Mr4C%hahW9WIwl(FfvQPZ1M1ni|H=`Q~2WtlyZa|=;;m%G<rw{EBNUv=a zkHKw@q#m9?0aMU^lqz4w^xr9+e2;Y4AAk8CoqH@R-OHuhRYIf7mHAbSM%qXeIQRr6 z<XPXkI&cW<Z>q?$Pna<mO@Z-FnZT1N#p9%DvwF2gAp0tiMfr0ml{G|ewr%vAI=eFr za=;7IfBx*$)NML+Q-;gW(COS$^-+2PAxv<lh2U~L*`y>s(m@1SqTXbi2qH$tg*G(^ z4V>-S9_5wBh93$gF+SH2AP>Wi)j3<;3MrlxY7g8^&HDdpc>WZW6gO#ceVS_Bz3RFx z_hJad+aB*<YXNZsF=X5Kl(TKc2dj_1I(5CU(1?_;goVinq<!;ZGa@Nb80h129V6e9 z8z5feB`mQWL+|6#$(SFcLXhJPo1Rmc5y0KEMk6HBlOkjZ2buB^zM$V)@|?yXsLAc{ zQHm-|qL(?Y4n8<<7dJyLXY(UnVJx2RDHzP_`rV^inwm4q+AmH@vi}T>hpFfoq>w!A z4ogdzaZ0D+)d#LVeI1f>V{r&0{#y}i>G++}OSUnyZhD6Cl7f+R9{Wdi_2vMlE*Pff z5DE^fTvsKWEi*{|3@}L5c88I^#H59VC(=KI#jrcwR5Or1f76#m8Ia;fNNV_RMKvDv ztyaaJm=*O7WGf%<WgNTV1a=E7rl!3xT!*}EW1utgm`_qXqZvgOnc=ENAdlXX8<5t< z^xw83yYjWv`>v4K$0Q?hBaYH<T>X`fiA_RkMMonK2Y5F`ju>1SgxY6D_zWhuH*YFq zDlN|JQaiC$UAg8w-B=u_EV{Snv}xT~N|{U<h3Rr&vFZoA6?i=h$Yb2uXRkbG!zgQf zj6cV$&sJ*CB8E3F)x!)L_Kc%CSkuGMdEn$3_3~%1vPZ@&SPRE(#pN<4T_!i!gu``! zT%t6X|9=@;fAn>e#~P93%L#U>c1ndqXWAY-YlYdB&;)EMEmn<ka!0W@67qnfzyil| zY<FSXhz@}@*;uWncO~E8!Hn5uiS+-m#tRCm!zsA6<?@8y_<I!OaMLw_cj5zD6%~LE z#k3N4D0#SVb%%+?rb$#2?@uj~zft7!0*y)iH@kq8TjX~$2`H)&J{wf4>t{FCo@*;` zQ@lv<nlzb`eDQDy_AN4BfrA15y6MyUDwPb67vBk4dy`{-41(;OEGSrJ=O;%8tJ7d~ z8KQ&8Dy1OhWy2AZm*5EnR!g(_*{c~ggN6B#CM6weElcXLDcjzIU{>kcEa6Q7{@9_8 z5NerpeTUbWv^_U6%;Ppy!SpxzZ~r&>xiETK_re=e*3n!tA-C#lQ%($<NbH=ONNH%H zD-~|qbOEd`Y){zaD5Z(oRVq+>({r_5mfZ-rXV^hMBo0zj1r2LL&r5Ob?Gvj(I4~~G zKEIo5FTA<M7cFaI-?Q9~1dlUG9n0LbBLy$kb{c`ii^})KXBeLTVC+sDiMrFf4GvVv zkt|m8;Q>~%!CKpA5tlb#-`J6ebD`t(>?Va!{MGHFoFZ~1hVe(->SHzb)k(lm`|gig zEYDXfF;Z0P^7e0gVh_PCH*=J=hPWL{A1$=JpWz>NcaQtk*N2h+c515(ns3g!$1<Es zR|fa**-?a$K7#^2jlOEOuBLF<Zd0llY!1K4`(PYzKpoL!S5QQk<r@T_cw2cG6}q>N z`B!ATXmnnfJEnc+T)!qWd~A7r%I@9+YvL-mt|r5sS^HFf9{`WE<EhDU^tmN;!m-cS z^KvxiF~T687^OjNR(=yhP1KQLV&)Y>o*~h8Rb5pP0o9f?9U`^4K`WDX!N@fOH4LTU z0W1L!LYSI|Ti>^Hvma9RMRbTsXnUu}v?ZPV8NOSnE`~8vSV_U-LGYDK7tmixIBE13 zK`HhUGm)CN_U%41g-Ac(PD*>uG1gV+wbsxbEH8Nq5!&tsJA-}|&NiIw5#8(_Wyzs4 z`;|B~6^X%hjz2^Lps*%CU-pC*kFG1lZ8p;xvMt`Kw@Xq5f=fumCC(ekTjAL<CM;#F zP^K!Cug;p4pIYZ)@o&ocw_mxFDX-u<*F8K9HJ)8V0o6J0LfzThm+g}(NADm}{SD1h zo?S00vZZqO*ft?wZvx<B-<zAJoZ5eeX2LKTAGGTkgm>7EvkxCWM55S-wB?3X(}y?x z>BShh{!`ZrME0?cetjsD{{c8GpMPtf=PTBj9U37L6~_i`SRyX%qFxdk;-_{>)dD3N zv@}0&)LYLltKvh%s=espl?>ye5ar=No0NID*S+p~gI!zIC@tI>&n9P$-}*X|a7K#o z`EYM6zvcSKJDKVe=&-@?=y(Z&p}ncz>DTtKI<epkjq#e`8Gf&^(bZ#lS<}eCt2s~9 zT5DK&Kr*Q^Tv+8@UaW(u!RDcylfLeYb-E!CdVyw8LM(7&k}uY1BLvPF<%;y0eM$p9 zyr6(<_ZH92am~<5Sy8FZdE0}#PI1fE%z-Z%70n>;!VSS}b+m5Lj0qFy=8F_4ptlPg znCFRJ3K#0al{g`&OQa9D7B{M^EQN``jT<!%5z6TPoka?Kte+e;Tii@sp}!GN<oINN z6$p&vI|um@-YdL9@zH&4(_0g_uwg?zLKbo+>|3!&eR}!%R<zQ?)5*Eeg8)VUpnxNz z1`81*f(N$9yJlbmfO}SB!yk@)k!KVuN@8)~U0B<s%gM3pYY$|2d)BQqrkIbp<4v5? zKNYDYT_;{Ad7)YNiki!waaXu31@AmZl0<ywc-31tnpWSG6Jy=;bWe8pLEh{n84V!p zO?|DHNL$fxzUc58#npJEYo)kVXy)uHH&PRgS5JJ*-eU(OF(<OeBUiz^%#U*0P?Rd* zQ>cYDq)8h0ZT1`RAG2i%NG*j8@izr<@PlPA_kLHmAc?G-6e7khAeaS8z_2b5XE-d} z_R=^Z(fIrfnCS2n9Vj+60nw~=`B|0^s9BhK)#7~weyI>%MJMtIP1gLYl)BDsB{Es) z;SPAw>0S+tiEQwn2@HUoPK&z7zW&%3$NcDarP<_n@!3I(gvi-I=subu!V>=n2ev*4 zE?mJ3cs2zAlyk|Vj(^NVScQ5oNDY&76n248h-zrX0k~92#PF`05zhq~H()tk49#Hg zJ(o%9iS^F_tbuhk!j%p-5=Rh*VD@$vWxgbD!{!oqxJT0M5CN-t80nU+bF_>&c~jZh zPb*HkskLo<aFo$;kk}etTo5DH>k+O|S{@^GATHggWu&CM6<fzx!b&1E6MKliAFI)7 z{nogOf;32m0+w)lZjw=h7;0mZI>d{i#&uKF=I#rYy}gZ6>2A;yrh}tpVd&iAl8nl; z<<_&rnJk;#gY!^nZ$6I`7o&Br+GBu{)@t+9{)(e|q*I9_{xkEc1Eax~MVUW(&V0F% zqG63zC&r|q7tA9;Y_F&yNBeVsVTO07$!de9>?^h7--{nFy=d1xAp=I!W&^0%#b1_A z7Ze)be*mU0q9JD7s;oAZ4a;|fn`*!n$y{7nAKqgPXBbLQnQu^NN8=|cw}z<=O|mF! zNDbf|Mb^d53aPIFU?`J=o5NZcT<h-BtMoF{xZKBQBOR}vS{%s7&c|?(nM&;ThmUFk zP#n%wzAeNRs(&oJW_$=<Kzo7cX&CpN;Jq#`?GauAN<~;Hx&W<073rQu17(QigGNny z7n9YtYfQwWXXxa4zvftg-;;?=T9~8fWsa(go29R2?VOoC8*i96J%zilfv5Usa`*!e zZhXEa*>N^sKQYiLzh~<77gW)>!#c)J@TfxQYUGJ%?$L?NYFTC6k>gU!9U`ex7f6z_ z9ijU<cB$XDy110f+z_t0ar)G~|M`;OeH4uVaVcd^;@OMN0QW-VNU#umQsGA6Uy=Fp z3}_Z=XHSIc19QM$EM<;GDk`qLg0rwQJ|3%i(baK6V^q_laxs(Db@4d1K@3l*GG}RF zA^fp;{3JE=Zo;LCT+2U^LY(Ygh<XUgb7tj%B?scU!_*H(n~b{3xA`}CNB;3vHpk`? zim1@OYEy{26-<6E5O|H6dpLj|2H4dt$Yp;sc)^i%U#6TAI+~eQc6i!xD`QCB1#54? zjqI%``<f^CveN1z<>mPqd4@zPP1It{`|y~l_JH>HOCX6@ta@u0lnFZz2tkoBCMg7s z$@=HhdWP0LN9NcFV=@6gEks;IW%T9*Vg*99A5X<i50?ciw2Q$L6zncABD1e=4}vST z(lWIxo+jGeIxZ>^$R?TqM)d*Kj~J^4g+d7!M81dn*sW<U*^r0@i#b$`iG9<Fi@01Y zZ!W{_64&STjp=*S6PQ_Fp4SB>BaoFUYr-4(HFZ$%%G%uH=Zn73;`Ea<!u_nXN+ZK3 z`CX=$R^{OYzds9;T-=tF9lI`8BeafsGLv+f!d1gOi+;MBUkN%VHg%R&6fr7HEKDs- zQOW?(0I`aKqSS>>s1eg0tcqr_b58p&>dzkV1t##G;hCLhzLq`H_}iad)_mNanTwf_ z9n?bXgct*|4?WcPFnCWtCuwe#7Ndi6?jQY?^+juZ!vPvv>${o;+LzL!ugkEvAiw2@ z3mK=fGb$Y2%$&J$OP*u92m}eRPoF_3tg!!`A2$3wL7bRrRQRk;pS^QT8NdYw44&!f zK?ErLQ}IdG+a2A7K6<393ls(fV6y__{{L)tPro{VL>WFSK2@f=qXEhNfvN!k5@$er zf3I7`%NT0PArJSs+_?EDqACd#%F*Z7gGbTVhtda^{=Fg;Hc~3kUqsiBltVFalxrAe z_-B|TB-qp;Qd&M*tUsA=K1g0vL6fhjq=ZIv|DLrWCPfsJ<8agVviULnvIXwLD+VZ> zHr$*gvXLXC%yP%I`funbwB04JnIcd0UVeyS<?pr)@+2v4DpT_mTw$t*B&=v!;wY6_ z0@;9DkmA-i6Xd{~LR53`7+nDK&)?mdyo;E;qnEiPP%#JZ$v230Qjy{Wk^7`K499^# zjsc;Ev+>(>--qfLN@_#Q%Uww>e5!qgm-z9d9$UJ29j<*oxF(b13=Gbc>`6E_SuaV4 zAa!59pauf!rl~8f1eB?JS#=P#7XF3;_14%SPJ`(w-5YooQC(B-xF1}@$Sco-0DVBi zBm<0u=pLc@QKbe$AJeWXAG|Bs2Lk!)Ok;`HdOuR%8=kj_Mhhc2oP2XjvK~_q@qSuE zj42oR0`t`=I-y@&=jAOc15(E~8oF^$68yL?qty0U>n2&(De#%3Cg(v9dLZw+30?^j z930P_shlr+gzunC<!A+LwB5IzfKCcqv;?n_@elb=ypOv`Pd-DfGTmbc=RcmUq1}v8 z=R@R={cgjjotpfVnkVdF+vraf^uj>MMEl%>fdV_99+KoNGSJVGQ8#75Bsb%0n9R~+ zSjgW1r72T6zxM;lT89m>iSwV^Z9XyfRI6S;gA76`Knb8K&{B%{XeLBzz$grP>ahRQ z%XvmMm34ib-V>z+2$0Z0KspE`1XPL;sz3lSAjJ|zDT9O(I!ZHug+Y)ef*?&m5tJ4n z2ucZ{l!&xYL;)f6$b|3)XJ*Ya^FE`W=DGW<eg5~i&pr39weN?s?z(4R8QqJW>D{?B z+F)LA7G;o>QQRKS6jR7H7%OAi>Vp`$Hg#;T-4rMmy0tjTrhh1`9Me}4Q06mIQE7XK zT4YNpj#}C6GZf%DwSl{_%@wdhlb(Wms81OQ<Da)pvQsJJL5zkWBU}`Pv8cQz3_n>t zj7g`$_a|DL|Iq3^vbS^RP?Qxh<|;9obD~&7A5Rl&j#4ElQ2^B8OnI_EZfig=XXTVO z71|u9LuMb1xNg{*M^Yu>cq64~#?(X#|Em17O?zH~;lBbRohYuPK+<s!+PLl%dlgH! zpDZfEmhx%5l2JTvU0lHP1>1J^yV!8uC?nk%uNSI#M*Zt2)o#yQ>CV?hFE*UGeMd^x z1fG;8prBx9S6FCf{#~Hq6s+pk3zE_mr|Ai8^pJyaoZI%c+IlK@(7ZlR*L+Man6S~q z3VXJ<<t`4KI+!vgEJui((Y^cSYRfkFffKdc+$Oq0v|l5pLix$01{@2PRJy6%xme}% zh%u{*^+Odi|3&L_xop`%$?H%2Cx&_(>O-8=R}??GzG^JX2m-A?@ti>Pyu%$^XX@qF zQtfa2f!GsU{xBIoPdV=AtFA^Mi{mGyCQN=3nnR`qoFaS^Jnt`@dKYCO<~-fP$DXcR z)gCPiJnZkD$owY~Th1Pj;?1sGV?5ldfs7Wvq2vN2^rU#b5Nvemc!Bi?&uDa<$94<f zQ&)-C3rQz2K-;7<Ua#xGz_Px~=?=w(M`cxh`PL07lvtYw!b|O^&n4cKwNBSP{Z0Ia z4G!y5_V&1)uLqDacWgf%n4_xAW>;vr^VhnIqNj|PwA?e-H0I8Cb*J4^P_=hUr!Vlp z7JorSjg}#qYoMS(IH@k>X3E9wp`^*%AZ^!E?|ySQRVC8MIh`qa0#(4LrEhA+&vhTf zWjWT0MUq}DP%N-#^(N9o*oUW7s5+wy4@Igt&r|{Q{QADai1Ez2*QIsiY;yLnw0SaX z80Xnm->Eu+esnRJCl_xK%vwp>CTllF8@{L)C)toM23x!lrQFoqn}zT3$Y*E5D#UMy z?9%^%oj<{9$mK)dGWuJ-<q3IgVZs_%W<gSU^8usW6(&Nwrg+>Af&MC!y-ww|*pS$$ zJ(1#gdB1l%=`*TgPLN1WTwEX$K)kL@jDx(G?}0Ai8O7Ro#qh<>xvhUb8kr@xO*0s% zN&+>;P2xw$Y_zD=lN5d`xH%f9d$DckQcFgkdY+d-XIRQtvGt!m2K3DTZ-Lv;J;%*Q zi115@vo1r7j)74Kh&zJa`42f1K;LCq@fqX2@LX^~TQ7He@_;*MI;k@{TzSwtF0Tzh z$=oAM<c%~MR^ABh9j^N=h26F{v{(AP^PlxTTo4xhBY&BH=FcA%+ehN2@HNLww)oPQ zLN_jw#oXGIf&sYrN)mwDilA~t#u(!1ds1%($ZhoP5bKNG$L*_@6`Z#P${XZI#_ujJ zzX{x)t;hU44tu$riwrWP-VCCo;u|{8GMTEy`Z60lrM=#f#wnyyi}csB)?WK|6--f= zaCIq~ozI9iZ354Q)ndS5qJBmMz)w1}W48kSBKEXrso^ag7syvz`sS!eLSN3_%XOvt z<0d+fKe2)OB3{{_U3GBO_XpV^U-3Jwwj=(Vw1gFr)B&lNf-Uj<i#Fwk)wK4VqD`yC ztT5s%UQYbT*Qz)1Rt8s+#swN=(H2YTMCIx;o^|a$iQ|0x;7Z_;N_Om2`9;T<4s&+D zSRi1}y2=zQOB+iU+!2uS(pW@bRVmPMsI`<@zV@uEPtmaK|BL))%^SYV5p!-kT}>Om zU_L8tzpeFtk5>LLu~FOf`RbzK2aPKjqYvp9KVU{v9z#y)JwzLSNf_mQE`9mrV9H~W zdzX=={v{oGB9WJ=B`>$`dsqIGn|$PVug#S!NMY<xL7s$l2lAuFSzJaii=n>zee~i{ zKYFhdJ#6)W9&(K@U%V9vdHTd?IPi5n@brk}qFjGH6!9$%5E0SRLZSNmALJi5ij89f zd>`D6lLJ_Z+}F~&gnHXwm~U?Meef{f#OP+6AV68fMXLsd8tDH%xP*GwUzBfUlofXb za8+bftLD1^B63vg8tPMje}2A^QG6T_@Lhn9yuY(uwc;^}Z-@`Fp~1dx&raTr{6l=j zo%_|7!cI9!+^j*vDqPnuj5NO2_C@z*eNS!<2EkTf$RArc^2f%E{IMA!e{9moADb8Q z$M)-mg>cQy=FHGZ%O>BV2_5^Dtdi~C!9VcMu{dTh@SN@~(%(fI$u`ai3_P#<H<5Bq z_YTP{P8$4o(aw^V5ygiaOyoQ?5WokM|K`*gW#u0H7U8$`WP)2BqlbS4n^XoC`To=W z=z@QqdgH_IH}9`rx!&2#5B=3TcqzgFEib+=^hp0f|2Q$&G~FE(-L&`ZwrNr}d`9cp z(Om2+H>{(L?F20LRp6><Z9CkHs^z#E+S?tAMb!%2Q(D)K(qd(Kp0i<f#57>9U6hun zL*@6S$WM@M8cBHxFkXz}mcvpB(Z0Ia)8<uf5)AYWWD+&2FM9U1=O)7VF>+g$OZ#A+ zz6BxoCQJyUvV~lFNeuV~+!C4&VkEatEZrb-R||QnwtoW(TXsv2h!)lNJXK%0B{d0P z_Q5YirD`Y7%67NJCO(Yp7W^9+@vMC1cC$$kqrBy^^qlBjy$_m!)i^e+JKH2%<Lz}* zg6L1avwsz5?C}*PB*#76(4Oz+!~L`7aPP}6hMHE?COjJP!SSoW8Ayj7Hc0pz=nPI> zh0ee_FxYM374-?P#!gB{*4w<L_q*Vt=3B+h*Nf3E_Ied1Im3h2-j|EDpTE05ddccb zP0`M|Qkx=rEL?+*7VgpqIOJJ|?^^$@(MSHn1mDHW_ZA{bzPX%XtRXm3gLaPkhH`gR ze#WLF?_U9lIg@|3OP-$dLBF@64C2?E@3#zXIdnvTcGu7$rvha1&;PzCx~zpLC=sz5 zLY11@Yu_&&a7g|EdUraR#+7W=6t!PU=J!K`!I@!-5y?nkvShlzbcd;#X@^;k*_hdd zIgB}-naIM#V$AZ8WtLTqHHEc`O@b|gt%mIrI|sW4yB&KehX%*b90X1tPBYF3&T>vF zmkO6L*A=d8u6iyCw;DH^yM~8@$C0O$hX7y#C<1%{xquG98c-0Z26O<TfmmSq0geN1 z2kswOKB#{%jQ1d~HE$SiJuijtARmhFA>S<jQT`PEHUR+v2Z1{R^&mFT5s)7!8#Dph z64Vz&)9ta~hLEXHn$Ra<pm36K3s@hV1nvOOf|o?lB3O}jk!eve(JP`@(H>E%7*xzw zELiNR*aCzBA_zGQNrGfUo<Le4Zy~b~GSmPHha#aq&?x8~Xd$!~`WiYat|4wL?kJ8D z4;9CX=Zg18=t{sPkP<!;Q4)6~1|{_+EhUkXS0uwEQzRcq9hTCSGLv$aLP>>6VWo1V zs-)Va6{U|#!=+uMFH46?W2N89=*pPMILn}9LS>R<re!u{d1U=$7vu!w;BwdH?#orl z5#*NSHsodG9p(MxZ^_rl6Xn+qX&ox1XKKH~-7v!m237_b0}BHay@H=A1HB?2Bak7J zUJ-~fI6KFo&zt|aKtxs{?$d$7(1RebexlZ0D{B{Hvk$h4`6tR?oEqafK1EjOd+#|V zcbWA=1#P(8LRSsRQqWy?mQ<Q#gr?qA`!gWRjQ741&D%{nD;Q4Rajsn3V{`Mo;8f=O z1*%XdR9r+b@=-%8EY({}sF56;Bp6fO`A$o;aWgm$t(XPvdz;+llsf;}db7ThyS>a; z-%ZA?;max?>~Yh{jt58L@1ox;15ud<A;{<voz$|8jEGQV!Q6u{s{-RZZXx<sBQYaT zdXcuN*v@CyaBGB_kWlBZ%d1RH<#A9|rpT?}#N3hGw`>aVDxP6=^Q1%%&TLj&WAL1o z>D+J`yHWvvx!{e#_EOAXA;?*Wl+#*y0BX9}A~Jv$N--=*(;bL|r=9B5xKOPYV{+PH zSn)F$-R9z<S?=Ziq^kF@#m=3Ii%0uv)D$gvcX-114r9@+nRbl!D)zNXb}f)b3seHN zZ7cMo9KM+Wg;%B58A2z2Iy!t_#nd~&F`>@2YMYxEX{?d_=t3YmJ*fhKFuc&KMs+gB zsCbZ6ea&A=7r{!n#}et)t72xEOy2k*GP}MwO3cvLo7#T&3}TcEVl6y5f<itvb_6f$ zm7xL8u5=8Y?y5xAzCOJqoHlP%W2!ePjJYO<@{onCU==gmO>a5_uN!#2Z!NhcmHhq~ z_I>j?7r9b3J1-`OE~%2_mUX|!2;Q>Sa947fLqq680_c7Q=x6-psYW;;?BsGcFZ#CD zTH07XqkhRn%jQuO)>K+WKY+y(dOo&e@0~~uEC`V9BgRKbFKC6#Go0Afeh!}3NWTHM zvjnbWGEy6?IX^obdM;{R(;Y=D9W(W0#I0RZ@1R|Y%o`Adw^^b=iiaKy^lOi7$av37 zwc&B6J&KLSrb&5v0lC=?Ph^5W;B1W<E$-Dm@Qtrk{|MQ}O($h6C0GzNbF_BOGny1M zJfC<!@n!HTBaY#bk_vAByJml<Z!aT5%W9wZSwu<G_C(nh^wD#A=T<V{gqt67tLBvC zvu>|Hh&hR6_N*kb6VOXa$<J)ocG;WxsO$1)YWG4;*_y=|dzf>XJ31#xCBYGv{<3Kf z@Jr{@5Y{0e2M6=>2nU2!p!T4H#nr$;giSa=YtYQgN^8h6K)LWz$uGmBsIr``qn{q% zCDeZ^%~}pM8?jdfYitgmJiEK0({e19=SaIPo=2z6tN?qY!+8d)(`tOz^2jTPcVdD4 zE{n+OZoS2;&t5^}AzZRVF?daf_2h2VkdLVA_OK2Hvf6EfgZTA8>cn(h)4yDJAwON) z0DZX<9X}y7hFPeVx6b%n24S7d4e?1&HTZC@RxF@L)9Fe0!;jtk35jPUCQpbTe%kJ7 zp<LHv@@qv+n_^k`+ACSroNHZ5zK!Tn@Vu)Z{`CA<O)Fvq3A|xs{$_P7yQXFXn}&*z zNjPs3Yjzo}1#O=Wxh)|ta$R%g_=f6>a&L#=Y^$3M*7#!wO@bP&A3As?R3)@P*ZuB) zao#ajuxyJ7&bU2o3^%dyus4cLlS@diB-eMJB})o~#rQwgeKiEDuP^9Z9*~*lI8l^% zj*AiR;T*R%%JZoUrD1Lv8<6T1-nHvy1*t2lE&u#;>7~&tu%>S1TT2Tcl^N%3)9u`3 z!gs8vjK$aU4cyPJtO6x4BH&dvNSo@lgWlIhfgEhg6!XReJ1WNrKY783`w00ItPq}Q zRotvoi)pB;dg@=_J1{<0>*PodQ!itfcviRRf@SvVdz+aY(?jem3O+t}(?5M4uDm{d V`l^D1+v7dzXWE*Ozus}S{{vS5Z1n&D diff --git a/src/UI/Content/fonts/opensans-semibold.eot b/src/UI/Content/fonts/opensans-semibold.eot deleted file mode 100644 index 55d28c37833d2c0c5426d6639134a1be99bd66c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19716 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 z%0D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 GZr9ov{QH0a diff --git a/src/UI/Content/fonts/opensans-semibold.ttf b/src/UI/Content/fonts/opensans-semibold.ttf deleted file mode 100644 index 1a7679e3949fb045f152f456bc4adad31e8b9f55..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 221328 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%0DJH?LF<R|0Vml}~i-{G6Mo30|jDE8oi8Plx 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 Hn)d$y2uM<Z diff --git a/src/UI/Content/fonts/opensans-semibold.woff b/src/UI/Content/fonts/opensans-semibold.woff deleted file mode 100644 index e83bb333d2ea4655120d1791947d19d28ac710bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22604 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 diff --git a/src/UI/Content/form.less b/src/UI/Content/form.less deleted file mode 100644 index 28474c962..000000000 --- a/src/UI/Content/form.less +++ /dev/null @@ -1,133 +0,0 @@ -@import "../Shared/Styles/clickable.less"; - -.form-group { - .input-group { - .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : 0px; - } - - .help-inline-checkbox { - display : inline-block; - margin-top : -20px; - margin-bottom : 0; - margin-left : 10px; - vertical-align : middle; - } - - .btn { - i { - margin-right : 0px; - color : inherit; - } - } - } - - .btn { - i { - margin-right : 0px; - color : inherit; - } - } - - i { - font-size : 16px; - color : #595959; - margin-right : 5px; - } - - .help-inline { - display : inline-block; - margin-top : 8px; - padding-left : 0px; - - @media (max-width: @screen-xs-max) { - margin-left: 0px; - } - } -} - -.text-area-help { - display : block; - color : #777777; - font-size : 12px; -} - -textarea.release-restrictions { - width : 100%; - max-width : 100%; -} - -.help-inline-text-area { - margin-top: 25px !important; -} - -.help-link { - text-decoration : none !important; - - i { - .clickable; - } -} - -h3 { - .help-inline { - font-size: 16px; - padding-left: 0px; - margin-top: -5px; - text-transform: none; - } -} - -.form-inline { - div { - display : inline-block; - } -} - -.has-error { - .help-inline { - color: #b94a48; - margin-left: 0px; - } -} - -.validation-error { - i { - text-decoration: none; - color: #b94a48; - } -} - -.has-warning { - .help-inline { - color: orange; - margin-left: 0px; - } -} - -.validation-warning { - i { - text-decoration: none; - color: orange; - } -} - -// Tooltips - -.help-inline-checkbox, .help-inline { - .tooltip-inner { - white-space : pre-wrap; - min-width : 200px; - } - - .help-link ~ .tooltip { - .tooltip-inner { - white-space : normal; - min-width : 0px; - } - } -} diff --git a/src/UI/Content/fullcalendar.css b/src/UI/Content/fullcalendar.css deleted file mode 100644 index 1022ff39c..000000000 --- a/src/UI/Content/fullcalendar.css +++ /dev/null @@ -1,1413 +0,0 @@ -/*! - * FullCalendar v3.4.0 Stylesheet - * Docs & License: https://fullcalendar.io/ - * (c) 2017 Adam Shaw - */ - - -.fc { - direction: ltr; - text-align: left; -} - -.fc-rtl { - text-align: right; -} - -body .fc { /* extra precedence to overcome jqui */ - font-size: 1em; -} - - -/* Colors ---------------------------------------------------------------------------------------------------*/ - -.fc-unthemed th, -.fc-unthemed td, -.fc-unthemed thead, -.fc-unthemed tbody, -.fc-unthemed .fc-divider, -.fc-unthemed .fc-row, -.fc-unthemed .fc-content, /* for gutter border */ -.fc-unthemed .fc-popover, -.fc-unthemed .fc-list-view, -.fc-unthemed .fc-list-heading td { - border-color: #ddd; -} - -.fc-unthemed .fc-popover { - background-color: #fff; -} - -.fc-unthemed .fc-divider, -.fc-unthemed .fc-popover .fc-header, -.fc-unthemed .fc-list-heading td { - background: #eee; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - color: #666; -} - -.fc-unthemed td.fc-today { - background: #fcf8e3; -} - -.fc-highlight { /* when user is selecting cells */ - background: #bce8f1; - opacity: .3; -} - -.fc-bgevent { /* default look for background events */ - background: rgb(143, 223, 130); - opacity: .3; -} - -.fc-nonbusiness { /* default look for non-business-hours areas */ - /* will inherit .fc-bgevent's styles */ - background: #d7d7d7; -} - -.fc-unthemed .fc-disabled-day { - background: #d7d7d7; - opacity: .3; -} - -.ui-widget .fc-disabled-day { /* themed */ - background-image: none; -} - - -/* Icons (inline elements with styled text that mock arrow icons) ---------------------------------------------------------------------------------------------------*/ - -.fc-icon { - display: inline-block; - height: 1em; - line-height: 1em; - font-size: 1em; - text-align: center; - overflow: hidden; - font-family: "Courier New", Courier, monospace; - - /* don't allow browser text-selection */ - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - -/* -Acceptable font-family overrides for individual icons: - "Arial", sans-serif - "Times New Roman", serif - -NOTE: use percentage font sizes or else old IE chokes -*/ - -.fc-icon:after { - position: relative; -} - -.fc-icon-left-single-arrow:after { - content: "\02039"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-right-single-arrow:after { - content: "\0203A"; - font-weight: bold; - font-size: 200%; - top: -7%; -} - -.fc-icon-left-double-arrow:after { - content: "\000AB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-right-double-arrow:after { - content: "\000BB"; - font-size: 160%; - top: -7%; -} - -.fc-icon-left-triangle:after { - content: "\25C4"; - font-size: 125%; - top: 3%; -} - -.fc-icon-right-triangle:after { - content: "\25BA"; - font-size: 125%; - top: 3%; -} - -.fc-icon-down-triangle:after { - content: "\25BC"; - font-size: 125%; - top: 2%; -} - -.fc-icon-x:after { - content: "\000D7"; - font-size: 200%; - top: 6%; -} - - -/* Buttons (styled <button> tags, normalized to work cross-browser) ---------------------------------------------------------------------------------------------------*/ - -.fc button { - /* force height to include the border and padding */ - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - /* dimensions */ - margin: 0; - height: 2.1em; - padding: 0 .6em; - - /* text & cursor */ - font-size: 1em; /* normalize */ - white-space: nowrap; - cursor: pointer; -} - -/* Firefox has an annoying inner border */ -.fc button::-moz-focus-inner { margin: 0; padding: 0; } - -.fc-state-default { /* non-theme */ - border: 1px solid; -} - -.fc-state-default.fc-corner-left { /* non-theme */ - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -.fc-state-default.fc-corner-right { /* non-theme */ - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - -/* icons in buttons */ - -.fc button .fc-icon { /* non-theme */ - position: relative; - top: -0.05em; /* seems to be a good adjustment across browsers */ - margin: 0 .2em; - vertical-align: middle; -} - -/* - button states - borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) -*/ - -.fc-state-default { - background-color: #f5f5f5; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); - background-repeat: repeat-x; - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - color: #333; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-hover, -.fc-state-down, -.fc-state-active, -.fc-state-disabled { - color: #333333; - background-color: #e6e6e6; -} - -.fc-state-hover { - color: #333333; - text-decoration: none; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} - -.fc-state-down, -.fc-state-active { - background-color: #cccccc; - background-image: none; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.fc-state-disabled { - cursor: default; - background-image: none; - opacity: 0.65; - box-shadow: none; -} - - -/* Buttons Groups ---------------------------------------------------------------------------------------------------*/ - -.fc-button-group { - display: inline-block; -} - -/* -every button that is not first in a button group should scootch over one pixel and cover the -previous button's border... -*/ - -.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */ - float: left; - margin: 0 0 0 -1px; -} - -.fc .fc-button-group > :first-child { /* same */ - margin-left: 0; -} - - -/* Popover ---------------------------------------------------------------------------------------------------*/ - -.fc-popover { - position: absolute; - box-shadow: 0 2px 6px rgba(0,0,0,.15); -} - -.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */ - padding: 2px 4px; -} - -.fc-popover .fc-header .fc-title { - margin: 0 2px; -} - -.fc-popover .fc-header .fc-close { - cursor: pointer; -} - -.fc-ltr .fc-popover .fc-header .fc-title, -.fc-rtl .fc-popover .fc-header .fc-close { - float: left; -} - -.fc-rtl .fc-popover .fc-header .fc-title, -.fc-ltr .fc-popover .fc-header .fc-close { - float: right; -} - -/* unthemed */ - -.fc-unthemed .fc-popover { - border-width: 1px; - border-style: solid; -} - -.fc-unthemed .fc-popover .fc-header .fc-close { - font-size: .9em; - margin-top: 2px; -} - -/* jqui themed */ - -.fc-popover > .ui-widget-header + .ui-widget-content { - border-top: 0; /* where they meet, let the header have the border */ -} - - -/* Misc Reusable Components ---------------------------------------------------------------------------------------------------*/ - -.fc-divider { - border-style: solid; - border-width: 1px; -} - -hr.fc-divider { - height: 0; - margin: 0; - padding: 0 0 2px; /* height is unreliable across browsers, so use padding */ - border-width: 1px 0; -} - -.fc-clear { - clear: both; -} - -.fc-bg, -.fc-bgevent-skeleton, -.fc-highlight-skeleton, -.fc-helper-skeleton { - /* these element should always cling to top-left/right corners */ - position: absolute; - top: 0; - left: 0; - right: 0; -} - -.fc-bg { - bottom: 0; /* strech bg to bottom edge */ -} - -.fc-bg table { - height: 100%; /* strech bg to bottom edge */ -} - - -/* Tables ---------------------------------------------------------------------------------------------------*/ - -.fc table { - width: 100%; - box-sizing: border-box; /* fix scrollbar issue in firefox */ - table-layout: fixed; - border-collapse: collapse; - border-spacing: 0; - font-size: 1em; /* normalize cross-browser */ -} - -.fc th { - text-align: center; -} - -.fc th, -.fc td { - border-style: solid; - border-width: 1px; - padding: 0; - vertical-align: top; -} - -.fc td.fc-today { - border-style: double; /* overcome neighboring borders */ -} - - -/* Internal Nav Links ---------------------------------------------------------------------------------------------------*/ - -a[data-goto] { - cursor: pointer; -} - -a[data-goto]:hover { - text-decoration: underline; -} - - -/* Fake Table Rows ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */ - /* no visible border by default. but make available if need be (scrollbar width compensation) */ - border-style: solid; - border-width: 0; -} - -.fc-row table { - /* don't put left/right border on anything within a fake row. - the outer tbody will worry about this */ - border-left: 0 hidden transparent; - border-right: 0 hidden transparent; - - /* no bottom borders on rows */ - border-bottom: 0 hidden transparent; -} - -.fc-row:first-child table { - border-top: 0 hidden transparent; /* no top border on first row */ -} - - -/* Day Row (used within the header and the DayGrid) ---------------------------------------------------------------------------------------------------*/ - -.fc-row { - position: relative; -} - -.fc-row .fc-bg { - z-index: 1; -} - -/* highlighting cells & background event skeleton */ - -.fc-row .fc-bgevent-skeleton, -.fc-row .fc-highlight-skeleton { - bottom: 0; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-bgevent-skeleton table, -.fc-row .fc-highlight-skeleton table { - height: 100%; /* stretch skeleton to bottom of row */ -} - -.fc-row .fc-highlight-skeleton td, -.fc-row .fc-bgevent-skeleton td { - border-color: transparent; -} - -.fc-row .fc-bgevent-skeleton { - z-index: 2; - -} - -.fc-row .fc-highlight-skeleton { - z-index: 3; -} - -/* -row content (which contains day/week numbers and events) as well as "helper" (which contains -temporary rendered events). -*/ - -.fc-row .fc-content-skeleton { - position: relative; - z-index: 4; - padding-bottom: 2px; /* matches the space above the events */ -} - -.fc-row .fc-helper-skeleton { - z-index: 5; -} - -.fc-row .fc-content-skeleton td, -.fc-row .fc-helper-skeleton td { - /* see-through to the background below */ - background: none; /* in case <td>s are globally styled */ - border-color: transparent; - - /* don't put a border between events and/or the day number */ - border-bottom: 0; -} - -.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */ -.fc-row .fc-helper-skeleton tbody td { - /* don't put a border between event cells */ - border-top: 0; -} - - -/* Scrolling Container ---------------------------------------------------------------------------------------------------*/ - -.fc-scroller { - -webkit-overflow-scrolling: touch; -} - -/* TODO: move to agenda/basic */ -.fc-scroller > .fc-day-grid, -.fc-scroller > .fc-time-grid { - position: relative; /* re-scope all positions */ - width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */ -} - - -/* Global Event Styles ---------------------------------------------------------------------------------------------------*/ - -.fc-event { - position: relative; /* for resize handle and other inner positioning */ - display: block; /* make the <a> tag block */ - font-size: .85em; - line-height: 1.3; - border-radius: 3px; - border: 1px solid #3a87ad; /* default BORDER color */ - font-weight: normal; /* undo jqui's ui-widget-header bold */ -} - -.fc-event, -.fc-event-dot { - background-color: #3a87ad; /* default BACKGROUND color */ -} - -/* overpower some of bootstrap's and jqui's styles on <a> tags */ -.fc-event, -.fc-event:hover, -.ui-widget .fc-event { - color: #fff; /* default TEXT color */ - text-decoration: none; /* if <a> has an href */ -} - -.fc-event[href], -.fc-event.fc-draggable { - cursor: pointer; /* give events with links and draggable events a hand mouse pointer */ -} - -.fc-not-allowed, /* causes a "warning" cursor. applied on body */ -.fc-not-allowed .fc-event { /* to override an event's custom cursor */ - cursor: not-allowed; -} - -.fc-event .fc-bg { /* the generic .fc-bg already does position */ - z-index: 1; - background: #fff; - opacity: .25; -} - -.fc-event .fc-content { - position: relative; - z-index: 2; -} - -/* resizer (cursor AND touch devices) */ - -.fc-event .fc-resizer { - position: absolute; - z-index: 4; -} - -/* resizer (touch devices) */ - -.fc-event .fc-resizer { - display: none; -} - -.fc-event.fc-allow-mouse-resize .fc-resizer, -.fc-event.fc-selected .fc-resizer { - /* only show when hovering or selected (with touch) */ - display: block; -} - -/* hit area */ - -.fc-event.fc-selected .fc-resizer:before { - /* 40x40 touch area */ - content: ""; - position: absolute; - z-index: 9999; /* user of this util can scope within a lower z-index */ - top: 50%; - left: 50%; - width: 40px; - height: 40px; - margin-left: -20px; - margin-top: -20px; -} - - -/* Event Selection (only for touch devices) ---------------------------------------------------------------------------------------------------*/ - -.fc-event.fc-selected { - z-index: 9999 !important; /* overcomes inline z-index */ - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); -} - -.fc-event.fc-selected.fc-dragging { - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3); -} - - -/* Horizontal Events ---------------------------------------------------------------------------------------------------*/ - -/* bigger touch area when selected */ -.fc-h-event.fc-selected:before { - content: ""; - position: absolute; - z-index: 3; /* below resizers */ - top: -10px; - bottom: -10px; - left: 0; - right: 0; -} - -/* events that are continuing to/from another week. kill rounded corners and butt up against edge */ - -.fc-ltr .fc-h-event.fc-not-start, -.fc-rtl .fc-h-event.fc-not-end { - margin-left: 0; - border-left-width: 0; - padding-left: 1px; /* replace the border with padding */ - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} - -.fc-ltr .fc-h-event.fc-not-end, -.fc-rtl .fc-h-event.fc-not-start { - margin-right: 0; - border-right-width: 0; - padding-right: 1px; /* replace the border with padding */ - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} - -/* resizer (cursor AND touch devices) */ - -/* left resizer */ -.fc-ltr .fc-h-event .fc-start-resizer, -.fc-rtl .fc-h-event .fc-end-resizer { - cursor: w-resize; - left: -1px; /* overcome border */ -} - -/* right resizer */ -.fc-ltr .fc-h-event .fc-end-resizer, -.fc-rtl .fc-h-event .fc-start-resizer { - cursor: e-resize; - right: -1px; /* overcome border */ -} - -/* resizer (mouse devices) */ - -.fc-h-event.fc-allow-mouse-resize .fc-resizer { - width: 7px; - top: -1px; /* overcome top border */ - bottom: -1px; /* overcome bottom border */ -} - -/* resizer (touch devices) */ - -.fc-h-event.fc-selected .fc-resizer { - /* 8x8 little dot */ - border-radius: 4px; - border-width: 1px; - width: 6px; - height: 6px; - border-style: solid; - border-color: inherit; - background: #fff; - /* vertically center */ - top: 50%; - margin-top: -4px; -} - -/* left resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-start-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-end-resizer { - margin-left: -4px; /* centers the 8x8 dot on the left edge */ -} - -/* right resizer */ -.fc-ltr .fc-h-event.fc-selected .fc-end-resizer, -.fc-rtl .fc-h-event.fc-selected .fc-start-resizer { - margin-right: -4px; /* centers the 8x8 dot on the right edge */ -} - - -/* DayGrid events ----------------------------------------------------------------------------------------------------- -We use the full "fc-day-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-day-grid-event { - margin: 1px 2px 0; /* spacing between events and edges */ - padding: 0 1px; -} - -tr:first-child > td > .fc-day-grid-event { - margin-top: 2px; /* a little bit more space before the first event */ -} - -.fc-day-grid-event.fc-selected:after { - content: ""; - position: absolute; - z-index: 1; /* same z-index as fc-bg, behind text */ - /* overcome the borders */ - top: -1px; - right: -1px; - bottom: -1px; - left: -1px; - /* darkening effect */ - background: #000; - opacity: .25; -} - -.fc-day-grid-event .fc-content { /* force events to be one-line tall */ - white-space: nowrap; - overflow: hidden; -} - -.fc-day-grid-event .fc-time { - font-weight: bold; -} - -/* resizer (cursor devices) */ - -/* left resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer { - margin-left: -2px; /* to the day cell's edge */ -} - -/* right resizer */ -.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer, -.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer { - margin-right: -2px; /* to the day cell's edge */ -} - - -/* Event Limiting ---------------------------------------------------------------------------------------------------*/ - -/* "more" link that represents hidden events */ - -a.fc-more { - margin: 1px 3px; - font-size: .85em; - cursor: pointer; - text-decoration: none; -} - -a.fc-more:hover { - text-decoration: underline; -} - -.fc-limited { /* rows and cells that are hidden because of a "more" link */ - display: none; -} - -/* popover that appears when "more" link is clicked */ - -.fc-day-grid .fc-row { - z-index: 1; /* make the "more" popover one higher than this */ -} - -.fc-more-popover { - z-index: 2; - width: 220px; -} - -.fc-more-popover .fc-event-container { - padding: 10px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-now-indicator { - position: absolute; - border: 0 solid red; -} - - -/* Utilities ---------------------------------------------------------------------------------------------------*/ - -.fc-unselectable { - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} - - - -/* Toolbar ---------------------------------------------------------------------------------------------------*/ - -.fc-toolbar { - text-align: center; -} - -.fc-toolbar.fc-header-toolbar { - margin-bottom: 1em; -} - -.fc-toolbar.fc-footer-toolbar { - margin-top: 1em; -} - -.fc-toolbar .fc-left { - float: left; -} - -.fc-toolbar .fc-right { - float: right; -} - -.fc-toolbar .fc-center { - display: inline-block; -} - -/* the things within each left/right/center section */ -.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */ - float: left; - margin-left: .75em; -} - -/* the first thing within each left/center/right section */ -.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */ - margin-left: 0; -} - -/* title text */ - -.fc-toolbar h2 { - margin: 0; -} - -/* button layering (for border precedence) */ - -.fc-toolbar button { - position: relative; -} - -.fc-toolbar .fc-state-hover, -.fc-toolbar .ui-state-hover { - z-index: 2; -} - -.fc-toolbar .fc-state-down { - z-index: 3; -} - -.fc-toolbar .fc-state-active, -.fc-toolbar .ui-state-active { - z-index: 4; -} - -.fc-toolbar button:focus { - z-index: 5; -} - - -/* View Structure ---------------------------------------------------------------------------------------------------*/ - -/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */ -/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */ -.fc-view-container *, -.fc-view-container *:before, -.fc-view-container *:after { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} - -.fc-view, /* scope positioning and z-index's for everything within the view */ -.fc-view > table { /* so dragged elements can be above the view's main element */ - position: relative; - z-index: 1; -} - - - -/* BasicView ---------------------------------------------------------------------------------------------------*/ - -/* day row structure */ - -.fc-basicWeek-view .fc-content-skeleton, -.fc-basicDay-view .fc-content-skeleton { - /* there may be week numbers in these views, so no padding-top */ - padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */ -} - -.fc-basic-view .fc-body .fc-row { - min-height: 4em; /* ensure that all rows are at least this tall */ -} - -/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */ - -.fc-row.fc-rigid { - overflow: hidden; -} - -.fc-row.fc-rigid .fc-content-skeleton { - position: absolute; - top: 0; - left: 0; - right: 0; -} - -/* week and day number styling */ - -.fc-day-top.fc-other-month { - opacity: 0.3; -} - -.fc-basic-view .fc-week-number, -.fc-basic-view .fc-day-number { - padding: 2px; -} - -.fc-basic-view th.fc-week-number, -.fc-basic-view th.fc-day-number { - padding: 0 2px; /* column headers can't have as much v space */ -} - -.fc-ltr .fc-basic-view .fc-day-top .fc-day-number { float: right; } -.fc-rtl .fc-basic-view .fc-day-top .fc-day-number { float: left; } - -.fc-ltr .fc-basic-view .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; } -.fc-rtl .fc-basic-view .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; } - -.fc-basic-view .fc-day-top .fc-week-number { - min-width: 1.5em; - text-align: center; - background-color: #f2f2f2; - color: #808080; -} - -/* when week/day number have own column */ - -.fc-basic-view td.fc-week-number { - text-align: center; -} - -.fc-basic-view td.fc-week-number > * { - /* work around the way we do column resizing and ensure a minimum width */ - display: inline-block; - min-width: 1.25em; -} - - -/* AgendaView all-day area ---------------------------------------------------------------------------------------------------*/ - -.fc-agenda-view .fc-day-grid { - position: relative; - z-index: 2; /* so the "more.." popover will be over the time grid */ -} - -.fc-agenda-view .fc-day-grid .fc-row { - min-height: 3em; /* all-day section will never get shorter than this */ -} - -.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton { - padding-bottom: 1em; /* give space underneath events for clicking/selecting days */ -} - - -/* TimeGrid axis running down the side (for both the all-day area and the slot area) ---------------------------------------------------------------------------------------------------*/ - -.fc .fc-axis { /* .fc to overcome default cell styles */ - vertical-align: middle; - padding: 0 4px; - white-space: nowrap; -} - -.fc-ltr .fc-axis { - text-align: right; -} - -.fc-rtl .fc-axis { - text-align: left; -} - -.ui-widget td.fc-axis { - font-weight: normal; /* overcome jqui theme making it bold */ -} - - -/* TimeGrid Structure ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid-container, /* so scroll container's z-index is below all-day */ -.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */ - position: relative; - z-index: 1; -} - -.fc-time-grid { - min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */ -} - -.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */ - border: 0 hidden transparent; -} - -.fc-time-grid > .fc-bg { - z-index: 1; -} - -.fc-time-grid .fc-slats, -.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */ - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-content-col { - position: relative; /* because now-indicator lives directly inside */ -} - -.fc-time-grid .fc-content-skeleton { - position: absolute; - z-index: 3; - top: 0; - left: 0; - right: 0; -} - -/* divs within a cell within the fc-content-skeleton */ - -.fc-time-grid .fc-business-container { - position: relative; - z-index: 1; -} - -.fc-time-grid .fc-bgevent-container { - position: relative; - z-index: 2; -} - -.fc-time-grid .fc-highlight-container { - position: relative; - z-index: 3; -} - -.fc-time-grid .fc-event-container { - position: relative; - z-index: 4; -} - -.fc-time-grid .fc-now-indicator-line { - z-index: 5; -} - -.fc-time-grid .fc-helper-container { /* also is fc-event-container */ - position: relative; - z-index: 6; -} - - -/* TimeGrid Slats (lines that run horizontally) ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-slats td { - height: 1.5em; - border-bottom: 0; /* each cell is responsible for its top border */ -} - -.fc-time-grid .fc-slats .fc-minor td { - border-top-style: dotted; -} - -.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */ - background: none; /* see through to fc-bg */ -} - - -/* TimeGrid Highlighting Slots ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */ - position: relative; /* scopes the left/right of the fc-highlight to be in the column */ -} - -.fc-time-grid .fc-highlight { - position: absolute; - left: 0; - right: 0; - /* top and bottom will be in by JS */ -} - - -/* TimeGrid Event Containment ---------------------------------------------------------------------------------------------------*/ - -.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */ - margin: 0 2.5% 0 2px; -} - -.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */ - margin: 0 2px 0 2.5%; -} - -.fc-time-grid .fc-event, -.fc-time-grid .fc-bgevent { - position: absolute; - z-index: 1; /* scope inner z-index's */ -} - -.fc-time-grid .fc-bgevent { - /* background events always span full width */ - left: 0; - right: 0; -} - - -/* Generic Vertical Event ---------------------------------------------------------------------------------------------------*/ - -.fc-v-event.fc-not-start { /* events that are continuing from another day */ - /* replace space made by the top border with padding */ - border-top-width: 0; - padding-top: 1px; - - /* remove top rounded corners */ - border-top-left-radius: 0; - border-top-right-radius: 0; -} - -.fc-v-event.fc-not-end { - /* replace space made by the top border with padding */ - border-bottom-width: 0; - padding-bottom: 1px; - - /* remove bottom rounded corners */ - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - - -/* TimeGrid Event Styling ----------------------------------------------------------------------------------------------------- -We use the full "fc-time-grid-event" class instead of using descendants because the event won't -be a descendant of the grid when it is being dragged. -*/ - -.fc-time-grid-event { - overflow: hidden; /* don't let the bg flow over rounded corners */ -} - -.fc-time-grid-event.fc-selected { - /* need to allow touch resizers to extend outside event's bounding box */ - /* common fc-selected styles hide the fc-bg, so don't need this anyway */ - overflow: visible; -} - -.fc-time-grid-event.fc-selected .fc-bg { - display: none; /* hide semi-white background, to appear darker */ -} - -.fc-time-grid-event .fc-content { - overflow: hidden; /* for when .fc-selected */ -} - -.fc-time-grid-event .fc-time, -.fc-time-grid-event .fc-title { - padding: 0 1px; -} - -.fc-time-grid-event .fc-time { - font-size: .85em; - white-space: nowrap; -} - -/* short mode, where time and title are on the same line */ - -.fc-time-grid-event.fc-short .fc-content { - /* don't wrap to second line (now that contents will be inline) */ - white-space: nowrap; -} - -.fc-time-grid-event.fc-short .fc-time, -.fc-time-grid-event.fc-short .fc-title { - /* put the time and title on the same line */ - display: inline-block; - vertical-align: top; -} - -.fc-time-grid-event.fc-short .fc-time span { - display: none; /* don't display the full time text... */ -} - -.fc-time-grid-event.fc-short .fc-time:before { - content: attr(data-start); /* ...instead, display only the start time */ -} - -.fc-time-grid-event.fc-short .fc-time:after { - content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */ -} - -.fc-time-grid-event.fc-short .fc-title { - font-size: .85em; /* make the title text the same size as the time */ - padding: 0; /* undo padding from above */ -} - -/* resizer (cursor device) */ - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer { - left: 0; - right: 0; - bottom: 0; - height: 8px; - overflow: hidden; - line-height: 8px; - font-size: 11px; - font-family: monospace; - text-align: center; - cursor: s-resize; -} - -.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after { - content: "="; -} - -/* resizer (touch device) */ - -.fc-time-grid-event.fc-selected .fc-resizer { - /* 10x10 dot */ - border-radius: 5px; - border-width: 1px; - width: 8px; - height: 8px; - border-style: solid; - border-color: inherit; - background: #fff; - /* horizontally center */ - left: 50%; - margin-left: -5px; - /* center on the bottom edge */ - bottom: -5px; -} - - -/* Now Indicator ---------------------------------------------------------------------------------------------------*/ - -.fc-time-grid .fc-now-indicator-line { - border-top-width: 1px; - left: 0; - right: 0; -} - -/* arrow on axis */ - -.fc-time-grid .fc-now-indicator-arrow { - margin-top: -5px; /* vertically center on top coordinate */ -} - -.fc-ltr .fc-time-grid .fc-now-indicator-arrow { - left: 0; - /* triangle pointing right... */ - border-width: 5px 0 5px 6px; - border-top-color: transparent; - border-bottom-color: transparent; -} - -.fc-rtl .fc-time-grid .fc-now-indicator-arrow { - right: 0; - /* triangle pointing left... */ - border-width: 5px 6px 5px 0; - border-top-color: transparent; - border-bottom-color: transparent; -} - - - -/* List View ---------------------------------------------------------------------------------------------------*/ - -/* possibly reusable */ - -.fc-event-dot { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 5px; -} - -/* view wrapper */ - -.fc-rtl .fc-list-view { - direction: rtl; /* unlike core views, leverage browser RTL */ -} - -.fc-list-view { - border-width: 1px; - border-style: solid; -} - -/* table resets */ - -.fc .fc-list-table { - table-layout: auto; /* for shrinkwrapping cell content */ -} - -.fc-list-table td { - border-width: 1px 0 0; - padding: 8px 14px; -} - -.fc-list-table tr:first-child td { - border-top-width: 0; -} - -/* day headings with the list */ - -.fc-list-heading { - border-bottom-width: 1px; -} - -.fc-list-heading td { - font-weight: bold; -} - -.fc-ltr .fc-list-heading-main { float: left; } -.fc-ltr .fc-list-heading-alt { float: right; } - -.fc-rtl .fc-list-heading-main { float: right; } -.fc-rtl .fc-list-heading-alt { float: left; } - -/* event list items */ - -.fc-list-item.fc-has-url { - cursor: pointer; /* whole row will be clickable */ -} - -.fc-list-item:hover td { - background-color: #f5f5f5; -} - -.fc-list-item-marker, -.fc-list-item-time { - white-space: nowrap; - width: 1px; -} - -/* make the dot closer to the event title */ -.fc-ltr .fc-list-item-marker { padding-right: 0; } -.fc-rtl .fc-list-item-marker { padding-left: 0; } - -.fc-list-item-title a { - /* every event title cell has an <a> tag */ - text-decoration: none; - color: inherit; -} - -.fc-list-item-title a[href]:hover { - /* hover effect only on titles with hrefs */ - text-decoration: underline; -} - -/* message when no events */ - -.fc-list-empty-wrap2 { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -.fc-list-empty-wrap1 { - width: 100%; - height: 100%; - display: table; -} - -.fc-list-empty { - display: table-cell; - vertical-align: middle; - text-align: center; -} - -.fc-unthemed .fc-list-empty { /* theme will provide own background */ - background-color: #eee; -} diff --git a/src/UI/Content/icons.less b/src/UI/Content/icons.less deleted file mode 100644 index a3eb16c29..000000000 --- a/src/UI/Content/icons.less +++ /dev/null @@ -1,505 +0,0 @@ -@import "FontAwesome/font-awesome"; -@import "Bootstrap/variables"; -@import "variables"; - -/* Icon rotations and mirroring */ -.fa-rotate-90() { - -webkit-transform : rotate(90deg); - -moz-transform : rotate(90deg); - -ms-transform : rotate(90deg); - -o-transform : rotate(90deg); - transform : rotate(90deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=1); -} - -.fa-rotate-180() { - -webkit-transform : rotate(180deg); - -moz-transform : rotate(180deg); - -ms-transform : rotate(180deg); - -o-transform : rotate(180deg); - transform : rotate(180deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=2); -} - -.fa-rotate-270() { - -webkit-transform : rotate(270deg); - -moz-transform : rotate(270deg); - -ms-transform : rotate(270deg); - -o-transform : rotate(270deg); - transform : rotate(270deg); - filter : progid:DXImageTransform.Microsoft.BasicImage(rotation=3); -} - -.fa-flip-horizontal() { - -webkit-transform : scale(-1, 1); - -moz-transform : scale(-1, 1); - -ms-transform : scale(-1, 1); - -o-transform : scale(-1, 1); - transform : scale(-1, 1); -} - -.fa-flip-vertical() { - -webkit-transform : scale(1, -1); - -moz-transform : scale(1, -1); - -ms-transform : scale(1, -1); - -o-transform : scale(1, -1); - transform : scale(1, -1); -} - -.fa-icon-content(@fa-icon) { - .fa-icon(); - &:before { content: @fa-icon; } -} - -.fa-icon-color(@color) { - &:before { color: @color; } -} - -.icon-lidarr-warning { - .fa-icon-content(@fa-var-exclamation-triangle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-edit { - .fa-icon-content(@fa-var-wrench); -} - -.icon-lidarr-blacklist { - .fa-icon-content(@fa-var-ban); - .fa-icon-color(@brand-danger); - -} - -.icon-lidarr-spinner { - .fa-icon-content(@fa-var-spinner); -} - -.fa-spin-overlay { - .fa-icon(); - position : relative; - text-align : center; - vertical-align : baseline; - - i { - opacity : 0.0; - margin : 0 !important; - - &.icon-lidarr-spinner { - opacity : 1.0; - margin : 0 -0.5em !important; - } - } - - span { - position : absolute; - top : 0; - left : 0; - right : 0; - bottom : 0; - } -} - -.icon-lidarr-rename { - .fa-icon-content(@fa-var-sitemap) -} - -.icon-lidarr-add { - .fa-icon-content(@fa-var-plus); -} - -.icon-lidarr-form-info { - .fa-icon-content(@fa-var-question-circle); -} - -.icon-lidarr-form-warning { - .fa-icon-content(@fa-var-exclamation-triangle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-form-danger { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-form-info-link { - .clickable(); - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(@brand-primary) -} - -.icon-lidarr-form-external-link { - .fa-icon-content(@fa-var-external-link); -} - -.icon-lidarr-update { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-download { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-downloading { - .fa-icon-content(@fa-var-cloud-download); -} - -.icon-lidarr-downloaded { - .fa-icon-content(@fa-var-inbox); -} - -.icon-lidarr-pending { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-queued { - .fa-icon-content(@fa-var-cloud); -} - -.icon-lidarr-paused { - .fa-icon-content(@fa-var-pause); -} - -.icon-lidarr-active { - .fa-icon-content(@fa-var-play); -} - -.icon-lidarr-tba { - .fa-icon-content(@fa-var-question-circle); -} - -.icon-lidarr-missing { - .fa-icon-content(@fa-var-exclamation-triangle); -} - -.icon-lidarr-not-aired { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-import { - .fa-icon-content(@fa-var-inbox); -} - -.icon-lidarr-import-manual { - .fa-icon-content(@fa-var-user); -} - -.icon-lidarr-imported { - .fa-icon-content(@fa-var-download); -} - -.icon-lidarr-status { - .fa-icon-content(@fa-var-circle); -} - -.icon-lidarr-monitored { - .fa-icon-content(@fa-var-bookmark); -} - -.icon-lidarr-unmonitored { - .fa-icon-content(@fa-var-bookmark-o); -} - -.icon-lidarr-log-info { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(dodgerblue); -} - -.icon-lidarr-log-debug { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(gray); -} - -.icon-lidarr-log-trace { - .fa-icon-content(@fa-var-info-circle); - .fa-icon-color(lightgrey); -} - -.icon-lidarr-log-warn { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-log-error { - .fa-icon-content(@fa-var-bug); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-log-fatal { - .fa-icon-content(@fa-var-times-circle); - .fa-icon-color(purple); -} - -.icon-lidarr-import-failed { - .fa-icon-content(@fa-var-download); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-download-failed { - .fa-icon-content(@fa-var-cloud-download); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-download-warning { - .fa-icon-content(@fa-var-cloud-download); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-shutdown { - .fa-icon-content(@fa-var-power-off); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-restart { - .fa-icon-content(@fa-var-repeat); -} - -.icon-lidarr-health-warning { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-warning); -} - -.icon-lidarr-health-error { - .fa-icon-content(@fa-var-exclamation-circle); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-search { - .fa-icon-content(@fa-var-search); -} - -.icon-lidarr-search-manual { - .fa-icon-content(@fa-var-user); -} - -.icon-lidarr-search-automatic { - .fa-icon-content(@fa-var-rocket); -} - -.icon-lidarr-delete { - .fa-icon-content(@fa-var-remove); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-deleted { - .fa-icon-content(@fa-var-trash); -} - -.icon-lidarr-clear { - .fa-icon-content(@fa-var-trash); -} - -.icon-lidarr-existing { - .fa-icon-content(@fa-var-minus); - .fa-icon-color(@brand-danger); -} - -.icon-lidarr-suggested { - .fa-icon-content(@fa-var-plus); - .fa-icon-color(@brand-success); -} - -.icon-lidarr-info { - .fa-icon-content(@fa-var-info-circle); -} - -.icon-lidarr-all { - .fa-icon-content(@fa-var-circle-o); -} - -//Navbar -.icon-lidarr-navbar-collapsed { - .fa-icon-content(@fa-var-bars); -} - -.icon-lidarr-navbar-artist { - .fa-icon-content(@fa-var-music); -} - -.icon-lidarr-navbar-calendar { - .fa-icon-content(@fa-var-calendar); -} - -.icon-lidarr-navbar-activity { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-navbar-wanted { - .fa-icon-content(@fa-var-exclamation-triangle); -} - -.icon-lidarr-navbar-settings { - .fa-icon-content(@fa-var-cogs); -} - -.icon-lidarr-navbar-system { - .fa-icon-content(@fa-var-laptop); -} - -.icon-lidarr-navbar-donate { - .fa-icon-content(@fa-var-heart); - .fa-icon-color(@nzbdroneRed); -} - -.icon-lidarr-back-to-top { - .fa-icon-content(@fa-var-arrow-circle-up); -} - -.icon-lidarr-hdd { - .fa-icon-content(@fa-var-hdd-o); -} - -.icon-lidarr-copy { - .fa-icon-content(@fa-var-clipboard); -} - -.icon-lidarr-unknown { - .fa-icon-content(@fa-var-question); -} - -.icon-lidarr-load-more { - .fa-icon-content(@fa-var-angle-down); -} - -.icon-lidarr-ok { - .fa-icon-content(@fa-var-check); -} - -.icon-lidarr-calendar-o { - .fa-icon-content(@fa-var-calendar-o); -} - -.icon-lidarr-folder-open { - .fa-icon-content(@fa-var-folder-open); -} - -.icon-lidarr-refresh { - .fa-icon-content(@fa-var-refresh); -} - -.icon-lidarr-artist-ended { - .fa-icon-content(@fa-var-stop); -} - -.icon-lidarr-artist-continuing { - .fa-icon-content(@fa-var-play); -} - -.icon-lidarr-artist-unmonitored { - .fa-icon-content(@fa-var-pause); -} - -.icon-lidarr-checked { - .fa-icon-content(@fa-var-check-square); -} - -.icon-lidarr-unchecked { - .fa-icon-content(@fa-var-square-o); -} - -.icon-lidarr-expand { - .fa-icon-content(@fa-var-chevron-right); -} - -.icon-lidarr-expanded { - .fa-icon-content(@fa-var-chevron-down); -} - -.icon-lidarr-panel-show { - .fa-icon-content(@fa-var-chevron-circle-down); -} - -.icon-lidarr-panel-hide { - .fa-icon-content(@fa-var-chevron-circle-up); -} - -.icon-lidarr-comment { - .fa-icon-content(@fa-var-comment) -} - -.icon-lidarr-rss { - .fa-icon-content(@fa-var-rss) -} - -.icon-lidarr-view-poster { - .fa-icon-content(@fa-var-th-large) -} - -.icon-lidarr-view-list { - .fa-icon-content(@fa-var-th-list) -} - -.icon-lidarr-view-table { - .fa-icon-content(@fa-var-table) -} - -.icon-lidarr-reorder { - .fa-icon-content(@fa-var-bars); -} - -.icon-lidarr-browser-computer { - .fa-icon-content(@fa-var-desktop); -} - -.icon-lidarr-browser-up { - .fa-icon-content(@fa-var-level-up); -} - -.icon-lidarr-browser-folder { - .fa-icon-content(@fa-var-folder-o); -} - -.icon-lidarr-browser-file { - .fa-icon-content(@fa-var-file-o); -} - -.icon-lidarr-sort-asc { - .fa-icon-content(@fa-var-sort-asc); -} - -.icon-lidarr-sort-desc { - .fa-icon-content(@fa-var-sort-desc); -} - -.icon-lidarr-pager-first { - .fa-icon-content(@fa-var-fast-backward); -} - -.icon-lidarr-pager-previous { - .fa-icon-content(@fa-var-backward); -} - -.icon-lidarr-pager-next { - .fa-icon-content(@fa-var-forward); -} - -.icon-lidarr-pager-last { - .fa-icon-content(@fa-var-fast-forward); -} - -.icon-lidarr-logout { - .fa-icon-content(@fa-var-sign-out); -} - -.icon-lidarr-file-text { - .fa-icon-content(@fa-var-file-text); -} - -.icon-lidarr-backup-scheduled { - .fa-icon-content(@fa-var-clock-o); -} - -.icon-lidarr-backup-manual { - .fa-icon-content(@fa-var-book); -} - -.icon-lidarr-backup-update { - .fa-icon-content(@fa-var-retweet); -} - -.icon-lidarr-track-file { - .fa-icon-content(@fa-var-file-video-o); -} - -.icon-lidarr-header-rejections { - .fa-icon-content(@fa-var-exclamation-circle); -} \ No newline at end of file diff --git a/src/UI/Content/legend.less b/src/UI/Content/legend.less deleted file mode 100644 index 2335acd30..000000000 --- a/src/UI/Content/legend.less +++ /dev/null @@ -1,32 +0,0 @@ -@import "./Bootstrap/mixins"; - -.legend { - margin: 5px; - - ul { - margin: 0; - margin-bottom: 5px; - padding: 0; - float: left; - list-style: none; - - li { - font-size: 80%; - list-style: none; - margin-left: 0; - line-height: 18px; - margin-bottom: 2px; - - span { - display: block; - float: left; - height: 16px; - width: 30px; - margin-right: 5px; - margin-left: 0; - border: none; - border-radius: 3px; - } - } - } -} diff --git a/src/UI/Content/mixins.less b/src/UI/Content/mixins.less deleted file mode 100644 index d6da04c3a..000000000 --- a/src/UI/Content/mixins.less +++ /dev/null @@ -1,21 +0,0 @@ -.selectable() { - -moz-user-select : all; - -webkit-user-select : all; - -ms-user-select : all; -} - -.not-selectable() { - -moz-user-select : none; - -webkit-user-select : none; - -ms-user-select : none; -} - -.color-impaired-background-gradient(@angle, @color) { - .color-impaired-mode & { - background : repeating-linear-gradient(@angle, - darken(@color, 3%), - darken(@color, 3%) 6px, - @color 6px, - @color 12px); - } -} diff --git a/src/UI/Content/navbar.less b/src/UI/Content/navbar.less deleted file mode 100644 index bc220bdd4..000000000 --- a/src/UI/Content/navbar.less +++ /dev/null @@ -1,235 +0,0 @@ -@import "prefixer"; -@import "variables"; - -@grid-float-breakpoint: @screen-xs-min; - -.backdrop { - .navbar-nzbdrone { - .opacity(0.85); - background-color : #000000; - padding-bottom: 10px; - z-index: 10; - } -} - -.navbar-nzbdrone { - text-align : center; - - i:before { - font-size : 35px; - display : block; - margin-bottom : 1px; - } - - .icon-lidarr-navbar-icon { - display: inline; - } - - .navbar-nav, .navbar-nav>li { - float : none; - } - - .navbar-toggle { - border-color: #333; - - &:hover, - &:focus { - color : #222; - background-color : #333; - } - } - - .navbar-brand { - position: absolute; - - @media (max-width: @screen-xs-max) { - padding: 9px 15px; - font-size: 14px; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - padding: 20px 15px; - } - - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - padding: 30px 15px; - } - - @media (min-width: @screen-lg-min) { - padding: 22px 15px; - } - } - - .logo-text { - color: white; - font-weight: 300; - - .highlight { - font-weight: 400; - color: @droneTeal; - } - } - - li { - list-style-type : none; - display : inline-block; - position : relative; - - a { - display : block; - color : #b9b9b9; - font-weight : 100; - - &:focus { - background-color : transparent; - text-decoration : none; - } - - &:hover { - background-color : #555555; - text-decoration : none; - } - - .label { - cursor: pointer; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) { - border-radius : 6px; - padding : 5px 0px 5px; - min-height : 76px; - min-width : 64px; - margin : 20px 5px 5px; - } - - @media (min-width: @screen-md-min) and (max-width: @screen-md-max) { - border-radius : 6px; - padding : 15px 10px 5px; - min-height : 76px; - min-width : 64px; - margin : 20px 10px 5px; - } - - @media (min-width: @screen-lg-min) { - border-radius : 6px; - padding : 15px 10px 5px; - min-height : 76px; - min-width : 84px; - margin : 20px 10px 5px; - } - } - - .navbar-info { - .label { - position : absolute; - top : 10px; - right : 10px; - padding-left : 4px; - padding-right : 4px; - } - } - } - - @media (max-width: @screen-xs-max) { - text-align : left; - - i:before { - font-size : 14px; - display: inline-block; - } - - li { - display: block; - - a:hover { - background-color: transparent; - } - - .navbar-info { - margin-left: 5px; - - .label { - position : static; - } - } - } - } - - @media (max-width: @screen-xs-max) { - .navbar-collapse { - .navbar-nav { - li { - &:focus, &:hover { - background-color : #555555; - } - } - } - } - } -} - -.search { - - i:before { - font-size: 14px; - } - - .input-group { - input, .input-group-addon { - background-color: #333333; - } - } - - input, .input-group-addon { - border-color: #333333; - color: #cccccc; - } - - ul { - text-align: left; - } - - .tt-dropdown-menu { - - background-color: #333333; - color: #cccccc; - .opacity(0.95); - - .tt-suggestion { - color: #cccccc; - cursor: pointer; - - &.tt-cursor { - //item selected - - background-color: @droneTeal; - color: #222222; - - a { - //link in item selected - color: #222222; - } - } - } - } - - ::-webkit-input-placeholder { - color: #cccccc; - opacity: 0.25; - } - - :-moz-placeholder { /* Firefox 18- */ - color: #cccccc; - opacity: 0.25; - } - - ::-moz-placeholder { /* Firefox 19+ */ - color: #cccccc; - opacity: 0.25; - } - - :-ms-input-placeholder { - color: #cccccc; - opacity: 0.25; - } -} diff --git a/src/UI/Content/overrides.less b/src/UI/Content/overrides.less deleted file mode 100644 index abee53862..000000000 --- a/src/UI/Content/overrides.less +++ /dev/null @@ -1,6 +0,0 @@ -@import "Overrides/bootstrap"; -@import "Overrides/browser"; -@import "Overrides/bootstrap.toggle-switch"; -@import "Overrides/bootstrap.tagsinput.less"; -@import "Overrides/fullcalendar"; -@import "Overrides/messenger"; diff --git a/src/UI/Content/prefixer.less b/src/UI/Content/prefixer.less deleted file mode 100644 index c9040baa1..000000000 --- a/src/UI/Content/prefixer.less +++ /dev/null @@ -1,344 +0,0 @@ -//--------------------------------------------------- -// LESS Prefixer -//--------------------------------------------------- -// -// All of the CSS3 fun, none of the prefixes! -// -// As a rule, you can use the CSS properties you -// would expect just by adding a '.': -// -// box-shadow => .box-shadow(@args) -// -// Also, when shorthand is available, arguments are -// not parameterized. Learn CSS, not LESS Prefixer. -// -// ------------------------------------------------- -// TABLE OF CONTENTS -// (*) denotes a syntax-sugar helper -// ------------------------------------------------- -// -// .animation(@args) -// .animation-delay(@delay) -// .animation-direction(@direction) -// .animation-duration(@duration) -// .animation-iteration-count(@count) -// .animation-name(@name) -// .animation-play-state(@state) -// .animation-timing-function(@function) -// .background-size(@args) -// .border-radius(@args) -// .box-shadow(@args) -// .inner-shadow(@args) * -// .box-sizing(@args) -// .border-box() * -// .content-box() * -// .columns(@args) -// .column-count(@count) -// .column-gap(@gap) -// .column-rule(@args) -// .column-width(@width) -// .gradient(@default,@start,@stop) * -// .linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])* -// .linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,[@color3,@stop3,@color4,@stop4])* -// .opacity(@factor) -// .transform(@args) -// .rotate(@deg) -// .scale(@factor) -// .translate(@x,@y) -// .translate3d(@x,@y,@z) -// .translateHardware(@x,@y) * -// .text-shadow(@args) -// .transition(@args) -// .transition-delay(@delay) -// .transition-duration(@duration) -// .transition-property(@property) -// .transition-timing-function(@function) -// -// -// -// Credit to LESS Elements for the motivation and -// to CSS3Please.com for implementation. -// -// Copyright (c) 2012 Joel Sutherland -// MIT Licensed: -// http://www.opensource.org/licenses/mit-license.php -// -//--------------------------------------------------- - - -// Animation - -.animation(@args) { - -webkit-animation: @args; - -moz-animation: @args; - -ms-animation: @args; - -o-animation: @args; - animation: @args; -} -.animation-delay(@delay) { - -webkit-animation-delay: @delay; - -moz-animation-delay: @delay; - -ms-animation-delay: @delay; - -o-animation-delay: @delay; - animation-delay: @delay; -} -.animation-direction(@direction) { - -webkit-animation-direction: @direction; - -moz-animation-direction: @direction; - -ms-animation-direction: @direction; - -o-animation-direction: @direction; - animation-direction: @direction; -} -.animation-duration(@duration) { - -webkit-animation-duration: @duration; - -moz-animation-duration: @duration; - -ms-animation-duration: @duration; - -o-animation-duration: @duration; - animation-duration: @duration; -} -.animation-iteration-count(@count) { - -webkit-animation-iteration-count: @count; - -moz-animation-iteration-count: @count; - -ms-animation-iteration-count: @count; - -o-animation-iteration-count: @count; - animation-iteration-count: @count; -} -.animation-name(@name) { - -webkit-animation-name: @name; - -moz-animation-name: @name; - -ms-animation-name: @name; - -o-animation-name: @name; - animation-name: @name; -} -.animation-play-state(@state) { - -webkit-animation-play-state: @state; - -moz-animation-play-state: @state; - -ms-animation-play-state: @state; - -o-animation-play-state: @state; - animation-play-state: @state; -} -.animation-timing-function(@function) { - -webkit-animation-timing-function: @function; - -moz-animation-timing-function: @function; - -ms-animation-timing-function: @function; - -o-animation-timing-function: @function; - animation-timing-function: @function; -} - - -// Background Size - -.background-size(@args) { - -webkit-background-size: @args; - -moz-background-size: @args; - background-size: @args; -} - - -// Border Radius - -.border-radius(@args) { - -webkit-border-radius: @args; - -moz-border-radius: @args; - border-radius: @args; - - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; -} - - -// Box Shadows - -.box-shadow(@args) { - -webkit-box-shadow: @args; - -moz-box-shadow: @args; - box-shadow: @args; -} -.inner-shadow(@args) { - .box-shadow(inset @args); -} - - -// Box Sizing - -.box-sizing(@args){ - -webkit-box-sizing: @args; - -moz-box-sizing: @args; - box-sizing: @args; -} -.border-box(){ - .box-sizing(border-box); -} -.content-box(){ - .box-sizing(content-box); -} - - -// Columns - -.columns(@args){ - -webkit-columns: @args; - -moz-columns: @args; - columns: @args; -} -.column-count(@count) { - -webkit-column-count: @count; - -moz-column-count: @count; - column-count: @count; -} -.column-gap(@gap) { - -webkit-column-gap: @gap; - -moz-column-gap: @gap; - column-gap: @gap; -} -.column-width(@width){ - -webkit-column-width: @width; - -moz-column-width: @width; - column-width: @width; -} -.column-rule(@args){ - -webkit-column-rule: @args; - -moz-column-rule: @args; - column-rule: @args; -} - - -// Gradients - -.gradient(@default: #F5F5F5, @start: #EEE, @stop: #FFF) { - .linear-gradient-top(@default,@start,0%,@stop,100%); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3); -} -.linear-gradient-top(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4)); - background-image: -webkit-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -moz-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -ms-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -o-linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: linear-gradient(top, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3); -} -.linear-gradient-left(@default,@color1,@stop1,@color2,@stop2,@color3,@stop3,@color4,@stop4) { - background-color: @default; - background-image: -webkit-gradient(linear, left top, left top, color-stop(@stop1, @color1), color-stop(@stop2 @color2), color-stop(@stop3 @color3), color-stop(@stop4 @color4)); - background-image: -webkit-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -moz-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -ms-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: -o-linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); - background-image: linear-gradient(left, @color1 @stop1, @color2 @stop2, @color3 @stop3, @color4 @stop4); -} - - -// Opacity - -.opacity(@factor){ - opacity: @factor; - @iefactor: @factor*100; - filter: alpha(opacity=@iefactor); -} - - -// Text Shadow - -.text-shadow(@args){ - text-shadow: @args; -} - - -// Transforms - -.transform(@args) { - -webkit-transform: @args; - -moz-transform: @args; - -ms-transform: @args; - -o-transform: @args; - transform: @args; -} -.rotate(@deg:45deg){ - .transform(rotate(@deg)); -} -.scale(@factor:.5){ - .transform(scale(@factor)); -} -.translate(@x,@y){ - .transform(translate(@x,@y)); -} -.translate3d(@x,@y,@z) { - .transform(translate3d(@x,@y,@z)); -} -.translateHardware(@x,@y){ - .translate(@x,@y); - -webkit-transform: translate3d(@x,@y,0); - -moz-transform: translate3d(@x,@y,0); -} - - -// Transitions - -.transition(@args:200ms) { - -webkit-transition: @args; - -moz-transition: @args; - -o-transition: @args; - transition: @args; -} -.transition-delay(@delay:0) { - -webkit-transition-delay: @delay; - -moz-transition-delay: @delay; - -o-transition-delay: @delay; - transition-delay: @delay; -} -.transition-duration(@duration:200ms) { - -webkit-transition-duration: @duration; - -moz-transition-duration: @duration; - -o-transition-duration: @duration; - transition-duration: @duration; -} -.transition-property(@property:all) { - -webkit-transition-property: @property; - -moz-transition-property: @property; - -o-transition-property: @property; - transition-property: @property; -} -.transition-timing-function(@function:ease) { - -webkit-transition-timing-function: @function; - -moz-transition-timing-function: @function; - -o-transition-timing-function: @function; - transition-timing-function: @function; -} - diff --git a/src/UI/Content/progress-bars.less b/src/UI/Content/progress-bars.less deleted file mode 100644 index bc9d058dc..000000000 --- a/src/UI/Content/progress-bars.less +++ /dev/null @@ -1,39 +0,0 @@ -@import "Bootstrap/mixins"; -@import "Bootstrap/variables"; -@import "variables"; - -.progress.track-progress { - position : relative; - margin-bottom : 2px; - - &, .progressbar-back-text, .progressbar-front-text { - width : 125px; - } - - .progressbar-back-text, .progressbar-front-text { - font-size : 12px; - font-weight : bold; - text-align : center; - cursor : default; - line-height : 20px; - } - - .progressbar-back-text { - position : absolute; - height : 100%; - } - - .progressbar-front-text { - display : block; - height : 100%; - } - - .progress-bar { - position : absolute; - overflow : hidden; - } -} - -.progress-bar-purple { - #gradient > .vertical(@purple, @nzbdronePurple); -} diff --git a/src/UI/Content/robots.txt b/src/UI/Content/robots.txt deleted file mode 100644 index 77470cb39..000000000 --- a/src/UI/Content/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file diff --git a/src/UI/Content/spinner.less b/src/UI/Content/spinner.less deleted file mode 100644 index 2a02f136b..000000000 --- a/src/UI/Content/spinner.less +++ /dev/null @@ -1,130 +0,0 @@ -@import "prefixer"; -@import "Bootstrap/variables"; - -@colorDark : @gray-dark; -@colorLight : @gray-lighter; - -#followingBalls { - position : relative; - height : 20px; - width : 256px; - margin : 50px auto; - display : block; - - .ball { - background-color : @colorDark; - position : absolute; - top : 0; - left : 0; - width : 20px; - height : 20px; - .border-radius(10px); - .animation-name(bounce); - .animation-duration(1.9s); - .animation-iteration-count(infinite); - .animation-direction(linear); - } - - #ball-1 { - .animation-delay(0s); - } - - #ball-2 { - .animation-delay(0.19s); - } - - #ball-3 { - .animation-delay(0.38s); - } - - #ball-4 { - .animation-delay(0.57s); - } - - @keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } - - @-moz-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - - } - - @-webkit-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - - } - - @-ms-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } - - @-o-keyframes bounce { - 0% { - left : 0px; - background-color : @colorDark; - } - - 50% { - left : 236px; - background-color : @colorLight; - } - - 100% { - left : 0px; - background-color : @colorDark; - } - } -} diff --git a/src/UI/Content/theme.less b/src/UI/Content/theme.less deleted file mode 100644 index cf943be73..000000000 --- a/src/UI/Content/theme.less +++ /dev/null @@ -1,306 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; -@import "Bootstrap/type"; -@import "font"; -@import "form"; -@import "navbar"; -@import "Backgrid/backgrid"; -@import "prefixer"; -@import "icons"; -@import "checkbox-button"; -@import "spinner"; -@import "legend"; -@import "progress-bars"; -@import "../Shared/Styles/clickable"; -@import "../Shared/Styles/card"; -@import "../Rename/rename"; -@import "typeahead"; -@import "utilities"; -@import "../Hotkeys/hotkeys"; -@import "../Shared/FileBrowser/filebrowser"; -@import "badges"; -@import "../ManualImport/manualimport"; -@import "../AlbumStudio/albumstudio"; - -.main-region { - @media (min-width : @screen-lg-min) { - padding-left : 30px; - padding-right : 30px; - } -} - -.toolbar { - - &:after { - visibility : hidden; - display : block; - font-size : 0; - content : " "; - clear : both; - height : 0; - } - - .page-toolbar { - margin-top : 10px; - margin-bottom : 30px; - - .toolbar-group { - display : inline-block; - } - - .sorting-buttons { - .sorting-title { - display : inline-block; - width : 110px; - } - } - } -} - -.toolbars { - margin-top : 5px; - margin-bottom : 30px; - - .page-toolbar { - margin-top : 5px; - margin-bottom : 0px; - } -} - -.page-container { - min-height : 500px; -} - -#scroll-up { - - i { - .clickable; - .opacity(0.3); - margin: 0px 20px; - - &:hover { - .opacity(0.4); - } - } - - position : fixed; - z-index : 9999; - bottom : 30px; - right : 0px; - display : none; - font-size : 56px; - color : gray; -} - -.control-panel-visible { - #scroll-up { - bottom : 100px; - } -} - -.label-large { - padding : 4px 6px; - font-size : 16px; -} - -.label-white { - color : black; - background-color : white; -} - -.label-disabled { - opacity : 0.5; -} - -th { - cursor : default; - - &.sortable { - &:hover { - background : @table-bg-hover; - } - .clickable(); - - } -} - -a, .btn { - i { - cursor : pointer; - } -} - -body { - background : url('../Content/Images/background/logo.png') 50px 75px no-repeat; - background-color : #272727; - margin-bottom : 100px; - p { - font-size : 0.9em; - } -} - -.footer { - font-size : 13px; - font-weight : lighter; - padding-top : 0px; - padding-bottom : 20px; - color : #999999; - margin : 0; - text-decoration : none; - - a { - color : #999999; - text-decoration : underline; - } - - p { - margin-bottom : 0px; - } - - #footer-region { - .text-center(); - position : relative; - width : 256px; - margin : 50px auto 0px auto; - display : block; - } -} - -.started #page { - .card(#aaaaaa); - /* width : 1210px; - min-width : 1210px; */ - max-width : 1210px; - margin : auto; - // margin-top : -70px; - padding : 20px 0px; - - .header { - padding-bottom : 10px; - margin-bottom : 20px; - border-bottom : 1px solid #eeeeee; - } -} - -.backdrop #page { - background-color : transparent; - box-shadow : none; -} - -.validation-errors { - i { - padding-right : 5px; - } -} - -.status-primary { - color : @link-color; -} - -.status-success { - color : @state-success-text; -} - -.status-warning { - color : @state-warning-text; -} - -.status-danger { - color : @state-danger-text; -} - -.error { - background : #FF0000; -} - -#errors { - display : none; -} - -.mono-space { - font-family : "ubuntu mono" -} - -.file-path { - .mono-space(); -} - -.control-panel { - .card(#333333); - - color : #f5f5f5; - background-color : #333333; - margin : 0px; - margin-bottom : -100px; - position : fixed; - left : 0; - bottom : 0; - width : 100%; - height : 80px; - opacity : 0; - - @media (max-width : @screen-sm-max) { - height : initial; - position : static; - } -} - -.tab-content { - .tab-pane { - padding-top : 10px; - } -} - -.modal-header { - h3 { - margin-top : 0px; - margin-bottom : 0px; - } -} - -.modal-body { - table { - font-size : 12px; - font-weight : bold; - - i { - font-size : 14px; - } - } -} - -.tooltip { - .tooltip-inner { - max-width : 250px; - } -} - -dl.info { - dt, dd { - padding-bottom : 5px; - } -} - -.label { - &.protocol-torrent { - background-color : #00853D; - } - - &.protocol-usenet { - background-color : #17B1D9; - } -} - -.login { - color : #ececec; - - h2 { - vertical-align : bottom; - } -} - -.sort-direction-icon { - .pull-right(); - position : relative; - width : 0px; -} diff --git a/src/UI/Content/typeahead.less b/src/UI/Content/typeahead.less deleted file mode 100644 index 5a901c3ee..000000000 --- a/src/UI/Content/typeahead.less +++ /dev/null @@ -1,152 +0,0 @@ -/* - * typehead.js-bootstrap3.less - * @version 0.2.3 - * https://github.com/hyspace/typeahead.js-bootstrap3.less - * - * Licensed under the MIT license: - * http://www.opensource.org/licenses/MIT - */ - -//custom mixin for .form-control-validation -.typeahead-form-control(@border-color: #ccc;) { - border-color: @border-color; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@border-color, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@border-color, 20%); - .box-shadow(@shadow); - } -} - -//main styles for control -.tt-input, -.tt-hint { - .twitter-typeahead &{ - //validation states - .has-warning &{ - .typeahead-form-control(@state-warning-text); - } - .has-error &{ - .typeahead-form-control(@state-danger-text); - } - .has-success &{ - .typeahead-form-control(@state-success-text); - } - } - - //border - .input-group .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-base); - } - .input-group .twitter-typeahead:last-child &{ - .border-right-radius(@border-radius-base); - } - - //sizing - small:size and border - .input-group.input-group-sm .twitter-typeahead &{ - .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); - } - .input-group.input-group-sm .twitter-typeahead:not(:first-child):not(:last-child) &{ - border-radius: 0; - } - .input-group.input-group-sm .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-small); - .border-right-radius(0); - } - .input-group.input-group-sm .twitter-typeahead:last-child &{ - .border-left-radius(0); - .border-right-radius(@border-radius-small); - } - - //sizing - large:size and border - .input-group.input-group-lg .twitter-typeahead &{ - .input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); - } - .input-group.input-group-lg .twitter-typeahead:not(:first-child):not(:last-child) &{ - border-radius: 0; - } - .input-group.input-group-lg .twitter-typeahead:first-child &{ - .border-left-radius(@border-radius-large); - .border-right-radius(0); - } - .input-group.input-group-lg .twitter-typeahead:last-child &{ - .border-left-radius(0); - .border-right-radius(@border-radius-large); - } -} - -//for wrapper -.twitter-typeahead { - width: 100%; - .input-group &{ - //overwrite `display:inline-block` style - display: table-cell!important; - float: left; - } -} - -//particular style for each other -.twitter-typeahead .tt-hint { - color: @text-muted;//color - hint -} -.twitter-typeahead .tt-input { - z-index: 2; - //disabled status - //overwrite inline styles of .tt-query - &[disabled], - &[readonly], - fieldset[disabled] & { - cursor: not-allowed; - //overwirte inline style - background-color: @input-bg-disabled!important; - } -} - -//dropdown styles -.tt-dropdown-menu { - //dropdown menu - position: absolute; - top: 100%; - left: 0; - z-index: @zindex-dropdown; - min-width: 160px; - width: 100%; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - font-size: @font-size-base; - background-color: @dropdown-bg; - border: 1px solid @dropdown-fallback-border; - border: 1px solid @dropdown-border; - border-radius: @border-radius-base; - .box-shadow(0 6px 12px rgba(0,0,0,.175)); - background-clip: padding-box; - *border-right-width: 2px; - *border-bottom-width: 2px; - - .tt-suggestion { - //item - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: @line-height-base; - color: @dropdown-link-color; - white-space: nowrap; - - &.tt-cursor { - //item selected - text-decoration: none; - outline: 0; - background-color: @dropdown-link-hover-bg; - color: @dropdown-link-hover-color; - a { - //link in item selected - color: @dropdown-link-hover-color; - } - } - p { - margin: 0; - } - } -} diff --git a/src/UI/Content/utilities.less b/src/UI/Content/utilities.less deleted file mode 100644 index cc2f2cc75..000000000 --- a/src/UI/Content/utilities.less +++ /dev/null @@ -1,19 +0,0 @@ -@import "Bootstrap/variables"; -@import "Bootstrap/mixins"; - -@media (max-width: @screen-sm-max) { - .pull-none-xs { - float : none !important; - } - - .btn-group { - &.btn-group-collapse { - > .btn { - margin : 2px; - display : block; - float : none; - border-radius : @border-radius-base !important; - } - } - } -} \ No newline at end of file diff --git a/src/UI/Content/variables.less b/src/UI/Content/variables.less deleted file mode 100644 index 4b898e1d0..000000000 --- a/src/UI/Content/variables.less +++ /dev/null @@ -1,13 +0,0 @@ -@nzbdroneRed : #c4273c; -@purple : #7a43b6; -@nzbdronePurple : #7932ea; -@nzbdronePink : #F43565; -@droneTeal : #35c5f4; -@brand-info : @droneTeal; - -@screen-tn-max: @screen-xs-min - 1; -@tn: ~'(max-width: @{screen-tn-max})'; -@xs: ~'(min-width: @{screen-xs-max}) and (max-width: @{screen-xs-max})'; -@sm: ~'(min-width: @{screen-sm-min}) and (max-width: @{screen-sm-max})'; -@md: ~'(min-width: @{screen-md-min}) and (max-width: @{screen-md-max})'; -@lg: ~'(min-width: @{screen-lg-min})'; \ No newline at end of file diff --git a/src/UI/Content/zero.clipboard.swf b/src/UI/Content/zero.clipboard.swf deleted file mode 100644 index 8bad6a3e34f1b0b055da3ee8e506fe627cf75987..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6580 zcmV;l8B69vS5pqYF8}~|+O;|hc$3$4_xw*umW3pYG4CHRmTj(WK^{#o5R7es9YDkn zLI7j^mVQ|RS#tDCek9Nm66e+C)g+`1B~@Q(+R&s)wsqasbxqrKYqw64hv~YlTf24b z-dp$X9_-xz|0UVRr0w^$##i^=bI(2J+;h%7=Uka7B7Bb!*UN;|0qOFrB80qW`8R-& zo#{w)M_*S!nHozZ^&L3d<%?x9<2%~gCMPFbCvR#^rAOPg-G2M+ZT{_T+qbtMMvFe3 z%&1c>Nqw_#w?Uvw)5GcbcqX1oDr~Nfq_UY^KA%N35-t-R&!!Uw$w;_OOK4+SGNZR` zYu$!~BH<m;RC-L!>{iFe6Y;Rh$hA$i=&@Az^rSkWwL}xD9^2VgL1cR}@k~P7-4RKR zXi6ZVO({1k9hLnCwu!=!BNah+&pW2dcDII8V{PN<R3sZlnW9L_*gSs=BQc&GNyPP- zmfoFBo=&DFO%@d>!wIJ~HIrJ1QG&7cgqj@9s-xQO?nA~}d2Yz4X0+X1T3FeBJ5{#( zx7}=t!Js?a7HQjppaFN2F82@loureycJ10R2agt(unE#fjuGSMt3Pgdp%*RwO~)}U zo$5@)$463XIzql!bGC<p-c3I;L$SZqiB^5t(qr`gxNh!bB<_k@u!j2~AB(!I4v1 zI8$vHq%{)P#}n$b)IXk%XEdh?)Fzl|9S2icUF&9JsXvpBCr538Y%<J57rWExRJwYB zF!ob=_gFlW(bDoM{ct)pH7)m=y**sQ@szQ9l1zwHS};E1$#|w_RLcYy`y*;v52RCL z`!iz+2JD_1M^r6RUbK&=wQ)7A4fe>XBvK5Y7BuASOeHH4yVXd<nC(}Sk%X4kZDU$S zMMJ1HT2jxZHS<J)ctlIYGt*Vb$J9$t#1Yp!Q)A-^Eu*>O`u_N6tc-<3JJm#DL=B&I zrM0otgjSjgVH^m$mW<Re5t1=A8P+1@<4SSkytbpdKA4FobooekUw=>Vkhr~dd#iut z;lAL}p6;&U&W-~I_I7mMIefUUJJ55qYIqoZdMd6(hKFl<`-i(a20Cyl*xB9RAM6_r z9C}EIS{6|dF|6r&7oM<@9g`}$al*qUJe3Y4o0R(%B_358Gt=W*DylFvW!J7<$|e(a zlhV*&09zAUax@cDb}N2kk7A)-Ptsu3)UGIJ6-7(v+S24xhGyS^Jcb0NaY9Wiab)6e z$8P6*OayG*YN4949FGqn=HfFWbg5&vvt4I@HW`(TyOd-$kytDsvK&|9Ndu=W5hG}b z8&mb*WD<FgYw65%BSUT~V_A|j;F7`{3pbSC5WjScw3f-HO(ELPwtGvi(S4{h*wxJ( zW1#F8S~8r9XpBn$iLooxSeD%)bG_@Bn+E%O*hNSs@jf-p=hM|);xkz06Vb|i@b3P! zjLcn&8CA_gT(;l<z5VWmjC%W*n^)^_LX9T}w5d!mnV9yPu+lYT$THf7^=@V(L@DFD zbS)iM6Y={rBdFPQ6@wAZT%~~-DXQc85yXnHsDv>B<BzW0mDSSIqMpI%FB(zD)zQ_3 z*LWZ}+&|E<ubTxS1AZW`XSAf2mPWFfOe$Gz#ih<{8ZErsj3f&Xtw5|#<8;i{uZ6Q| zBmN{3smZR?7>e%7q_emf38v$t@ua@d%7~S0*wKGe+jhVI*0zysJduefYZe6j9cI9< zUkL0ndovPtrm#|oqc9s5BK0IO#;a)c3C%=Y^A1GT&d(@2j5D%zH50~sE3#r?x=Npn zmSS&hW67Z!jV%aA2Xc57g<Zasg@w1r$NQ+G+q@XKA0d%cILk_s<0Y&i<;<Bfw3Q_i zvu4>l-4kgnAvICQa+AujPKti2XA_xrha+ZQYDO6Yf)y-q*190&8Ii+RlGyi7sI4qR zEy8CfTWhjrv32p5c87qM-Es2zsP$6{@ly#<R~BFq^F~E!Yg4*dyvxKi#VYtvMNut{ zcT%HFA~iYfaFr~;;$^#|+Em@fW1WorJ&b%NmQGD7No`Uwu2`e5yRR?Uw`0*c`tJ@X z;Z!yeQIe^Ql1O0@s4YV2?YDej<B4Q_sV*ps*3-2^@hMx&Zqn555Sa!Y8x+g3?3A5` zCMxcQ)~;K4)bbJ8(G1m?Cv$kpk*wq>+}WmD%njk|Grp*?qY6nZ%RI&%Xvrqk2{oQj zM-rOLghaH_v>MS^>2w536f}u@!t7hd;`$i7Bo0)qwYDsrN=D<O<t5v=m1nF4ENUVc zqOw{QUe2tVFviuomZ^jq5$o8<v`1T9Q7yA7w$e3pmoQ2ISLI$Zt6KTY)B-vJRWMOg z1%skG*3C54yx{&Y*G^*4K7`6JT`;(}x4Uz|aKQF*el?Q6LCU%g;aEHoK}%H10IUme zT&f2g8%;SZzKeEYHPG&um!V5oD3CQ=B-G)KzK(;&1uEgPsL_$Z_>Xa0YxFT=qRbdp ze7>gEOzJ>tQcHKLx`s+IZx&N1Mk?Jn*=@=KJ+Yj>B-sgOD3h+~Xf`Y1J0!Df`UzK2 zFW<#grMp;BiRHU+$Hvh_YD7)6+=o}HT#=javX+r_JTj_DX6*q~#0VbG2$*6tM?Xt% z9f|m8(l!u0Jba)#Fd(XI>M~aPvGCOnq8{ztM>{$Ph66nVcD)318Yp+6Gh+4<_ICFV z$oHBP8+!hDEFRYFBO2zfKs=Ff7>V@mcq9|6GKTv#ti3a~NLrojLoY<G<1y7?rcg%R z#>#PKY{!U(4uo~V__V-2>}nKK6dTEUHq0ssft4GQaV2$ug-3ff8PTGc-6E2j$lR$- zJ1}RC;YGor%$^D+D49v85)2g1q*-s%WAUho&fXoJsVqK@B8VPO*jW;1b~#!e-6I+2 zI4TdDy*a(dn%?7DST4eF)jXHt(T<3i%3^SF91ITjcMl&79_hA*IO?-o!@&Uy*cH6{ zkj)$p9u_+f^mN|2Y*EIoUXYwU*QN5>r7Hl*ynvD!S-sbnhu-T8Ia?_VOR*FyRH{ef zkvJNO1rar2!)k#=!pO3y^|<Obo@Ei2RWMAUWQgbKwg!f%Q8lc2$MAl|uoCK?3Twtg zS}(EcvX>+5x&;BN6t<Z|w<~9xc=UvtapV3V%Yzm{R^=_&c755giQ7cGehyVfyR#f1 zk<WUIUhDAn)jINk_6=o(CCY5X;4w<1%gJfu#_Q{rB`LFaPwBjy7H?RR(}ErC;1Euw z;z>s&9y!eXtG8e5NT*f2Er}E}=FdY@wDfk;bo8D`|75iE<TeAhvT1{fU?#mK*1csZ zN-OOjgB#R$$Csos^b=I|3(zfk#m$SPHq)DxPj4V-d#knS_|p1f56}4?zQWWE^K#Ql znOIzJ@2o)8^r&v-)g_MEczmc;$3TcL4rJ1Z$c8cXrl$676&mIAH}5d5M(N+k*inN? zlPP-HiEpH*Mw41oyO|r?SKgp@t6or*I!lLUO0Z92?!k~7NG`X&UJ1-w<u<i<+{im_ zsMN)_^ZjHq7;^bc*Jj@}y%T!7^+sWeD!)=vH+rM|g8XrJNrI9;EM=V=JA`XNCDd5c zw1f+6UF-l$xLn!(n4R&7AoTVJ582JaB_5sTsA4xWNK(sGW5pdG#b?AoYOwEs16!YV zZ&uSYRi`jfm|w*1fJUyVT^!c6L{#cG-rGV3vyT%C=gQZ2=0jW<&+0K7tDTH*6ii9< zDwAK9eXnQVJDt&t7+Cs#Rnt#rQseeADfcLQ->ZqrLm`~d)U>-i4;WvM>s15YN3og; z_8sgPkWm(cqeV@KWyTU!`v(plun_okCR#no9`u6)0Vaa(#!S3_cpeRLgn_ZN(RHjn zWTO=7>0)1Agilvv@a<=V0W1bI$^3%e*{P1JBXLa4aZTq^x@g!{#4MSelKQk!e135! zPQWOt0V{^Ew)W)lj+SF;%Y8#u?{B$%c*lvB6Rktd^{7%kBge7knhHi?^SUjF6Hr^C zh%<CY<MES~*_rw#ev98PZrOf|-_JKRY=s6V8V+A6ahBJ0ejIOlE!2v&a-CBoE1_<g zNa{S;8fv>lV%xvkxw>a{@9H~OA6R`*B&(Oz{x;P958%jC`#4woK29V!ZjhWJNYyT3 zncL%CUL)I9faH>EL6RM-aAaRCuajWCt7^k9fef4sybOZONiuJf!7hVChAJ7HvREyP zE?HV8i*8x+$f8#km&;;}EUu7IK5?Zi*2&^3SzImK*2p%mY+EbCIvLi>ut5ezhK(|8 zl3}y#sFxjH8Meq$gA6{|)+F1u%C;8S)+XDw$+qpX?IziAv+TG<cHAmE-X%M3li_w5 zcF52!!%i7?$^32^?vP=REOp4RSB6g6*)2oBz7J&AA$t()8rV;PEa|e8k)^DC20-Q> z!NWa@?J*obj{SSFy${DvVE-hxr?5SZ?HO$E$M!6?=deAG?E<zJz<v?HehJ%4NDn@Y z%wB^vIC=?3p9TAy4Eh!}rp8~!_AA&vhwWEE=0A^|ehu5NWBU!Te*w5Poc%SX?Jpw6 zZ-Kn>YuNt|w%-N&ml*u(V1FAp`<JmH>Q}J)Dg*x>p6mCK;U8iDZAAZL?EeJY-(vea zY=4hy|B3Mb!uDfqUjVu8f3f`^*q8CL^Fhx35GTkm!{KQJJdI#~1n1JDoGd-Y$!p%r z$!niLHcw)E3dc`l{|vVGbN1)hzUMjn1=hcSun*v#4`M$m!;9Em#CD03ogc<N$H}gb zaBBp+hsU#uYeYP&D8t7P{4%yzuziY?>psib-{3@h1K(Qu0}lK94ghBiIKuuo#G1f> zA|V_v@x%sBr!fOTC~sqb0+P+Y4w)YoG0ylWnF!Y3i3AQ^pKt&Y!;px;MrBeQ@I2wn zPvDTd@xk*pNiqrA03VoO^*m4Ji;f=#8pl3(sj@}{JN_f!bPysromGT5oz7}PTu$dQ zB<cntcz}prlxH~*xdsT{Qev+K;#di!%E{Yh;&Ie5b*uv7T@7UU8Xz@mfvi{uq;@@! zx(z^9DL__l1hU4(+wEjCzlj}rGm!dvAX~QZ_9{YAt_BIDMyQs3*f&F?#7PvIBwlKT zW=W9zuvHReLM7BuizLfro!IK(*SC3u4StWPZ1YGPw|nGGKH(-0-*~e}Xu8ECHs9)z zwhB(L3qaO_6VKTIE`e|@utgwfTn=n@d)|Mtfi}h(_Az6E-Qmdl^F1UVTt@aW%OYfL z-XD6G*G3TNCT`SGK6G33_I%VG+`;W*HsBl%Td2Lza9Q1nbD@fJIN##){=1v6_%%?@ z@ngU`VN57*+{NXGoV`w>R%wtAcnQM^?T+3N+O0tXqI-fJ94<tw0&B?37!D2S-kC8z zPY=_(ayaF3v@b{db95j_2XpjDj^3T4M|1QZV{j}-kLT!#939HhlQ}w^qoEvCb95v} z!#NtssSd_>IWdG9av9`1dtIPbGp1{a$Bn0?Q94RvgAiQ7xxb5ZTv<17C2p>KjSypd z*M(*ex_VOt#|=1k;n-X-AeS$Ax0QCDrl-u6^UWS~)F$OTO&Cb!`5@0oCk)a&600(a zacqi|wwEZ4(IgGh6a!aijWf&&DS?sNh&vE01XnV((tG#t!S!68rY*+CQT7p`8?Ofm z5WFY*2;P%+f_GUIc)Q~n$orL<IW*=K+<$(e*=tAYrn1<~X3f@-3GaxZD>{LOGEG=A z)ntxN<>+*d-j}2I=jfRnJ)5KNHq7&$96bk?aRLg=%oV*W2nlToZJ`f9F&DTM0yn`q z-i>EM8BmJCe!Q_Z!S1-sBtrnUIc4T5s^dJ+eMbn@eDstuC@_fw9`vwgOwgohkg)A@ z#pqt^3^;n8J_wh+%L&2p%+=7A*}yH}#mG~bRreO7d&-D=OhhNH;DkN|f&HNFDMmZ# z!*IFTBN<4Y4VR6*4QgjG+QoLPmW&NuY8Mk)7CE~k$gWhVC)%stiT#1-LG=*!!RTT2 zt|6gUK)<|Fi1xKWuph9mC(WUEMF+4A29F4ZHc;<I@<$6kb!Wa1yayOr8`H%eLltxE zj92sY187vp(+@&m1*&c?AFVD1>cLog5$HvjjUJ;Pf+D?yqvM9h;qA5aNM~2xUsP&P zU5wIPv7nv^4Y7TOxxF64zWJ!57*IF_yB%dfa3eR1!eAj`cf4U5=BhFi77RKC?{kFE z9I)Sy0R1S?j{*HS&`%h@F9ZD~&{vG#PZ__T2Kp+{%RoN^gLWiTPr~Bt`LN_}Lor9B z5pg|IE{q6)lVF($C(&W$)eBw>;l)`tF(B&jc1iSn{n|B~WD|?@b<}Anppu+=qDbEW zbtGSmo+z5fuj0`<cjf(skQfc;qqP{~*f3HVYL#KMG90Z8WBF)JWoSTAB~DCiw#|9J zMnb$l6!(Wt`9r7up@csq;O|kg&iIMhQHE;FazYEnWgl&h`A#-c8f!e+L|bD`C!481 z)_ij79#?GZNxElQjGk=S<Bql9CYLDl{F_iu41E_2Pi^+HfK-T%)sxq~7($AXLI<3` z;(iqU@p<~2ixBDw!QF1OuDvijw6!oBO%?-NIW_q<KZ__UesJxY`%q;kyG#Y9>m6)g zSS&EhY+huU6nHUS;t1)XB`ku>8gr&Km}-{s9M9VkBQ%c72ymAH_nH7t6$m0KFHgS& z3Vj=V^vl@3g6*r|8x&)8M#fsjGM1O;MaK(J%052KD`^&NRXrcA3#Fr(a$r=mCS;=F z^7&FYtBYnYTsW^zhSCkz2Ko)ac!a5K%3X_Y?l1U{k><H(|JE@Kq|-iiYMqUn$AOWU zJYR~3b<ruLb{~TWRulF9ebCi^x9tp^#ZrVUuSQAJ^90pURQgQ_HBz8|0ERF*n#ZVE z09ph(3!$bQ{X?8zfl#wKnge<jXyiy0{T2k?ZAv-@?iGt9HOQl&Ow9dIHE<MVcn_1I z22im!R4ikLieACte1$omoG{(PIK~7n*0RMKT*M$SZ|#1zwi4AYt{G}61X|$SIk*yf z0P=-E2X}sMh!z7B5T&k05arJqmbq%w1@ah=-OQMvL4nCwWSKs}DK$1TSwV0EFkaqs zED!EkX>JJl*isI+8EO|;?lW@PHy3H`Pl0~NC>+e`pRuuEj{h7`#oq<`J)nO9^e=&a zALw5J{Q=M)0@LYV1N|GO*5#&Jg9>17JP%k!k_WIXl*xlYc$qwe<)K7YRr7*)kC7e? zY460m<tyZ7=0fSBa%0H~Ir<Mc2KtXc{|V?nFJSDP$C#{k2!fc!j4^cnY5^6`@{C~v z?+HRo^`gq>mSXcGp#K8&UxEG`<M;0f{11aq<cP!MFmnZOfD%d<jzBSh4>D%5t3xfb zXvXtbStws4ih}s8k^NZ!77E^n&^qj#%URS{@}-?vvbp07R2RL^6T(0b2EMxiL&9w6 zVJNcuA9w`NbXP}&ddL-{kAf4=GE4s(a#!CLMg#^~k-v6r=rI_29OyMB^EoazL}&4c zIr;#X>wO%6p67y(0Sne^F{bkFynpDuz=A4&6xPL@2w#U_0SizZu1E3hk&@4q+${7y zv!Z+gf=>b)a?z)tL45+;x3Zfv%Wh6su&$1==)*;0_%wuE8kYe25Jw;80^9lEGk{l1 z*az;W7TX!_{lH47XVFV|vVk%G5?}H7=YV-|S&HW&dI5qj0I`Izg0{U>IX(ds-s46M z$V4#FLmz<X2TRfiUIgxEmipOwQX6zGUdQ<24H$n2tYw4o#R_A%j`77sjGtSA@ulk+ zUqZ%DGR8yCLGN=wER7|2Yc=?iVJ;qTfmIUo=rb&PtzSai4;#3Ktd}aXa@WavX_2fK z7RtJh-=k`_GoOcV!*{@T=1uNl{&C)RhM#Zh90mlGSK&Bb7{=IZB}tZP*p=-u(rnqn zk~N)Z_N*|57Z<KvtgKvGxPl78OSzE~cp;wm`{sOC7qapOFx@V}XkzYXV={4d_JT>X z$QW*NBExNYKOHSZv2=TpD@3t;yNDf@a3A6dfsX^JJV0LJiqt(jDB;G<0w;*|m@`73 zfauF;sd_Q+N#L;hq95fhUx;mDnY$3&%;H=<kGNjM%||y)n8XSb>Ly&-!pnmAvKfSY zRyA21B<fR8JVMl`VTt<V>a3Nb3bU*}WG+@jW6%23S72WK5pop*e()j>HrfUgVF9`0 ziwjHmrV(L+k-(K=u$42V@|y6{li(?K4KI|j+PESfx}g-8K4rxvZe7fUEPR!?)F?{4 z@Cu%&p0HPoQLn9bvKLPwa5JmX9;;MZHp3{NHr%{{6<c#|FDufDh+K@m%3en5tME2g zWJoK%k7e3{CCaqhj519?HHqvsXde+r1XL5M=Q4nI8GG#@z1cFo?V{lM7Ai@!39LwK z640~#=m8Ic=_;1L)#1j_XRwav=qI?n9QJ|NfSYl1ECb-aW_BZd^Y$^t7A~N3A<HJV z^OM|OC<I;y!?U*vGA}&L<^7HG-wi)PCio5vd=?;kb+-Q6Di;$Ma|rqpSiEMg6zHe9 zGRkG<%Y_*?nyFAvW|`B?APQsnswFBIlA5r~7VI<JJm6~<@bwbViwSP%4Jh=!0qjM^ z+^)#nZr{AJw^(|RumHu9-m4Hg2XD=vqnRd*=jMoD0s?OW2}zGIt)pwcX+2c-vtwb& mB^lqyAdi-kU%h4d6~8XFh1I;v*>_wE|6BGRg#14X#L6Ph4w-iV diff --git a/src/UI/Controller.js b/src/UI/Controller.js deleted file mode 100644 index 0b69eb02f..000000000 --- a/src/UI/Controller.js +++ /dev/null @@ -1,61 +0,0 @@ -var NzbDroneController = require('./Shared/NzbDroneController'); -var AppLayout = require('./AppLayout'); -var Marionette = require('marionette'); -var ActivityLayout = require('./Activity/ActivityLayout'); -var SettingsLayout = require('./Settings/SettingsLayout'); -//var AddSeriesLayout = require('./AddSeries/AddSeriesLayout'); -var AddArtistLayout = require('./AddArtist/AddArtistLayout'); -var WantedLayout = require('./Wanted/WantedLayout'); -var CalendarLayout = require('./Calendar/CalendarLayout'); -var ReleaseLayout = require('./Release/ReleaseLayout'); -var SystemLayout = require('./System/SystemLayout'); -var AlbumStudioLayout = require('./AlbumStudio/AlbumStudioLayout'); -//var SeriesEditorLayout = require('./Series/Editor/SeriesEditorLayout'); -var ArtistEditorLayout = require('./Artist/Editor/ArtistEditorLayout'); - -module.exports = NzbDroneController.extend({ - addArtist : function(action) { - this.setTitle('Add Artist'); - this.showMainRegion(new AddArtistLayout({ action : action })); - }, - - calendar : function() { - this.setTitle('Calendar'); - this.showMainRegion(new CalendarLayout()); - }, - - settings : function(action) { - this.setTitle('Settings'); - this.showMainRegion(new SettingsLayout({ action : action })); - }, - - wanted : function(action) { - this.setTitle('Wanted'); - this.showMainRegion(new WantedLayout({ action : action })); - }, - - activity : function(action) { - this.setTitle('Activity'); - this.showMainRegion(new ActivityLayout({ action : action })); - }, - - rss : function() { - this.setTitle('RSS'); - this.showMainRegion(new ReleaseLayout()); - }, - - system : function(action) { - this.setTitle('System'); - this.showMainRegion(new SystemLayout({ action : action })); - }, - - albumStudio : function() { - this.setTitle('Album Studio'); - this.showMainRegion(new AlbumStudioLayout()); - }, - - artistEditor : function() { - this.setTitle('Artist Editor'); - this.showMainRegion(new ArtistEditorLayout()); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/EpisodeDetailsLayout.js b/src/UI/Episode/EpisodeDetailsLayout.js deleted file mode 100644 index 8aa58a91d..000000000 --- a/src/UI/Episode/EpisodeDetailsLayout.js +++ /dev/null @@ -1,130 +0,0 @@ -var Marionette = require('marionette'); -var SummaryLayout = require('./Summary/EpisodeSummaryLayout'); -var SearchLayout = require('./Search/EpisodeSearchLayout'); -var EpisodeHistoryLayout = require('./History/EpisodeHistoryLayout'); -var SeriesCollection = require('../Series/SeriesCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Episode/EpisodeDetailsLayoutTemplate', - - regions : { - summary : '#episode-summary', - history : '#episode-history', - search : '#episode-search' - }, - - ui : { - summary : '.x-episode-summary', - history : '.x-episode-history', - search : '.x-episode-search', - monitored : '.x-episode-monitored' - }, - - events : { - - 'click .x-episode-summary' : '_showSummary', - 'click .x-episode-history' : '_showHistory', - 'click .x-episode-search' : '_showSearch', - 'click .x-episode-monitored' : '_toggleMonitored' - }, - - templateHelpers : {}, - - initialize : function(options) { - this.templateHelpers.hideSeriesLink = options.hideSeriesLink; - - this.series = SeriesCollection.get(this.model.get('seriesId')); - this.templateHelpers.series = this.series.toJSON(); - this.openingTab = options.openingTab || 'summary'; - - this.listenTo(this.model, 'sync', this._setMonitoredState); - }, - - onShow : function() { - this.searchLayout = new SearchLayout({ model : this.model }); - - if (this.openingTab === 'search') { - this.searchLayout.startManualSearch = true; - this._showSearch(); - } - - else { - this._showSummary(); - } - - this._setMonitoredState(); - - if (this.series.get('monitored')) { - this.$el.removeClass('series-not-monitored'); - } - - else { - this.$el.addClass('series-not-monitored'); - } - }, - - _showSummary : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.summary.tab('show'); - this.summary.show(new SummaryLayout({ - model : this.model, - series : this.series - })); - }, - - _showHistory : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.history.tab('show'); - this.history.show(new EpisodeHistoryLayout({ - model : this.model, - series : this.series - })); - }, - - _showSearch : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.search.tab('show'); - this.search.show(this.searchLayout); - }, - - _toggleMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name), { silent : true }); - - this.ui.monitored.addClass('icon-lidarr-spinner fa-spin'); - this.model.save(); - }, - - _setMonitoredState : function() { - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (this.model.get('monitored')) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs b/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs deleted file mode 100644 index a65c9b27a..000000000 --- a/src/UI/Episode/EpisodeDetailsLayoutTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="modal-content"> - <div class="episode-detail-modal"> - <div class="modal-header"> - <span class="hidden-series-title x-series-title">{{series.title}}</span> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - <i class="icon-lidarr-monitored x-episode-monitored episode-monitored" title="Toggle monitored status" /> - {{series.title}} - {{EpisodeNumber}} - {{title}} - </h3> - - </div> - <div class="modal-body"> - <ul class="nav nav-tabs" id="myTab"> - <li><a href="#episode-summary" class="x-episode-summary">Summary</a></li> - <li><a href="#episode-history" class="x-episode-history">History</a></li> - <li><a href="#episode-search" class="x-episode-search">Search</a></li> - </ul> - <div class="tab-content"> - <div class="tab-pane" id="episode-summary"/> - <div class="tab-pane" id="episode-history"/> - <div class="tab-pane" id="episode-search"/> - </div> - </div> - <div class="modal-footer"> - {{#unless hideSeriesLink}} - {{#with series}} - <a href="{{route}}" class="btn btn-default pull-left" data-dismiss="modal">Go to Series</a> - {{/with}} - {{/unless}} - - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/Episode/History/EpisodeHistoryActionsCell.js b/src/UI/Episode/History/EpisodeHistoryActionsCell.js deleted file mode 100644 index ae4bbfafb..000000000 --- a/src/UI/Episode/History/EpisodeHistoryActionsCell.js +++ /dev/null @@ -1,35 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-actions-cell', - - events : { - 'click .x-failed' : '_markAsFailed' - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('eventType') === 'grabbed') { - this.$el.html('<i class="icon-lidarr-delete x-failed" title="Mark download as failed"></i>'); - } - - return this; - }, - - _markAsFailed : function() { - var url = window.NzbDrone.ApiRoot + '/history/failed'; - var data = { - id : this.model.get('id') - }; - - $.ajax({ - url : url, - type : 'POST', - data : data - }); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryDetailsCell.js b/src/UI/Episode/History/EpisodeHistoryDetailsCell.js deleted file mode 100644 index 231c40473..000000000 --- a/src/UI/Episode/History/EpisodeHistoryDetailsCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var HistoryDetailsView = require('../../Activity/History/Details/HistoryDetailsView'); -require('bootstrap'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-history-details-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<i class="icon-lidarr-form-info"></i>'); - - var html = new HistoryDetailsView({ model : this.model }).render().$el; - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Details', - placement : 'left', - container : this.$el - }); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryLayout.js b/src/UI/Episode/History/EpisodeHistoryLayout.js deleted file mode 100644 index f474f4566..000000000 --- a/src/UI/Episode/History/EpisodeHistoryLayout.js +++ /dev/null @@ -1,84 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HistoryCollection = require('../../Activity/History/HistoryCollection'); -var EventTypeCell = require('../../Cells/EventTypeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeHistoryActionsCell = require('./EpisodeHistoryActionsCell'); -var EpisodeHistoryDetailsCell = require('./EpisodeHistoryDetailsCell'); -var NoHistoryView = require('./NoHistoryView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/History/EpisodeHistoryLayoutTemplate', - - regions : { - historyTable : '.history-table' - }, - - columns : [ - { - name : 'eventType', - label : '', - cell : EventTypeCell, - cellValue : 'this' - }, - { - name : 'sourceTitle', - label : 'Source Title', - cell : 'string' - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'date', - label : 'Date', - cell : RelativeDateCell - }, - { - name : 'this', - label : '', - cell : EpisodeHistoryDetailsCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : EpisodeHistoryActionsCell, - sortable : false - } - ], - - initialize : function(options) { - this.model = options.model; - this.series = options.series; - - this.collection = new HistoryCollection({ - episodeId : this.model.id, - tableName : 'episodeHistory' - }); - this.collection.fetch(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.historyTable.show(new LoadingView()); - }, - - _showTable : function() { - if (this.collection.any()) { - this.historyTable.show(new Backgrid.Grid({ - collection : this.collection, - columns : this.columns, - className : 'table table-hover table-condensed' - })); - } - - else { - this.historyTable.show(new NoHistoryView()); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs b/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs deleted file mode 100644 index 54fb50522..000000000 --- a/src/UI/Episode/History/EpisodeHistoryLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div class="history-table table-responsive"></div> \ No newline at end of file diff --git a/src/UI/Episode/History/NoHistoryView.js b/src/UI/Episode/History/NoHistoryView.js deleted file mode 100644 index 883b5dfdc..000000000 --- a/src/UI/Episode/History/NoHistoryView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/History/NoHistoryViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/History/NoHistoryViewTemplate.hbs b/src/UI/Episode/History/NoHistoryViewTemplate.hbs deleted file mode 100644 index 561e84d59..000000000 --- a/src/UI/Episode/History/NoHistoryViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No history for this episode. -</p> \ No newline at end of file diff --git a/src/UI/Episode/Search/ButtonsView.js b/src/UI/Episode/Search/ButtonsView.js deleted file mode 100644 index 6972f1201..000000000 --- a/src/UI/Episode/Search/ButtonsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Search/ButtonsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/ButtonsViewTemplate.hbs b/src/UI/Episode/Search/ButtonsViewTemplate.hbs deleted file mode 100644 index 9e578f9db..000000000 --- a/src/UI/Episode/Search/ButtonsViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="search-buttons"> - <button class="btn btn-lg btn-block x-search-auto"><i class="icon-lidarr-search-automatic"/> Automatic Search</button> - <button class="btn btn-lg btn-block btn-primary x-search-manual"><i class="icon-lidarr-search-manual"/> Manual Search</button> -</div> \ No newline at end of file diff --git a/src/UI/Episode/Search/EpisodeSearchLayout.js b/src/UI/Episode/Search/EpisodeSearchLayout.js deleted file mode 100644 index 14ee5ca42..000000000 --- a/src/UI/Episode/Search/EpisodeSearchLayout.js +++ /dev/null @@ -1,82 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var ButtonsView = require('./ButtonsView'); -var ManualSearchLayout = require('./ManualLayout'); -var ReleaseCollection = require('../../Release/ReleaseCollection'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var NoResultsView = require('./NoResultsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Search/EpisodeSearchLayoutTemplate', - - regions : { - main : '#episode-search-region' - }, - - events : { - 'click .x-search-auto' : '_searchAuto', - 'click .x-search-manual' : '_searchManual', - 'click .x-search-back' : '_showButtons' - }, - - initialize : function() { - this.mainView = new ButtonsView(); - this.releaseCollection = new ReleaseCollection(); - - this.listenTo(this.releaseCollection, 'sync', this._showSearchResults); - }, - - onShow : function() { - if (this.startManualSearch) { - this._searchManual(); - } - - else { - this._showMainView(); - } - }, - - _searchAuto : function(e) { - if (e) { - e.preventDefault(); - } - - CommandController.Execute('episodeSearch', { - episodeIds : [this.model.get('id')] - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _searchManual : function(e) { - if (e) { - e.preventDefault(); - } - - this.mainView = new LoadingView(); - this._showMainView(); - this.releaseCollection.fetchEpisodeReleases(this.model.id); - }, - - _showMainView : function() { - this.main.show(this.mainView); - }, - - _showButtons : function() { - this.mainView = new ButtonsView(); - this._showMainView(); - }, - - _showSearchResults : function() { - if (this.releaseCollection.length === 0) { - this.mainView = new NoResultsView(); - } - - else { - this.mainView = new ManualSearchLayout({ collection : this.releaseCollection }); - } - - this._showMainView(); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs b/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs deleted file mode 100644 index 879e0b356..000000000 --- a/src/UI/Episode/Search/EpisodeSearchLayoutTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="episode-search-region"></div> \ No newline at end of file diff --git a/src/UI/Episode/Search/ManualLayout.js b/src/UI/Episode/Search/ManualLayout.js deleted file mode 100644 index 7bb39bab5..000000000 --- a/src/UI/Episode/Search/ManualLayout.js +++ /dev/null @@ -1,86 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseTitleCell = require('../../Cells/ReleaseTitleCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var ApprovalStatusCell = require('../../Cells/ApprovalStatusCell'); -var DownloadReportCell = require('../../Release/DownloadReportCell'); -var AgeCell = require('../../Release/AgeCell'); -var ProtocolCell = require('../../Release/ProtocolCell'); -var PeersCell = require('../../Release/PeersCell'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Search/ManualLayoutTemplate', - - regions : { - grid : '#episode-release-grid' - }, - - columns : [ - { - name : 'protocol', - label : 'Source', - cell : ProtocolCell - }, - { - name : 'age', - label : 'Age', - cell : AgeCell - }, - { - name : 'title', - label : 'Title', - cell : ReleaseTitleCell - }, - { - name : 'indexer', - label : 'Indexer', - cell : Backgrid.StringCell - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell - }, - { - name : 'seeders', - label : 'Peers', - cell : PeersCell - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending', - title : 'Release Rejected' - }, - { - name : 'download', - label : '<i class="icon-lidarr-download" />', - tooltip : 'Auto-Search Prioritization', - cell : DownloadReportCell, - sortable : true, - sortType : 'fixed', - direction : 'ascending' - } - ], - - onShow : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/ManualLayoutTemplate.hbs b/src/UI/Episode/Search/ManualLayoutTemplate.hbs deleted file mode 100644 index 1797eb289..000000000 --- a/src/UI/Episode/Search/ManualLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div id="episode-release-grid" class="table-responsive"></div> -<button class="btn x-search-back">Back</button> \ No newline at end of file diff --git a/src/UI/Episode/Search/NoResultsView.js b/src/UI/Episode/Search/NoResultsView.js deleted file mode 100644 index a1a68c4fa..000000000 --- a/src/UI/Episode/Search/NoResultsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Search/NoResultsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Search/NoResultsViewTemplate.hbs b/src/UI/Episode/Search/NoResultsViewTemplate.hbs deleted file mode 100644 index 7904e5520..000000000 --- a/src/UI/Episode/Search/NoResultsViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No results found</div> \ No newline at end of file diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayout.js b/src/UI/Episode/Summary/EpisodeSummaryLayout.js deleted file mode 100644 index 29eaad626..000000000 --- a/src/UI/Episode/Summary/EpisodeSummaryLayout.js +++ /dev/null @@ -1,119 +0,0 @@ -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EpisodeFileModel = require('../../Series/EpisodeFileModel'); -var EpisodeFileCollection = require('../../Series/EpisodeFileCollection'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var QualityCell = require('../../Cells/QualityCell'); -var DeleteEpisodeFileCell = require('../../Cells/DeleteEpisodeFileCell'); -var NoFileView = require('./NoFileView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Episode/Summary/EpisodeSummaryLayoutTemplate', - - regions : { - overview : '.episode-overview', - activity : '.episode-file-info' - }, - - columns : [ - { - name : 'path', - label : 'Path', - cell : 'string', - sortable : false - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : false, - editable : true - }, - { - name : 'this', - label : '', - cell : DeleteEpisodeFileCell, - sortable : false - } - ], - - templateHelpers : {}, - - initialize : function(options) { - if (!this.model.series) { - this.templateHelpers.series = options.series.toJSON(); - } - }, - - onShow : function() { - if (this.model.get('hasFile')) { - var episodeFileId = this.model.get('episodeFileId'); - - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - this.episodeFileCollection = new EpisodeFileCollection(episodeFile, { seriesId : this.model.get('seriesId') }); - this.listenTo(episodeFile, 'destroy', this._episodeFileDeleted); - - this._showTable(); - } - - else { - this.activity.show(new LoadingView()); - - var self = this; - var newEpisodeFile = new EpisodeFileModel({ id : episodeFileId }); - this.episodeFileCollection = new EpisodeFileCollection(newEpisodeFile, { seriesId : this.model.get('seriesId') }); - var promise = newEpisodeFile.fetch(); - this.listenTo(newEpisodeFile, 'destroy', this._episodeFileDeleted); - - promise.done(function() { - self._showTable(); - }); - } - - this.listenTo(this.episodeFileCollection, 'add remove', this._collectionChanged); - } - - else { - this._showNoFileView(); - } - }, - - _showTable : function() { - this.activity.show(new Backgrid.Grid({ - collection : this.episodeFileCollection, - columns : this.columns, - className : 'table table-bordered', - emptyText : 'Nothing to see here!' - })); - }, - - _showNoFileView : function() { - this.activity.show(new NoFileView()); - }, - - _collectionChanged : function() { - if (!this.episodeFileCollection.any()) { - this._showNoFileView(); - } - - else { - this._showTable(); - } - }, - - _episodeFileDeleted : function() { - this.model.set({ - episodeFileId : 0, - hasFile : false - }); - } -}); \ No newline at end of file diff --git a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs b/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs deleted file mode 100644 index 9cfeca2da..000000000 --- a/src/UI/Episode/Summary/EpisodeSummaryLayoutTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="episode-info"> - {{#with series}} - {{profile profileId}} - <span class="label label-info">{{network}}</span> - {{/with}} - <span class="label label-info">{{StartTime airDateUtc}}</span> - <span class="label label-info">{{RelativeDate airDateUtc}}</span> -</div> - -<div class="episode-overview"> - {{overview}} -</div> - -<div class="episode-file-info"></div> diff --git a/src/UI/Episode/Summary/NoFileView.js b/src/UI/Episode/Summary/NoFileView.js deleted file mode 100644 index 07aabc810..000000000 --- a/src/UI/Episode/Summary/NoFileView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Episode/Summary/NoFileViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Episode/Summary/NoFileViewTemplate.hbs b/src/UI/Episode/Summary/NoFileViewTemplate.hbs deleted file mode 100644 index 0f923737d..000000000 --- a/src/UI/Episode/Summary/NoFileViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<p class="text-warning"> - No file available for this episode. -</p> \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/EmptyView.js b/src/UI/EpisodeFile/Editor/EmptyView.js deleted file mode 100644 index e84453524..000000000 --- a/src/UI/EpisodeFile/Editor/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'EpisodeFile/Editor/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs b/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs deleted file mode 100644 index 0a51692de..000000000 --- a/src/UI/EpisodeFile/Editor/EmptyViewTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - No episode files - </div> -</div> diff --git a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js b/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js deleted file mode 100644 index a974c8f7c..000000000 --- a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayout.js +++ /dev/null @@ -1,200 +0,0 @@ -var _ = require('underscore'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell'); -var SeasonEpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var EpisodeFilePathCell = require('../../Cells/EpisodeFilePathCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeCollection = require('../../Series/EpisodeCollection'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); -var QualitySelectView = require('./QualitySelectView'); -var EmptyView = require('./EmptyView'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate', - - regions : { - episodeGrid : '.x-episode-list', - quality : '.x-quality' - }, - - ui : { - seasonMonitored : '.x-season-monitored' - }, - - events : { - 'click .x-season-monitored' : '_seasonMonitored', - 'click .x-delete-files' : '_deleteFiles' - }, - - initialize : function(options) { - if (!options.series) { - throw 'series is required'; - } - - if (!options.episodeCollection) { - throw 'episodeCollection is required'; - } - - var filtered = options.episodeCollection.filter(function(episode) { - return episode.get('episodeFileId') > 0; - }); - - this.series = options.series; - this.episodeCollection = options.episodeCollection; - this.filteredEpisodes = new EpisodeCollection(filtered); - - this.templateHelpers = {}; - this.templateHelpers.series = this.series.toJSON(); - - this._getColumns(); - }, - - onRender : function() { - this._getQualities(); - this._showEpisodes(); - }, - - _getColumns : function () { - var episodeCell = {}; - - if (this.model) { - episodeCell.name = 'episodeNumber'; - episodeCell.label = '#'; - episodeCell.cell = EpisodeNumberCell; - } - - else { - episodeCell.name = 'seasonEpisode'; - episodeCell.cellValue = 'this'; - episodeCell.label = 'Episode'; - episodeCell.cell = SeasonEpisodeNumberCell; - episodeCell.sortValue = this._seasonEpisodeSorter; - } - - this.columns = [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - episodeCell, - { - name : 'episodeNumber', - label : 'Relative Path', - cell : EpisodeFilePathCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Quality', - cell : EpisodeStatusCell, - sortable : false - } - ]; - }, - - _showEpisodes : function() { - if (this.filteredEpisodes.length === 0) { - this.episodeGrid.show(new EmptyView()); - return; - } - - this._setInitialSort(); - - this.episodeGridView = new Backgrid.Grid({ - columns : this.columns, - collection : this.filteredEpisodes, - className : 'table table-hover season-grid' - }); - - this.episodeGrid.show(this.episodeGridView); - }, - - _setInitialSort : function () { - if (!this.model) { - this.filteredEpisodes.setSorting('seasonEpisode', 1, { sortValue: this._seasonEpisodeSorter }); - this.filteredEpisodes.fullCollection.sort(); - } - }, - - _getQualities : function() { - var self = this; - - var profileSchemaCollection = new ProfileSchemaCollection(); - var promise = profileSchemaCollection.fetch(); - - promise.done(function() { - var profile = profileSchemaCollection.first(); - - self.qualitySelectView = new QualitySelectView({ qualities: _.map(profile.get('items'), 'quality') }); - self.listenTo(self.qualitySelectView, 'seasonedit:quality', self._changeQuality); - - self.quality.show(self.qualitySelectView); - }); - }, - - _changeQuality : function(options) { - var newQuality = { - quality : options.selected, - revision : { - version : 1, - real : 0 - } - }; - - var selected = this._getSelectedEpisodeFileIds(); - - _.each(selected, function(episodeFileId) { - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - episodeFile.set('quality', newQuality); - episodeFile.save(); - } - }); - }, - - _deleteFiles : function() { - if (!window.confirm('Are you sure you want to delete the episode files for the selected episodes?')) { - return; - } - - var selected = this._getSelectedEpisodeFileIds(); - - _.each(selected, function(episodeFileId) { - if (reqres.hasHandler(reqres.Requests.GetEpisodeFileById)) { - var episodeFile = reqres.request(reqres.Requests.GetEpisodeFileById, episodeFileId); - - episodeFile.destroy(); - } - }); - - _.each(this.episodeGridView.getSelectedModels(), function(episode) { - this.episodeGridView.removeRow(episode); - }, this); - }, - - _getSelectedEpisodeFileIds: function () { - return _.uniq(_.map(this.episodeGridView.getSelectedModels(), function (episode) { - return episode.get('episodeFileId'); - })); - }, - - _seasonEpisodeSorter : function (model, attr) { - var seasonNumber = FormatHelpers.pad(model.get('seasonNumber'), 4, 0); - var episodeNumber = FormatHelpers.pad(model.get('episodeNumber'), 4, 0); - - return seasonNumber + episodeNumber; - } -}); diff --git a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs b/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs deleted file mode 100644 index 6f7e84109..000000000 --- a/src/UI/EpisodeFile/Editor/EpisodeFileEditorLayoutTemplate.hbs +++ /dev/null @@ -1,28 +0,0 @@ -<div class="modal-content"> - <div class="edit-season-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - {{#if seasonNumber}} - {{#if_eq seasonNumber compare="0"}} - {{series.title}} - Specials - {{else}} - {{series.title}} - Season {{seasonNumber}} - {{/if_eq}} - {{else}} - {{series.title}} - {{/if}} - </h3> - - </div> - <div class="modal-body"> - <div class="x-episode-list"></div> - <div class="x-quality"></div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger x-delete-files">Delete Files</button> - <button class="btn btn-default" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/EpisodeFile/Editor/QualitySelectView.js b/src/UI/EpisodeFile/Editor/QualitySelectView.js deleted file mode 100644 index beac4f304..000000000 --- a/src/UI/EpisodeFile/Editor/QualitySelectView.js +++ /dev/null @@ -1,35 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'EpisodeFile/Editor/QualitySelectViewTemplate', - - ui : { - select : '.x-select' - }, - - events : { - 'change .x-select' : '_changeSelect' - }, - - initialize : function (options) { - this.qualities = options.qualities; - - this.templateHelpers = { - qualities : this.qualities - }; - }, - - _changeSelect : function () { - var value = this.ui.select.val(); - - if (value === 'choose') { - return; - } - - var quality = _.find(this.qualities, { 'id': parseInt(value) }); - - this.trigger('seasonedit:quality', { selected : quality }); - this.ui.select.val('choose'); - } -}); \ No newline at end of file diff --git a/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs b/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs deleted file mode 100644 index 4ab83c931..000000000 --- a/src/UI/EpisodeFile/Editor/QualitySelectViewTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="form-group col-md-3 col-md-offset-9"> - <select class="form-control x-select"> - <option value="choose">Select quality</option> - {{#eachReverse qualities}} - <option value="{{id}}">{{name}}</option> - {{/eachReverse}} - </select> - </div> -</div> diff --git a/src/UI/Form/ActionTemplate.hbs b/src/UI/Form/ActionTemplate.hbs deleted file mode 100644 index ecb861f99..000000000 --- a/src/UI/Form/ActionTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label"></label> - - <div class="col-sm-5"> - <button class="form-control {{name}}" validation-name="{{name}}" data-value="{{value}}">{{label}}</button> - </div> -</div> diff --git a/src/UI/Form/CaptchaTemplate.hbs b/src/UI/Form/CaptchaTemplate.hbs deleted file mode 100644 index 806c3a32e..000000000 --- a/src/UI/Form/CaptchaTemplate.hbs +++ /dev/null @@ -1,15 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <div class="input-group"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control x-captcha" readonly placeholder="(optional)" /> - <span class="input-group-btn"><button class="btn btn-primary x-captcha-refresh" title="Refresh CAPTCHA Token"><i class="icon-lidarr-refresh" /></button></span> - </div> - </div> - - <span class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-warning" title="Expires periodically and will need to be refreshed."/> - <i class="icon-lidarr-form-warning" title="Refreshing the CAPTCHA Token will embed a temporary Google reCaptcha widget on this page."/> - </span> -</div> diff --git a/src/UI/Form/CheckboxTemplate.hbs b/src/UI/Form/CheckboxTemplate.hbs deleted file mode 100644 index d382aaa00..000000000 --- a/src/UI/Form/CheckboxTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="fields.{{order}}.value"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - {{#if helpText}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="{{helpText}}"/> - </span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Form/FormBuilder.js b/src/UI/Form/FormBuilder.js deleted file mode 100644 index eef48eb91..000000000 --- a/src/UI/Form/FormBuilder.js +++ /dev/null @@ -1,66 +0,0 @@ -var Marionette = require('marionette'); -var Handlebars = require('handlebars'); -var _ = require('underscore'); -require('./FormMessage'); - -var _templateRenderer = function(templateName) { - var templateFunction = Marionette.TemplateCache.get(templateName); - return new Handlebars.SafeString(templateFunction(this)); -}; - -var _fieldBuilder = function(field) { - if (!field.type) { - return _templateRenderer.call(field, 'Form/TextboxTemplate'); - } - - if (field.type === 'hidden') { - return _templateRenderer.call(field, 'Form/HiddenTemplate'); - } - - if (field.type === 'url') { - return _templateRenderer.call(field, 'Form/UrlTemplate'); - } - - if (field.type === 'password') { - return _templateRenderer.call(field, 'Form/PasswordTemplate'); - } - - if (field.type === 'checkbox') { - return _templateRenderer.call(field, 'Form/CheckboxTemplate'); - } - - if (field.type === 'select') { - return _templateRenderer.call(field, 'Form/SelectTemplate'); - } - - if (field.type === 'hidden') { - return _templateRenderer.call(field, 'Form/HiddenTemplate'); - } - - if (field.type === 'path' || field.type === 'filepath') { - return _templateRenderer.call(field, 'Form/PathTemplate'); - } - - if (field.type === 'tag') { - return _templateRenderer.call(field, 'Form/TagTemplate'); - } - - if (field.type === 'action') { - return _templateRenderer.call(field, 'Form/ActionTemplate'); - } - - if (field.type === 'captcha') { - return _templateRenderer.call(field, 'Form/CaptchaTemplate'); - } - - return _templateRenderer.call(field, 'Form/TextboxTemplate'); -}; - -Handlebars.registerHelper('formBuilder', function() { - var ret = ''; - _.each(this.fields, function(field) { - ret += _fieldBuilder(field); - }); - - return new Handlebars.SafeString(ret); -}); diff --git a/src/UI/Form/FormHelpPartial.hbs b/src/UI/Form/FormHelpPartial.hbs deleted file mode 100644 index e8072f190..000000000 --- a/src/UI/Form/FormHelpPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<span class="col-sm-1 help-inline"> - {{#if helpText}} - <i class="icon-lidarr-form-info" title="{{helpText}}"/> - {{/if}} - {{#if helpLink}} - <a href="{{helpLink}}" class="help-link"><i class="icon-lidarr-form-info-link"/></a> - {{/if}} -</span> diff --git a/src/UI/Form/FormMessage.js b/src/UI/Form/FormMessage.js deleted file mode 100644 index 209bccd42..000000000 --- a/src/UI/Form/FormMessage.js +++ /dev/null @@ -1,17 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('formMessage', function(message) { - if (!message) { - return ''; - } - - var level = message.type; - - if (message.type === 'error') { - level = 'danger'; - } - - var messageHtml = '<div class="alert alert-{0}" role="alert">{1}</div>'.format(level, message.message); - - return new Handlebars.SafeString(messageHtml); -}); \ No newline at end of file diff --git a/src/UI/Form/HiddenTemplate.hbs b/src/UI/Form/HiddenTemplate.hbs deleted file mode 100644 index 03933b122..000000000 --- a/src/UI/Form/HiddenTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<input type="hidden" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false"/> \ No newline at end of file diff --git a/src/UI/Form/PasswordTemplate.hbs b/src/UI/Form/PasswordTemplate.hbs deleted file mode 100644 index 3a96cab7f..000000000 --- a/src/UI/Form/PasswordTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="password" name="fields.{{order}}.value" validation-name="{{name}}" autocomplete="new-password" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/PathTemplate.hbs b/src/UI/Form/PathTemplate.hbs deleted file mode 100644 index 5a95305e1..000000000 --- a/src/UI/Form/PathTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control x-path {{#if_eq type compare="filepath"}}x-filepath{{/if_eq}}"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/SelectTemplate.hbs b/src/UI/Form/SelectTemplate.hbs deleted file mode 100644 index 978d432df..000000000 --- a/src/UI/Form/SelectTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <select name="fields.{{order}}.value" class="form-control"> - {{#each selectOptions}} - <option value="{{value}}">{{name}}</option> - {{/each}} - </select> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/TagTemplate.hbs b/src/UI/Form/TagTemplate.hbs deleted file mode 100644 index 4df3ca6ba..000000000 --- a/src/UI/Form/TagTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" class="form-control x-form-tag"/> - </div> - - {{> FormHelpPartial}} -</div> \ No newline at end of file diff --git a/src/UI/Form/TextboxTemplate.hbs b/src/UI/Form/TextboxTemplate.hbs deleted file mode 100644 index e7054cfac..000000000 --- a/src/UI/Form/TextboxTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="text" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Form/UrlTemplate.hbs b/src/UI/Form/UrlTemplate.hbs deleted file mode 100644 index 7f41272f1..000000000 --- a/src/UI/Form/UrlTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="form-group {{#if advanced}}advanced-setting{{/if}}"> - <label class="col-sm-3 control-label">{{label}}</label> - - <div class="col-sm-5"> - <input type="url" name="fields.{{order}}.value" validation-name="{{name}}" spellcheck="false" class="form-control"/> - </div> - {{> FormHelpPartial}} -</div> diff --git a/src/UI/Handlebars/Handlebars.Debug.js b/src/UI/Handlebars/Handlebars.Debug.js deleted file mode 100644 index 84360f665..000000000 --- a/src/UI/Handlebars/Handlebars.Debug.js +++ /dev/null @@ -1,7 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('debug', function() { - console.group('Handlebar context'); - console.log(this); - console.groupEnd(); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Album.js b/src/UI/Handlebars/Helpers/Album.js deleted file mode 100644 index beb346b78..000000000 --- a/src/UI/Handlebars/Helpers/Album.js +++ /dev/null @@ -1,75 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var moment = require('moment'); -var _ = require('underscore'); - -Handlebars.registerHelper('cover', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/cover-dark.png'; - var cover = _.where(this.images, { coverType : 'cover' }); - - if (cover[0]) { - if (!cover[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, cover[0].url, 250))); - } else { - var url = cover[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="album-cover x-album-cover" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="album-cover placeholder-image" src="{0}">'.format(placeholder)); -}); - -Handlebars.registerHelper('StatusLevel', function() { - var hasFile = false; //this.hasFile; #TODO Refactor for Albums - var downloading = false; //require('../../Activity/Queue/QueueCollection').findEpisode(this.id) || this.downloading; #TODO Queue Refactor for Albums - var currentTime = moment(); - var start = moment(this.releaseDate); - var end = moment(this.end); - var monitored = this.artist.monitored && this.monitored; - - if (hasFile) { - return 'success'; - } - - if (downloading) { - return 'purple'; - } - - else if (!monitored) { - return 'unmonitored'; - } - - if (currentTime.isAfter(start) && currentTime.isBefore(end)) { - return 'warning'; - } - - if (start.isBefore(currentTime) && !hasFile) { - return 'danger'; - } - - return 'primary'; -}); - -Handlebars.registerHelper('MBAlbumUrl', function() { - return 'https://musicbrainz.org/release-group/' + this.mbId; -}); - -Handlebars.registerHelper('TADBAlbumUrl', function() { - return 'http://www.theaudiodb.com/album/' + this.tadbId; -}); - -Handlebars.registerHelper('discogsAlbumUrl', function() { - return 'https://www.discogs.com/master/' + this.discogsId; -}); - -Handlebars.registerHelper('allMusicAlbumUrl', function() { - return 'http://www.allmusic.com/album/' + this.allMusicId; -}); - -Handlebars.registerHelper('albumYear', function() { - return new Handlebars.SafeString('<span class="year">{0}</span>'.format(moment(this.releaseDate).format('YYYY'))); -}); -Handlebars.registerHelper('albumReleaseDate', function() { - return new Handlebars.SafeString('<span class="release">{0}</span>'.format(moment(this.releaseDate).format('L'))); -}); diff --git a/src/UI/Handlebars/Helpers/Artist.js b/src/UI/Handlebars/Helpers/Artist.js deleted file mode 100644 index 4840d9467..000000000 --- a/src/UI/Handlebars/Helpers/Artist.js +++ /dev/null @@ -1,117 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var _ = require('underscore'); - -Handlebars.registerHelper('poster', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - var poster = _.where(this.images, { coverType : 'poster' }); - - if (poster[0]) { - if (!poster[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); - } else { - var url = poster[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="artist-poster x-artist-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="artist-poster placeholder-image" src="{0}">'.format(placeholder)); -}); - - - -Handlebars.registerHelper('MBUrl', function() { - return 'https://musicbrainz.org/artist/' + this.foreignArtistId; -}); - -Handlebars.registerHelper('TADBUrl', function() { - return 'http://www.theaudiodb.com/artist/' + this.tadbId; -}); - -Handlebars.registerHelper('discogsUrl', function() { - return 'https://www.discogs.com/artist/' + this.discogsId; -}); - -Handlebars.registerHelper('allMusicUrl', function() { - return 'http://www.allmusic.com/artist/' + this.allMusicId; -}); - -Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/artist/' + this.nameSlug; -}); - -// Handlebars.registerHelper('percentOfEpisodes', function() { -// var episodeCount = this.episodeCount; -// var episodeFileCount = this.episodeFileCount; - -// var percent = 100; - -// if (episodeCount > 0) { -// percent = episodeFileCount / episodeCount * 100; -// } - -// return percent; -// }); - -Handlebars.registerHelper('percentOfTracks', function() { - var trackCount = this.trackCount; - var trackFileCount = this.trackFileCount; - - var percent = 100; - - if (trackCount > 0) { - percent = trackFileCount / trackCount * 100; - } - - return percent; -}); - -Handlebars.registerHelper('seasonCountHelper', function() { - var seasonCount = this.seasonCount; - var continuing = this.status === 'continuing'; - - if (continuing) { - return new Handlebars.SafeString('<span class="label label-info">Season {0}</span>'.format(seasonCount)); - } - - if (seasonCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount)); -}); - -Handlebars.registerHelper ('truncate', function (str, len) { - if (str && str.length > len && str.length > 0) { - var new_str = str + " "; - new_str = str.substr (0, len); - new_str = str.substr (0, new_str.lastIndexOf(" ")); - new_str = (new_str.length > 0) ? new_str : str.substr (0, len); - - return new Handlebars.SafeString ( new_str +'...' ); - } - return str; -}); - -Handlebars.registerHelper('albumCountHelper', function() { - var albumCount = this.albumCount; - - if (albumCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Album</span>'.format(albumCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Albums</span>'.format(albumCount)); -}); - -/*Handlebars.registerHelper('titleWithYear', function() { - if (this.title.endsWith(' ({0})'.format(this.year))) { - return this.title; - } - - if (!this.year) { - return this.title; - } - - return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); -});*/ diff --git a/src/UI/Handlebars/Helpers/DateTime.js b/src/UI/Handlebars/Helpers/DateTime.js deleted file mode 100644 index 18a5f7cbc..000000000 --- a/src/UI/Handlebars/Helpers/DateTime.js +++ /dev/null @@ -1,90 +0,0 @@ -var Handlebars = require('handlebars'); -var moment = require('moment'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var UiSettings = require('../../Shared/UiSettingsModel'); - -Handlebars.registerHelper('ShortDate', function(input) { - if (!input) { - return ''; - } - - var date = moment(input); - var result = '<span title="' + date.format(UiSettings.longDateTime()) + '">' + date.format(UiSettings.get('shortDateFormat')) + '</span>'; - - return new Handlebars.SafeString(result); -}); - -Handlebars.registerHelper('RelativeDate', function(input) { - if (!input) { - return ''; - } - - var date = moment(input); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(input); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - - result = result.format(tooltip, text); - - return new Handlebars.SafeString(result); -}); - -Handlebars.registerHelper('Day', function(input) { - if (!input) { - return ''; - } - - return moment(input).format('DD'); -}); - -Handlebars.registerHelper('Month', function(input) { - if (!input) { - return ''; - } - - return moment(input).format('MMM'); -}); - -Handlebars.registerHelper('StartTime', function(input) { - if (!input) { - return ''; - } - - return moment(input).format(UiSettings.time(false, false)); -}); - -Handlebars.registerHelper('LTS', function(input) { - if (!input) { - return ''; - } - - return moment(input).format(UiSettings.time(true, true)); -}); - -Handlebars.registerHelper('if_today', function(context, options) { - var date = moment(context).startOf('day'); - var today = moment().startOf('day'); - - if (date.isSame(today)) { - return options.fn(this); - } - - return options.inverse(this); -}); - -Handlebars.registerHelper('unless_today', function(context, options) { - var date = moment(context).startOf('day'); - var today = moment().startOf('day'); - - if (date.isSame(today)) { - return options.inverse(this); - } - - return options.fn(this); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/EachReverse.js b/src/UI/Handlebars/Helpers/EachReverse.js deleted file mode 100644 index 7e5e0983a..000000000 --- a/src/UI/Handlebars/Helpers/EachReverse.js +++ /dev/null @@ -1,16 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('eachReverse', function(context) { - var options = arguments[arguments.length - 1]; - var ret = ''; - - if (context && context.length > 0) { - for (var i = context.length - 1; i >= 0; i--) { - ret += options.fn(context[i]); - } - } else { - ret = options.inverse(this); - } - - return ret; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Enumerable.js b/src/UI/Handlebars/Helpers/Enumerable.js deleted file mode 100644 index ae7f8708d..000000000 --- a/src/UI/Handlebars/Helpers/Enumerable.js +++ /dev/null @@ -1,21 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('times', function(n, block) { - var accum = ''; - - for (var i = 0; i < n; ++i) { - accum += block.fn(i); - } - - return accum; -}); - -Handlebars.registerHelper('for', function(from, to, incr, block) { - var accum = ''; - - for (var i = from; i < to; i += incr) { - accum += block.fn(i); - } - - return accum; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Episode.js b/src/UI/Handlebars/Helpers/Episode.js deleted file mode 100644 index 3bd821b66..000000000 --- a/src/UI/Handlebars/Helpers/Episode.js +++ /dev/null @@ -1,33 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../Shared/FormatHelpers'); -var moment = require('moment'); -require('../../Activity/Queue/QueueCollection'); - -Handlebars.registerHelper('EpisodeNumber', function() { - - if (this.series.seriesType === 'daily') { - return moment(this.airDate).format('L'); - } else if (this.series.seriesType === 'anime' && this.absoluteEpisodeNumber !== undefined) { - return '{0}x{1} ({2})'.format(this.seasonNumber, FormatHelpers.pad(this.episodeNumber, 2), FormatHelpers.pad(this.absoluteEpisodeNumber, 2)); - } else { - return '{0}x{1}'.format(this.seasonNumber, FormatHelpers.pad(this.episodeNumber, 2)); - } -}); - - - -Handlebars.registerHelper('EpisodeProgressClass', function() { - if (this.episodeFileCount === this.episodeCount) { - if (this.status === 'continuing') { - return ''; - } - - return 'progress-bar-success'; - } - - if (this.monitored) { - return 'progress-bar-danger'; - } - - return 'progress-bar-warning'; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Html.js b/src/UI/Handlebars/Helpers/Html.js deleted file mode 100644 index 962ccf10d..000000000 --- a/src/UI/Handlebars/Helpers/Html.js +++ /dev/null @@ -1,40 +0,0 @@ -var $ = require('jquery'); -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); - -var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - -window.NzbDrone.imageError = function(img) { - if (!img.src.contains(placeholder)) { - img.src = placeholder; - img.srcset = ""; - $(img).addClass('placeholder-image'); - } - - img.onerror = null; -}; - -Handlebars.registerHelper('defaultImg', function(src, size) { - var endOfPath = /\.jpg($|\?)/g; - var errorAttr = 'onerror="window.NzbDrone.imageError(this);"'; - var srcsetAttr = ''; - var oneX = src, twoX; - - if (!src) { - return new Handlebars.SafeString(errorAttr); - } - - if (size) { - oneX = src.replace(endOfPath, '-' + size + '.jpg$1'); - twoX = src.replace(endOfPath, '-' + size * 2 + '.jpg$1'); - srcsetAttr = 'srcset="{0} 1x, {1} 2x"'.format(oneX, twoX); - } - - return new Handlebars.SafeString( - 'src="{0}" {1} {2}'.format(oneX, srcsetAttr, errorAttr) - ); -}); - -Handlebars.registerHelper('UrlBase', function() { - return new Handlebars.SafeString(StatusModel.get('urlBase')); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Numbers.js b/src/UI/Handlebars/Helpers/Numbers.js deleted file mode 100644 index 19d7c63ab..000000000 --- a/src/UI/Handlebars/Helpers/Numbers.js +++ /dev/null @@ -1,14 +0,0 @@ -var Handlebars = require('handlebars'); -var FormatHelpers = require('../../Shared/FormatHelpers'); - -Handlebars.registerHelper('Bytes', function(size) { - return new Handlebars.SafeString(FormatHelpers.bytes(size)); -}); - -Handlebars.registerHelper('Pad2', function(input) { - return FormatHelpers.pad(input, 2); -}); - -Handlebars.registerHelper('Number', function(input) { - return FormatHelpers.number(input); -}); diff --git a/src/UI/Handlebars/Helpers/Quality.js b/src/UI/Handlebars/Helpers/Quality.js deleted file mode 100644 index 96b9c840f..000000000 --- a/src/UI/Handlebars/Helpers/Quality.js +++ /dev/null @@ -1,12 +0,0 @@ -var Handlebars = require('handlebars'); -var ProfileCollection = require('../../Profile/ProfileCollection'); - -Handlebars.registerHelper('profile', function(profileId) { - var profile = ProfileCollection.get(profileId); - - if (profile) { - return new Handlebars.SafeString('<span class="label label-default profile-label">' + profile.get('name') + '</span>'); - } - - return undefined; -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/Series.js b/src/UI/Handlebars/Helpers/Series.js deleted file mode 100644 index dc9af4c97..000000000 --- a/src/UI/Handlebars/Helpers/Series.js +++ /dev/null @@ -1,96 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); -var _ = require('underscore'); - -Handlebars.registerHelper('poster', function() { - - var placeholder = StatusModel.get('urlBase') + '/Content/Images/poster-dark.png'; - var poster = _.where(this.images, { coverType : 'poster' }); - - if (poster[0]) { - if (!poster[0].url.match(/^https?:\/\//)) { - return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, poster[0].url, 250))); - } else { - var url = poster[0].url.replace(/^https?\:/, ''); - return new Handlebars.SafeString('<img class="series-poster x-series-poster" {0}>'.format(Handlebars.helpers.defaultImg.call(null, url))); - } - } - - return new Handlebars.SafeString('<img class="series-poster placeholder-image" src="{0}">'.format(placeholder)); -}); - -Handlebars.registerHelper('traktUrl', function() { - return 'http://trakt.tv/search/tvdb/' + this.tvdbId + '?id_type=show'; -}); - -Handlebars.registerHelper('imdbUrl', function() { - return 'http://imdb.com/title/' + this.imdbId; -}); - -Handlebars.registerHelper('tvdbUrl', function() { - return 'http://www.thetvdb.com/?tab=series&id=' + this.tvdbId; -}); - -Handlebars.registerHelper('tvRageUrl', function() { - return 'http://www.tvrage.com/shows/id-' + this.tvRageId; -}); - -Handlebars.registerHelper('tvMazeUrl', function() { - return 'http://www.tvmaze.com/shows/' + this.tvMazeId + '/_'; -}); - -Handlebars.registerHelper('route', function() { - return StatusModel.get('urlBase') + '/series/' + this.titleSlug; -}); - -Handlebars.registerHelper('percentOfEpisodes', function() { - var episodeCount = this.episodeCount; - var episodeFileCount = this.episodeFileCount; - - var percent = 100; - - if (episodeCount > 0) { - percent = episodeFileCount / episodeCount * 100; - } - - return percent; -}); - -Handlebars.registerHelper('seasonCountHelper', function() { - var seasonCount = this.seasonCount; - var continuing = this.status === 'continuing'; - - if (continuing) { - return new Handlebars.SafeString('<span class="label label-info">Season {0}</span>'.format(seasonCount)); - } - - if (seasonCount === 1) { - return new Handlebars.SafeString('<span class="label label-info">{0} Season</span>'.format(seasonCount)); - } - - return new Handlebars.SafeString('<span class="label label-info">{0} Seasons</span>'.format(seasonCount)); -}); - -Handlebars.registerHelper ('truncate', function (str, len) { - if (str && str.length > len && str.length > 0) { - var new_str = str + " "; - new_str = str.substr (0, len); - new_str = str.substr (0, new_str.lastIndexOf(" ")); - new_str = (new_str.length > 0) ? new_str : str.substr (0, len); - - return new Handlebars.SafeString ( new_str +'...' ); - } - return str; -}); - -/*Handlebars.registerHelper('titleWithYear', function() { - if (this.title.endsWith(' ({0})'.format(this.year))) { - return this.title; - } - - if (!this.year) { - return this.title; - } - - return new Handlebars.SafeString('{0} <span class="year">({1})</span>'.format(this.title, this.year)); -});*/ diff --git a/src/UI/Handlebars/Helpers/String.js b/src/UI/Handlebars/Helpers/String.js deleted file mode 100644 index 761f565c0..000000000 --- a/src/UI/Handlebars/Helpers/String.js +++ /dev/null @@ -1,7 +0,0 @@ -var Handlebars = require('handlebars'); - -Handlebars.registerHelper('TitleCase', function(input) { - return new Handlebars.SafeString(input.replace(/\w\S*/g, function(txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - })); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/Helpers/System.js b/src/UI/Handlebars/Helpers/System.js deleted file mode 100644 index 269414666..000000000 --- a/src/UI/Handlebars/Helpers/System.js +++ /dev/null @@ -1,18 +0,0 @@ -var Handlebars = require('handlebars'); -var StatusModel = require('../../System/StatusModel'); - -Handlebars.registerHelper('if_windows', function(options) { - if (StatusModel.get('isWindows')) { - return options.fn(this); - } - - return options.inverse(this); -}); - -Handlebars.registerHelper('if_mono', function(options) { - if (StatusModel.get('isMono')) { - return options.fn(this); - } - - return options.inverse(this); -}); \ No newline at end of file diff --git a/src/UI/Handlebars/backbone.marionette.templates.js b/src/UI/Handlebars/backbone.marionette.templates.js deleted file mode 100644 index 6b47d0253..000000000 --- a/src/UI/Handlebars/backbone.marionette.templates.js +++ /dev/null @@ -1,38 +0,0 @@ -var Handlebars = require('handlebars'); -require('handlebars.helpers'); -require('./Helpers/DateTime'); -require('./Helpers/Html'); -require('./Helpers/Numbers'); -require('./Helpers/Episode'); -//require('./Helpers/Series'); -require('./Helpers/Artist'); -require('./Helpers/Album'); -require('./Helpers/Quality'); -require('./Helpers/System'); -require('./Helpers/EachReverse'); -require('./Helpers/String'); -require('./Handlebars.Debug'); - -module.exports = function() { - this.get = function(templateId) { - var templateKey = templateId.toLowerCase().replace('template', ''); - - var templateFunction = window.T[templateKey]; - - if (!templateFunction) { - throw 'couldn\'t find pre-compiled template ' + templateKey; - } - - return function(data) { - try { - var wrappedTemplate = Handlebars.template.call(Handlebars, templateFunction); - return wrappedTemplate(data); - } - catch (error) { - console.error('template render failed for ' + templateKey + ' ' + error); - console.error(data); - throw error; - } - }; - }; -}; \ No newline at end of file diff --git a/src/UI/Health/HealthCollection.js b/src/UI/Health/HealthCollection.js deleted file mode 100644 index e0935a885..000000000 --- a/src/UI/Health/HealthCollection.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); -var HealthModel = require('./HealthModel'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/health', - model : HealthModel -}); - -var collection = new Collection().bindSignalR(); -collection.fetch(); - -module.exports = collection; \ No newline at end of file diff --git a/src/UI/Health/HealthModel.js b/src/UI/Health/HealthModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Health/HealthModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Health/HealthView.js b/src/UI/Health/HealthView.js deleted file mode 100644 index 61ebd525a..000000000 --- a/src/UI/Health/HealthView.js +++ /dev/null @@ -1,37 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var HealthCollection = require('./HealthCollection'); - -module.exports = Marionette.ItemView.extend({ - tagName : 'span', - - initialize : function() { - this.listenTo(HealthCollection, 'sync', this._healthSync); - HealthCollection.fetch(); - }, - - render : function() { - this.$el.empty(); - - if (HealthCollection.length === 0) { - return this; - } - - var count = HealthCollection.length; - var label = 'label-warning'; - var errors = HealthCollection.some(function(model) { - return model.get('type') === 'error'; - }); - - if (errors) { - label = 'label-danger'; - } - - this.$el.html('<span class="label {0}">{1}</span>'.format(label, count)); - return this; - }, - - _healthSync : function() { - this.render(); - } -}); \ No newline at end of file diff --git a/src/UI/Hotkeys/Hotkeys.js b/src/UI/Hotkeys/Hotkeys.js deleted file mode 100644 index b72a574da..000000000 --- a/src/UI/Hotkeys/Hotkeys.js +++ /dev/null @@ -1,34 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -var HotkeysView = require('./HotkeysView'); - -$(document).on('keypress', function(e) { - if ($(e.target).is('input') || $(e.target).is('textarea')) { - return; - } - - if (e.charCode === 63) { - vent.trigger(vent.Commands.OpenModalCommand, new HotkeysView()); - } -}); - -$(document).on('keydown', function(e) { - if (e.ctrlKey && e.keyCode === 83) { - vent.trigger(vent.Hotkeys.SaveSettings); - e.preventDefault(); - return; - } - - if ($(e.target).is('input') || $(e.target).is('textarea')) { - return; - } - - if (e.ctrlKey || e.metaKey || e.altKey) { - return; - } - - if (e.keyCode === 84) { - vent.trigger(vent.Hotkeys.NavbarSearch); - e.preventDefault(); - } -}); diff --git a/src/UI/Hotkeys/HotkeysView.js b/src/UI/Hotkeys/HotkeysView.js deleted file mode 100644 index ee643fbb2..000000000 --- a/src/UI/Hotkeys/HotkeysView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Hotkeys/HotkeysViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Hotkeys/HotkeysViewTemplate.hbs b/src/UI/Hotkeys/HotkeysViewTemplate.hbs deleted file mode 100644 index bce6e86c8..000000000 --- a/src/UI/Hotkeys/HotkeysViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Keyboard Shortcuts</h3> - </div> - <div class="modal-body hotkeys-modal"> - <div class="row hotkey-group"> - <div class="col-md-12"> - <div class="row"> - <div class="col-md-5 col-md-offset-1"> - <h3>Focus Search Box</h3> - </div> - <div class="col-md-3"> - <kbd class="hotkey">t</kbd> - </div> - </div> - <div class="row"> - <div class="col-md-11 col-md-offset-1"> - Pressing 't' puts the cursor in the search box below the navigation links - </div> - </div> - </div> - </div> - <div class="row hotkey-group"> - <div class="col-md-12"> - <div class="row"> - <div class="col-md-5 col-md-offset-1"> - <h3>Save Settings</h3> - </div> - <div class="col-md-3"> - <kbd class="hotkey">ctrl + s</kbd> - </div> - </div> - <div class="row"> - <div class="col-md-11 col-md-offset-1"> - Pressing ctrl + 's' saves your settings (only in settings) - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Hotkeys/hotkeys.less b/src/UI/Hotkeys/hotkeys.less deleted file mode 100644 index b3213825d..000000000 --- a/src/UI/Hotkeys/hotkeys.less +++ /dev/null @@ -1,23 +0,0 @@ -.hotkeys-modal { - h3 { - margin-top : 0px; - margin-botton : 0px; - } - - .hotkey-group { - &:first-of-type { - margin-top : 0px; - } - - &:last-of-type { - margin-bottom : 0px; - } - - margin-top : 25px; - margin-bottom : 25px; - - .hotkey { - font-size : 22px; - } - } -} \ No newline at end of file diff --git a/src/UI/Instrumentation/ErrorHandler.js b/src/UI/Instrumentation/ErrorHandler.js deleted file mode 100644 index 189173662..000000000 --- a/src/UI/Instrumentation/ErrorHandler.js +++ /dev/null @@ -1,86 +0,0 @@ -var $ = require('jquery'); -var Messenger = require('messenger'); - -window.alert = function(message) { - new Messenger().post(message); -}; - -var addError = function(message) { - $('#errors').append('<div>' + message + '</div>'); -}; - -window.onerror = function(msg, url, line) { - - try { - - var a = document.createElement('a'); - a.href = url; - var filename = a.pathname.split('/').pop(); - - //Suppress Firefox debug errors when console window is closed - if (filename.toLowerCase() === 'markupview.jsm' || filename.toLowerCase() === 'markup-view.js') { - return false; - } - - var messageText = filename + ' : ' + line + '</br>' + msg; - - var message = { - message : messageText, - type : 'error', - hideAfter : 1000, - showCloseButton : true - }; - - new Messenger().post(message); - - addError(message.message); - - } - catch (error) { - console.log('An error occurred while reporting error. ' + error); - console.log(msg); - new Messenger().post('Couldn\'t report JS error. ' + msg); - } - - return false; //don't suppress default alerts and logs. -}; - -$(document).ajaxError(function(event, xmlHttpRequest, ajaxOptions) { - - //don't report 200 error codes - if (xmlHttpRequest.status >= 200 && xmlHttpRequest.status <= 300) { - return undefined; - } - - //don't report aborted requests - if (xmlHttpRequest.statusText === 'abort') { - return undefined; - } - - var message = { - type : 'error', - hideAfter : 1000, - showCloseButton : true - }; - - if (xmlHttpRequest.status === 0 && xmlHttpRequest.readyState === 0) { - return false; - } - - if (xmlHttpRequest.status === 400 && ajaxOptions.isValidatedCall) { - return false; - } - - if (xmlHttpRequest.status === 503) { - message.message = xmlHttpRequest.responseJSON.message; - } else if (xmlHttpRequest.status === 409) { - message.message = xmlHttpRequest.responseJSON.message; - } else { - message.message = '[{0}] {1} : {2}'.format(ajaxOptions.type, xmlHttpRequest.statusText, ajaxOptions.url); - } - - new Messenger().post(message); - addError(message.message); - - return false; -}); \ No newline at end of file diff --git a/src/UI/Instrumentation/StringFormat.js b/src/UI/Instrumentation/StringFormat.js deleted file mode 100644 index 059d25f5e..000000000 --- a/src/UI/Instrumentation/StringFormat.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -String.prototype.format = function() { - var args = arguments; - - return this.replace(/{(\d+)}/g, function(match, number) { - if (typeof args[number] !== 'undefined') { - return args[number]; - } else { - return match; - } - }); -}; \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.backgrid.js b/src/UI/JsLibraries/backbone.backgrid.js deleted file mode 100644 index 6a0af616c..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.js +++ /dev/null @@ -1,2764 +0,0 @@ -/*! - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors <wyuenho@gmail.com> - Licensed under the MIT license. -*/ - -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(module.exports, - require("underscore"), - require("backbone")); - } - // Browser - else if (typeof _ !== "undefined" && - typeof Backbone !== "undefined") { - factory(window, _, Backbone); - } -}(function (root, _, Backbone) { - - "use strict"; -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -// Copyright 2009, 2010 Kristopher Michael Kowal -// https://github.com/kriskowal/es5-shim -// ES5 15.5.4.20 -// http://es5.github.com/#x15.5.4.20 -var ws = "\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003" + - "\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028" + - "\u2029\uFEFF"; -if (!String.prototype.trim || ws.trim()) { - // http://blog.stevenlevithan.com/archives/faster-trim-javascript - // http://perfectionkills.com/whitespace-deviations/ - ws = "[" + ws + "]"; - var trimBeginRegexp = new RegExp("^" + ws + ws + "*"), - trimEndRegexp = new RegExp(ws + ws + "*$"); - String.prototype.trim = function trim() { - if (this === undefined || this === null) { - throw new TypeError("can't convert " + this + " to object"); - } - return String(this) - .replace(trimBeginRegexp, "") - .replace(trimEndRegexp, ""); - }; -} - -function lpad(str, length, padstr) { - var paddingLen = length - (str + '').length; - paddingLen = paddingLen < 0 ? 0 : paddingLen; - var padding = ''; - for (var i = 0; i < paddingLen; i++) { - padding = padding + padstr; - } - return padding + str; -} - -var $ = Backbone.$; - -var Backgrid = root.Backgrid = { - - VERSION: "0.3.0", - - Extension: {}, - - resolveNameToClass: function (name, suffix) { - if (_.isString(name)) { - var key = _.map(name.split('-'), function (e) { - return e.slice(0, 1).toUpperCase() + e.slice(1); - }).join('') + suffix; - var klass = Backgrid[key] || Backgrid.Extension[key]; - if (_.isUndefined(klass)) { - throw new ReferenceError("Class '" + key + "' not found"); - } - return klass; - } - - return name; - }, - - callByNeed: function () { - var value = arguments[0]; - if (!_.isFunction(value)) return value; - - var context = arguments[1]; - var args = [].slice.call(arguments, 2); - return value.apply(context, !!(args + '') ? args : void 0); - } - -}; -_.extend(Backgrid, Backbone.Events); - -/** - Command translates a DOM Event into commands that Backgrid - recognizes. Interested parties can listen on selected Backgrid events that - come with an instance of this class and act on the commands. - - It is also possible to globally rebind the keyboard shortcuts by replacing - the methods in this class' prototype. - - @class Backgrid.Command - @constructor - */ -var Command = Backgrid.Command = function (evt) { - _.extend(this, { - altKey: !!evt.altKey, - "char": evt["char"], - charCode: evt.charCode, - ctrlKey: !!evt.ctrlKey, - key: evt.key, - keyCode: evt.keyCode, - locale: evt.locale, - location: evt.location, - metaKey: !!evt.metaKey, - repeat: !!evt.repeat, - shiftKey: !!evt.shiftKey, - which: evt.which - }); -}; -_.extend(Command.prototype, { - /** - Up Arrow - - @member Backgrid.Command - */ - moveUp: function () { return this.keyCode == 38; }, - /** - Down Arrow - - @member Backgrid.Command - */ - moveDown: function () { return this.keyCode === 40; }, - /** - Shift Tab - - @member Backgrid.Command - */ - moveLeft: function () { return this.shiftKey && this.keyCode === 9; }, - /** - Tab - - @member Backgrid.Command - */ - moveRight: function () { return !this.shiftKey && this.keyCode === 9; }, - /** - Enter - - @member Backgrid.Command - */ - save: function () { return this.keyCode === 13; }, - /** - Esc - - @member Backgrid.Command - */ - cancel: function () { return this.keyCode === 27; }, - /** - None of the above. - - @member Backgrid.Command - */ - passThru: function () { - return !(this.moveUp() || this.moveDown() || this.moveLeft() || - this.moveRight() || this.save() || this.cancel()); - } -}); - - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Just a convenient class for interested parties to subclass. - - The default Cell classes don't require the formatter to be a subclass of - Formatter as long as the fromRaw(rawData) and toRaw(formattedData) methods - are defined. - - @abstract - @class Backgrid.CellFormatter - @constructor -*/ -var CellFormatter = Backgrid.CellFormatter = function () {}; -_.extend(CellFormatter.prototype, { - - /** - Takes a raw value from a model and returns an optionally formatted string - for display. The default implementation simply returns the supplied value - as is without any type conversion. - - @member Backgrid.CellFormatter - @param {*} rawData - @param {Backbone.Model} model Used for more complicated formatting - @return {*} - */ - fromRaw: function (rawData, model) { - return rawData; - }, - - /** - Takes a formatted string, usually from user input, and returns a - appropriately typed value for persistence in the model. - - If the user input is invalid or unable to be converted to a raw value - suitable for persistence in the model, toRaw must return `undefined`. - - @member Backgrid.CellFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {*|undefined} - */ - toRaw: function (formattedData, model) { - return formattedData; - } - -}); - -/** - A floating point number formatter. Doesn't understand scientific notation at - the moment. - - @class Backgrid.NumberFormatter - @extends Backgrid.CellFormatter - @constructor - @throws {RangeError} If decimals < 0 or > 20. -*/ -var NumberFormatter = Backgrid.NumberFormatter = function (options) { - _.extend(this, this.defaults, options || {}); - - if (this.decimals < 0 || this.decimals > 20) { - throw new RangeError("decimals must be between 0 and 20"); - } -}; -NumberFormatter.prototype = new CellFormatter(); -_.extend(NumberFormatter.prototype, { - - /** - @member Backgrid.NumberFormatter - @cfg {Object} options - - @cfg {number} [options.decimals=2] Number of decimals to display. Must be an integer. - - @cfg {string} [options.decimalSeparator='.'] The separator to use when - displaying decimals. - - @cfg {string} [options.orderSeparator=','] The separator to use to - separator thousands. May be an empty string. - */ - defaults: { - decimals: 2, - decimalSeparator: '.', - orderSeparator: ',' - }, - - HUMANIZED_NUM_RE: /(\d)(?=(?:\d{3})+$)/g, - - /** - Takes a floating point number and convert it to a formatted string where - every thousand is separated by `orderSeparator`, with a `decimal` number of - decimals separated by `decimalSeparator`. The number returned is rounded - the usual way. - - @member Backgrid.NumberFormatter - @param {number} number - @param {Backbone.Model} model Used for more complicated formatting - @return {string} - */ - fromRaw: function (number, model) { - if (_.isNull(number) || _.isUndefined(number)) return ''; - - number = number.toFixed(~~this.decimals); - - var parts = number.split('.'); - var integerPart = parts[0]; - var decimalPart = parts[1] ? (this.decimalSeparator || '.') + parts[1] : ''; - - return integerPart.replace(this.HUMANIZED_NUM_RE, '$1' + this.orderSeparator) + decimalPart; - }, - - /** - Takes a string, possibly formatted with `orderSeparator` and/or - `decimalSeparator`, and convert it back to a number. - - @member Backgrid.NumberFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {number|undefined} Undefined if the string cannot be converted to - a number. - */ - toRaw: function (formattedData, model) { - formattedData = formattedData.trim(); - - if (formattedData === '') return null; - - var rawData = ''; - - var thousands = formattedData.split(this.orderSeparator); - for (var i = 0; i < thousands.length; i++) { - rawData += thousands[i]; - } - - var decimalParts = rawData.split(this.decimalSeparator); - rawData = ''; - for (var i = 0; i < decimalParts.length; i++) { - rawData = rawData + decimalParts[i] + '.'; - } - - if (rawData[rawData.length - 1] === '.') { - rawData = rawData.slice(0, rawData.length - 1); - } - - var result = (rawData * 1).toFixed(~~this.decimals) * 1; - if (_.isNumber(result) && !_.isNaN(result)) return result; - } - -}); - -/** - Formatter to converts between various datetime formats. - - This class only understands ISO-8601 formatted datetime strings and UNIX - offset (number of milliseconds since UNIX Epoch). See - Backgrid.Extension.MomentFormatter if you need a much more flexible datetime - formatter. - - @class Backgrid.DatetimeFormatter - @extends Backgrid.CellFormatter - @constructor - @throws {Error} If both `includeDate` and `includeTime` are false. -*/ -var DatetimeFormatter = Backgrid.DatetimeFormatter = function (options) { - _.extend(this, this.defaults, options || {}); - - if (!this.includeDate && !this.includeTime) { - throw new Error("Either includeDate or includeTime must be true"); - } -}; -DatetimeFormatter.prototype = new CellFormatter(); -_.extend(DatetimeFormatter.prototype, { - - /** - @member Backgrid.DatetimeFormatter - - @cfg {Object} options - - @cfg {boolean} [options.includeDate=true] Whether the values include the - date part. - - @cfg {boolean} [options.includeTime=true] Whether the values include the - time part. - - @cfg {boolean} [options.includeMilli=false] If `includeTime` is true, - whether to include the millisecond part, if it exists. - */ - defaults: { - includeDate: true, - includeTime: true, - includeMilli: false - }, - - DATE_RE: /^([+\-]?\d{4})-(\d{2})-(\d{2})$/, - TIME_RE: /^(\d{2}):(\d{2}):(\d{2})(\.(\d{3}))?$/, - ISO_SPLITTER_RE: /T|Z| +/, - - _convert: function (data, validate) { - if ((data + '').trim() === '') return null; - - var date, time = null; - if (_.isNumber(data)) { - var jsDate = new Date(data); - date = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0); - time = lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0); - } - else { - data = data.trim(); - var parts = data.split(this.ISO_SPLITTER_RE) || []; - date = this.DATE_RE.test(parts[0]) ? parts[0] : ''; - time = date && parts[1] ? parts[1] : this.TIME_RE.test(parts[0]) ? parts[0] : ''; - } - - var YYYYMMDD = this.DATE_RE.exec(date) || []; - var HHmmssSSS = this.TIME_RE.exec(time) || []; - - if (validate) { - if (this.includeDate && _.isUndefined(YYYYMMDD[0])) return; - if (this.includeTime && _.isUndefined(HHmmssSSS[0])) return; - if (!this.includeDate && date) return; - if (!this.includeTime && time) return; - } - - var jsDate = new Date(Date.UTC(YYYYMMDD[1] * 1 || 0, - YYYYMMDD[2] * 1 - 1 || 0, - YYYYMMDD[3] * 1 || 0, - HHmmssSSS[1] * 1 || null, - HHmmssSSS[2] * 1 || null, - HHmmssSSS[3] * 1 || null, - HHmmssSSS[5] * 1 || null)); - - var result = ''; - - if (this.includeDate) { - result = lpad(jsDate.getUTCFullYear(), 4, 0) + '-' + lpad(jsDate.getUTCMonth() + 1, 2, 0) + '-' + lpad(jsDate.getUTCDate(), 2, 0); - } - - if (this.includeTime) { - result = result + (this.includeDate ? 'T' : '') + lpad(jsDate.getUTCHours(), 2, 0) + ':' + lpad(jsDate.getUTCMinutes(), 2, 0) + ':' + lpad(jsDate.getUTCSeconds(), 2, 0); - - if (this.includeMilli) { - result = result + '.' + lpad(jsDate.getUTCMilliseconds(), 3, 0); - } - } - - if (this.includeDate && this.includeTime) { - result += "Z"; - } - - return result; - }, - - /** - Converts an ISO-8601 formatted datetime string to a datetime string, date - string or a time string. The timezone is ignored if supplied. - - @member Backgrid.DatetimeFormatter - @param {string} rawData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|null|undefined} ISO-8601 string in UTC. Null and undefined - values are returned as is. - */ - fromRaw: function (rawData, model) { - if (_.isNull(rawData) || _.isUndefined(rawData)) return ''; - return this._convert(rawData); - }, - - /** - Converts an ISO-8601 formatted datetime string to a datetime string, date - string or a time string. The timezone is ignored if supplied. This method - parses the input values exactly the same way as - Backgrid.Extension.MomentFormatter#fromRaw(), in addition to doing some - sanity checks. - - @member Backgrid.DatetimeFormatter - @param {string} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|undefined} ISO-8601 string in UTC. Undefined if a date is - found when `includeDate` is false, or a time is found when `includeTime` is - false, or if `includeDate` is true and a date is not found, or if - `includeTime` is true and a time is not found. - */ - toRaw: function (formattedData, model) { - return this._convert(formattedData, true); - } - -}); - -/** - Formatter to convert any value to string. - - @class Backgrid.StringFormatter - @extends Backgrid.CellFormatter - @constructor - */ -var StringFormatter = Backgrid.StringFormatter = function () {}; -StringFormatter.prototype = new CellFormatter(); -_.extend(StringFormatter.prototype, { - /** - Converts any value to a string using Ecmascript's implicit type - conversion. If the given value is `null` or `undefined`, an empty string is - returned instead. - - @member Backgrid.StringFormatter - @param {*} rawValue - @param {Backbone.Model} model Used for more complicated formatting - @return {string} - */ - fromRaw: function (rawValue, model) { - if (_.isUndefined(rawValue) || _.isNull(rawValue)) return ''; - return rawValue + ''; - } -}); - -/** - Simple email validation formatter. - - @class Backgrid.EmailFormatter - @extends Backgrid.CellFormatter - @constructor - */ -var EmailFormatter = Backgrid.EmailFormatter = function () {}; -EmailFormatter.prototype = new CellFormatter(); -_.extend(EmailFormatter.prototype, { - /** - Return the input if it is a string that contains an '@' character and if - the strings before and after '@' are non-empty. If the input does not - validate, `undefined` is returned. - - @member Backgrid.EmailFormatter - @param {*} formattedData - @param {Backbone.Model} model Used for more complicated formatting - @return {string|undefined} - */ - toRaw: function (formattedData, model) { - var parts = formattedData.trim().split("@"); - if (parts.length === 2 && _.all(parts)) { - return formattedData; - } - } -}); - -/** - Formatter for SelectCell. - - @class Backgrid.SelectFormatter - @extends Backgrid.CellFormatter - @constructor -*/ -var SelectFormatter = Backgrid.SelectFormatter = function () {}; -SelectFormatter.prototype = new CellFormatter(); -_.extend(SelectFormatter.prototype, { - - /** - Normalizes raw scalar or array values to an array. - - @member Backgrid.SelectFormatter - @param {*} rawValue - @param {Backbone.Model} model Used for more complicated formatting - @return {Array.<*>} - */ - fromRaw: function (rawValue, model) { - return _.isArray(rawValue) ? rawValue : rawValue != null ? [rawValue] : []; - } -}); - - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Generic cell editor base class. Only defines an initializer for a number of - required parameters. - - @abstract - @class Backgrid.CellEditor - @extends Backbone.View -*/ -var CellEditor = Backgrid.CellEditor = Backbone.View.extend({ - - /** - Initializer. - - @param {Object} options - @param {Backgrid.CellFormatter} options.formatter - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - - @throws {TypeError} If `formatter` is not a formatter instance, or when - `model` or `column` are undefined. - */ - initialize: function (options) { - this.formatter = options.formatter; - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - this.listenTo(this.model, "backgrid:editing", this.postRender); - }, - - /** - Post-rendering setup and initialization. Focuses the cell editor's `el` in - this default implementation. **Should** be called by Cell classes after - calling Backgrid.CellEditor#render. - */ - postRender: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - this.$el.focus(); - } - return this; - } - -}); - -/** - InputCellEditor the cell editor type used by most core cell types. This cell - editor renders a text input box as its editor. The input will render a - placeholder if the value is empty on supported browsers. - - @class Backgrid.InputCellEditor - @extends Backgrid.CellEditor -*/ -var InputCellEditor = Backgrid.InputCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "input", - - /** @property */ - attributes: { - type: "text" - }, - - /** @property */ - events: { - "blur": "saveOrCancel", - "keydown": "saveOrCancel" - }, - - /** - Initializer. Removes this `el` from the DOM when a `done` event is - triggered. - - @param {Object} options - @param {Backgrid.CellFormatter} options.formatter - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - @param {string} [options.placeholder] - */ - initialize: function (options) { - InputCellEditor.__super__.initialize.apply(this, arguments); - - if (options.placeholder) { - this.$el.attr("placeholder", options.placeholder); - } - }, - - /** - Renders a text input with the cell value formatted for display, if it - exists. - */ - render: function () { - var model = this.model - this.$el.val(this.formatter.fromRaw(model.get(this.column.get("name")), model)); - return this; - }, - - /** - If the key pressed is `enter`, `tab`, `up`, or `down`, converts the value - in the editor to a raw value for saving into the model using the formatter. - - If the key pressed is `esc` the changes are undone. - - If the editor goes out of focus (`blur`) but the value is invalid, the - event is intercepted and cancelled so the cell remains in focus pending for - further action. The changes are saved otherwise. - - Triggers a Backbone `backgrid:edited` event from the model when successful, - and `backgrid:error` if the value cannot be converted. Classes listening to - the `error` event, usually the Cell classes, should respond appropriately, - usually by rendering some kind of error feedback. - - @param {Event} e - */ - saveOrCancel: function (e) { - - var formatter = this.formatter; - var model = this.model; - var column = this.column; - - var command = new Command(e); - var blurred = e.type === "blur"; - - if (command.moveUp() || command.moveDown() || command.moveLeft() || command.moveRight() || - command.save() || blurred) { - - e.preventDefault(); - e.stopPropagation(); - - var val = this.$el.val(); - var newValue = formatter.toRaw(val, model); - if (_.isUndefined(newValue)) { - model.trigger("backgrid:error", model, column, val); - } - else { - model.set(column.get("name"), newValue); - model.trigger("backgrid:edited", model, column, command); - } - } - // esc - else if (command.cancel()) { - // undo - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, command); - } - }, - - postRender: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - // move the cursor to the end on firefox if text is right aligned - if (this.$el.css("text-align") === "right") { - var val = this.$el.val(); - this.$el.focus().val(null).val(val); - } - else this.$el.focus(); - } - return this; - } - -}); - -/** - The super-class for all Cell types. By default, this class renders a plain - table cell with the model value converted to a string using the - formatter. The table cell is clickable, upon which the cell will go into - editor mode, which is rendered by a Backgrid.InputCellEditor instance by - default. Upon encountering any formatting errors, this class will add an - `error` CSS class to the table cell. - - @abstract - @class Backgrid.Cell - @extends Backbone.View -*/ -var Cell = Backgrid.Cell = Backbone.View.extend({ - - /** @property */ - tagName: "td", - - /** - @property {Backgrid.CellFormatter|Object|string} [formatter=CellFormatter] - */ - formatter: CellFormatter, - - /** - @property {Backgrid.CellEditor} [editor=Backgrid.InputCellEditor] The - default editor for all cell instances of this class. This value must be a - class, it will be automatically instantiated upon entering edit mode. - - See Backgrid.CellEditor - */ - editor: InputCellEditor, - - /** @property */ - events: { - "click": "enterEditMode" - }, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - - @throws {ReferenceError} If formatter is a string but a formatter class of - said name cannot be found in the Backgrid module. - */ - initialize: function (options) { - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - var column = this.column, model = this.model, $el = this.$el; - - var formatter = Backgrid.resolveNameToClass(column.get("formatter") || - this.formatter, "Formatter"); - - if (!_.isFunction(formatter.fromRaw) && !_.isFunction(formatter.toRaw)) { - formatter = new formatter(); - } - - this.formatter = formatter; - - this.editor = Backgrid.resolveNameToClass(this.editor, "CellEditor"); - - this.listenTo(model, "change:" + column.get("name"), function () { - if (!$el.hasClass("editor")) this.render(); - }); - - this.listenTo(model, "backgrid:error", this.renderError); - - this.listenTo(column, "change:editable change:sortable change:renderable", - function (column) { - var changed = column.changedAttributes(); - for (var key in changed) { - if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); - } - } - }); - - if (column.get("editable")) $el.addClass("editable"); - if (column.get("sortable")) $el.addClass("sortable"); - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Render a text string in a table cell. The text is converted from the - model's raw value for this cell's column. - */ - render: function () { - this.$el.empty(); - var model = this.model; - this.$el.text(this.formatter.fromRaw(model.get(this.column.get("name")), model)); - this.delegateEvents(); - return this; - }, - - /** - If this column is editable, a new CellEditor instance is instantiated with - its required parameters. An `editor` CSS class is added to the cell upon - entering edit mode. - - This method triggers a Backbone `backgrid:edit` event from the model when - the cell is entering edit mode and an editor instance has been constructed, - but before it is rendered and inserted into the DOM. The cell and the - constructed cell editor instance are sent as event parameters when this - event is triggered. - - When this cell has finished switching to edit mode, a Backbone - `backgrid:editing` event is triggered from the model. The cell and the - constructed cell instance are also sent as parameters in the event. - - When the model triggers a `backgrid:error` event, it means the editor is - unable to convert the current user input to an apprpriate value for the - model's column, and an `error` CSS class is added to the cell accordingly. - */ - enterEditMode: function () { - var model = this.model; - var column = this.column; - - var editable = Backgrid.callByNeed(column.editable(), column, model); - if (editable) { - - this.currentEditor = new this.editor({ - column: this.column, - model: this.model, - formatter: this.formatter - }); - - model.trigger("backgrid:edit", model, column, this, this.currentEditor); - - // Need to redundantly undelegate events for Firefox - this.undelegateEvents(); - this.$el.empty(); - this.$el.append(this.currentEditor.$el); - this.currentEditor.render(); - this.$el.addClass("editor"); - - model.trigger("backgrid:editing", model, column, this, this.currentEditor); - } - }, - - /** - Put an `error` CSS class on the table cell. - */ - renderError: function (model, column) { - if (column == null || column.get("name") == this.column.get("name")) { - this.$el.addClass("error"); - } - }, - - /** - Removes the editor and re-render in display mode. - */ - exitEditMode: function () { - this.$el.removeClass("error"); - this.currentEditor.remove(); - this.stopListening(this.currentEditor); - delete this.currentEditor; - this.$el.removeClass("editor"); - this.render(); - }, - - /** - Clean up this cell. - - @chainable - */ - remove: function () { - if (this.currentEditor) { - this.currentEditor.remove.apply(this.currentEditor, arguments); - delete this.currentEditor; - } - return Cell.__super__.remove.apply(this, arguments); - } - -}); - -/** - StringCell displays HTML escaped strings and accepts anything typed in. - - @class Backgrid.StringCell - @extends Backgrid.Cell -*/ -var StringCell = Backgrid.StringCell = Cell.extend({ - - /** @property */ - className: "string-cell", - - formatter: StringFormatter - -}); - -/** - UriCell renders an HTML `<a>` anchor for the value and accepts URIs as user - input values. No type conversion or URL validation is done by the formatter - of this cell. Users who need URL validation are encourage to subclass UriCell - to take advantage of the parsing capabilities of the HTMLAnchorElement - available on HTML5-capable browsers or using a third-party library like - [URI.js](https://github.com/medialize/URI.js). - - @class Backgrid.UriCell - @extends Backgrid.Cell -*/ -var UriCell = Backgrid.UriCell = Cell.extend({ - - /** @property */ - className: "uri-cell", - - /** - @property {string} [title] The title attribute of the generated anchor. It - uses the display value formatted by the `formatter.fromRaw` by default. - */ - title: null, - - /** - @property {string} [target="_blank"] The target attribute of the generated - anchor. - */ - target: "_blank", - - initialize: function (options) { - UriCell.__super__.initialize.apply(this, arguments); - this.title = options.title || this.title; - this.target = options.target || this.target; - }, - - render: function () { - this.$el.empty(); - var rawValue = this.model.get(this.column.get("name")); - var formattedValue = this.formatter.fromRaw(rawValue, this.model); - this.$el.append($("<a>", { - tabIndex: -1, - href: rawValue, - title: this.title || formattedValue, - target: this.target, - }).text(formattedValue)); - this.delegateEvents(); - return this; - } - -}); - -/** - Like Backgrid.UriCell, EmailCell renders an HTML `<a>` anchor for the - value. The `href` in the anchor is prefixed with `mailto:`. EmailCell will - complain if the user enters a string that doesn't contain the `@` sign. - - @class Backgrid.EmailCell - @extends Backgrid.StringCell -*/ -var EmailCell = Backgrid.EmailCell = StringCell.extend({ - - /** @property */ - className: "email-cell", - - formatter: EmailFormatter, - - render: function () { - this.$el.empty(); - var model = this.model; - var formattedValue = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.append($("<a>", { - tabIndex: -1, - href: "mailto:" + formattedValue, - title: formattedValue - }).text(formattedValue)); - this.delegateEvents(); - return this; - } - -}); - -/** - NumberCell is a generic cell that renders all numbers. Numbers are formatted - using a Backgrid.NumberFormatter. - - @class Backgrid.NumberCell - @extends Backgrid.Cell -*/ -var NumberCell = Backgrid.NumberCell = Cell.extend({ - - /** @property */ - className: "number-cell", - - /** - @property {number} [decimals=2] Must be an integer. - */ - decimals: NumberFormatter.prototype.defaults.decimals, - - /** @property {string} [decimalSeparator='.'] */ - decimalSeparator: NumberFormatter.prototype.defaults.decimalSeparator, - - /** @property {string} [orderSeparator=','] */ - orderSeparator: NumberFormatter.prototype.defaults.orderSeparator, - - /** @property {Backgrid.CellFormatter} [formatter=Backgrid.NumberFormatter] */ - formatter: NumberFormatter, - - /** - Initializes this cell and the number formatter. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - */ - initialize: function (options) { - NumberCell.__super__.initialize.apply(this, arguments); - var formatter = this.formatter; - formatter.decimals = this.decimals; - formatter.decimalSeparator = this.decimalSeparator; - formatter.orderSeparator = this.orderSeparator; - } - -}); - -/** - An IntegerCell is just a Backgrid.NumberCell with 0 decimals. If a floating - point number is supplied, the number is simply rounded the usual way when - displayed. - - @class Backgrid.IntegerCell - @extends Backgrid.NumberCell -*/ -var IntegerCell = Backgrid.IntegerCell = NumberCell.extend({ - - /** @property */ - className: "integer-cell", - - /** - @property {number} decimals Must be an integer. - */ - decimals: 0 -}); - -/** - DatetimeCell is a basic cell that accepts datetime string values in RFC-2822 - or W3C's subset of ISO-8601 and displays them in ISO-8601 format. For a much - more sophisticated date time cell with better datetime formatting, take a - look at the Backgrid.Extension.MomentCell extension. - - @class Backgrid.DatetimeCell - @extends Backgrid.Cell - - See: - - - Backgrid.Extension.MomentCell - - Backgrid.DatetimeFormatter -*/ -var DatetimeCell = Backgrid.DatetimeCell = Cell.extend({ - - /** @property */ - className: "datetime-cell", - - /** - @property {boolean} [includeDate=true] - */ - includeDate: DatetimeFormatter.prototype.defaults.includeDate, - - /** - @property {boolean} [includeTime=true] - */ - includeTime: DatetimeFormatter.prototype.defaults.includeTime, - - /** - @property {boolean} [includeMilli=false] - */ - includeMilli: DatetimeFormatter.prototype.defaults.includeMilli, - - /** @property {Backgrid.CellFormatter} [formatter=Backgrid.DatetimeFormatter] */ - formatter: DatetimeFormatter, - - /** - Initializes this cell and the datetime formatter. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - */ - initialize: function (options) { - DatetimeCell.__super__.initialize.apply(this, arguments); - var formatter = this.formatter; - formatter.includeDate = this.includeDate; - formatter.includeTime = this.includeTime; - formatter.includeMilli = this.includeMilli; - - var placeholder = this.includeDate ? "YYYY-MM-DD" : ""; - placeholder += (this.includeDate && this.includeTime) ? "T" : ""; - placeholder += this.includeTime ? "HH:mm:ss" : ""; - placeholder += (this.includeTime && this.includeMilli) ? ".SSS" : ""; - - this.editor = this.editor.extend({ - attributes: _.extend({}, this.editor.prototype.attributes, this.editor.attributes, { - placeholder: placeholder - }) - }); - } - -}); - -/** - DateCell is a Backgrid.DatetimeCell without the time part. - - @class Backgrid.DateCell - @extends Backgrid.DatetimeCell -*/ -var DateCell = Backgrid.DateCell = DatetimeCell.extend({ - - /** @property */ - className: "date-cell", - - /** @property */ - includeTime: false - -}); - -/** - TimeCell is a Backgrid.DatetimeCell without the date part. - - @class Backgrid.TimeCell - @extends Backgrid.DatetimeCell -*/ -var TimeCell = Backgrid.TimeCell = DatetimeCell.extend({ - - /** @property */ - className: "time-cell", - - /** @property */ - includeDate: false - -}); - -/** - BooleanCellEditor renders a checkbox as its editor. - - @class Backgrid.BooleanCellEditor - @extends Backgrid.CellEditor -*/ -var BooleanCellEditor = Backgrid.BooleanCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "input", - - /** @property */ - attributes: { - tabIndex: -1, - type: "checkbox" - }, - - /** @property */ - events: { - "mousedown": function () { - this.mouseDown = true; - }, - "blur": "enterOrExitEditMode", - "mouseup": function () { - this.mouseDown = false; - }, - "change": "saveOrCancel", - "keydown": "saveOrCancel" - }, - - /** - Renders a checkbox and check it if the model value of this column is true, - uncheck otherwise. - */ - render: function () { - var model = this.model; - var val = this.formatter.fromRaw(model.get(this.column.get("name")), model); - this.$el.prop("checked", val); - return this; - }, - - /** - Event handler. Hack to deal with the case where `blur` is fired before - `change` and `click` on a checkbox. - */ - enterOrExitEditMode: function (e) { - if (!this.mouseDown) { - var model = this.model; - model.trigger("backgrid:edited", model, this.column, new Command(e)); - } - }, - - /** - Event handler. Save the value into the model if the event is `change` or - one of the keyboard navigation key presses. Exit edit mode without saving - if `escape` was pressed. - */ - saveOrCancel: function (e) { - var model = this.model; - var column = this.column; - var formatter = this.formatter; - var command = new Command(e); - // skip ahead to `change` when space is pressed - if (command.passThru() && e.type != "change") return true; - if (command.cancel()) { - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, command); - } - - var $el = this.$el; - if (command.save() || command.moveLeft() || command.moveRight() || command.moveUp() || - command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - var val = formatter.toRaw($el.prop("checked"), model); - model.set(column.get("name"), val); - model.trigger("backgrid:edited", model, column, command); - } - else if (e.type == "change") { - var val = formatter.toRaw($el.prop("checked"), model); - model.set(column.get("name"), val); - $el.focus(); - } - } - -}); - -/** - BooleanCell renders a checkbox both during display mode and edit mode. The - checkbox is checked if the model value is true, unchecked otherwise. - - @class Backgrid.BooleanCell - @extends Backgrid.Cell -*/ -var BooleanCell = Backgrid.BooleanCell = Cell.extend({ - - /** @property */ - className: "boolean-cell", - - /** @property */ - editor: BooleanCellEditor, - - /** @property */ - events: { - "click": "enterEditMode" - }, - - /** - Renders a checkbox and check it if the model value of this column is true, - uncheck otherwise. - */ - render: function () { - this.$el.empty(); - var model = this.model, column = this.column; - var editable = Backgrid.callByNeed(column.editable(), column, model); - this.$el.append($("<input>", { - tabIndex: -1, - type: "checkbox", - checked: this.formatter.fromRaw(model.get(column.get("name")), model), - disabled: !editable - })); - this.delegateEvents(); - return this; - } - -}); - -/** - SelectCellEditor renders an HTML `<select>` fragment as the editor. - - @class Backgrid.SelectCellEditor - @extends Backgrid.CellEditor -*/ -var SelectCellEditor = Backgrid.SelectCellEditor = CellEditor.extend({ - - /** @property */ - tagName: "select", - - /** @property */ - events: { - "change": "save", - "blur": "close", - "keydown": "close" - }, - - /** @property {function(Object, ?Object=): string} template */ - template: _.template('<option value="<%- value %>" <%= selected ? \'selected="selected"\' : "" %>><%- text %></option>', null, {variable: null}), - - setOptionValues: function (optionValues) { - this.optionValues = optionValues; - this.optionValues = _.result(this, "optionValues"); - }, - - setMultiple: function (multiple) { - this.multiple = multiple; - this.$el.prop("multiple", multiple); - }, - - _renderOptions: function (nvps, selectedValues) { - var options = ''; - for (var i = 0; i < nvps.length; i++) { - options = options + this.template({ - text: nvps[i][0], - value: nvps[i][1], - selected: selectedValues.indexOf(nvps[i][1]) > -1 - }); - } - return options; - }, - - /** - Renders the options if `optionValues` is a list of name-value pairs. The - options are contained inside option groups if `optionValues` is a list of - object hashes. The name is rendered at the option text and the value is the - option value. If `optionValues` is a function, it is called without a - parameter. - */ - render: function () { - this.$el.empty(); - - var optionValues = _.result(this, "optionValues"); - var model = this.model; - var selectedValues = this.formatter.fromRaw(model.get(this.column.get("name")), model); - - if (!_.isArray(optionValues)) throw new TypeError("optionValues must be an array"); - - var optionValue = null; - var optionText = null; - var optionValue = null; - var optgroupName = null; - var optgroup = null; - - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; - - if (_.isArray(optionValue)) { - optionText = optionValue[0]; - optionValue = optionValue[1]; - - this.$el.append(this.template({ - text: optionText, - value: optionValue, - selected: selectedValues.indexOf(optionValue) > -1 - })); - } - else if (_.isObject(optionValue)) { - optgroupName = optionValue.name; - optgroup = $("<optgroup></optgroup>", { label: optgroupName }); - optgroup.append(this._renderOptions(optionValue.values, selectedValues)); - this.$el.append(optgroup); - } - else { - throw new TypeError("optionValues elements must be a name-value pair or an object hash of { name: 'optgroup label', value: [option name-value pairs] }"); - } - } - - this.delegateEvents(); - - return this; - }, - - /** - Saves the value of the selected option to the model attribute. Triggers a - `backgrid:edited` Backbone event from the model. - */ - save: function (e) { - var model = this.model; - var column = this.column; - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); - model.trigger("backgrid:edited", model, column, new Command(e)); - }, - - /** - Triggers a `backgrid:edited` event from the model so the body can close - this editor. - */ - close: function (e) { - var model = this.model; - var column = this.column; - var command = new Command(e); - if (command.cancel()) { - e.stopPropagation(); - model.trigger("backgrid:edited", model, column, new Command(e)); - } - else if (command.save() || command.moveLeft() || command.moveRight() || - command.moveUp() || command.moveDown() || e.type == "blur") { - e.preventDefault(); - e.stopPropagation(); - if (e.type == "blur" && this.$el.find("option").length === 1) { - model.set(column.get("name"), this.formatter.toRaw(this.$el.val(), model)); - } - model.trigger("backgrid:edited", model, column, new Command(e)); - } - } - -}); - -/** - SelectCell is also a different kind of cell in that upon going into edit mode - the cell renders a list of options to pick from, as opposed to an input box. - - SelectCell cannot be referenced by its string name when used in a column - definition because it requires an `optionValues` class attribute to be - defined. `optionValues` can either be a list of name-value pairs, to be - rendered as options, or a list of object hashes which consist of a key *name* - which is the option group name, and a key *values* which is a list of - name-value pairs to be rendered as options under that option group. - - In addition, `optionValues` can also be a parameter-less function that - returns one of the above. If the options are static, it is recommended the - returned values to be memoized. `_.memoize()` is a good function to help with - that. - - During display mode, the default formatter will normalize the raw model value - to an array of values whether the raw model value is a scalar or an - array. Each value is compared with the `optionValues` values using - Ecmascript's implicit type conversion rules. When exiting edit mode, no type - conversion is performed when saving into the model. This behavior is not - always desirable when the value type is anything other than string. To - control type conversion on the client-side, you should subclass SelectCell to - provide a custom formatter or provide the formatter to your column - definition. - - See: - [$.fn.val()](http://api.jquery.com/val/) - - @class Backgrid.SelectCell - @extends Backgrid.Cell -*/ -var SelectCell = Backgrid.SelectCell = Cell.extend({ - - /** @property */ - className: "select-cell", - - /** @property */ - editor: SelectCellEditor, - - /** @property */ - multiple: false, - - /** @property */ - formatter: SelectFormatter, - - /** - @property {Array.<Array>|Array.<{name: string, values: Array.<Array>}>} optionValues - */ - optionValues: undefined, - - /** @property */ - delimiter: ', ', - - /** - Initializer. - - @param {Object} options - @param {Backbone.Model} options.model - @param {Backgrid.Column} options.column - - @throws {TypeError} If `optionsValues` is undefined. - */ - initialize: function (options) { - SelectCell.__super__.initialize.apply(this, arguments); - this.listenTo(this.model, "backgrid:edit", function (model, column, cell, editor) { - if (column.get("name") == this.column.get("name")) { - editor.setOptionValues(this.optionValues); - editor.setMultiple(this.multiple); - } - }); - }, - - /** - Renders the label using the raw value as key to look up from `optionValues`. - - @throws {TypeError} If `optionValues` is malformed. - */ - render: function () { - this.$el.empty(); - - var optionValues = _.result(this, "optionValues"); - var model = this.model; - var rawData = this.formatter.fromRaw(model.get(this.column.get("name")), model); - - var selectedText = []; - - try { - if (!_.isArray(optionValues) || _.isEmpty(optionValues)) throw new TypeError; - - for (var k = 0; k < rawData.length; k++) { - var rawDatum = rawData[k]; - - for (var i = 0; i < optionValues.length; i++) { - var optionValue = optionValues[i]; - - if (_.isArray(optionValue)) { - var optionText = optionValue[0]; - var optionValue = optionValue[1]; - - if (optionValue == rawDatum) selectedText.push(optionText); - } - else if (_.isObject(optionValue)) { - var optionGroupValues = optionValue.values; - - for (var j = 0; j < optionGroupValues.length; j++) { - var optionGroupValue = optionGroupValues[j]; - if (optionGroupValue[1] == rawDatum) { - selectedText.push(optionGroupValue[0]); - } - } - } - else { - throw new TypeError; - } - } - } - - this.$el.append(selectedText.join(this.delimiter)); - } - catch (ex) { - if (ex instanceof TypeError) { - throw new TypeError("'optionValues' must be of type {Array.<Array>|Array.<{name: string, values: Array.<Array>}>}"); - } - throw ex; - } - - this.delegateEvents(); - - return this; - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - A Column is a placeholder for column metadata. - - You usually don't need to create an instance of this class yourself as a - collection of column instances will be created for you from a list of column - attributes in the Backgrid.js view class constructors. - - @class Backgrid.Column - @extends Backbone.Model -*/ -var Column = Backgrid.Column = Backbone.Model.extend({ - - /** - @cfg {Object} defaults Column defaults. To override any of these default - values, you can either change the prototype directly to override - Column.defaults globally or extend Column and supply the custom class to - Backgrid.Grid: - - // Override Column defaults globally - Column.prototype.defaults.sortable = false; - - // Override Column defaults locally - var MyColumn = Column.extend({ - defaults: _.defaults({ - editable: false - }, Column.prototype.defaults) - }); - - var grid = new Backgrid.Grid(columns: new Columns([{...}, {...}], { - model: MyColumn - })); - - @cfg {string} [defaults.name] The default name of the model attribute. - - @cfg {string} [defaults.label] The default label to show in the header. - - @cfg {string|Backgrid.Cell} [defaults.cell] The default cell type. If this - is a string, the capitalized form will be used to look up a cell class in - Backbone, i.e.: string => StringCell. If a Cell subclass is supplied, it is - initialized with a hash of parameters. If a Cell instance is supplied, it - is used directly. - - @cfg {string|Backgrid.HeaderCell} [defaults.headerCell] The default header - cell type. - - @cfg {boolean|string} [defaults.sortable=true] Whether this column is - sortable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be sortable. The method's signature must be `function (Backgrid.Column, - Backbone.Model): boolean`. - - @cfg {boolean|string} [defaults.editable=true] Whether this column is - editable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be editable. The method's signature must be `function (Backgrid.Column, - Backbone.Model): boolean`. - - @cfg {boolean|string} [defaults.renderable=true] Whether this column is - renderable. If the value is a string, a method will the same name will be - looked up from the column instance to determine whether the column should - be renderable. The method's signature must be `function (Backrid.Column, - Backbone.Model): boolean`. - - @cfg {Backgrid.CellFormatter | Object | string} [defaults.formatter] The - formatter to use to convert between raw model values and user input. - - @cfg {"toggle"|"cycle"} [defaults.sortType="cycle"] Whether sorting will - toggle between ascending and descending order, or cycle between insertion - order, ascending and descending order. - - @cfg {(function(Backbone.Model, string): *) | string} [defaults.sortValue] - The function to use to extract a value from the model for comparison during - sorting. If this value is a string, a method with the same name will be - looked up from the column instance. - - @cfg {"ascending"|"descending"|null} [defaults.direction=null] The initial - sorting direction for this column. The default is ordered by - Backbone.Model.cid, which usually means the collection is ordered by - insertion order. - */ - defaults: { - name: undefined, - label: undefined, - sortable: true, - editable: true, - renderable: true, - formatter: undefined, - sortType: "cycle", - sortValue: undefined, - direction: null, - cell: undefined, - headerCell: undefined - }, - - /** - Initializes this Column instance. - - @param {Object} attrs - - @param {string} attrs.name The model attribute this column is responsible - for. - - @param {string|Backgrid.Cell} attrs.cell The cell type to use to render - this column. - - @param {string} [attrs.label] - - @param {string|Backgrid.HeaderCell} [attrs.headerCell] - - @param {boolean|string} [attrs.sortable=true] - - @param {boolean|string} [attrs.editable=true] - - @param {boolean|string} [attrs.renderable=true] - - @param {Backgrid.CellFormatter | Object | string} [attrs.formatter] - - @param {"toggle"|"cycle"} [attrs.sortType="cycle"] - - @param {(function(Backbone.Model, string): *) | string} [attrs.sortValue] - - @throws {TypeError} If attrs.cell or attrs.options are not supplied. - - @throws {ReferenceError} If formatter is a string but a formatter class of - said name cannot be found in the Backgrid module. - - See: - - - Backgrid.Column.defaults - - Backgrid.Cell - - Backgrid.CellFormatter - */ - initialize: function (attrs) { - if (!this.has("label")) { - this.set({ label: this.get("name") }, { silent: true }); - } - - var headerCell = Backgrid.resolveNameToClass(this.get("headerCell"), "HeaderCell"); - - var cell = Backgrid.resolveNameToClass(this.get("cell"), "Cell"); - - this.set({cell: cell, headerCell: headerCell}, { silent: true }); - }, - - /** - Returns an appropriate value extraction function from a model for sorting. - - If the column model contains an attribute `sortValue`, if it is a string, a - method from the column instance identifified by the `sortValue` string is - returned. If it is a function, it it returned as is. If `sortValue` isn't - found from the column model's attributes, a default value extraction - function is returned which will compare according to the natural order of - the value's type. - - @return {function(Backbone.Model, string): *} - */ - sortValue: function () { - var sortValue = this.get("sortValue"); - if (_.isString(sortValue)) return this[sortValue]; - else if (_.isFunction(sortValue)) return sortValue; - - return function (model, colName) { - return model.get(colName); - }; - } - - /** - @member Backgrid.Column - @protected - @method sortable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ - - /** - @member Backgrid.Column - @protected - @method editable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ - - /** - @member Backgrid.Column - @protected - @method renderable - @return {function(Backgrid.Column, Backbone.Model): boolean | boolean} - */ -}); - -_.each(["sortable", "renderable", "editable"], function (key) { - Column.prototype[key] = function () { - var value = this.get(key); - if (_.isString(value)) return this[value]; - return !!value; - }; -}); - -/** - A Backbone collection of Column instances. - - @class Backgrid.Columns - @extends Backbone.Collection - */ -var Columns = Backgrid.Columns = Backbone.Collection.extend({ - - /** - @property {Backgrid.Column} model - */ - model: Column -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Row is a simple container view that takes a model instance and a list of - column metadata describing how each of the model's attribute is to be - rendered, and apply the appropriate cell to each attribute. - - @class Backgrid.Row - @extends Backbone.View -*/ -var Row = Backgrid.Row = Backbone.View.extend({ - - /** @property */ - tagName: "tr", - - /** - Initializes a row view instance. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Model} options.model The model instance to render. - - @throws {TypeError} If options.columns or options.model is undefined. - */ - initialize: function (options) { - - var columns = this.columns = options.columns; - if (!(columns instanceof Backbone.Collection)) { - columns = this.columns = new Columns(columns); - } - - var cells = this.cells = []; - for (var i = 0; i < columns.length; i++) { - cells.push(this.makeCell(columns.at(i), options)); - } - - this.listenTo(columns, "add", function (column, columns) { - var i = columns.indexOf(column); - var cell = this.makeCell(column, options); - cells.splice(i, 0, cell); - - var $el = this.$el; - if (i === 0) { - $el.prepend(cell.render().$el); - } - else if (i === columns.length - 1) { - $el.append(cell.render().$el); - } - else { - $el.children().eq(i).before(cell.render().$el); - } - }); - - this.listenTo(columns, "remove", function (column, columns, opts) { - cells[opts.index].remove(); - cells.splice(opts.index, 1); - }); - }, - - /** - Factory method for making a cell. Used by #initialize internally. Override - this to provide an appropriate cell instance for a custom Row subclass. - - @protected - - @param {Backgrid.Column} column - @param {Object} options The options passed to #initialize. - - @return {Backgrid.Cell} - */ - makeCell: function (column) { - return new (column.get("cell"))({ - column: column, - model: this.model - }); - }, - - /** - Renders a row of cells for this row's model. - */ - render: function () { - this.$el.empty(); - - var fragment = document.createDocumentFragment(); - for (var i = 0; i < this.cells.length; i++) { - fragment.appendChild(this.cells[i].render().el); - } - - this.el.appendChild(fragment); - - this.delegateEvents(); - - return this; - }, - - /** - Clean up this row and its cells. - - @chainable - */ - remove: function () { - for (var i = 0; i < this.cells.length; i++) { - var cell = this.cells[i]; - cell.remove.apply(cell, arguments); - } - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); - -/** - EmptyRow is a simple container view that takes a list of column and render a - row with a single column. - - @class Backgrid.EmptyRow - @extends Backbone.View -*/ -var EmptyRow = Backgrid.EmptyRow = Backbone.View.extend({ - - /** @property */ - tagName: "tr", - - /** @property */ - emptyText: null, - - /** - Initializer. - - @param {Object} options - @param {string} options.emptyText - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - */ - initialize: function (options) { - this.emptyText = options.emptyText; - this.columns = options.columns; - }, - - /** - Renders an empty row. - */ - render: function () { - this.$el.empty(); - - var td = document.createElement("td"); - td.setAttribute("colspan", this.columns.length); - td.textContent = this.emptyText; - - this.el.setAttribute("class", "empty"); - this.el.appendChild(td); - - return this; - } -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - HeaderCell is a special cell class that renders a column header cell. If the - column is sortable, a sorter is also rendered and will trigger a table - refresh after sorting. - - @class Backgrid.HeaderCell - @extends Backbone.View - */ -var HeaderCell = Backgrid.HeaderCell = Backbone.View.extend({ - - /** @property */ - tagName: "th", - - /** @property */ - events: { - "click a": "onClick" - }, - - /** - Initializer. - - @param {Object} options - @param {Backgrid.Column|Object} options.column - - @throws {TypeError} If options.column or options.collection is undefined. - */ - initialize: function (options) { - this.column = options.column; - if (!(this.column instanceof Column)) { - this.column = new Column(this.column); - } - - this.listenTo(this.collection, "backgrid:sort", this._resetCellDirection); - - var column = this.column, $el = this.$el; - - this.listenTo(column, "change:editable change:sortable change:renderable", - function (column) { - var changed = column.changedAttributes(); - for (var key in changed) { - if (changed.hasOwnProperty(key)) { - $el.toggleClass(key, changed[key]); - } - } - }); - - this.listenTo(column, "change:name change:label", this.render); - - if (column.get("editable")) $el.addClass("editable"); - if (column.get("sortable")) $el.addClass("sortable"); - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Gets or sets the direction of this cell. If called directly without - parameters, returns the current direction of this cell, otherwise sets - it. If a `null` is given, sets this cell back to the default order. - - @param {null|"ascending"|"descending"} dir - @return {null|string} The current direction or the changed direction. - */ - direction: function (dir) { - if (arguments.length) { - var direction = this.column.get('direction'); - if (direction) this.$el.removeClass(direction); - if (dir) this.$el.addClass(dir); - this.column.set('direction', dir) - } - - return this.column.get('direction'); - }, - - /** - Event handler for the Backbone `backgrid:sort` event. Resets this cell's - direction to default if sorting is being done on another column. - - @private - */ - _resetCellDirection: function (columnToSort, direction) { - if (columnToSort !== this.column) this.direction(null); - else this.direction(direction); - }, - - /** - Event handler for the `click` event on the cell's anchor. If the column is - sortable, clicking on the anchor will cycle through 3 sorting orderings - - `ascending`, `descending`, and default. - */ - onClick: function (e) { - e.preventDefault(); - - var collection = this.collection, event = "backgrid:sort"; - - function cycleSort(header, col) { - if (header.direction() === "ascending") collection.trigger(event, col, "descending"); - else if (header.direction() === "descending") collection.trigger(event, col, null); - else collection.trigger(event, col, "ascending"); - } - - function toggleSort(header, col) { - if (header.direction() === "ascending") collection.trigger(event, col, "descending"); - else collection.trigger(event, col, "ascending"); - } - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - if (sortable) { - var sortType = column.get("sortType"); - if (sortType === "toggle") toggleSort(this, column); - else cycleSort(this, column); - } - }, - - /** - Renders a header cell with a sorter, a label, and a class name for this - column. - */ - render: function () { - this.$el.empty(); - var column = this.column; - var $label = $("<a>").text(column.get("label")); - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - if (sortable) $label.append("<b class='sort-caret'></b>"); - this.$el.append($label); - this.$el.addClass(column.get("name")); - this.delegateEvents(); - this.direction(column.get("direction")); - return this; -} - -}); - -/** - HeaderRow is a controller for a row of header cells. - - @class Backgrid.HeaderRow - @extends Backgrid.Row - */ -var HeaderRow = Backgrid.HeaderRow = Backgrid.Row.extend({ - - requiredOptions: ["columns", "collection"], - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - @param {Backgrid.HeaderCell} [options.headerCell] Customized default - HeaderCell for all the columns. Supply a HeaderCell class or instance to a - the `headerCell` key in a column definition for column-specific header - rendering. - - @throws {TypeError} If options.columns or options.collection is undefined. - */ - initialize: function () { - Backgrid.Row.prototype.initialize.apply(this, arguments); - }, - - makeCell: function (column, options) { - var headerCell = column.get("headerCell") || options.headerCell || HeaderCell; - headerCell = new headerCell({ - column: column, - collection: this.collection - }); - return headerCell; - } - -}); - -/** - Header is a special structural view class that renders a table head with a - single row of header cells. - - @class Backgrid.Header - @extends Backbone.View - */ -var Header = Backgrid.Header = Backbone.View.extend({ - - /** @property */ - tagName: "thead", - - /** - Initializer. Initializes this table head view to contain a single header - row view. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Model} options.model The model instance to render. - - @throws {TypeError} If options.columns or options.model is undefined. - */ - initialize: function (options) { - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Columns(this.columns); - } - - this.row = new Backgrid.HeaderRow({ - columns: this.columns, - collection: this.collection - }); - }, - - /** - Renders this table head with a single row of header cells. - */ - render: function () { - this.$el.append(this.row.render().$el); - this.delegateEvents(); - return this; - }, - - /** - Clean up this header and its row. - - @chainable - */ - remove: function () { - this.row.remove.apply(this.row, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Body is the table body which contains the rows inside a table. Body is - responsible for refreshing the rows after sorting, insertion and removal. - - @class Backgrid.Body - @extends Backbone.View -*/ -var Body = Backgrid.Body = Backbone.View.extend({ - - /** @property */ - tagName: "tbody", - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - Column metadata. - @param {Backgrid.Row} [options.row=Backgrid.Row] The Row class to use. - @param {string} [options.emptyText] The text to display in the empty row. - - @throws {TypeError} If options.columns or options.collection is undefined. - - See Backgrid.Row. - */ - initialize: function (options) { - - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Columns(this.columns); - } - - this.row = options.row || Row; - this.rows = this.collection.map(function (model) { - var row = new this.row({ - columns: this.columns, - model: model - }); - - return row; - }, this); - - this.emptyText = options.emptyText; - this._unshiftEmptyRowMayBe(); - - var collection = this.collection; - this.listenTo(collection, "add", this.insertRow); - this.listenTo(collection, "remove", this.removeRow); - this.listenTo(collection, "sort", this.refresh); - this.listenTo(collection, "reset", this.refresh); - this.listenTo(collection, "backgrid:sort", this.sort); - this.listenTo(collection, "backgrid:edited", this.moveToNextCell); - }, - - _unshiftEmptyRowMayBe: function () { - if (this.rows.length === 0 && this.emptyText != null) { - this.rows.unshift(new EmptyRow({ - emptyText: this.emptyText, - columns: this.columns - })); - } - }, - - /** - This method can be called either directly or as a callback to a - [Backbone.Collecton#add](http://backbonejs.org/#Collection-add) event. - - When called directly, it accepts a model or an array of models and an - option hash just like - [Backbone.Collection#add](http://backbonejs.org/#Collection-add) and - delegates to it. Once the model is added, a new row is inserted into the - body and automatically rendered. - - When called as a callback of an `add` event, splices a new row into the - body and renders it. - - @param {Backbone.Model} model The model to render as a row. - @param {Backbone.Collection} collection When called directly, this - parameter is actually the options to - [Backbone.Collection#add](http://backbonejs.org/#Collection-add). - @param {Object} options When called directly, this must be null. - - See: - - - [Backbone.Collection#add](http://backbonejs.org/#Collection-add) - */ - insertRow: function (model, collection, options) { - - if (this.rows[0] instanceof EmptyRow) this.rows.pop().remove(); - - // insertRow() is called directly - if (!(collection instanceof Backbone.Collection) && !options) { - this.collection.add(model, (options = collection)); - return; - } - - options = _.extend({render: true}, options || {}); - - var row = new this.row({ - columns: this.columns, - model: model - }); - - var index = collection.indexOf(model); - this.rows.splice(index, 0, row); - - var $el = this.$el; - var $children = $el.children(); - var $rowEl = row.render().$el; - - if (options.render) { - if (index >= $children.length) { - $el.append($rowEl); - } - else { - $children.eq(index).before($rowEl); - } - } - - return this; - }, - - /** - The method can be called either directly or as a callback to a - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) - event. - - When called directly, it accepts a model or an array of models and an - option hash just like - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) and - delegates to it. Once the model is removed, a corresponding row is removed - from the body. - - When called as a callback of a `remove` event, splices into the rows and - removes the row responsible for rendering the model. - - @param {Backbone.Model} model The model to remove from the body. - @param {Backbone.Collection} collection When called directly, this - parameter is actually the options to - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove). - @param {Object} options When called directly, this must be null. - - See: - - - [Backbone.Collection#remove](http://backbonejs.org/#Collection-remove) - */ - removeRow: function (model, collection, options) { - - // removeRow() is called directly - if (!options) { - this.collection.remove(model, (options = collection)); - this._unshiftEmptyRowMayBe(); - return; - } - - if (_.isUndefined(options.render) || options.render) { - this.rows[options.index].remove(); - } - - this.rows.splice(options.index, 1); - this._unshiftEmptyRowMayBe(); - - return this; - }, - - /** - Reinitialize all the rows inside the body and re-render them. Triggers a - Backbone `backgrid:refresh` event from the collection along with the body - instance as its sole parameter when done. - */ - refresh: function () { - for (var i = 0; i < this.rows.length; i++) { - this.rows[i].remove(); - } - - this.rows = this.collection.map(function (model) { - var row = new this.row({ - columns: this.columns, - model: model - }); - - return row; - }, this); - this._unshiftEmptyRowMayBe(); - - this.render(); - - this.collection.trigger("backgrid:refresh", this); - - return this; - }, - - /** - Renders all the rows inside this body. If the collection is empty and - `options.emptyText` is defined and not null in the constructor, an empty - row is rendered, otherwise no row is rendered. - */ - render: function () { - this.$el.empty(); - - var fragment = document.createDocumentFragment(); - for (var i = 0; i < this.rows.length; i++) { - var row = this.rows[i]; - fragment.appendChild(row.render().el); - } - - this.el.appendChild(fragment); - - this.delegateEvents(); - - return this; - }, - - /** - Clean up this body and it's rows. - - @chainable - */ - remove: function () { - for (var i = 0; i < this.rows.length; i++) { - var row = this.rows[i]; - row.remove.apply(row, arguments); - } - return Backbone.View.prototype.remove.apply(this, arguments); - }, - - /** - If the underlying collection is a Backbone.PageableCollection in - server-mode or infinite-mode, a page of models is fetched after sorting is - done on the server. - - If the underlying collection is a Backbone.PageableCollection in - client-mode, or any - [Backbone.Collection](http://backbonejs.org/#Collection) instance, sorting - is done on the client side. If the collection is an instance of a - Backbone.PageableCollection, sorting will be done globally on all the pages - and the current page will then be returned. - - Triggers a Backbone `backgrid:sort` event from the collection when done - with the column, direction, comparator and a reference to the collection. - - @param {Backgrid.Column} column - @param {null|"ascending"|"descending"} direction - - See [Backbone.Collection#comparator](http://backbonejs.org/#Collection-comparator) - */ - sort: function (column, direction) { - - if (_.isString(column)) column = this.columns.findWhere({name: column}); - - var collection = this.collection; - - var order; - if (direction === "ascending") order = -1; - else if (direction === "descending") order = 1; - else order = null; - - var comparator = this.makeComparator(column.get("name"), order, - order ? - column.sortValue() : - function (model) { - return model.cid; - }); - - if (Backbone.PageableCollection && - collection instanceof Backbone.PageableCollection) { - - collection.setSorting(order && column.get("name"), order, - {sortValue: column.sortValue()}); - - if (collection.mode == "client") { - if (collection.fullCollection.comparator == null) { - collection.fullCollection.comparator = comparator; - } - collection.fullCollection.sort(); - } - else collection.fetch({reset: true}); - } - else { - collection.comparator = comparator; - collection.sort(); - } - - return this; - }, - - makeComparator: function (attr, order, func) { - - return function (left, right) { - // extract the values from the models - var l = func(left, attr), r = func(right, attr), t; - - // if descending order, swap left and right - if (order === 1) t = l, l = r, r = t; - - // compare as usual - if (l === r) return 0; - else if (l < r) return -1; - return 1; - }; - }, - - /** - Moves focus to the next renderable and editable cell and return the - currently editing cell to display mode. - - @param {Backbone.Model} model The originating model - @param {Backgrid.Column} column The originating model column - @param {Backgrid.Command} command The Command object constructed from a DOM - Event - */ - moveToNextCell: function (model, column, command) { - var i = this.collection.indexOf(model); - var j = this.columns.indexOf(column); - var cell, renderable, editable; - - this.rows[i].cells[j].exitEditMode(); - - if (command.moveUp() || command.moveDown() || command.moveLeft() || - command.moveRight() || command.save()) { - var l = this.columns.length; - var maxOffset = l * this.collection.length; - - if (command.moveUp() || command.moveDown()) { - var row = this.rows[i + (command.moveUp() ? -1 : 1)]; - if (row) { - cell = row.cells[j]; - if (Backgrid.callByNeed(cell.column.editable(), cell.column, model)) { - cell.enterEditMode(); - } - } - } - else if (command.moveLeft() || command.moveRight()) { - var right = command.moveRight(); - for (var offset = i * l + j + (right ? 1 : -1); - offset >= 0 && offset < maxOffset; - right ? offset++ : offset--) { - var m = ~~(offset / l); - var n = offset - m * l; - cell = this.rows[m].cells[n]; - renderable = Backgrid.callByNeed(cell.column.renderable(), cell.column, cell.model); - editable = Backgrid.callByNeed(cell.column.editable(), cell.column, model); - if (renderable && editable) { - cell.enterEditMode(); - break; - } - } - } - } - - return this; - } -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - A Footer is a generic class that only defines a default tag `tfoot` and - number of required parameters in the initializer. - - @abstract - @class Backgrid.Footer - @extends Backbone.View - */ -var Footer = Backgrid.Footer = Backbone.View.extend({ - - /** @property */ - tagName: "tfoot", - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Column>|Array.<Backgrid.Column>|Array.<Object>} options.columns - Column metadata. - @param {Backbone.Collection} options.collection - - @throws {TypeError} If options.columns or options.collection is undefined. - */ - initialize: function (options) { - this.columns = options.columns; - if (!(this.columns instanceof Backbone.Collection)) { - this.columns = new Backgrid.Columns(this.columns); - } - } - -}); - -/* - backgrid - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT license. -*/ - -/** - Grid represents a data grid that has a header, body and an optional footer. - - By default, a Grid treats each model in a collection as a row, and each - attribute in a model as a column. To render a grid you must provide a list of - column metadata and a collection to the Grid constructor. Just like any - Backbone.View class, the grid is rendered as a DOM node fragment when you - call render(). - - var grid = Backgrid.Grid({ - columns: [{ name: "id", label: "ID", type: "string" }, - // ... - ], - collections: books - }); - - $("#table-container").append(grid.render().el); - - Optionally, if you want to customize the rendering of the grid's header and - footer, you may choose to extend Backgrid.Header and Backgrid.Footer, and - then supply that class or an instance of that class to the Grid constructor. - See the documentation for Header and Footer for further details. - - var grid = Backgrid.Grid({ - columns: [{ name: "id", label: "ID", type: "string" }], - collections: books, - header: Backgrid.Header.extend({ - //... - }), - footer: Backgrid.Paginator - }); - - Finally, if you want to override how the rows are rendered in the table body, - you can supply a Body subclass as the `body` attribute that uses a different - Row class. - - @class Backgrid.Grid - @extends Backbone.View - - See: - - - Backgrid.Column - - Backgrid.Header - - Backgrid.Body - - Backgrid.Row - - Backgrid.Footer -*/ -var Grid = Backgrid.Grid = Backbone.View.extend({ - - /** @property */ - tagName: "table", - - /** @property */ - className: "backgrid", - - /** @property */ - header: Header, - - /** @property */ - body: Body, - - /** @property */ - footer: null, - - /** - Initializes a Grid instance. - - @param {Object} options - @param {Backbone.Collection.<Backgrid.Columns>|Array.<Backgrid.Column>|Array.<Object>} options.columns Column metadata. - @param {Backbone.Collection} options.collection The collection of tabular model data to display. - @param {Backgrid.Header} [options.header=Backgrid.Header] An optional Header class to override the default. - @param {Backgrid.Body} [options.body=Backgrid.Body] An optional Body class to override the default. - @param {Backgrid.Row} [options.row=Backgrid.Row] An optional Row class to override the default. - @param {Backgrid.Footer} [options.footer=Backgrid.Footer] An optional Footer class. - */ - initialize: function (options) { - // Convert the list of column objects here first so the subviews don't have - // to. - if (!(options.columns instanceof Backbone.Collection)) { - options.columns = new Columns(options.columns); - } - this.columns = options.columns; - - var filteredOptions = _.omit(options, ["el", "id", "attributes", - "className", "tagName", "events"]); - - // must construct body first so it listens to backgrid:sort first - this.body = options.body || this.body; - this.body = new this.body(filteredOptions); - - this.header = options.header || this.header; - if (this.header) { - this.header = new this.header(filteredOptions); - } - - this.footer = options.footer || this.footer; - if (this.footer) { - this.footer = new this.footer(filteredOptions); - } - - this.listenTo(this.columns, "reset", function () { - if (this.header) { - this.header = new (this.header.remove().constructor)(filteredOptions); - } - this.body = new (this.body.remove().constructor)(filteredOptions); - if (this.footer) { - this.footer = new (this.footer.remove().constructor)(filteredOptions); - } - this.render(); - }); - }, - - /** - Delegates to Backgrid.Body#insertRow. - */ - insertRow: function (model, collection, options) { - this.body.insertRow(model, collection, options); - return this; - }, - - /** - Delegates to Backgrid.Body#removeRow. - */ - removeRow: function (model, collection, options) { - this.body.removeRow(model, collection, options); - return this; - }, - - /** - Delegates to Backgrid.Columns#add for adding a column. Subviews can listen - to the `add` event from their internal `columns` if rerendering needs to - happen. - - @param {Object} [options] Options for `Backgrid.Columns#add`. - @param {boolean} [options.render=true] Whether to render the column - immediately after insertion. - */ - insertColumn: function (column, options) { - options = options || {render: true}; - this.columns.add(column, options); - return this; - }, - - /** - Delegates to Backgrid.Columns#remove for removing a column. Subviews can - listen to the `remove` event from the internal `columns` if rerendering - needs to happen. - - @param {Object} [options] Options for `Backgrid.Columns#remove`. - */ - removeColumn: function (column, options) { - this.columns.remove(column, options); - return this; - }, - - /** - Delegates to Backgrid.Body#sort. - */ - sort: function () { - this.body.sort(arguments); - return this; - }, - - /** - Renders the grid's header, then footer, then finally the body. Triggers a - Backbone `backgrid:rendered` event along with a reference to the grid when - the it has successfully been rendered. - */ - render: function () { - this.$el.empty(); - - if (this.header) { - this.$el.append(this.header.render().$el); - } - - if (this.footer) { - this.$el.append(this.footer.render().$el); - } - - this.$el.append(this.body.render().$el); - - this.delegateEvents(); - - this.trigger("backgrid:rendered", this); - - return this; - }, - - /** - Clean up this grid and its subviews. - - @chainable - */ - remove: function () { - this.header && this.header.remove.apply(this.header, arguments); - this.body.remove.apply(this.body, arguments); - this.footer && this.footer.remove.apply(this.footer, arguments); - return Backbone.View.prototype.remove.apply(this, arguments); - } - -}); -return Backgrid; -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.backgrid.paginator.js b/src/UI/JsLibraries/backbone.backgrid.paginator.js deleted file mode 100644 index 03255f84d..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.paginator.js +++ /dev/null @@ -1,352 +0,0 @@ -/* - backgrid-paginator - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("underscore"), - require("backbone"), - require("backgrid"), - require("backbone-pageable")); - } - // Browser - else if (typeof _ !== "undefined" && - typeof Backbone !== "undefined" && - typeof Backgrid !== "undefined") { - factory(_, Backbone, Backgrid); - } - -}(function (_, Backbone, Backgrid) { - - "use strict"; - - /** - PageHandle is a class that renders the actual page handles and reacts to - click events for pagination. - - This class acts in two modes - control or discrete page handle modes. If - one of the `is*` flags is `true`, an instance of this class is under - control page handle mode. Setting a `pageIndex` to an instance of this - class under control mode has no effect and the correct page index will - always be inferred from the `is*` flag. Only one of the `is*` flags should - be set to `true` at a time. For example, an instance of this class cannot - simultaneously be a rewind control and a fast forward control. A `label` - and a `title` template or a string are required to be passed to the - constuctor under this mode. If a `title` template is provided, it __MUST__ - accept a parameter `label`. When the `label` is provided to the `title` - template function, its result will be used to render the generated anchor's - title attribute. - - If all of the `is*` flags is set to `false`, which is the default, an - instance of this class will be in discrete page handle mode. An instance - under this mode requires the `pageIndex` to be passed from the constructor - as an option and it __MUST__ be a 0-based index of the list of page numbers - to render. The constuctor will normalize the base to the same base the - underlying PageableCollection collection instance uses. A `label` is not - required under this mode, which will default to the equivalent 1-based page - index calculated from `pageIndex` and the underlying PageableCollection - instance. A provided `label` will still be honored however. The `title` - parameter is also not required under this mode, in which case the default - `title` template will be used. You are encouraged to provide your own - `title` template however if you wish to localize the title strings. - - If this page handle represents the current page, an `active` class will be - placed on the root list element. - - if this page handle is at the border of the list of pages, a `disabled` - class will be placed on the root list element. - - Only page handles that are neither `active` nor `disabled` will respond to - click events and triggers pagination. - - @class Backgrid.Extension.PageHandle - */ - var PageHandle = Backgrid.Extension.PageHandle = Backbone.View.extend({ - - /** @property */ - tagName: "li", - - /** @property */ - events: { - "click a": "changePage" - }, - - /** - @property {string|function(Object.<string, string>): string} title - The title to use for the `title` attribute of the generated page handle - anchor elements. It can be a string or an Underscore template function - that takes a mandatory `label` parameter. - */ - title: _.template('Page <%- label %>', null, {variable: null}), - - /** - @property {boolean} isRewind Whether this handle represents a rewind - control - */ - isRewind: false, - - /** - @property {boolean} isBack Whether this handle represents a back - control - */ - isBack: false, - - /** - @property {boolean} isForward Whether this handle represents a forward - control - */ - isForward: false, - - /** - @property {boolean} isFastForward Whether this handle represents a fast - forward control - */ - isFastForward: false, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {number} pageIndex 0-based index of the page number this handle - handles. This parameter will be normalized to the base the underlying - PageableCollection uses. - @param {string} [options.label] If provided it is used to render the - anchor text, otherwise the normalized pageIndex will be used - instead. Required if any of the `is*` flags is set to `true`. - @param {string} [options.title] - @param {boolean} [options.isRewind=false] - @param {boolean} [options.isBack=false] - @param {boolean} [options.isForward=false] - @param {boolean} [options.isFastForward=false] - */ - initialize: function (options) { - Backbone.View.prototype.initialize.apply(this, arguments); - - var collection = this.collection; - var state = collection.state; - var currentPage = state.currentPage; - var firstPage = state.firstPage; - var lastPage = state.lastPage; - - _.extend(this, _.pick(options, - ["isRewind", "isBack", "isForward", "isFastForward"])); - - var pageIndex; - if (this.isRewind) pageIndex = firstPage; - else if (this.isBack) pageIndex = Math.max(firstPage, currentPage - 1); - else if (this.isForward) pageIndex = Math.min(lastPage, currentPage + 1); - else if (this.isFastForward) pageIndex = lastPage; - else { - pageIndex = +options.pageIndex; - pageIndex = (firstPage ? pageIndex + 1 : pageIndex); - } - this.pageIndex = pageIndex; - - if (((this.isRewind || this.isBack) && currentPage == firstPage) || - ((this.isForward || this.isFastForward) && currentPage == lastPage)) { - this.$el.addClass("disabled"); - } - else if (!(this.isRewind || - this.isBack || - this.isForward || - this.isFastForward) && - currentPage == pageIndex) { - this.$el.addClass("active"); - } - - this.label = (options.label || (firstPage ? pageIndex : pageIndex + 1)) + ''; - var title = options.title || this.title; - this.title = _.isFunction(title) ? title({label: this.label}) : title; - }, - - /** - Renders a clickable anchor element under a list item. - */ - render: function () { - this.$el.empty(); - var anchor = document.createElement("a"); - anchor.href = '#'; - if (this.title) anchor.title = this.title; - anchor.innerHTML = this.label; - this.el.appendChild(anchor); - this.delegateEvents(); - return this; - }, - - /** - jQuery click event handler. Goes to the page this PageHandle instance - represents. No-op if this page handle is currently active or disabled. - */ - changePage: function (e) { - e.preventDefault(); - var $el = this.$el; - if (!$el.hasClass("active") && !$el.hasClass("disabled")) { - this.collection.getPage(this.pageIndex); - } - return this; - } - - }); - - /** - Paginator is a Backgrid extension that renders a series of configurable - pagination handles. This extension is best used for splitting a large data - set across multiple pages. If the number of pages is larger then a - threshold, which is set to 10 by default, the page handles are rendered - within a sliding window, plus the rewind, back, forward and fast forward - control handles. The individual control handles can be turned off. - - @class Backgrid.Extension.Paginator - */ - Backgrid.Extension.Paginator = Backbone.View.extend({ - - /** @property */ - className: "backgrid-paginator", - - /** @property */ - windowSize: 10, - - /** - @property {Object.<string, Object.<string, string>>} controls You can - disable specific control handles by omitting certain keys. - */ - controls: { - rewind: { - label: "《", - title: "First" - }, - back: { - label: "〈", - title: "Previous" - }, - forward: { - label: "〉", - title: "Next" - }, - fastForward: { - label: "》", - title: "Last" - } - }, - - /** - @property {Backgrid.Extension.PageHandle} pageHandle. The PageHandle - class to use for rendering individual handles - */ - pageHandle: PageHandle, - - /** @property */ - goBackFirstOnSort: true, - - /** - Initializer. - - @param {Object} options - @param {Backbone.Collection} options.collection - @param {boolean} [options.controls] - @param {boolean} [options.pageHandle=Backgrid.Extension.PageHandle] - @param {boolean} [options.goBackFirstOnSort=true] - */ - initialize: function (options) { - this.controls = options.controls || this.controls; - this.pageHandle = options.pageHandle || this.pageHandle; - - var collection = this.collection; - this.listenTo(collection, "add", this.render); - this.listenTo(collection, "remove", this.render); - this.listenTo(collection, "reset", this.render); - if ((options.goBackFirstOnSort || this.goBackFirstOnSort) && - collection.fullCollection) { - this.listenTo(collection.fullCollection, "sort", function () { - collection.getFirstPage(); - }); - } - }, - - _calculateWindow: function () { - var collection = this.collection; - var state = collection.state; - - // convert all indices to 0-based here - var firstPage = state.firstPage; - var lastPage = +state.lastPage; - lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = firstPage ? currentPage - 1 : currentPage; - var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; - var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); - return [windowStart, windowEnd]; - }, - - /** - Creates a list of page handle objects for rendering. - - @return {Array.<Object>} an array of page handle objects hashes - */ - makeHandles: function () { - - var handles = []; - var collection = this.collection; - - var window = this._calculateWindow(); - var winStart = window[0], winEnd = window[1]; - - for (var i = winStart; i < winEnd; i++) { - handles.push(new this.pageHandle({ - collection: collection, - pageIndex: i - })); - } - - var controls = this.controls; - _.each(["back", "rewind", "forward", "fastForward"], function (key) { - var value = controls[key]; - if (value) { - var handleCtorOpts = { - collection: collection, - title: value.title, - label: value.label - }; - handleCtorOpts["is" + key.slice(0, 1).toUpperCase() + key.slice(1)] = true; - var handle = new this.pageHandle(handleCtorOpts); - if (key == "rewind" || key == "back") handles.unshift(handle); - else handles.push(handle); - } - }, this); - - return handles; - }, - - /** - Render the paginator handles inside an unordered list. - */ - render: function () { - this.$el.empty(); - - if (this.handles) { - for (var i = 0, l = this.handles.length; i < l; i++) { - this.handles[i].remove(); - } - } - - var handles = this.handles = this.makeHandles(); - - var ul = document.createElement("ul"); - for (var i = 0; i < handles.length; i++) { - ul.appendChild(handles[i].render().el); - } - - this.el.appendChild(ul); - - return this; - } - - }); - -})); diff --git a/src/UI/JsLibraries/backbone.backgrid.selectall.js b/src/UI/JsLibraries/backbone.backgrid.selectall.js deleted file mode 100644 index 7d36c73ae..000000000 --- a/src/UI/JsLibraries/backbone.backgrid.selectall.js +++ /dev/null @@ -1,243 +0,0 @@ -/* - backgrid-select-all - http://github.com/wyuenho/backgrid - - Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors - Licensed under the MIT @license. -*/ -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("backbone"), require("backgrid")); - } - // Browser - else if (typeof Backbone !== "undefined" && typeof Backgrid !== "undefined") { - factory(Backbone, Backgrid); - } - -}(function (Backbone, Backgrid) { - - "use strict"; - - var $ = Backbone.$; - - /** - Renders a checkbox for row selection. - - @class Backgrid.Extension.SelectRowCell - @extends Backbone.View - */ - var SelectRowCell = Backgrid.Extension.SelectRowCell = Backbone.View.extend({ - - /** @property */ - className: "select-row-cell", - - /** @property */ - tagName: "td", - - /** @property */ - events: { - "keydown :checkbox": "onKeydown", - "change :checkbox": "onChange", - "click :checkbox": "enterEditMode" - }, - - /** - Initializer. If the underlying model triggers a `select` event, this cell - will change its checked value according to the event's `selected` value. - - @param {Object} options - @param {Backgrid.Column} options.column - @param {Backbone.Model} options.model - */ - initialize: function (options) { - - this.column = options.column; - if (!(this.column instanceof Backgrid.Column)) { - this.column = new Backgrid.Column(this.column); - } - - this.listenTo(this.model, "backgrid:select", function (model, selected) { - this.$el.find(":checkbox").prop("checked", selected).change(); - }); - - var column = this.column, $el = this.$el; - this.listenTo(column, "change:renderable", function (column, renderable) { - $el.toggleClass("renderable", renderable); - }); - - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Focuses the checkbox. - */ - enterEditMode: function () { - this.$el.find(":checkbox").focus(); - }, - - /** - Unfocuses the checkbox. - */ - exitEditMode: function () { - this.$el.find(":checkbox").blur(); - }, - - /** - Process keyboard navigation. - */ - onKeydown: function (e) { - var command = new Backgrid.Command(e); - if (command.passThru()) return true; // skip ahead to `change` - if (command.cancel()) { - e.stopPropagation(); - this.$el.find(":checkbox").blur(); - } - else if (command.save() || command.moveLeft() || command.moveRight() || - command.moveUp() || command.moveDown()) { - e.preventDefault(); - e.stopPropagation(); - this.model.trigger("backgrid:edited", this.model, this.column, command); - } - }, - - /** - When the checkbox's value changes, this method will trigger a Backbone - `backgrid:selected` event with a reference of the model and the - checkbox's `checked` value. - */ - onChange: function (e) { - var checked = $(e.target).prop('checked'); - this.$el.parent().toggleClass('selected', checked); - this.model.trigger("backgrid:selected", this.model, checked); - }, - - /** - Renders a checkbox in a table cell. - */ - render: function () { - this.$el.empty().append('<input tabindex="-1" type="checkbox" />'); - this.delegateEvents(); - return this; - } - - }); - - /** - Renders a checkbox to select all rows on the current page. - - @class Backgrid.Extension.SelectAllHeaderCell - @extends Backgrid.Extension.SelectRowCell - */ - var SelectAllHeaderCell = Backgrid.Extension.SelectAllHeaderCell = SelectRowCell.extend({ - - /** @property */ - className: "select-all-header-cell", - - /** @property */ - tagName: "th", - - /** - Initializer. When this cell's checkbox is checked, a Backbone - `backgrid:select` event will be triggered for each model for the current - page in the underlying collection. If a `SelectRowCell` instance exists - for the rows representing the models, they will check themselves. If any - of the SelectRowCell instances trigger a Backbone `backgrid:selected` - event with a `false` value, this cell will uncheck its checkbox. In the - event of a Backbone `backgrid:refresh` event, which is triggered when the - body refreshes its rows, which can happen under a number of conditions - such as paging or the columns were reset, this cell will still remember - the previously selected models and trigger a Backbone `backgrid:select` - event on them such that the SelectRowCells can recheck themselves upon - refreshing. - - @param {Object} options - @param {Backgrid.Column} options.column - @param {Backbone.Collection} options.collection - */ - initialize: function (options) { - - this.column = options.column; - if (!(this.column instanceof Backgrid.Column)) { - this.column = new Backgrid.Column(this.column); - } - - var collection = this.collection; - var selectedModels = this.selectedModels = {}; - this.listenTo(collection, "backgrid:selected", function (model, selected) { - if (selected) selectedModels[model.id || model.cid] = model; - else { - delete selectedModels[model.id || model.cid]; - this.$el.find(":checkbox").prop("checked", false); - } - }); - - this.listenTo(collection, "remove", function (model) { - delete selectedModels[model.id || model.cid]; - }); - - this.listenTo(collection, "backgrid:refresh", function () { - this.$el.find(":checkbox").prop("checked", false); - for (var i = 0; i < collection.length; i++) { - var model = collection.at(i); - if (selectedModels[model.id || model.cid]) { - model.trigger('backgrid:select', model, true); - } - } - }); - - var column = this.column, $el = this.$el; - this.listenTo(column, "change:renderable", function (column, renderable) { - $el.toggleClass("renderable", renderable); - }); - - if (column.get("renderable")) $el.addClass("renderable"); - }, - - /** - Progagates the checked value of this checkbox to all the models of the - underlying collection by triggering a Backbone `backgrid:select` event on - the models themselves, passing each model and the current `checked` value - of the checkbox in each event. - */ - onChange: function (e) { - var checked = $(e.target).prop("checked"); - - var collection = this.collection; - collection.each(function (model) { - model.trigger("backgrid:select", model, checked); - }); - } - - }); - - /** - Convenient method to retrieve a list of selected models. This method only - exists when the `SelectAll` extension has been included. - - @member Backgrid.Grid - @return {Array.<Backbone.Model>} - */ - Backgrid.Grid.prototype.getSelectedModels = function () { - var selectAllHeaderCell; - var headerCells = this.header.row.cells; - for (var i = 0, l = headerCells.length; i < l; i++) { - var headerCell = headerCells[i]; - if (headerCell instanceof SelectAllHeaderCell) { - selectAllHeaderCell = headerCell; - break; - } - } - - var result = []; - if (selectAllHeaderCell) { - for (var modelId in selectAllHeaderCell.selectedModels) { - result.push(this.collection.get(modelId)); - } - } - - return result; - }; - -})); diff --git a/src/UI/JsLibraries/backbone.collectionview.js b/src/UI/JsLibraries/backbone.collectionview.js deleted file mode 100644 index 29ec982ff..000000000 --- a/src/UI/JsLibraries/backbone.collectionview.js +++ /dev/null @@ -1,1072 +0,0 @@ -/*! -* Backbone.CollectionView, v0.8.1 -* Copyright (c)2013 Rotunda Software, LLC. -* Distributed under MIT license -* http://github.com/rotundasoftware/backbone-collection-view -*/ - - -(function() { - var mDefaultModelViewConstructor = Backbone.View; - - var kDefaultReferenceBy = "model"; - - var kAllowedOptions = [ - "collection", "modelView", "modelViewOptions", "itemTemplate", "emptyListCaption", - "selectable", "clickToSelect", "selectableModelsFilter", "visibleModelsFilter", - "selectMultiple", "clickToToggle", "processKeyEvents", "sortable", "sortableOptions", "sortableModelsFilter", "itemTemplateFunction", "detachedRendering" - ]; - - var kOptionsRequiringRerendering = [ "collection", "modelView", "modelViewOptions", "itemTemplate", "selectableModelsFilter", "sortableModelsFilter", "visibleModelsFilter", "itemTemplateFunction", "detachedRendering", "sortableOptions" ]; - - var kStylesForEmptyListCaption = { - "background" : "transparent", - "border" : "none", - "box-shadow" : "none" - }; - - Backbone.CollectionView = Backbone.View.extend( { - - tagName : "ul", - - events : { - "mousedown li, td" : "_listItem_onMousedown", - "dblclick li, td" : "_listItem_onDoubleClick", - "click" : "_listBackground_onClick", - "click ul.collection-list, table.collection-list" : "_listBackground_onClick", - "keydown" : "_onKeydown" - }, - - // only used if Backbone.Courier is available - spawnMessages : { - "focus" : "focus" - }, - - //only used if Backbone.Courier is available - passMessages : { "*" : "." }, - - initialize : function( options ) { - var _this = this; - - this._hasBeenRendered = false; - - // default options - options = _.extend( {}, { - collection : null, - modelView : this.modelView || null, - modelViewOptions : {}, - itemTemplate : null, - itemTemplateFunction : null, - selectable : true, - clickToSelect : true, - selectableModelsFilter : null, - visibleModelsFilter : null, - sortableModelsFilter : null, - selectMultiple : false, - clickToToggle : false, - processKeyEvents : true, - sortable : false, - sortableOptions : null, - detachedRendering : false, - emptyListCaption : null - }, options ); - - // add each of the white-listed options to the CollectionView object itself - _.each( kAllowedOptions, function( option ) { - _this[ option ] = options[option]; - } ); - - if( ! this.collection ) this.collection = new Backbone.Collection(); - - if( this._isBackboneCourierAvailable() ) { - Backbone.Courier.add( this ); - } - - this.$el.data( "view", this ); // needed for connected sortable lists - this.$el.addClass( "collection-list" ); - if( this.processKeyEvents ) - this.$el.attr( "tabindex", 0 ); // so we get keyboard events - - this.selectedItems = []; - - this._updateItemTemplate(); - - if( this.collection ) - this._registerCollectionEvents(); - - this.viewManager = new ChildViewContainer(); - - //this.listenTo( this.collection, "change", function() { this.render(); this.spawn( "change" ); } ); // don't want changes to models bubbling up and triggering the list's render() function - - // note we do NOT call render here anymore, because if we inherit from this class we will likely call this - // function using __super__ before the rest of the initialization logic for the decedent class. however, we may - // override the render() function in that decedent class as well, and that will certainly expect all the initialization - // to be done already. so we have to make sure to not jump the gun and start rending at this point. - // this.render(); - }, - - setOption : function( name, value ) { - - var _this = this; - - if( name === "collection" ) { - this._setCollection( value ); - } - else { - if( _.contains( kAllowedOptions, name ) ) { - - switch( name ) { - case "selectMultiple" : - this[ name ] = value; - if( !value && this.selectedItems.length > 1 ) - this.setSelectedModel( _.first( this.selectedItems ), { by : "cid" } ); - break; - case "selectable" : - if( !value && this.selectedItems.length > 0 ) - this.setSelectedModels( [] ); - this[ name ] = value; - break; - case "selectableModelsFilter" : - this[ name ] = value; - if( value && _.isFunction( value ) ) - this._validateSelection(); - break; - case "itemTemplate" : - this[ name ] = value; - this._updateItemTemplate(); - break; - case "processKeyEvents" : - this[ name ] = value; - if( value ) this.$el.attr( "tabindex", 0 ); // so we get keyboard events - break; - case "modelView" : - this[ name ] = value; - //need to remove all old view instances - this.viewManager.each( function( view ) { - _this.viewManager.remove( view ); - // destroy the View itself - view.remove(); - } ); - break; - default : - this[ name ] = value; - } - - if( _.contains( kOptionsRequiringRerendering, name ) ) this.render(); - } - else throw name + " is not an allowed option"; - } - }, - - getSelectedModel : function( options ) { - return _.first( this.getSelectedModels( options ) ); - }, - - getSelectedModels : function ( options ) { - var _this = this; - - options = _.extend( {}, { - by : kDefaultReferenceBy - }, options ); - - var referenceBy = options.by; - var items = []; - - switch( referenceBy ) { - case "id" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.collection.get( item ).id ); - } ); - break; - case "cid" : - items = items.concat( this.selectedItems ); - break; - case "offset" : - var curLineNumber = 0; - - var itemElements; - if( this._isRenderedAsTable() ) - itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); - else if( this._isRenderedAsList() ) - itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); - - itemElements.each( function() { - var thisItemEl = $( this ); - if( thisItemEl.is( ".selected" ) ) - items.push( curLineNumber ); - curLineNumber++; - } ); - break; - case "model" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.collection.get( item ) ); - } ); - break; - case "view" : - _.each( this.selectedItems, function ( item ) { - items.push( _this.viewManager.findByModel( _this.collection.get( item ) ) ); - } ); - break; - } - - return items; - - }, - - setSelectedModels : function( newSelectedItems, options ) { - if( ! this.selectable ) return; // used to throw error, but there are some circumstances in which a list can be selectable at times and not at others, don't want to have to worry about catching errors - if( ! _.isArray( newSelectedItems ) ) throw "Invalid parameter value"; - - options = _.extend( {}, { - silent : false, - by : kDefaultReferenceBy - }, options ); - - var referenceBy = options.by; - var newSelectedCids = []; - - switch( referenceBy ) { - case "cid" : - newSelectedCids = newSelectedItems; - break; - case "id" : - this.collection.each( function( thisModel ) { - if( _.contains( newSelectedItems, thisModel.id ) ) newSelectedCids.push( thisModel.cid ); - } ); - break; - case "model" : - newSelectedCids = _.pluck( newSelectedItems, "cid" ); - break; - case "view" : - _.each( newSelectedItems, function( item ) { - newSelectedCids.push( item.model.cid ); - } ); - break; - case "offset" : - var curLineNumber = 0; - var selectedItems = []; - - var itemElements; - if( this._isRenderedAsTable() ) - itemElements = this.$el.find( "> tbody > [data-model-cid]:not(.not-visible)" ); - else if( this._isRenderedAsList() ) - itemElements = this.$el.find( "> [data-model-cid]:not(.not-visible)" ); - - itemElements.each( function() { - var thisItemEl = $( this ); - if( _.contains( newSelectedItems, curLineNumber ) ) - newSelectedCids.push( thisItemEl.attr( "data-model-cid" ) ); - curLineNumber++; - } ); - break; - } - - var oldSelectedModels = this.getSelectedModels(); - var oldSelectedCids = _.clone( this.selectedItems ); - - this.selectedItems = this._convertStringsToInts( newSelectedCids ); - this._validateSelection(); - - var newSelectedModels = this.getSelectedModels(); - - if( ! this._containSameElements( oldSelectedCids, this.selectedItems ) ) - { - this._addSelectedClassToSelectedItems( oldSelectedCids ); - - if( ! options.silent ) - { - this.trigger( "selectionChanged", newSelectedModels, oldSelectedModels ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "selectionChanged", { - selectedModels : newSelectedModels, - oldSelectedModels : oldSelectedModels - } ); - } - } - - this.updateDependentControls(); - } - }, - - setSelectedModel : function( newSelectedItem, options ) { - if( ! newSelectedItem && newSelectedItem !== 0 ) - this.setSelectedModels( [], options ); - else - this.setSelectedModels( [ newSelectedItem ], options ); - }, - - render : function(){ - var _this = this; - - this._hasBeenRendered = true; - - if( this.selectable ) this._saveSelection(); - - var modelViewContainerEl; - - // If collection view element is a table and it has a tbody - // within it, render the model views inside of the tbody - if( this._isRenderedAsTable() ) { - var tbodyChild = this.$el.find( "> tbody" ); - if( tbodyChild.length > 0 ) - modelViewContainerEl = tbodyChild; - } - - if( _.isUndefined( modelViewContainerEl ) ) - modelViewContainerEl = this.$el; - - var oldViewManager = this.viewManager; - this.viewManager = new ChildViewContainer(); - - // detach each of our subviews that we have already created to represent models - // in the collection. We are going to re-use the ones that represent models that - // are still here, instead of creating new ones, so that we don't loose state - // information in the views. - oldViewManager.each( function( thisModelView ) { - // to boost performance, only detach those views that will be sticking around. - // we won't need the other ones later, so no need to detach them individually. - if( _this.collection.get( thisModelView.model.cid ) ) - thisModelView.$el.detach(); - else - thisModelView.remove(); - } ); - - modelViewContainerEl.empty(); - var fragmentContainer; - - if( this.detachedRendering ) - fragmentContainer = document.createDocumentFragment(); - - this.collection.each( function( thisModel ) { - var thisModelView; - - thisModelView = oldViewManager.findByModelCid( thisModel.cid ); - if( _.isUndefined( thisModelView ) ) { - // if the model view was not already created on previous render, - // then create and initialize it now. - - var modelViewOptions = this._getModelViewOptions( thisModel ); - thisModelView = this._createNewModelView( thisModel, modelViewOptions ); - - thisModelView.collectionListView = _this; - } - - var thisModelViewWrapped = this._wrapModelView( thisModelView ); - if( this.detachedRendering ) - fragmentContainer.appendChild( thisModelViewWrapped[0] ); - else - modelViewContainerEl.append( thisModelViewWrapped ); - - // we have to render the modelView after it has been put in context, as opposed to in the - // initialize function of the modelView, because some rendering might be dependent on - // the modelView's context in the DOM tree. For example, if the modelView stretch()'s itself, - // it must be in full context in the DOM tree or else the stretch will not behave as intended. - var renderResult = thisModelView.render(); - - // return false from the view's render function to hide this item - if( renderResult === false ) { - thisModelViewWrapped.hide(); - thisModelViewWrapped.addClass( "not-visible" ); - } - - if( _.isFunction( this.visibleModelsFilter ) ) { - if( ! this.visibleModelsFilter( thisModel ) ) { - if( thisModelViewWrapped.children().length === 1 ) - thisModelViewWrapped.hide(); - else thisModelView.$el.hide(); - - thisModelViewWrapped.addClass( "not-visible" ); - } - } - - this.viewManager.add( thisModelView ); - }, this ); - - if( this.detachedRendering ) - modelViewContainerEl.append( fragmentContainer ); - - if( this.sortable ) - { - var sortableOptions = _.extend( { - axis: "y", - distance: 10, - forcePlaceholderSize : true, - start : _.bind( this._sortStart, this ), - change : _.bind( this._sortChange, this ), - stop : _.bind( this._sortStop, this ), - receive : _.bind( this._receive, this ), - over : _.bind( this._over, this ) - }, _.result( this, "sortableOptions" ) ); - - if( _this._isRenderedAsTable() ) { - sortableOptions.items = "> tbody > tr:not(.not-sortable)"; - } - else if( _this._isRenderedAsList() ) { - sortableOptions.items = "> li:not(.not-sortable)"; - } - - this.$el = this.$el.sortable( sortableOptions ); - } - - if( this.emptyListCaption ) { - var visibleView = this.viewManager.find( function( view ) { - return ! view.$el.hasClass( "not-visible" ); - } ); - - if( _.isUndefined( visibleView ) ) { - var emptyListString; - - if( _.isFunction( this.emptyListCaption ) ) - emptyListString = this.emptyListCaption(); - else - emptyListString = this.emptyListCaption; - - var $emptyCaptionEl; - var $varEl = $( "<var class='empty-list-caption'>" + emptyListString + "</var>" ); - - //need to wrap the empty caption to make it fit the rendered list structure (either with an li or a tr td) - if( this._isRenderedAsList() ) - $emptyListCaptionEl = $varEl.wrapAll( "<li class='not-sortable'></li>" ).parent().css( kStylesForEmptyListCaption ); - else - $emptyListCaptionEl = $varEl.wrapAll( "<tr class='not-sortable'><td></td></tr>" ).parent().parent().css( kStylesForEmptyListCaption ); - - this.$el.append( $emptyListCaptionEl ); - - } - } - - this.trigger( "render" ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "render" ); - - if( this.selectable ) { - this._restoreSelection(); - this.updateDependentControls(); - } - - if( _.isFunction( this.onAfterRender ) ) - this.onAfterRender(); - }, - - updateDependentControls : function() { - this.trigger( "updateDependentControls", this.getSelectedModels() ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "updateDependentControls", { - selectedModels : this.getSelectedModels() - } ); - } - }, - - // Override `Backbone.View.remove` to also destroy all Views in `viewManager` - remove : function() { - this.viewManager.each( function( view ) { - view.remove(); - } ); - - Backbone.View.prototype.remove.apply( this, arguments ); - }, - - _validateSelectionAndRender : function() { - this._validateSelection(); - this.render(); - }, - - _registerCollectionEvents : function() { - this.listenTo( this.collection, "add", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "add" ); - } ); - - this.listenTo( this.collection, "remove", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "remove" ); - } ); - - this.listenTo( this.collection, "reset", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "reset" ); - } ); - - // It should be up to the model to rerender itself when it changes. - // this.listenTo( this.collection, "change", function( model ) { - // if( this._hasBeenRendered ) this.viewManager.findByModel( model ).render(); - // if( this._isBackboneCourierAvailable() ) - // this.spawn( "change", { model : model } ); - // } ); - - this.listenTo( this.collection, "sort", function() { - if( this._hasBeenRendered ) this.render(); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sort" ); - } ); - }, - - _getClickedItemId : function( theEvent ) { - var clickedItemId = null; - - // important to use currentTarget as opposed to target, since we could be bubbling - // an event that took place within another collectionList - var clickedItemEl = $( theEvent.currentTarget ); - if( clickedItemEl.closest( ".collection-list" ).get(0) !== this.$el.get(0) ) return; - - // determine which list item was clicked. If we clicked in the blank area - // underneath all the elements, we want to know that too, since in this - // case we will want to deselect all elements. so check to see if the clicked - // DOM element is the list itself to find that out. - var clickedItem = clickedItemEl.closest( "[data-model-cid]" ); - if( clickedItem.length > 0 ) - { - clickedItemId = clickedItem.attr( "data-model-cid" ); - if( $.isNumeric( clickedItemId ) ) clickedItemId = parseInt( clickedItemId, 10 ); - } - - return clickedItemId; - }, - - _setCollection : function( newCollection ) { - if( newCollection !== this.collection ) - { - this.stopListening( this.collection ); - this.collection = newCollection; - this._registerCollectionEvents(); - } - - if( this._hasBeenRendered ) this.render(); - }, - - _updateItemTemplate : function() { - var itemTemplateHtml; - if( this.itemTemplate ) - { - if( $( this.itemTemplate ).length === 0 ) - throw "Could not find item template from selector: " + this.itemTemplate; - - itemTemplateHtml = $( this.itemTemplate ).html(); - } - else - itemTemplateHtml = this.$( ".item-template" ).html(); - - if( itemTemplateHtml ) this.itemTemplateFunction = _.template( itemTemplateHtml ); - - }, - - _validateSelection : function() { - // note can't use the collection's proxy to underscore because "cid" ais not an attribute, - // but an element of the model object itself. - var modelReferenceIds = _.pluck( this.collection.models, "cid" ); - this.selectedItems = _.intersection( modelReferenceIds, this.selectedItems ); - - if( _.isFunction( this.selectableModelsFilter ) ) - { - this.selectedItems = _.filter( this.selectedItems, function( thisItemId ) { - return this.selectableModelsFilter.call( this, this.collection.get( thisItemId ) ); - }, this ); - } - }, - - _saveSelection : function() { - // save the current selection. use restoreSelection() to restore the selection to the state it was in the last time saveSelection() was called. - if( ! this.selectable ) throw "Attempt to save selection on non-selectable list"; - this.savedSelection = { - items : this.selectedItems, - offset : this.getSelectedModel( { by : "offset" } ) - }; - }, - - _restoreSelection : function() { - if( ! this.savedSelection ) throw "Attempt to restore selection but no selection has been saved!"; - - // reset selectedItems to empty so that we "redraw" all "selected" classes - // when we set our new selection. We do this because it is likely that our - // contents have been refreshed, and we have thus lost all old "selected" classes. - this.setSelectedModels( [], { silent : true } ); - - if( this.savedSelection.items.length > 0 ) - { - // first try to restore the old selected items using their reference ids. - this.setSelectedModels( this.savedSelection.items, { by : "cid", silent : true } ); - - // all the items with the saved reference ids have been removed from the list. - // ok. try to restore the selection based on the offset that used to be selected. - // this is the expected behavior after a item is deleted from a list (i.e. select - // the line that immediately follows the deleted line). - if( this.selectedItems.length === 0 ) - this.setSelectedModel( this.savedSelection.offset, { by : "offset" } ); - - // Trigger a selection changed if the previously selected items were not all found - if (this.selectedItems.length !== this.savedSelection.items.length) - { - this.trigger( "selectionChanged", this.getSelectedModels(), [] ); - if( this._isBackboneCourierAvailable() ) { - this.spawn( "selectionChanged", { - selectedModels : this.getSelectedModels(), - oldSelectedModels : [] - } ); - } - } - } - - delete this.savedSelection; - }, - - _addSelectedClassToSelectedItems : function( oldItemsIdsWithSelectedClass ) { - if( _.isUndefined( oldItemsIdsWithSelectedClass ) ) oldItemsIdsWithSelectedClass = []; - - // oldItemsIdsWithSelectedClass is used for optimization purposes only. If this info is supplied then we - // only have to add / remove the "selected" class from those items that "selected" state has changed. - - var itemsIdsFromWhichSelectedClassNeedsToBeRemoved = oldItemsIdsWithSelectedClass; - itemsIdsFromWhichSelectedClassNeedsToBeRemoved = _.without( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, this.selectedItems ); - - _.each( itemsIdsFromWhichSelectedClassNeedsToBeRemoved, function( thisItemId ) { - this.$el.find( "[data-model-cid=" + thisItemId + "]" ).removeClass( "selected" ); - }, this ); - - var itemsIdsFromWhichSelectedClassNeedsToBeAdded = this.selectedItems; - itemsIdsFromWhichSelectedClassNeedsToBeAdded = _.without( itemsIdsFromWhichSelectedClassNeedsToBeAdded, oldItemsIdsWithSelectedClass ); - - _.each( itemsIdsFromWhichSelectedClassNeedsToBeAdded, function( thisItemId ) { - this.$el.find( "[data-model-cid=" + thisItemId + "]" ).addClass( "selected" ); - }, this ); - }, - - _reorderCollectionBasedOnHTML : function() { - var _this = this; - - this.$el.children().each( function() { - var thisModelCid = $( this ).attr( "data-model-cid" ); - - if( thisModelCid ) - { - // remove the current model and then add it back (at the end of the collection). - // When we are done looping through all models, they will be in the correct order. - var thisModel = _this.collection.get( thisModelCid ); - if( thisModel ) - { - _this.collection.remove( thisModel, { silent : true } ); - _this.collection.add( thisModel, { silent : true, sort : ! _this.collection.comparator } ); - } - } - } ); - - this.collection.trigger( "reorder" ); - - if( this._isBackboneCourierAvailable() ) this.spawn( "reorder" ); - - if( this.collection.comparator ) this.collection.sort(); - - }, - - _getModelViewConstructor : function( thisModel ) { - return this.modelView || mDefaultModelViewConstructor; - }, - - _getModelViewOptions : function( thisModel ) { - return _.extend( { model : thisModel }, this.modelViewOptions ); - }, - - _createNewModelView : function( model, modelViewOptions ) { - var modelViewConstructor = this._getModelViewConstructor( model ); - if( _.isUndefined( modelViewConstructor ) ) throw "Could not find modelView constructor for model"; - - return new ( modelViewConstructor )( modelViewOptions ); - }, - - _wrapModelView : function( modelView ) { - var _this = this; - - // we use items client ids as opposed to real ids, since we may not have a representation - // of these models on the server - var wrappedModelView; - - if( this._isRenderedAsTable() ) { - // if we are rendering the collection in a table, the template $el is a tr so we just need to set the data-model-cid - wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); - } - else if( this._isRenderedAsList() ) { - // if we are rendering the collection in a list, we need wrap each item in an <li></li> (if its not already an <li>) - // and set the data-model-cid - if( modelView.$el.prop( "tagName" ).toLowerCase() === "li" ) { - wrappedModelView = modelView.$el.attr( "data-model-cid", modelView.model.cid ); - } else { - wrappedModelView = modelView.$el.wrapAll( "<li data-model-cid='" + modelView.model.cid + "'></li>" ).parent(); - } - } - - if( _.isFunction( this.sortableModelsFilter ) ) - if( ! this.sortableModelsFilter.call( _this, modelView.model ) ) - wrappedModelView.addClass( "not-sortable" ); - - if( _.isFunction( this.selectableModelsFilter ) ) - if( ! this.selectableModelsFilter.call( _this, modelView.model ) ) - wrappedModelView.addClass( "not-selectable" ); - - return wrappedModelView; - }, - - _convertStringsToInts : function( theArray ) { - return _.map( theArray, function( thisEl ) { - if( ! _.isString( thisEl ) ) return thisEl; - var thisElAsNumber = parseInt( thisEl, 10 ); - return( thisElAsNumber == thisEl ? thisElAsNumber : thisEl ); - } ); - }, - - _containSameElements : function( arrayA, arrayB ) { - if( arrayA.length != arrayB.length ) return false; - var intersectionSize = _.intersection( arrayA, arrayB ).length; - return intersectionSize == arrayA.length; // and must also equal arrayB.length, since arrayA.length == arrayB.length - }, - - _isRenderedAsTable : function() { - return this.$el.prop('tagName').toLowerCase() === 'table'; - }, - - - _isRenderedAsList : function() { - return ! this._isRenderedAsTable(); - }, - - _charCodes : { - upArrow : 38, - downArrow : 40 - }, - - _isBackboneCourierAvailable : function() { - return !_.isUndefined( Backbone.Courier ); - }, - - _sortStart : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - this.trigger( "sortStart", modelBeingSorted ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortStart", { modelBeingSorted : modelBeingSorted } ); - }, - - _sortChange : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - this.trigger( "sortChange", modelBeingSorted ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortChange", { modelBeingSorted : modelBeingSorted } ); - }, - - _sortStop : function( event, ui ) { - var modelBeingSorted = this.collection.get( ui.item.attr( "data-model-cid" ) ); - var modelViewContainerEl = (this._isRenderedAsTable()) ? this.$el.find( "> tbody" ) : this.$el; - var newIndex = modelViewContainerEl.children().index( ui.item ); - - if( newIndex == -1 ) { - // the element was removed from this list. can happen if this sortable is connected - // to another sortable, and the item was dropped into the other sortable. - this.collection.remove( modelBeingSorted ); - } - - this._reorderCollectionBasedOnHTML(); - this.updateDependentControls(); - this.trigger( "sortStop", modelBeingSorted, newIndex ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "sortStop", { modelBeingSorted : modelBeingSorted, newIndex : newIndex } ); - }, - - _receive : function( event, ui ) { - var senderListEl = ui.sender; - var senderCollectionListView = senderListEl.data( "view" ); - if( ! senderCollectionListView || ! senderCollectionListView.collection ) return; - - var newIndex = this.$el.children().index( ui.item ); - var modelReceived = senderCollectionListView.collection.get( ui.item.attr( "data-model-cid" ) ); - this.collection.add( modelReceived, { at : newIndex } ); - modelReceived.collection = this.collection; // otherwise will not get properly set, since modelReceived.collection might already have a value. - this.setSelectedModel( modelReceived ); - }, - - _over : function( event, ui ) { - // when an item is being dragged into the sortable, - // hide the empty list caption if it exists - this.$el.find( ".empty-list-caption" ).hide(); - }, - - _onKeydown : function( event ) { - if( ! this.processKeyEvents ) return true; - - var trap = false; - - if( this.getSelectedModels( { by : "offset" } ).length == 1 ) - { - // need to trap down and up arrows or else the browser - // will end up scrolling a autoscroll div. - - var currentOffset = this.getSelectedModel( { by : "offset" } ); - if( event.which === this._charCodes.upArrow && currentOffset !== 0 ) - { - this.setSelectedModel( currentOffset - 1, { by : "offset" } ); - trap = true; - } - else if( event.which === this._charCodes.downArrow && currentOffset !== this.collection.length - 1 ) - { - this.setSelectedModel( currentOffset + 1, { by : "offset" } ); - trap = true; - } - } - - return ! trap; - }, - - _listItem_onMousedown : function( theEvent ) { - if( ! this.selectable || ! this.clickToSelect ) return; - - var clickedItemId = this._getClickedItemId( theEvent ); - - if( clickedItemId ) - { - // Exit if an unselectable item was clicked - if( _.isFunction( this.selectableModelsFilter ) && - ! this.selectableModelsFilter.call( this, this.collection.get( clickedItemId ) ) ) - { - return; - } - - // a selectable list item was clicked - if( this.selectMultiple && theEvent.shiftKey ) - { - var firstSelectedItemIndex = -1; - - if( this.selectedItems.length > 0 ) - { - this.collection.find( function( thisItemModel ) { - firstSelectedItemIndex++; - - // exit when we find our first selected element - return _.contains( this.selectedItems, thisItemModel.cid ); - }, this ); - } - - var clickedItemIndex = -1; - this.collection.find( function( thisItemModel ) { - clickedItemIndex++; - - // exit when we find the clicked element - return thisItemModel.cid == clickedItemId; - }, this ); - - var shiftKeyRootSelectedItemIndex = firstSelectedItemIndex == -1 ? clickedItemIndex : firstSelectedItemIndex; - var minSelectedItemIndex = Math.min( clickedItemIndex, shiftKeyRootSelectedItemIndex ); - var maxSelectedItemIndex = Math.max( clickedItemIndex, shiftKeyRootSelectedItemIndex ); - - var newSelectedItems = []; - for( var thisIndex = minSelectedItemIndex; thisIndex <= maxSelectedItemIndex; thisIndex ++ ) - newSelectedItems.push( this.collection.at( thisIndex ).cid ); - this.setSelectedModels( newSelectedItems, { by : "cid" } ); - - // shift clicking will usually highlight selectable text, which we do not want. - // this is a cross browser (hopefully) snippet that deselects all text selection. - if( document.selection && document.selection.empty ) - document.selection.empty(); - else if(window.getSelection) { - var sel = window.getSelection(); - if( sel && sel.removeAllRanges ) - sel.removeAllRanges(); - } - } - else if( this.selectMultiple && ( this.clickToToggle || theEvent.metaKey ) ) - { - if( _.contains( this.selectedItems, clickedItemId ) ) - this.setSelectedModels( _.without( this.selectedItems, clickedItemId ), { by : "cid" } ); - else this.setSelectedModels( _.union( this.selectedItems, [ clickedItemId ] ), { by : "cid" } ); - } - else - this.setSelectedModels( [ clickedItemId ], { by : "cid" } ); - } - else - // the blank area of the list was clicked - this.setSelectedModels( [] ); - - }, - - _listItem_onDoubleClick : function( theEvent ) { - var clickedItemId = this._getClickedItemId( theEvent ); - - if( clickedItemId ) - { - var clickedModel = this.collection.get( clickedItemId ); - this.trigger( "doubleClick", clickedModel ); - if( this._isBackboneCourierAvailable() ) - this.spawn( "doubleClick", { clickedModel : clickedModel } ); - } - }, - - _listBackground_onClick : function( theEvent ) { - if( ! this.selectable ) return; - if( ! $( theEvent.target ).is( ".collection-list" ) ) return; - - this.setSelectedModels( [] ); - } - - }, { - setDefaultModelViewConstructor : function( theConstructor ) { - mDefaultModelViewConstructor = theConstructor; - } - }); - - - // Backbone.BabySitter - // ------------------- - // v0.0.6 - // - // Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. - // Distributed under MIT license - // - // http://github.com/babysitterjs/backbone.babysitter - - // Backbone.ChildViewContainer - // --------------------------- - // - // Provide a container to store, retrieve and - // shut down child views. - - ChildViewContainer = (function(Backbone, _){ - - // Container Constructor - // --------------------- - - var Container = function(views){ - this._views = {}; - this._indexByModel = {}; - this._indexByCustom = {}; - this._updateLength(); - - _.each(views, this.add, this); - }; - - // Container Methods - // ----------------- - - _.extend(Container.prototype, { - - // Add a view to this container. Stores the view - // by `cid` and makes it searchable by the model - // cid (and model itself). Optionally specify - // a custom key to store an retrieve the view. - add: function(view, customIndex){ - var viewCid = view.cid; - - // store the view - this._views[viewCid] = view; - - // index it by model - if (view.model){ - this._indexByModel[view.model.cid] = viewCid; - } - - // index by custom - if (customIndex){ - this._indexByCustom[customIndex] = viewCid; - } - - this._updateLength(); - }, - - // Find a view by the model that was attached to - // it. Uses the model's `cid` to find it. - findByModel: function(model){ - return this.findByModelCid(model.cid); - }, - - // Find a view by the `cid` of the model that was attached to - // it. Uses the model's `cid` to find the view `cid` and - // retrieve the view using it. - findByModelCid: function(modelCid){ - var viewCid = this._indexByModel[modelCid]; - return this.findByCid(viewCid); - }, - - // Find a view by a custom indexer. - findByCustom: function(index){ - var viewCid = this._indexByCustom[index]; - return this.findByCid(viewCid); - }, - - // Find by index. This is not guaranteed to be a - // stable index. - findByIndex: function(index){ - return _.values(this._views)[index]; - }, - - // retrieve a view by it's `cid` directly - findByCid: function(cid){ - return this._views[cid]; - }, - - // Remove a view - remove: function(view){ - var viewCid = view.cid; - - // delete model index - if (view.model){ - delete this._indexByModel[view.model.cid]; - } - - // delete custom index - _.any(this._indexByCustom, function(cid, key) { - if (cid === viewCid) { - delete this._indexByCustom[key]; - return true; - } - }, this); - - // remove the view from the container - delete this._views[viewCid]; - - // update the length - this._updateLength(); - }, - - // Call a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.call`. - call: function(method){ - this.apply(method, _.tail(arguments)); - }, - - // Apply a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.apply`. - apply: function(method, args){ - _.each(this._views, function(view){ - if (_.isFunction(view[method])){ - view[method].apply(view, args || []); - } - }); - }, - - // Update the `.length` attribute on this container - _updateLength: function(){ - this.length = _.size(this._views); - } - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - Container.prototype[method] = function() { - var views = _.values(this._views); - var args = [views].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - // return the public API - return Container; - })(Backbone, _); -})(); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.deep.model.js b/src/UI/JsLibraries/backbone.deep.model.js deleted file mode 100644 index 7d65d8802..000000000 --- a/src/UI/JsLibraries/backbone.deep.model.js +++ /dev/null @@ -1,437 +0,0 @@ -/*jshint expr:true eqnull:true */ -/** - * - * Backbone.DeepModel v0.10.4 - * - * Copyright (c) 2013 Charles Davison, Pow Media Ltd - * - * https://github.com/powmedia/backbone-deep-model - * Licensed under the MIT License - */ - -/** - * Underscore mixins for deep objects - * - * Based on https://gist.github.com/echong/3861963 - */ -(function() { - var arrays, basicObjects, deepClone, deepExtend, deepExtendCouple, isBasicObject, - __slice = [].slice; - - deepClone = function(obj) { - var func, isArr; - if (!_.isObject(obj) || _.isFunction(obj)) { - return obj; - } - if (obj instanceof Backbone.Collection || obj instanceof Backbone.Model) { - return obj; - } - if (_.isDate(obj)) { - return new Date(obj.getTime()); - } - if (_.isRegExp(obj)) { - return new RegExp(obj.source, obj.toString().replace(/.*\//, "")); - } - isArr = _.isArray(obj || _.isArguments(obj)); - func = function(memo, value, key) { - if (isArr) { - memo.push(deepClone(value)); - } else { - memo[key] = deepClone(value); - } - return memo; - }; - return _.reduce(obj, func, isArr ? [] : {}); - }; - - isBasicObject = function(object) { - if (object == null) return false; - return (object.prototype === {}.prototype || object.prototype === Object.prototype) && _.isObject(object) && !_.isArray(object) && !_.isFunction(object) && !_.isDate(object) && !_.isRegExp(object) && !_.isArguments(object); - }; - - basicObjects = function(object) { - return _.filter(_.keys(object), function(key) { - return isBasicObject(object[key]); - }); - }; - - arrays = function(object) { - return _.filter(_.keys(object), function(key) { - return _.isArray(object[key]); - }); - }; - - deepExtendCouple = function(destination, source, maxDepth) { - var combine, recurse, sharedArrayKey, sharedArrayKeys, sharedObjectKey, sharedObjectKeys, _i, _j, _len, _len1; - if (maxDepth == null) { - maxDepth = 20; - } - if (maxDepth <= 0) { - console.warn('_.deepExtend(): Maximum depth of recursion hit.'); - return _.extend(destination, source); - } - sharedObjectKeys = _.intersection(basicObjects(destination), basicObjects(source)); - recurse = function(key) { - return source[key] = deepExtendCouple(destination[key], source[key], maxDepth - 1); - }; - for (_i = 0, _len = sharedObjectKeys.length; _i < _len; _i++) { - sharedObjectKey = sharedObjectKeys[_i]; - recurse(sharedObjectKey); - } - sharedArrayKeys = _.intersection(arrays(destination), arrays(source)); - combine = function(key) { - return source[key] = _.union(destination[key], source[key]); - }; - for (_j = 0, _len1 = sharedArrayKeys.length; _j < _len1; _j++) { - sharedArrayKey = sharedArrayKeys[_j]; - combine(sharedArrayKey); - } - return _.extend(destination, source); - }; - - deepExtend = function() { - var finalObj, maxDepth, objects, _i; - objects = 2 <= arguments.length ? __slice.call(arguments, 0, _i = arguments.length - 1) : (_i = 0, []), maxDepth = arguments[_i++]; - if (!_.isNumber(maxDepth)) { - objects.push(maxDepth); - maxDepth = 20; - } - if (objects.length <= 1) { - return objects[0]; - } - if (maxDepth <= 0) { - return _.extend.apply(this, objects); - } - finalObj = objects.shift(); - while (objects.length > 0) { - finalObj = deepExtendCouple(finalObj, deepClone(objects.shift()), maxDepth); - } - return finalObj; - }; - - _.mixin({ - deepClone: deepClone, - isBasicObject: isBasicObject, - basicObjects: basicObjects, - arrays: arrays, - deepExtend: deepExtend - }); - -}).call(this); - -/** - * Main source - */ - -;(function(factory) { - if (typeof define === 'function' && define.amd) { - // AMD - define(['underscore', 'backbone'], factory); - } else { - // globals - factory(_, Backbone); - } -}(function(_, Backbone) { - - /** - * Takes a nested object and returns a shallow object keyed with the path names - * e.g. { "level1.level2": "value" } - * - * @param {Object} Nested object e.g. { level1: { level2: 'value' } } - * @return {Object} Shallow object with path names e.g. { 'level1.level2': 'value' } - */ - function objToPaths(obj) { - var ret = {}, - separator = DeepModel.keyPathSeparator; - - for (var key in obj) { - var val = obj[key]; - - if (val && val.constructor === Object && !_.isEmpty(val)) { - //Recursion for embedded objects - var obj2 = objToPaths(val); - - for (var key2 in obj2) { - var val2 = obj2[key2]; - - ret[key + separator + key2] = val2; - } - } else { - ret[key] = val; - } - } - - return ret; - } - - /** - * @param {Object} Object to fetch attribute from - * @param {String} Object path e.g. 'user.name' - * @return {Mixed} - */ - function getNested(obj, path, return_exists) { - var separator = DeepModel.keyPathSeparator; - - var fields = path.split(separator); - var result = obj; - return_exists || (return_exists === false); - for (var i = 0, n = fields.length; i < n; i++) { - if (return_exists && !_.has(result, fields[i])) { - return false; - } - result = result[fields[i]]; - - if (result == null && i < n - 1) { - result = {}; - } - - if (typeof result === 'undefined') { - if (return_exists) - { - return true; - } - return result; - } - } - if (return_exists) - { - return true; - } - return result; - } - - /** - * @param {Object} obj Object to fetch attribute from - * @param {String} path Object path e.g. 'user.name' - * @param {Object} [options] Options - * @param {Boolean} [options.unset] Whether to delete the value - * @param {Mixed} Value to set - */ - function setNested(obj, path, val, options) { - options = options || {}; - - var separator = DeepModel.keyPathSeparator; - - var fields = path.split(separator); - var result = obj; - for (var i = 0, n = fields.length; i < n && result !== undefined ; i++) { - var field = fields[i]; - - //If the last in the path, set the value - if (i === n - 1) { - options.unset ? delete result[field] : result[field] = val; - } else { - //Create the child object if it doesn't exist, or isn't an object - if (typeof result[field] === 'undefined' || ! _.isObject(result[field])) { - result[field] = {}; - } - - //Move onto the next part of the path - result = result[field]; - } - } - } - - function deleteNested(obj, path) { - setNested(obj, path, null, { unset: true }); - } - - var DeepModel = Backbone.Model.extend({ - - // Override constructor - // Support having nested defaults by using _.deepExtend instead of _.extend - constructor: function(attributes, options) { - var defaults; - var attrs = attributes || {}; - this.cid = _.uniqueId('c'); - this.attributes = {}; - if (options && options.collection) this.collection = options.collection; - if (options && options.parse) attrs = this.parse(attrs, options) || {}; - if (defaults = _.result(this, 'defaults')) { - //<custom code> - // Replaced the call to _.defaults with _.deepExtend. - attrs = _.deepExtend({}, defaults, attrs); - //</custom code> - } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.deepClone(this.attributes); - }, - - // Override get - // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' - get: function(attr) { - return getNested(this.attributes, attr); - }, - - // Override set - // Supports nested attributes via the syntax 'obj.attr' e.g. 'author.user.name' - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val || {}; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.deepClone(this.attributes); //<custom>: Replaced _.clone with _.deepClone - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - //<custom code> - attrs = objToPaths(attrs); - //</custom code> - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - - //<custom code>: Using getNested, setNested and deleteNested - if (!_.isEqual(getNested(current, attr), val)) changes.push(attr); - if (!_.isEqual(getNested(prev, attr), val)) { - setNested(this.changed, attr, val); - } else { - deleteNested(this.changed, attr); - } - unset ? deleteNested(current, attr) : setNested(current, attr, val); - //</custom code> - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; - - //<custom code> - var separator = DeepModel.keyPathSeparator; - - for (var i = 0, l = changes.length; i < l; i++) { - var key = changes[i]; - - this.trigger('change:' + key, this, getNested(current, key), options); - - var fields = key.split(separator); - - //Trigger change events for parent keys with wildcard (*) notation - for(var n = fields.length - 1; n > 0; n--) { - var parentKey = _.first(fields, n).join(separator), - wildcardKey = parentKey + separator + '*'; - - this.trigger('change:' + wildcardKey, this, getNested(current, parentKey), options); - } - //</custom code> - } - } - - if (changing) return this; - if (!silent) { - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. - clear: function(options) { - var attrs = {}; - var shallowAttributes = objToPaths(this.attributes); - for (var key in shallowAttributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return getNested(this.changed, attr) !== undefined; - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - //<custom code>: objToPaths - if (!diff) return this.hasChanged() ? objToPaths(this.changed) : false; - //</custom code> - - var old = this._changing ? this._previousAttributes : this.attributes; - - //<custom code> - diff = objToPaths(diff); - old = objToPaths(old); - //</custom code> - - var val, changed = false; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - - //<custom code> - return getNested(this._previousAttributes, attr); - //</custom code> - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - //<custom code> - return _.deepClone(this._previousAttributes); - //</custom code> - } - }); - - - //Config; override in your app to customise - DeepModel.keyPathSeparator = '.'; - - - //Exports - Backbone.DeepModel = DeepModel; - - //For use in NodeJS - if (typeof module != 'undefined') module.exports = DeepModel; - - return Backbone; - -})); diff --git a/src/UI/JsLibraries/backbone.js b/src/UI/JsLibraries/backbone.js deleted file mode 100644 index 70a854d31..000000000 --- a/src/UI/JsLibraries/backbone.js +++ /dev/null @@ -1,1571 +0,0 @@ -// Backbone.js 1.0.0 - -// (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://backbonejs.org - -(function(){ - - // Initial Setup - // ------------- - - // Save a reference to the global object (`window` in the browser, `exports` - // on the server). - var root = this; - - // Save the previous value of the `Backbone` variable, so that it can be - // restored later on, if `noConflict` is used. - var previousBackbone = root.Backbone; - - // Create local references to array methods we'll want to use later. - var array = []; - var push = array.push; - var slice = array.slice; - var splice = array.splice; - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both the browser and the server. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '1.0.0'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore'); - - // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns - // the `$` variable. - Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option - // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and - // set a `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // --------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may bind with `on` or remove with `off` callback - // functions to an event; `trigger`-ing an event fires all callbacks in - // succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.on('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - var Events = Backbone.Events = { - - // Bind an event to a `callback` function. Passing `"all"` will bind - // the callback to all events fired. - on: function(name, callback, context) { - if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; - this._events || (this._events = {}); - var events = this._events[name] || (this._events[name] = []); - events.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - // Bind an event to only be triggered a single time. After the first time - // the callback is invoked, it will be removed. - once: function(name, callback, context) { - if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - return this.on(name, once, context); - }, - - // Remove one or many callbacks. If `context` is null, removes all - // callbacks with that function. If `callback` is null, removes all - // callbacks for the event. If `name` is null, removes all bound - // callbacks for all events. - off: function(name, callback, context) { - var retain, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = {}; - return this; - } - - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (events = this._events[name]) { - this._events[name] = retain = []; - if (callback || context) { - for (j = 0, k = events.length; j < k; j++) { - ev = events[j]; - if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || - (context && context !== ev.context)) { - retain.push(ev); - } - } - } - if (!retain.length) delete this._events[name]; - } - } - - return this; - }, - - // Trigger one or many events, firing all bound callbacks. Callbacks are - // passed the same arguments as `trigger` is, apart from the event name - // (unless you're listening on `"all"`, which will cause your callback to - // receive the true name of the event as the first argument). - trigger: function(name) { - if (!this._events) return this; - var args = slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - // Tell this object to stop listening to either specific events ... or - // to every object it's currently listening to. - stopListening: function(obj, name, callback) { - var listeners = this._listeners; - if (!listeners) return this; - var deleteListener = !name && !callback; - if (typeof name === 'object') callback = this; - if (obj) (listeners = {})[obj._listenerId] = obj; - for (var id in listeners) { - listeners[id].off(name, callback, this); - if (deleteListener) delete this._listeners[id]; - } - return this; - } - - }; - - // Regular expression used to split event strings. - var eventSplitter = /\s+/; - - // Implement fancy features of the Events API such as multiple event - // names `"change blur"` and jQuery-style event maps `{change: action}` - // in terms of the existing API. - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - - // Handle event maps. - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - return false; - } - - // Handle space separated event names. - if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - return false; - } - - return true; - }; - - // A difficult-to-believe, but optimized internal dispatch function for - // triggering events. Tries to keep the usual cases speedy (most internal - // Backbone events have 3 arguments). - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - - var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; - - // Inversion-of-control versions of `on` and `once`. Tell *this* object to - // listen to an event in another object ... keeping track of what it's - // listening to. - _.each(listenMethods, function(implementation, method) { - Events[method] = function(obj, name, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); - listeners[id] = obj; - if (typeof name === 'object') callback = this; - obj[implementation](name, callback, this); - return this; - }; - }); - - // Aliases for backwards compatibility. - Events.bind = Events.on; - Events.unbind = Events.off; - - // Allow the `Backbone` object to serve as a global event bus, for folks who - // want global "pubsub" in a convenient place. - _.extend(Backbone, Events); - - // Backbone.Model - // -------------- - - // Backbone **Models** are the basic data object in the framework -- - // frequently representing a row in a table in a database on your server. - // A discrete chunk of data and a bunch of useful, related methods for - // performing computations and transformations on that data. - - // Create a new model with the specified attributes. A client id (`cid`) - // is automatically generated and assigned for you. - var Model = Backbone.Model = function(attributes, options) { - var defaults; - var attrs = attributes || {}; - options || (options = {}); - this.cid = _.uniqueId('c'); - this.attributes = {}; - _.extend(this, _.pick(options, modelOptions)); - if (options.parse) attrs = this.parse(attrs, options) || {}; - if (defaults = _.result(this, 'defaults')) { - attrs = _.defaults({}, attrs, defaults); - } - this.set(attrs, options); - this.changed = {}; - this.initialize.apply(this, arguments); - }; - - // A list of options to be attached directly to the model, if provided. - var modelOptions = ['urlRoot', 'collection']; - - // Attach all inheritable methods to the Model prototype. - _.extend(Model.prototype, Events, { - - // A hash of attributes whose current and previous value differ. - changed: null, - - // The value returned during the last failed validation. - validationError: null, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute: 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Return a copy of the model's `attributes` object. - toJSON: function(options) { - return _.clone(this.attributes); - }, - - // Proxy `Backbone.sync` by default -- but override this if you need - // custom syncing semantics for *this* particular model. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Get the value of an attribute. - get: function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape: function(attr) { - return _.escape(this.get(attr)); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has: function(attr) { - return this.get(attr) != null; - }, - - // Set a hash of model attributes on the object, firing `"change"`. This is - // the core primitive operation of a model, updating the data and notifying - // anyone who needs to know about the change in state. The heart of the beast. - set: function(key, val, options) { - var attr, attrs, unset, changes, silent, changing, prev, current; - if (key == null) return this; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - options || (options = {}); - - // Run validation. - if (!this._validate(attrs, options)) return false; - - // Extract attributes and options. - unset = options.unset; - silent = options.silent; - changes = []; - changing = this._changing; - this._changing = true; - - if (!changing) { - this._previousAttributes = _.clone(this.attributes); - this.changed = {}; - } - current = this.attributes, prev = this._previousAttributes; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // For each `set` attribute, update or delete the current value. - for (attr in attrs) { - val = attrs[attr]; - if (!_.isEqual(current[attr], val)) changes.push(attr); - if (!_.isEqual(prev[attr], val)) { - this.changed[attr] = val; - } else { - delete this.changed[attr]; - } - unset ? delete current[attr] : current[attr] = val; - } - - // Trigger all relevant attribute changes. - if (!silent) { - if (changes.length) this._pending = true; - for (var i = 0, l = changes.length; i < l; i++) { - this.trigger('change:' + changes[i], this, current[changes[i]], options); - } - } - - // You might be wondering why there's a `while` loop here. Changes can - // be recursively nested within `"change"` events. - if (changing) return this; - if (!silent) { - while (this._pending) { - this._pending = false; - this.trigger('change', this, options); - } - } - this._pending = false; - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"`. `unset` is a noop - // if the attribute doesn't exist. - unset: function(attr, options) { - return this.set(attr, void 0, _.extend({}, options, {unset: true})); - }, - - // Clear all attributes on the model, firing `"change"`. - clear: function(options) { - var attrs = {}; - for (var key in this.attributes) attrs[key] = void 0; - return this.set(attrs, _.extend({}, options, {unset: true})); - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged: function(attr) { - if (attr == null) return !_.isEmpty(this.changed); - return _.has(this.changed, attr); - }, - - // Return an object containing all the attributes that have changed, or - // false if there are no changed attributes. Useful for determining what - // parts of a view need to be updated and/or what attributes need to be - // persisted to the server. Unset attributes will be set to undefined. - // You can also pass an attributes object to diff against the model, - // determining if there *would be* a change. - changedAttributes: function(diff) { - if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; - var val, changed = false; - var old = this._changing ? this._previousAttributes : this.attributes; - for (var attr in diff) { - if (_.isEqual(old[attr], (val = diff[attr]))) continue; - (changed || (changed = {}))[attr] = val; - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous: function(attr) { - if (attr == null || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes: function() { - return _.clone(this._previousAttributes); - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overridden, - // triggering a `"change"` event. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - if (!model.set(model.parse(resp, options), options)) return false; - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save: function(key, val, options) { - var attrs, method, xhr, attributes = this.attributes; - - // Handle both `"key", value` and `{key: value}` -style arguments. - if (key == null || typeof key === 'object') { - attrs = key; - options = val; - } else { - (attrs = {})[key] = val; - } - - // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. - if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; - - options = _.extend({validate: true}, options); - - // Do not persist invalid models. - if (!this._validate(attrs, options)) return false; - - // Set temporary attributes if `{wait: true}`. - if (attrs && options.wait) { - this.attributes = _.extend({}, attributes, attrs); - } - - // After a successful server-side save, the client is (optionally) - // updated with the server-side state. - if (options.parse === void 0) options.parse = true; - var model = this; - var success = options.success; - options.success = function(resp) { - // Ensure attributes are restored during synchronous saves. - model.attributes = attributes; - var serverAttrs = model.parse(resp, options); - if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); - if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { - return false; - } - if (success) success(model, resp, options); - model.trigger('sync', model, resp, options); - }; - wrapError(this, options); - - method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); - if (method === 'patch') options.attrs = attrs; - xhr = this.sync(method, this, options); - - // Restore attributes. - if (attrs && options.wait) this.attributes = attributes; - - return xhr; - }, - - // Destroy this model on the server if it was already persisted. - // Optimistically removes the model from its collection, if it has one. - // If `wait: true` is passed, waits for the server to respond before removal. - destroy: function(options) { - options = options ? _.clone(options) : {}; - var model = this; - var success = options.success; - - var destroy = function() { - model.trigger('destroy', model, model.collection, options); - }; - - options.success = function(resp) { - if (options.wait || model.isNew()) destroy(); - if (success) success(model, resp, options); - if (!model.isNew()) model.trigger('sync', model, resp, options); - }; - - if (this.isNew()) { - options.success(); - return false; - } - wrapError(this, options); - - var xhr = this.sync('delete', this, options); - if (!options.wait) destroy(); - return xhr; - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url: function() { - var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse: function(resp, options) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone: function() { - return new this.constructor(this.attributes); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew: function() { - return this.id == null; - }, - - // Check if the model is currently in a valid state. - isValid: function(options) { - return this._validate({}, _.extend(options || {}, { validate: true })); - }, - - // Run validation against the next complete set of model attributes, - // returning `true` if all is well. Otherwise, fire an `"invalid"` event. - _validate: function(attrs, options) { - if (!options.validate || !this.validate) return true; - attrs = _.extend({}, this.attributes, attrs); - var error = this.validationError = this.validate(attrs, options) || null; - if (!error) return true; - this.trigger('invalid', this, error, _.extend(options || {}, {validationError: error})); - return false; - } - - }); - - // Underscore methods that we want to implement on the Model. - var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; - - // Mix in each Underscore method as a proxy to `Model#attributes`. - _.each(modelMethods, function(method) { - Model.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.attributes); - return _[method].apply(_, args); - }; - }); - - // Backbone.Collection - // ------------------- - - // If models tend to represent a single row of data, a Backbone Collection is - // more analagous to a table full of data ... or a small slice or page of that - // table, or a collection of rows that belong together for a particular reason - // -- all of the messages in this particular folder, all of the documents - // belonging to this particular author, and so on. Collections maintain - // indexes of their models, both in order, and for lookup by `id`. - - // Create a new **Collection**, perhaps to contain a specific type of `model`. - // If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - var Collection = Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.url) this.url = options.url; - if (options.model) this.model = options.model; - if (options.comparator !== void 0) this.comparator = options.comparator; - this._reset(); - this.initialize.apply(this, arguments); - if (models) this.reset(models, _.extend({silent: true}, options)); - }; - - // Default options for `Collection#set`. - var setOptions = {add: true, remove: true, merge: true}; - var addOptions = {add: true, merge: false, remove: false}; - - // Define the Collection's inheritable methods. - _.extend(Collection.prototype, Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model: Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON: function(options) { - return this.map(function(model){ return model.toJSON(options); }); - }, - - // Proxy `Backbone.sync` by default. - sync: function() { - return Backbone.sync.apply(this, arguments); - }, - - // Add a model, or list of models to the set. - add: function(models, options) { - return this.set(models, _.defaults(options || {}, addOptions)); - }, - - // Remove a model, or a list of models from the set. - remove: function(models, options) { - models = _.isArray(models) ? models.slice() : [models]; - options || (options = {}); - var i, l, index, model; - for (i = 0, l = models.length; i < l; i++) { - model = this.get(models[i]); - if (!model) continue; - delete this._byId[model.id]; - delete this._byId[model.cid]; - index = this.indexOf(model); - this.models.splice(index, 1); - this.length--; - if (!options.silent) { - options.index = index; - model.trigger('remove', model, this, options); - } - this._removeReference(model); - } - return this; - }, - - // Update a collection by `set`-ing a new list of models, adding new ones, - // removing models that are no longer present, and merging models that - // already exist in the collection, as necessary. Similar to **Model#set**, - // the core operation for updating the data contained by the collection. - set: function(models, options) { - options = _.defaults(options || {}, setOptions); - if (options.parse) models = this.parse(models, options); - if (!_.isArray(models)) models = models ? [models] : []; - var i, l, model, attrs, existing, sort; - var at = options.at; - var sortable = this.comparator && (at == null) && options.sort !== false; - var sortAttr = _.isString(this.comparator) ? this.comparator : null; - var toAdd = [], toRemove = [], modelMap = {}; - - // Turn bare objects into model references, and prevent invalid models - // from being added. - for (i = 0, l = models.length; i < l; i++) { - if (!(model = this._prepareModel(models[i], options))) continue; - - // If a duplicate is found, prevent it from being added and - // optionally merge it into the existing model. - if (existing = this.get(model)) { - if (options.remove) modelMap[existing.cid] = true; - if (options.merge) { - existing.set(model.attributes, options); - if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; - } - - // This is a new model, push it to the `toAdd` list. - } else if (options.add) { - toAdd.push(model); - - // Listen to added models' events, and index models for lookup by - // `id` and by `cid`. - model.on('all', this._onModelEvent, this); - this._byId[model.cid] = model; - if (model.id != null) this._byId[model.id] = model; - } - } - - // Remove nonexistent models if appropriate. - if (options.remove) { - for (i = 0, l = this.length; i < l; ++i) { - if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); - } - if (toRemove.length) this.remove(toRemove, options); - } - - // See if sorting is needed, update `length` and splice in new models. - if (toAdd.length) { - if (sortable) sort = true; - this.length += toAdd.length; - if (at != null) { - splice.apply(this.models, [at, 0].concat(toAdd)); - } else { - push.apply(this.models, toAdd); - } - } - - // Silently sort the collection if appropriate. - if (sort) this.sort({silent: true}); - - if (options.silent) return this; - - // Trigger `add` events. - for (i = 0, l = toAdd.length; i < l; i++) { - (model = toAdd[i]).trigger('add', model, this, options); - } - - // Trigger `sort` if the collection was sorted. - if (sort) this.trigger('sort', this, options); - return this; - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any granular `add` or `remove` events. Fires `reset` when finished. - // Useful for bulk operations and optimizations. - reset: function(models, options) { - options || (options = {}); - for (var i = 0, l = this.models.length; i < l; i++) { - this._removeReference(this.models[i]); - } - options.previousModels = this.models; - this._reset(); - this.add(models, _.extend({silent: true}, options)); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Add a model to the end of the collection. - push: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: this.length}, options)); - return model; - }, - - // Remove a model from the end of the collection. - pop: function(options) { - var model = this.at(this.length - 1); - this.remove(model, options); - return model; - }, - - // Add a model to the beginning of the collection. - unshift: function(model, options) { - model = this._prepareModel(model, options); - this.add(model, _.extend({at: 0}, options)); - return model; - }, - - // Remove a model from the beginning of the collection. - shift: function(options) { - var model = this.at(0); - this.remove(model, options); - return model; - }, - - // Slice out a sub-array of models from the collection. - slice: function(begin, end) { - return this.models.slice(begin, end); - }, - - // Get a model from the set by id. - get: function(obj) { - if (obj == null) return void 0; - return this._byId[obj.id != null ? obj.id : obj.cid || obj]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Return models with matching attributes. Useful for simple cases of - // `filter`. - where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; - return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; - }); - }, - - // Return the first model with matching attributes. Useful for simple cases - // of `find`. - findWhere: function(attrs) { - return this.where(attrs, true); - }, - - // Force the collection to re-sort itself. You don't need to call this under - // normal circumstances, as the set will maintain sort order as each item - // is added. - sort: function(options) { - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - options || (options = {}); - - // Run sort based on type of `comparator`. - if (_.isString(this.comparator) || this.comparator.length === 1) { - this.models = this.sortBy(this.comparator, this); - } else { - this.models.sort(_.bind(this.comparator, this)); - } - - if (!options.silent) this.trigger('sort', this, options); - return this; - }, - - // Figure out the smallest index at which a model should be inserted so as - // to maintain order. - sortedIndex: function(model, value, context) { - value || (value = this.comparator); - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _.sortedIndex(this.models, model, iterator, context); - }, - - // Pluck an attribute from each model in the collection. - pluck: function(attr) { - return _.invoke(this.models, 'get', attr); - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `reset: true` is passed, the response - // data will be passed through the `reset` method instead of `set`. - fetch: function(options) { - options = options ? _.clone(options) : {}; - if (options.parse === void 0) options.parse = true; - var success = options.success; - var collection = this; - options.success = function(resp) { - var method = options.reset ? 'reset' : 'set'; - collection[method](resp, options); - if (success) success(collection, resp, options); - collection.trigger('sync', collection, resp, options); - }; - wrapError(this, options); - return this.sync('read', this, options); - }, - - // Create a new instance of a model in this collection. Add the model to the - // collection immediately, unless `wait: true` is passed, in which case we - // wait for the server to agree. - create: function(model, options) { - options = options ? _.clone(options) : {}; - if (!(model = this._prepareModel(model, options))) return false; - if (!options.wait) this.add(model, options); - var collection = this; - var success = options.success; - options.success = function(resp) { - if (options.wait) collection.add(model, options); - if (success) success(model, resp, options); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse: function(resp, options) { - return resp; - }, - - // Create a new collection with an identical list of models as this one. - clone: function() { - return new this.constructor(this.models); - }, - - // Private method to reset all internal state. Called when the collection - // is first initialized or reset. - _reset: function() { - this.length = 0; - this.models = []; - this._byId = {}; - }, - - // Prepare a hash of attributes (or other model) to be added to this - // collection. - _prepareModel: function(attrs, options) { - if (attrs instanceof Model) { - if (!attrs.collection) attrs.collection = this; - return attrs; - } - options || (options = {}); - options.collection = this; - var model = new this.model(attrs, options); - if (!model._validate(attrs, options)) { - this.trigger('invalid', this, attrs, options); - return false; - } - return model; - }, - - // Internal method to sever a model's ties to a collection. - _removeReference: function(model) { - if (this === model.collection) delete model.collection; - model.off('all', this._onModelEvent, this); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent: function(event, model, collection, options) { - if ((event === 'add' || event === 'remove') && collection !== this) return; - if (event === 'destroy') this.remove(model, options); - if (model && event === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - if (model.id != null) this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - // 90% of the core usefulness of Backbone Collections is actually implemented - // right here: - var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', - 'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', - 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', - 'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', - 'tail', 'drop', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', - 'isEmpty', 'chain']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Collection.prototype[method] = function() { - var args = slice.call(arguments); - args.unshift(this.models); - return _[method].apply(_, args); - }; - }); - - // Underscore methods that take a property name as an argument. - var attributeMethods = ['groupBy', 'countBy', 'sortBy']; - - // Use attributes instead of properties. - _.each(attributeMethods, function(method) { - Collection.prototype[method] = function(value, context) { - var iterator = _.isFunction(value) ? value : function(model) { - return model.get(value); - }; - return _[method](this.models, iterator, context); - }; - }); - - // Backbone.View - // ------------- - - // Backbone Views are almost more convention than they are actual code. A View - // is simply a JavaScript object that represents a logical chunk of UI in the - // DOM. This might be a single item, an entire list, a sidebar or panel, or - // even the surrounding frame which wraps your whole app. Defining a chunk of - // UI as a **View** allows you to define your DOM events declaratively, without - // having to worry about render order ... and makes it easy for the view to - // react to specific changes in the state of your models. - - // Creating a Backbone.View creates its initial element outside of the DOM, - // if an existing element is not provided... - var View = Backbone.View = function(options) { - this.cid = _.uniqueId('view'); - this._configure(options || {}); - this._ensureElement(); - this.initialize.apply(this, arguments); - this.delegateEvents(); - }; - - // Cached regex to split keys for `delegate`. - var delegateEventSplitter = /^(\S+)\s*(.*)$/; - - // List of view options to be merged as properties. - var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; - - // Set up all inheritable **Backbone.View** properties and methods. - _.extend(View.prototype, Events, { - - // The default `tagName` of a View's element is `"div"`. - tagName: 'div', - - // jQuery delegate for element lookup, scoped to DOM elements within the - // current view. This should be prefered to global lookups where possible. - $: function(selector) { - return this.$el.find(selector); - }, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // **render** is the core function that your view should override, in order - // to populate its element (`this.el`), with the appropriate HTML. The - // convention is for **render** to always return `this`. - render: function() { - return this; - }, - - // Remove this view by taking the element out of the DOM, and removing any - // applicable Backbone.Events listeners. - remove: function() { - this.$el.remove(); - this.stopListening(); - return this; - }, - - // Change the view's element (`this.el` property), including event - // re-delegation. - setElement: function(element, delegate) { - if (this.$el) this.undelegateEvents(); - this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); - this.el = this.$el[0]; - if (delegate !== false) this.delegateEvents(); - return this; - }, - - // Set callbacks, where `this.events` is a hash of - // - // *{"event selector": "callback"}* - // - // { - // 'mousedown .title': 'edit', - // 'click .button': 'save' - // 'click .open': function(e) { ... } - // } - // - // pairs. Callbacks will be bound to the view, with `this` set properly. - // Uses event delegation for efficiency. - // Omitting the selector binds the event to `this.el`. - // This only works for delegate-able events: not `focus`, `blur`, and - // not `change`, `submit`, and `reset` in Internet Explorer. - delegateEvents: function(events) { - if (!(events || (events = _.result(this, 'events')))) return this; - this.undelegateEvents(); - for (var key in events) { - var method = events[key]; - if (!_.isFunction(method)) method = this[events[key]]; - if (!method) continue; - - var match = key.match(delegateEventSplitter); - var eventName = match[1], selector = match[2]; - method = _.bind(method, this); - eventName += '.delegateEvents' + this.cid; - if (selector === '') { - this.$el.on(eventName, method); - } else { - this.$el.on(eventName, selector, method); - } - } - return this; - }, - - // Clears all callbacks previously bound to the view with `delegateEvents`. - // You usually don't need to use this, but may wish to if you have multiple - // Backbone views attached to the same DOM element. - undelegateEvents: function() { - this.$el.off('.delegateEvents' + this.cid); - return this; - }, - - // Performs the initial configuration of a View with a set of options. - // Keys with special meaning *(e.g. model, collection, id, className)* are - // attached directly to the view. See `viewOptions` for an exhaustive - // list. - _configure: function(options) { - if (this.options) options = _.extend({}, _.result(this, 'options'), options); - _.extend(this, _.pick(options, viewOptions)); - this.options = options; - }, - - // Ensure that the View has a DOM element to render into. - // If `this.el` is a string, pass it through `$()`, take the first - // matching element, and re-assign it to `el`. Otherwise, create - // an element from the `id`, `className` and `tagName` properties. - _ensureElement: function() { - if (!this.el) { - var attrs = _.extend({}, _.result(this, 'attributes')); - if (this.id) attrs.id = _.result(this, 'id'); - if (this.className) attrs['class'] = _.result(this, 'className'); - var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); - this.setElement($el, false); - } else { - this.setElement(_.result(this, 'el'), false); - } - } - - }); - - // Backbone.sync - // ------------- - - // Override this function to change the manner in which Backbone persists - // models to the server. You will be passed the type of request, and the - // model in question. By default, makes a RESTful Ajax request - // to the model's `url()`. Some possible customizations could be: - // - // * Use `setTimeout` to batch rapid-fire updates into a single request. - // * Send up the models as XML instead of JSON. - // * Persist models via WebSockets instead of Ajax. - // - // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests - // as `POST`, with a `_method` parameter containing the true HTTP method, - // as well as all requests with the body as `application/x-www-form-urlencoded` - // instead of `application/json` with the model in a param named `model`. - // Useful when interfacing with server-side languages like **PHP** that make - // it difficult to read the body of `PUT` requests. - Backbone.sync = function(method, model, options) { - var type = methodMap[method]; - - // Default options, unless specified. - _.defaults(options || (options = {}), { - emulateHTTP: Backbone.emulateHTTP, - emulateJSON: Backbone.emulateJSON - }); - - // Default JSON-request options. - var params = {type: type, dataType: 'json'}; - - // Ensure that we have a URL. - if (!options.url) { - params.url = _.result(model, 'url') || urlError(); - } - - // Ensure that we have the appropriate request data. - if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { - params.contentType = 'application/json'; - params.data = JSON.stringify(options.attrs || model.toJSON(options)); - } - - // For older servers, emulate JSON by encoding the request into an HTML-form. - if (options.emulateJSON) { - params.contentType = 'application/x-www-form-urlencoded'; - params.data = params.data ? {model: params.data} : {}; - } - - // For older servers, emulate HTTP by mimicking the HTTP method with `_method` - // And an `X-HTTP-Method-Override` header. - if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { - params.type = 'POST'; - if (options.emulateJSON) params.data._method = type; - var beforeSend = options.beforeSend; - options.beforeSend = function(xhr) { - xhr.setRequestHeader('X-HTTP-Method-Override', type); - if (beforeSend) return beforeSend.apply(this, arguments); - }; - } - - // Don't process data on a non-GET request. - if (params.type !== 'GET' && !options.emulateJSON) { - params.processData = false; - } - - // If we're sending a `PATCH` request, and we're in an old Internet Explorer - // that still has ActiveX enabled by default, override jQuery to use that - // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. - if (params.type === 'PATCH' && window.ActiveXObject && - !(window.external && window.external.msActiveXFilteringEnabled)) { - params.xhr = function() { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } - - // Make the request, allowing the user to override any Ajax options. - var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); - model.trigger('request', model, xhr, options); - return xhr; - }; - - // Map from CRUD to HTTP for our default `Backbone.sync` implementation. - var methodMap = { - 'create': 'POST', - 'update': 'PUT', - 'patch': 'PATCH', - 'delete': 'DELETE', - 'read': 'GET' - }; - - // Set the default implementation of `Backbone.ajax` to proxy through to `$`. - // Override this if you'd like to use a different library. - Backbone.ajax = function() { - return Backbone.$.ajax.apply(Backbone.$, arguments); - }; - - // Backbone.Router - // --------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - var Router = Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var optionalParam = /\((.*?)\)/g; - var namedParam = /(\(\?)?:\w+/g; - var splatParam = /\*\w+/g; - var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Router.prototype, Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize: function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route: function(route, name, callback) { - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - if (_.isFunction(name)) { - callback = name; - name = ''; - } - if (!callback) callback = this[name]; - var router = this; - Backbone.history.route(route, function(fragment) { - var args = router._extractParameters(route, fragment); - callback && callback.apply(router, args); - router.trigger.apply(router, ['route:' + name].concat(args)); - router.trigger('route', name, args); - Backbone.history.trigger('route', router, name, args); - }); - return this; - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate: function(fragment, options) { - Backbone.history.navigate(fragment, options); - return this; - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes: function() { - if (!this.routes) return; - this.routes = _.result(this, 'routes'); - var route, routes = _.keys(this.routes); - while ((route = routes.pop()) != null) { - this.route(route, this.routes[route]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp: function(route) { - route = route.replace(escapeRegExp, '\\$&') - .replace(optionalParam, '(?:$1)?') - .replace(namedParam, function(match, optional){ - return optional ? match : '([^\/]+)'; - }) - .replace(splatParam, '(.*?)'); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted decoded parameters. Empty or unmatched parameters will be - // treated as `null` to normalize cross-browser behavior. - _extractParameters: function(route, fragment) { - var params = route.exec(fragment).slice(1); - return _.map(params, function(param) { - return param ? decodeURIComponent(param) : null; - }); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on either - // [pushState](http://diveintohtml5.info/history.html) and real URLs, or - // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) - // and URL fragments. If the browser supports neither (old IE, natch), - // falls back to polling. - var History = Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - - // Ensure that `History` can be used outside of the browser. - if (typeof window !== 'undefined') { - this.location = window.location; - this.history = window.history; - } - }; - - // Cached regex for stripping a leading hash/slash and trailing space. - var routeStripper = /^[#\/]|\s+$/g; - - // Cached regex for stripping leading and trailing slashes. - var rootStripper = /^\/+|\/+$/g; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Cached regex for removing a trailing slash. - var trailingSlash = /\/$/; - - // Has the history handling already been started? - History.started = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(History.prototype, Events, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Gets the true hash value. Cannot use location.hash directly due to bug - // in Firefox where location.hash will always be decoded. - getHash: function(window) { - var match = (window || this).location.href.match(/#(.*)$/); - return match ? match[1] : ''; - }, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment: function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || !this._wantsHashChange || forcePushState) { - fragment = this.location.pathname; - var root = this.root.replace(trailingSlash, ''); - if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); - } else { - fragment = this.getHash(); - } - } - return fragment.replace(routeStripper, ''); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start: function(options) { - if (History.started) throw new Error("Backbone.history has already been started"); - History.started = true; - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - this.options = _.extend({}, {root: '/'}, this.options, options); - this.root = this.options.root; - this._wantsHashChange = this.options.hashChange !== false; - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - - // Normalize root to always include a leading and trailing slash. - this.root = ('/' + this.root + '/').replace(rootStripper, '/'); - - if (oldIE && this._wantsHashChange) { - this.iframe = Backbone.$('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow; - this.navigate(fragment); - } - - // Depending on whether we're using pushState or hashes, and whether - // 'onhashchange' is supported, determine how we check the URL state. - if (this._hasPushState) { - Backbone.$(window).on('popstate', this.checkUrl); - } else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { - Backbone.$(window).on('hashchange', this.checkUrl); - } else if (this._wantsHashChange) { - this._checkUrlInterval = setInterval(this.checkUrl, this.interval); - } - - // Determine if we need to change the base url, for a pushState link - // opened by a non-pushState browser. - this.fragment = fragment; - var loc = this.location; - var atRoot = loc.pathname.replace(/[^\/]$/, '$&/') === this.root; - - // If we've started off with a route from a `pushState`-enabled browser, - // but we're currently in a browser that doesn't support it... - if (this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) { - this.fragment = this.getFragment(null, true); - this.location.replace(this.root + this.location.search + '#' + this.fragment); - // Return immediately as browser will do redirect to new url - return true; - - // Or if we've started out with a hash-based route, but we're currently - // in a browser where it could be `pushState`-based instead... - } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) { - this.fragment = this.getHash().replace(routeStripper, ''); - this.history.replaceState({}, document.title, this.root + this.fragment + loc.search); - } - - if (!this.options.silent) return this.loadUrl(); - }, - - // Disable Backbone.history, perhaps temporarily. Not useful in a real app, - // but possibly useful for unit testing Routers. - stop: function() { - Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); - clearInterval(this._checkUrlInterval); - History.started = false; - }, - - // Add a route to be tested when the fragment changes. Routes added later - // may override previous routes. - route: function(route, callback) { - this.handlers.unshift({route: route, callback: callback}); - }, - - // Checks the current URL to see if it has changed, and if it has, - // calls `loadUrl`, normalizing across the hidden iframe. - checkUrl: function(e) { - var current = this.getFragment(); - if (current === this.fragment && this.iframe) { - current = this.getFragment(this.getHash(this.iframe)); - } - if (current === this.fragment) return false; - if (this.iframe) this.navigate(current); - this.loadUrl() || this.loadUrl(this.getHash()); - }, - - // Attempt to load the current URL fragment. If a route succeeds with a - // match, returns `true`. If no defined routes matches the fragment, - // returns `false`. - loadUrl: function(fragmentOverride) { - var fragment = this.fragment = this.getFragment(fragmentOverride); - var matched = _.any(this.handlers, function(handler) { - if (handler.route.test(fragment)) { - handler.callback(fragment); - return true; - } - }); - return matched; - }, - - // Save a fragment into the hash history, or replace the URL state if the - // 'replace' option is passed. You are responsible for properly URL-encoding - // the fragment in advance. - // - // The options object can contain `trigger: true` if you wish to have the - // route callback be fired (not usually desirable), or `replace: true`, if - // you wish to modify the current URL without adding an entry to the history. - navigate: function(fragment, options) { - if (!History.started) return false; - if (!options || options === true) options = {trigger: options}; - fragment = this.getFragment(fragment || ''); - if (this.fragment === fragment) return; - this.fragment = fragment; - var url = this.root + fragment; - - // If pushState is available, we use it to set the fragment as a real URL. - if (this._hasPushState) { - this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); - - // If hash changes haven't been explicitly disabled, update the hash - // fragment to store history. - } else if (this._wantsHashChange) { - this._updateHash(this.location, fragment, options.replace); - if (this.iframe && (fragment !== this.getFragment(this.getHash(this.iframe)))) { - // Opening and closing the iframe tricks IE7 and earlier to push a - // history entry on hash-tag change. When replace is true, we don't - // want this. - if(!options.replace) this.iframe.document.open().close(); - this._updateHash(this.iframe.location, fragment, options.replace); - } - - // If you've told us that you explicitly don't want fallback hashchange- - // based history, then `navigate` becomes a page refresh. - } else { - return this.location.assign(url); - } - if (options.trigger) this.loadUrl(fragment); - }, - - // Update the hash location, either replacing the current entry, or adding - // a new one to the browser history. - _updateHash: function(location, fragment, replace) { - if (replace) { - var href = location.href.replace(/(javascript:|#).*$/, ''); - location.replace(href + '#' + fragment); - } else { - // Some browsers require that `hash` contains a leading #. - location.hash = '#' + fragment; - } - } - - }); - - // Create the default Backbone.history. - Backbone.history = new History; - - // Helpers - // ------- - - // Helper function to correctly set up the prototype chain, for subclasses. - // Similar to `goog.inherits`, but uses a hash of prototype properties and - // class properties to be extended. - var extend = function(protoProps, staticProps) { - var parent = this; - var child; - - // The constructor function for the new subclass is either defined by you - // (the "constructor" property in your `extend` definition), or defaulted - // by us to simply call the parent's constructor. - if (protoProps && _.has(protoProps, 'constructor')) { - child = protoProps.constructor; - } else { - child = function(){ return parent.apply(this, arguments); }; - } - - // Add static properties to the constructor function, if supplied. - _.extend(child, parent, staticProps); - - // Set the prototype chain to inherit from `parent`, without calling - // `parent`'s constructor function. - var Surrogate = function(){ this.constructor = child; }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate; - - // Add prototype properties (instance properties) to the subclass, - // if supplied. - if (protoProps) _.extend(child.prototype, protoProps); - - // Set a convenience property in case the parent's prototype is needed - // later. - child.__super__ = parent.prototype; - - return child; - }; - - // Set up inheritance for the model, collection, router, view and history. - Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; - - // Throw an error when a URL is needed, and none is supplied. - var urlError = function() { - throw new Error('A "url" property or function must be specified'); - }; - - // Wrap an optional error callback with a fallback error event. - var wrapError = function (model, options) { - var error = options.error; - options.error = function(resp) { - if (error) error(model, resp, options); - model.trigger('error', model, resp, options); - }; - }; - -}).call(this); diff --git a/src/UI/JsLibraries/backbone.marionette.js b/src/UI/JsLibraries/backbone.marionette.js deleted file mode 100644 index 5ad3a5d9a..000000000 --- a/src/UI/JsLibraries/backbone.marionette.js +++ /dev/null @@ -1,2329 +0,0 @@ -// MarionetteJS (Backbone.Marionette) -// ---------------------------------- -// v1.0.4 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://marionettejs.com - - - -/*! - * Includes BabySitter - * https://github.com/marionettejs/backbone.babysitter/ - * - * Includes Wreqr - * https://github.com/marionettejs/backbone.wreqr/ - */ - -// Backbone.BabySitter -// ------------------- -// v0.0.6 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/babysitterjs/backbone.babysitter - -// Backbone.ChildViewContainer -// --------------------------- -// -// Provide a container to store, retrieve and -// shut down child views. - -Backbone.ChildViewContainer = (function(Backbone, _){ - - // Container Constructor - // --------------------- - - var Container = function(views){ - this._views = {}; - this._indexByModel = {}; - this._indexByCustom = {}; - this._updateLength(); - - _.each(views, this.add, this); - }; - - // Container Methods - // ----------------- - - _.extend(Container.prototype, { - - // Add a view to this container. Stores the view - // by `cid` and makes it searchable by the model - // cid (and model itself). Optionally specify - // a custom key to store an retrieve the view. - add: function(view, customIndex){ - var viewCid = view.cid; - - // store the view - this._views[viewCid] = view; - - // index it by model - if (view.model){ - this._indexByModel[view.model.cid] = viewCid; - } - - // index by custom - if (customIndex){ - this._indexByCustom[customIndex] = viewCid; - } - - this._updateLength(); - }, - - // Find a view by the model that was attached to - // it. Uses the model's `cid` to find it. - findByModel: function(model){ - return this.findByModelCid(model.cid); - }, - - // Find a view by the `cid` of the model that was attached to - // it. Uses the model's `cid` to find the view `cid` and - // retrieve the view using it. - findByModelCid: function(modelCid){ - var viewCid = this._indexByModel[modelCid]; - return this.findByCid(viewCid); - }, - - // Find a view by a custom indexer. - findByCustom: function(index){ - var viewCid = this._indexByCustom[index]; - return this.findByCid(viewCid); - }, - - // Find by index. This is not guaranteed to be a - // stable index. - findByIndex: function(index){ - return _.values(this._views)[index]; - }, - - // retrieve a view by it's `cid` directly - findByCid: function(cid){ - return this._views[cid]; - }, - - // Remove a view - remove: function(view){ - var viewCid = view.cid; - - // delete model index - if (view.model){ - delete this._indexByModel[view.model.cid]; - } - - // delete custom index - _.any(this._indexByCustom, function(cid, key) { - if (cid === viewCid) { - delete this._indexByCustom[key]; - return true; - } - }, this); - - // remove the view from the container - delete this._views[viewCid]; - - // update the length - this._updateLength(); - }, - - // Call a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.call`. - call: function(method){ - this.apply(method, _.tail(arguments)); - }, - - // Apply a method on every view in the container, - // passing parameters to the call method one at a - // time, like `function.apply`. - apply: function(method, args){ - _.each(this._views, function(view){ - if (_.isFunction(view[method])){ - view[method].apply(view, args || []); - } - }); - }, - - // Update the `.length` attribute on this container - _updateLength: function(){ - this.length = _.size(this._views); - } - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - Container.prototype[method] = function() { - var views = _.values(this._views); - var args = [views].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - // return the public API - return Container; -})(Backbone, _); - -// Backbone.Wreqr (Backbone.Marionette) -// ---------------------------------- -// v0.2.0 -// -// Copyright (c)2013 Derick Bailey, Muted Solutions, LLC. -// Distributed under MIT license -// -// http://github.com/marionettejs/backbone.wreqr - - -Backbone.Wreqr = (function(Backbone, Marionette, _){ - "use strict"; - var Wreqr = {}; - - // Handlers -// -------- -// A registry of functions to call, given a name - -Wreqr.Handlers = (function(Backbone, _){ - "use strict"; - - // Constructor - // ----------- - - var Handlers = function(options){ - this.options = options; - this._wreqrHandlers = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - Handlers.extend = Backbone.Model.extend; - - // Instance Members - // ---------------- - - _.extend(Handlers.prototype, Backbone.Events, { - - // Add multiple handlers using an object literal configuration - setHandlers: function(handlers){ - _.each(handlers, function(handler, name){ - var context = null; - - if (_.isObject(handler) && !_.isFunction(handler)){ - context = handler.context; - handler = handler.callback; - } - - this.setHandler(name, handler, context); - }, this); - }, - - // Add a handler for the given name, with an - // optional context to run the handler within - setHandler: function(name, handler, context){ - var config = { - callback: handler, - context: context - }; - - this._wreqrHandlers[name] = config; - - this.trigger("handler:add", name, handler, context); - }, - - // Determine whether or not a handler is registered - hasHandler: function(name){ - return !! this._wreqrHandlers[name]; - }, - - // Get the currently registered handler for - // the specified name. Throws an exception if - // no handler is found. - getHandler: function(name){ - var config = this._wreqrHandlers[name]; - - if (!config){ - throw new Error("Handler not found for '" + name + "'"); - } - - return function(){ - var args = Array.prototype.slice.apply(arguments); - return config.callback.apply(config.context, args); - }; - }, - - // Remove a handler for the specified name - removeHandler: function(name){ - delete this._wreqrHandlers[name]; - }, - - // Remove all handlers from this registry - removeAllHandlers: function(){ - this._wreqrHandlers = {}; - } - }); - - return Handlers; -})(Backbone, _); - - // Wreqr.CommandStorage -// -------------------- -// -// Store and retrieve commands for execution. -Wreqr.CommandStorage = (function(){ - "use strict"; - - // Constructor function - var CommandStorage = function(options){ - this.options = options; - this._commands = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - // Instance methods - _.extend(CommandStorage.prototype, Backbone.Events, { - - // Get an object literal by command name, that contains - // the `commandName` and the `instances` of all commands - // represented as an array of arguments to process - getCommands: function(commandName){ - var commands = this._commands[commandName]; - - // we don't have it, so add it - if (!commands){ - - // build the configuration - commands = { - command: commandName, - instances: [] - }; - - // store it - this._commands[commandName] = commands; - } - - return commands; - }, - - // Add a command by name, to the storage and store the - // args for the command - addCommand: function(commandName, args){ - var command = this.getCommands(commandName); - command.instances.push(args); - }, - - // Clear all commands for the given `commandName` - clearCommands: function(commandName){ - var command = this.getCommands(commandName); - command.instances = []; - } - }); - - return CommandStorage; -})(); - - // Wreqr.Commands -// -------------- -// -// A simple command pattern implementation. Register a command -// handler and execute it. -Wreqr.Commands = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - // default storage type - storageType: Wreqr.CommandStorage, - - constructor: function(options){ - this.options = options || {}; - - this._initializeStorage(this.options); - this.on("handler:add", this._executeCommands, this); - - var args = Array.prototype.slice.call(arguments); - Wreqr.Handlers.prototype.constructor.apply(this, args); - }, - - // Execute a named command with the supplied args - execute: function(name, args){ - name = arguments[0]; - args = Array.prototype.slice.call(arguments, 1); - - if (this.hasHandler(name)){ - this.getHandler(name).apply(this, args); - } else { - this.storage.addCommand(name, args); - } - - }, - - // Internal method to handle bulk execution of stored commands - _executeCommands: function(name, handler, context){ - var command = this.storage.getCommands(name); - - // loop through and execute all the stored command instances - _.each(command.instances, function(args){ - handler.apply(context, args); - }); - - this.storage.clearCommands(name); - }, - - // Internal method to initialize storage either from the type's - // `storageType` or the instance `options.storageType`. - _initializeStorage: function(options){ - var storage; - - var StorageType = options.storageType || this.storageType; - if (_.isFunction(StorageType)){ - storage = new StorageType(); - } else { - storage = StorageType; - } - - this.storage = storage; - } - }); - -})(Wreqr); - - // Wreqr.RequestResponse -// --------------------- -// -// A simple request/response implementation. Register a -// request handler, and return a response from it -Wreqr.RequestResponse = (function(Wreqr){ - "use strict"; - - return Wreqr.Handlers.extend({ - request: function(){ - var name = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - - return this.getHandler(name).apply(this, args); - } - }); - -})(Wreqr); - - // Event Aggregator -// ---------------- -// A pub-sub object that can be used to decouple various parts -// of an application through event-driven architecture. - -Wreqr.EventAggregator = (function(Backbone, _){ - "use strict"; - var EA = function(){}; - - // Copy the `extend` function used by Backbone's classes - EA.extend = Backbone.Model.extend; - - // Copy the basic Backbone.Events on to the event aggregator - _.extend(EA.prototype, Backbone.Events); - - return EA; -})(Backbone, _); - - - return Wreqr; -})(Backbone, Backbone.Marionette, _); - -var Marionette = (function(global, Backbone, _){ - "use strict"; - - // Define and export the Marionette namespace - var Marionette = {}; - Backbone.Marionette = Marionette; - - // Get the DOM manipulator for later use - Marionette.$ = Backbone.$; - -// Helpers -// ------- - -// For slicing `arguments` in functions -var protoSlice = Array.prototype.slice; -function slice(args) { - return protoSlice.call(args); -} - -function throwError(message, name) { - var error = new Error(message); - error.name = name || 'Error'; - throw error; -} - -// Marionette.extend -// ----------------- - -// Borrow the Backbone `extend` method so we can use it as needed -Marionette.extend = Backbone.Model.extend; - -// Marionette.getOption -// -------------------- - -// Retrieve an object, function or other value from a target -// object or its `options`, with `options` taking precedence. -Marionette.getOption = function(target, optionName){ - if (!target || !optionName){ return; } - var value; - - if (target.options && (optionName in target.options) && (target.options[optionName] !== undefined)){ - value = target.options[optionName]; - } else { - value = target[optionName]; - } - - return value; -}; - -// Trigger an event and a corresponding method name. Examples: -// -// `this.triggerMethod("foo")` will trigger the "foo" event and -// call the "onFoo" method. -// -// `this.triggerMethod("foo:bar") will trigger the "foo:bar" event and -// call the "onFooBar" method. -Marionette.triggerMethod = (function(){ - - // split the event name on the : - var splitter = /(^|:)(\w)/gi; - - // take the event section ("section1:section2:section3") - // and turn it in to uppercase name - function getEventName(match, prefix, eventName) { - return eventName.toUpperCase(); - } - - // actual triggerMethod name - var triggerMethod = function(event) { - // get the method name from the event name - var methodName = 'on' + event.replace(splitter, getEventName); - var method = this[methodName]; - - // trigger the event - this.trigger.apply(this, arguments); - - // call the onMethodName if it exists - if (_.isFunction(method)) { - // pass all arguments, except the event name - return method.apply(this, _.tail(arguments)); - } - }; - - return triggerMethod; -})(); - -// DOMRefresh -// ---------- -// -// Monitor a view's state, and after it has been rendered and shown -// in the DOM, trigger a "dom:refresh" event every time it is -// re-rendered. - -Marionette.MonitorDOMRefresh = (function(){ - // track when the view has been rendered - function handleShow(view){ - view._isShown = true; - triggerDOMRefresh(view); - } - - // track when the view has been shown in the DOM, - // using a Marionette.Region (or by other means of triggering "show") - function handleRender(view){ - view._isRendered = true; - triggerDOMRefresh(view); - } - - // Trigger the "dom:refresh" event and corresponding "onDomRefresh" method - function triggerDOMRefresh(view){ - if (view._isShown && view._isRendered){ - if (_.isFunction(view.triggerMethod)){ - view.triggerMethod("dom:refresh"); - } - } - } - - // Export public API - return function(view){ - view.listenTo(view, "show", function(){ - handleShow(view); - }); - - view.listenTo(view, "render", function(){ - handleRender(view); - }); - }; -})(); - - -// Marionette.bindEntityEvents & unbindEntityEvents -// --------------------------- -// -// These methods are used to bind/unbind a backbone "entity" (collection/model) -// to methods on a target object. -// -// The first parameter, `target`, must have a `listenTo` method from the -// EventBinder object. -// -// The second parameter is the entity (Backbone.Model or Backbone.Collection) -// to bind the events from. -// -// The third parameter is a hash of { "event:name": "eventHandler" } -// configuration. Multiple handlers can be separated by a space. A -// function can be supplied instead of a string handler name. - -(function(Marionette){ - "use strict"; - - // Bind the event to handlers specified as a string of - // handler names on the target object - function bindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - - var method = target[methodName]; - if(!method) { - throwError("Method '"+ methodName +"' was configured as an event handler, but does not exist."); - } - - target.listenTo(entity, evt, method, target); - }); - } - - // Bind the event to a supplied callback function - function bindToFunction(target, entity, evt, method){ - target.listenTo(entity, evt, method, target); - } - - // Bind the event to handlers specified as a string of - // handler names on the target object - function unbindFromStrings(target, entity, evt, methods){ - var methodNames = methods.split(/\s+/); - - _.each(methodNames,function(methodName) { - var method = target[methodName]; - target.stopListening(entity, evt, method, target); - }); - } - - // Bind the event to a supplied callback function - function unbindToFunction(target, entity, evt, method){ - target.stopListening(entity, evt, method, target); - } - - - // generic looping function - function iterateEvents(target, entity, bindings, functionCallback, stringCallback){ - if (!entity || !bindings) { return; } - - // allow the bindings to be a function - if (_.isFunction(bindings)){ - bindings = bindings.call(target); - } - - // iterate the bindings and bind them - _.each(bindings, function(methods, evt){ - - // allow for a function as the handler, - // or a list of event names as a string - if (_.isFunction(methods)){ - functionCallback(target, entity, evt, methods); - } else { - stringCallback(target, entity, evt, methods); - } - - }); - } - - // Export Public API - Marionette.bindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, bindToFunction, bindFromStrings); - }; - - Marionette.unbindEntityEvents = function(target, entity, bindings){ - iterateEvents(target, entity, bindings, unbindToFunction, unbindFromStrings); - }; - -})(Marionette); - - -// Callbacks -// --------- - -// A simple way of managing a collection of callbacks -// and executing them at a later point in time, using jQuery's -// `Deferred` object. -Marionette.Callbacks = function(){ - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; -}; - -_.extend(Marionette.Callbacks.prototype, { - - // Add a callback to be executed. Callbacks added here are - // guaranteed to execute, even if they are added after the - // `run` method is called. - add: function(callback, contextOverride){ - this._callbacks.push({cb: callback, ctx: contextOverride}); - - this._deferred.done(function(context, options){ - if (contextOverride){ context = contextOverride; } - callback.call(context, options); - }); - }, - - // Run all registered callbacks with the context specified. - // Additional callbacks can be added after this has been run - // and they will still be executed. - run: function(options, context){ - this._deferred.resolve(context, options); - }, - - // Resets the list of callbacks to be run, allowing the same list - // to be run multiple times - whenever the `run` method is called. - reset: function(){ - var callbacks = this._callbacks; - this._deferred = Marionette.$.Deferred(); - this._callbacks = []; - - _.each(callbacks, function(cb){ - this.add(cb.cb, cb.ctx); - }, this); - } -}); - - -// Marionette Controller -// --------------------- -// -// A multi-purpose object to use as a controller for -// modules and routers, and as a mediator for workflow -// and coordination of other objects, views, and more. -Marionette.Controller = function(options){ - this.triggerMethod = Marionette.triggerMethod; - this.options = options || {}; - - if (_.isFunction(this.initialize)){ - this.initialize(this.options); - } -}; - -Marionette.Controller.extend = Marionette.extend; - -// Controller Methods -// -------------- - -// Ensure it can trigger events with Backbone.Events -_.extend(Marionette.Controller.prototype, Backbone.Events, { - close: function(){ - this.stopListening(); - this.triggerMethod("close"); - this.unbind(); - } -}); - -// Region -// ------ -// -// Manage the visual regions of your composite application. See -// http://lostechies.com/derickbailey/2011/12/12/composite-js-apps-regions-and-region-managers/ - -Marionette.Region = function(options){ - this.options = options || {}; - - this.el = Marionette.getOption(this, "el"); - - if (!this.el){ - var err = new Error("An 'el' must be specified for a region."); - err.name = "NoElError"; - throw err; - } - - if (this.initialize){ - var args = Array.prototype.slice.apply(arguments); - this.initialize.apply(this, args); - } -}; - - -// Region Type methods -// ------------------- - -_.extend(Marionette.Region, { - - // Build an instance of a region by passing in a configuration object - // and a default region type to use if none is specified in the config. - // - // The config object should either be a string as a jQuery DOM selector, - // a Region type directly, or an object literal that specifies both - // a selector and regionType: - // - // ```js - // { - // selector: "#foo", - // regionType: MyCustomRegion - // } - // ``` - // - buildRegion: function(regionConfig, defaultRegionType){ - var regionIsString = (typeof regionConfig === "string"); - var regionSelectorIsString = (typeof regionConfig.selector === "string"); - var regionTypeIsUndefined = (typeof regionConfig.regionType === "undefined"); - var regionIsType = (typeof regionConfig === "function"); - - if (!regionIsType && !regionIsString && !regionSelectorIsString) { - throw new Error("Region must be specified as a Region type, a selector string or an object with selector property"); - } - - var selector, RegionType; - - // get the selector for the region - - if (regionIsString) { - selector = regionConfig; - } - - if (regionConfig.selector) { - selector = regionConfig.selector; - } - - // get the type for the region - - if (regionIsType){ - RegionType = regionConfig; - } - - if (!regionIsType && regionTypeIsUndefined) { - RegionType = defaultRegionType; - } - - if (regionConfig.regionType) { - RegionType = regionConfig.regionType; - } - - // build the region instance - var region = new RegionType({ - el: selector - }); - - // override the `getEl` function if we have a parentEl - // this must be overridden to ensure the selector is found - // on the first use of the region. if we try to assign the - // region's `el` to `parentEl.find(selector)` in the object - // literal to build the region, the element will not be - // guaranteed to be in the DOM already, and will cause problems - if (regionConfig.parentEl){ - - region.getEl = function(selector) { - var parentEl = regionConfig.parentEl; - if (_.isFunction(parentEl)){ - parentEl = parentEl(); - } - return parentEl.find(selector); - }; - } - - return region; - } - -}); - -// Region Instance Methods -// ----------------------- - -_.extend(Marionette.Region.prototype, Backbone.Events, { - - // Displays a backbone view instance inside of the region. - // Handles calling the `render` method for you. Reads content - // directly from the `el` attribute. Also calls an optional - // `onShow` and `close` method on your view, just after showing - // or just before closing the view, respectively. - show: function(view){ - - this.ensureEl(); - - var isViewClosed = view.isClosed || _.isUndefined(view.$el); - - var isDifferentView = view !== this.currentView; - - if (isDifferentView) { - this.close(); - } - - view.render(); - - if (isDifferentView || isViewClosed) { - this.open(view); - } - - this.currentView = view; - - Marionette.triggerMethod.call(this, "show", view); - Marionette.triggerMethod.call(view, "show"); - }, - - ensureEl: function(){ - if (!this.$el || this.$el.length === 0){ - this.$el = this.getEl(this.el); - } - }, - - // Override this method to change how the region finds the - // DOM element that it manages. Return a jQuery selector object. - getEl: function(selector){ - return Marionette.$(selector); - }, - - // Override this method to change how the new view is - // appended to the `$el` that the region is managing - open: function(view){ - this.$el.empty().append(view.el); - }, - - // Close the current view, if there is one. If there is no - // current view, it does nothing and returns immediately. - close: function(){ - var view = this.currentView; - if (!view || view.isClosed){ return; } - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - Marionette.triggerMethod.call(this, "close"); - - delete this.currentView; - }, - - // Attach an existing view to the region. This - // will not call `render` or `onShow` for the new view, - // and will not replace the current HTML for the `el` - // of the region. - attachView: function(view){ - this.currentView = view; - }, - - // Reset the region by closing any existing view and - // clearing out the cached `$el`. The next time a view - // is shown via this region, the region will re-query the - // DOM for the region's `el`. - reset: function(){ - this.close(); - delete this.$el; - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Region.extend = Marionette.extend; - -// Marionette.RegionManager -// ------------------------ -// -// Manage one or more related `Marionette.Region` objects. -Marionette.RegionManager = (function(Marionette){ - - var RegionManager = Marionette.Controller.extend({ - constructor: function(options){ - this._regions = {}; - Marionette.Controller.prototype.constructor.call(this, options); - }, - - // Add multiple regions using an object literal, where - // each key becomes the region name, and each value is - // the region definition. - addRegions: function(regionDefinitions, defaults){ - var regions = {}; - - _.each(regionDefinitions, function(definition, name){ - if (typeof definition === "string"){ - definition = { selector: definition }; - } - - if (definition.selector){ - definition = _.defaults({}, definition, defaults); - } - - var region = this.addRegion(name, definition); - regions[name] = region; - }, this); - - return regions; - }, - - // Add an individual region to the region manager, - // and return the region instance - addRegion: function(name, definition){ - var region; - - var isObject = _.isObject(definition); - var isString = _.isString(definition); - var hasSelector = !!definition.selector; - - if (isString || (isObject && hasSelector)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else if (_.isFunction(definition)){ - region = Marionette.Region.buildRegion(definition, Marionette.Region); - } else { - region = definition; - } - - this._store(name, region); - this.triggerMethod("region:add", name, region); - return region; - }, - - // Get a region by name - get: function(name){ - return this._regions[name]; - }, - - // Remove a region by name - removeRegion: function(name){ - var region = this._regions[name]; - this._remove(name, region); - }, - - // Close all regions in the region manager, and - // remove them - removeRegions: function(){ - _.each(this._regions, function(region, name){ - this._remove(name, region); - }, this); - }, - - // Close all regions in the region manager, but - // leave them attached - closeRegions: function(){ - _.each(this._regions, function(region, name){ - region.close(); - }, this); - }, - - // Close all regions and shut down the region - // manager entirely - close: function(){ - this.removeRegions(); - var args = Array.prototype.slice.call(arguments); - Marionette.Controller.prototype.close.apply(this, args); - }, - - // internal method to store regions - _store: function(name, region){ - this._regions[name] = region; - this._setLength(); - }, - - // internal method to remove a region - _remove: function(name, region){ - region.close(); - delete this._regions[name]; - this._setLength(); - this.triggerMethod("region:remove", name, region); - }, - - // set the number of regions current held - _setLength: function(){ - this.length = _.size(this._regions); - } - - }); - - // Borrowing this code from Backbone.Collection: - // http://backbonejs.org/docs/backbone.html#section-106 - // - // Mix in methods from Underscore, for iteration, and other - // collection related features. - var methods = ['forEach', 'each', 'map', 'find', 'detect', 'filter', - 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'toArray', 'first', 'initial', 'rest', - 'last', 'without', 'isEmpty', 'pluck']; - - _.each(methods, function(method) { - RegionManager.prototype[method] = function() { - var regions = _.values(this._regions); - var args = [regions].concat(_.toArray(arguments)); - return _[method].apply(_, args); - }; - }); - - return RegionManager; -})(Marionette); - - -// Template Cache -// -------------- - -// Manage templates stored in `<script>` blocks, -// caching them for faster access. -Marionette.TemplateCache = function(templateId){ - this.templateId = templateId; -}; - -// TemplateCache object-level methods. Manage the template -// caches from these method calls instead of creating -// your own TemplateCache instances -_.extend(Marionette.TemplateCache, { - templateCaches: {}, - - // Get the specified template by id. Either - // retrieves the cached version, or loads it - // from the DOM. - get: function(templateId){ - var cachedTemplate = this.templateCaches[templateId]; - - if (!cachedTemplate){ - cachedTemplate = new Marionette.TemplateCache(templateId); - this.templateCaches[templateId] = cachedTemplate; - } - - return cachedTemplate.load(); - }, - - // Clear templates from the cache. If no arguments - // are specified, clears all templates: - // `clear()` - // - // If arguments are specified, clears each of the - // specified templates from the cache: - // `clear("#t1", "#t2", "...")` - clear: function(){ - var i; - var args = slice(arguments); - var length = args.length; - - if (length > 0){ - for(i=0; i<length; i++){ - delete this.templateCaches[args[i]]; - } - } else { - this.templateCaches = {}; - } - } -}); - -// TemplateCache instance methods, allowing each -// template cache object to manage its own state -// and know whether or not it has been loaded -_.extend(Marionette.TemplateCache.prototype, { - - // Internal method to load the template - load: function(){ - // Guard clause to prevent loading this template more than once - if (this.compiledTemplate){ - return this.compiledTemplate; - } - - // Load the template and compile it - var template = this.loadTemplate(this.templateId); - this.compiledTemplate = this.compileTemplate(template); - - return this.compiledTemplate; - }, - - // Load a template from the DOM, by default. Override - // this method to provide your own template retrieval - // For asynchronous loading with AMD/RequireJS, consider - // using a template-loader plugin as described here: - // https://github.com/marionettejs/backbone.marionette/wiki/Using-marionette-with-requirejs - loadTemplate: function(templateId){ - var template = Marionette.$(templateId).html(); - - if (!template || template.length === 0){ - throwError("Could not find template: '" + templateId + "'", "NoTemplateError"); - } - - return template; - }, - - // Pre-compile the template before caching it. Override - // this method if you do not need to pre-compile a template - // (JST / RequireJS for example) or if you want to change - // the template engine used (Handebars, etc). - compileTemplate: function(rawTemplate){ - return _.template(rawTemplate); - } -}); - - -// Renderer -// -------- - -// Render a template with data by passing in the template -// selector and the data to render. -Marionette.Renderer = { - - // Render a template with data. The `template` parameter is - // passed to the `TemplateCache` object to retrieve the - // template function. Override this method to provide your own - // custom rendering and template handling for all of Marionette. - render: function(template, data){ - - if (!template) { - var error = new Error("Cannot render the template since it's false, null or undefined."); - error.name = "TemplateNotFoundError"; - throw error; - } - - var templateFunc; - if (typeof template === "function"){ - templateFunc = template; - } else { - templateFunc = Marionette.TemplateCache.get(template); - } - - return templateFunc(data); - } -}; - - - -// Marionette.View -// --------------- - -// The core view type that other Marionette views extend from. -Marionette.View = Backbone.View.extend({ - - constructor: function(){ - _.bindAll(this, "render"); - - var args = Array.prototype.slice.apply(arguments); - Backbone.View.prototype.constructor.apply(this, args); - - Marionette.MonitorDOMRefresh(this); - this.listenTo(this, "show", this.onShowCalled, this); - }, - - // import the "triggerMethod" to trigger events with corresponding - // methods if the method exists - triggerMethod: Marionette.triggerMethod, - - // Get the template for this view - // instance. You can set a `template` attribute in the view - // definition or pass a `template: "whatever"` parameter in - // to the constructor options. - getTemplate: function(){ - return Marionette.getOption(this, "template"); - }, - - // Mix in template helper methods. Looks for a - // `templateHelpers` attribute, which can either be an - // object literal, or a function that returns an object - // literal. All methods and attributes from this object - // are copies to the object passed in. - mixinTemplateHelpers: function(target){ - target = target || {}; - var templateHelpers = this.templateHelpers; - if (_.isFunction(templateHelpers)){ - templateHelpers = templateHelpers.call(this); - } - return _.extend(target, templateHelpers); - }, - - // Configure `triggers` to forward DOM events to view - // events. `triggers: {"click .foo": "do:foo"}` - configureTriggers: function(){ - if (!this.triggers) { return; } - - var triggerEvents = {}; - - // Allow `triggers` to be configured as a function - var triggers = _.result(this, "triggers"); - - // Configure the triggers, prevent default - // action and stop propagation of DOM events - _.each(triggers, function(value, key){ - - // build the event handler function for the DOM event - triggerEvents[key] = function(e){ - - // stop the event in its tracks - if (e && e.preventDefault){ e.preventDefault(); } - if (e && e.stopPropagation){ e.stopPropagation(); } - - // build the args for the event - var args = { - view: this, - model: this.model, - collection: this.collection - }; - - // trigger the event - this.triggerMethod(value, args); - }; - - }, this); - - return triggerEvents; - }, - - // Overriding Backbone.View's delegateEvents to handle - // the `triggers`, `modelEvents`, and `collectionEvents` configuration - delegateEvents: function(events){ - this._delegateDOMEvents(events); - Marionette.bindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.bindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // internal method to delegate DOM events and triggers - _delegateDOMEvents: function(events){ - events = events || this.events; - if (_.isFunction(events)){ events = events.call(this); } - - var combinedEvents = {}; - var triggers = this.configureTriggers(); - _.extend(combinedEvents, events, triggers); - - Backbone.View.prototype.delegateEvents.call(this, combinedEvents); - }, - - // Overriding Backbone.View's undelegateEvents to handle unbinding - // the `triggers`, `modelEvents`, and `collectionEvents` config - undelegateEvents: function(){ - var args = Array.prototype.slice.call(arguments); - Backbone.View.prototype.undelegateEvents.apply(this, args); - - Marionette.unbindEntityEvents(this, this.model, Marionette.getOption(this, "modelEvents")); - Marionette.unbindEntityEvents(this, this.collection, Marionette.getOption(this, "collectionEvents")); - }, - - // Internal method, handles the `show` event. - onShowCalled: function(){}, - - // Default `close` implementation, for removing a view from the - // DOM and unbinding it. Regions will call this method - // for you. You can specify an `onClose` method in your view to - // add custom code that is called after the view is closed. - close: function(){ - if (this.isClosed) { return; } - - // allow the close to be stopped by returning `false` - // from the `onBeforeClose` method - var shouldClose = this.triggerMethod("before:close"); - if (shouldClose === false){ - return; - } - - // mark as closed before doing the actual close, to - // prevent infinite loops within "close" event handlers - // that are trying to close other views - this.isClosed = true; - this.triggerMethod("close"); - - // unbind UI elements - this.unbindUIElements(); - - // remove the view from the DOM - this.remove(); - }, - - // This method binds the elements specified in the "ui" hash inside the view's code with - // the associated jQuery selectors. - bindUIElements: function(){ - if (!this.ui) { return; } - - // store the ui hash in _uiBindings so they can be reset later - // and so re-rendering the view will be able to find the bindings - if (!this._uiBindings){ - this._uiBindings = this.ui; - } - - // get the bindings result, as a function or otherwise - var bindings = _.result(this, "_uiBindings"); - - // empty the ui so we don't have anything to start with - this.ui = {}; - - // bind each of the selectors - _.each(_.keys(bindings), function(key) { - var selector = bindings[key]; - this.ui[key] = this.$(selector); - }, this); - }, - - // This method unbinds the elements specified in the "ui" hash - unbindUIElements: function(){ - if (!this.ui){ return; } - - // delete all of the existing ui bindings - _.each(this.ui, function($el, name){ - delete this.ui[name]; - }, this); - - // reset the ui element to the original bindings configuration - this.ui = this._uiBindings; - delete this._uiBindings; - } -}); - -// Item View -// --------- - -// A single item view implementation that contains code for rendering -// with underscore.js templates, serializing the view's model or collection, -// and calling several methods on extended views, such as `onRender`. -Marionette.ItemView = Marionette.View.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.View.prototype.constructor which allows overriding - constructor: function(){ - Marionette.View.prototype.constructor.apply(this, slice(arguments)); - }, - - // Serialize the model or collection for the view. If a model is - // found, `.toJSON()` is called. If a collection is found, `.toJSON()` - // is also called, but is used to populate an `items` array in the - // resulting data. If both are found, defaults to the model. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model) { - data = this.model.toJSON(); - } - else if (this.collection) { - data = { items: this.collection.toJSON() }; - } - - return data; - }, - - // Render the view, defaulting to underscore.js templates. - // You can override this in your view definition to provide - // a very specific rendering for your view. In general, though, - // you should override the `Marionette.Renderer` object to - // change how Marionette renders views. - render: function(){ - this.isClosed = false; - - this.triggerMethod("before:render", this); - this.triggerMethod("item:before:render", this); - - var data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - var html = Marionette.Renderer.render(template, data); - - this.$el.html(html); - this.bindUIElements(); - - this.triggerMethod("render", this); - this.triggerMethod("item:rendered", this); - - return this; - }, - - // Override the default close event to add a few - // more events that are triggered. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod('item:before:close'); - - Marionette.View.prototype.close.apply(this, slice(arguments)); - - this.triggerMethod('item:closed'); - } -}); - -// Collection View -// --------------- - -// A view that iterates over a Backbone.Collection -// and renders an individual ItemView for each model. -Marionette.CollectionView = Marionette.View.extend({ - // used as the prefix for item view events - // that are forwarded through the collectionview - itemViewEventPrefix: "itemview", - - // constructor - constructor: function(options){ - this._initChildViewStorage(); - - Marionette.View.prototype.constructor.apply(this, slice(arguments)); - - this._initialEvents(); - }, - - // Configured the initial events that the collection view - // binds to. Override this method to prevent the initial - // events, or to add your own initial events. - _initialEvents: function(){ - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView, this); - this.listenTo(this.collection, "remove", this.removeItemView, this); - this.listenTo(this.collection, "reset", this.render, this); - } - }, - - // Handle a child item added to the collection - addChildView: function(item, collection, options){ - this.closeEmptyView(); - var ItemView = this.getItemView(item); - var index = this.collection.indexOf(item); - this.addItemView(item, ItemView, index); - }, - - // Override from `Marionette.View` to guarantee the `onShow` method - // of child views is called. - onShowCalled: function(){ - this.children.each(function(child){ - Marionette.triggerMethod.call(child, "show"); - }); - }, - - // Internal method to trigger the before render callbacks - // and events - triggerBeforeRender: function(){ - this.triggerMethod("before:render", this); - this.triggerMethod("collection:before:render", this); - }, - - // Internal method to trigger the rendered callbacks and - // events - triggerRendered: function(){ - this.triggerMethod("render", this); - this.triggerMethod("collection:rendered", this); - }, - - // Render the collection of items. Override this method to - // provide your own implementation of a render function for - // the collection view. - render: function(){ - this.isClosed = false; - this.triggerBeforeRender(); - this._renderChildren(); - this.triggerRendered(); - return this; - }, - - // Internal method. Separated so that CompositeView can have - // more control over events being triggered, around the rendering - // process - _renderChildren: function(){ - this.closeEmptyView(); - this.closeChildren(); - - if (this.collection && this.collection.length > 0) { - this.showCollection(); - } else { - this.showEmptyView(); - } - }, - - // Internal method to loop through each item in the - // collection view and show it - showCollection: function(){ - var ItemView; - this.collection.each(function(item, index){ - ItemView = this.getItemView(item); - this.addItemView(item, ItemView, index); - }, this); - }, - - // Internal method to show an empty view in place of - // a collection of item views, when the collection is - // empty - showEmptyView: function(){ - var EmptyView = Marionette.getOption(this, "emptyView"); - - if (EmptyView && !this._showingEmptyView){ - this._showingEmptyView = true; - var model = new Backbone.Model(); - this.addItemView(model, EmptyView, 0); - } - }, - - // Internal method to close an existing emptyView instance - // if one exists. Called when a collection view has been - // rendered empty, and then an item is added to the collection. - closeEmptyView: function(){ - if (this._showingEmptyView){ - this.closeChildren(); - delete this._showingEmptyView; - } - }, - - // Retrieve the itemView type, either from `this.options.itemView` - // or from the `itemView` in the object definition. The "options" - // takes precedence. - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView"); - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Render the child item's view and add it to the - // HTML for the collection view. - addItemView: function(item, ItemView, index){ - // get the itemViewOptions if any were specified - var itemViewOptions = Marionette.getOption(this, "itemViewOptions"); - if (_.isFunction(itemViewOptions)){ - itemViewOptions = itemViewOptions.call(this, item, index); - } - - // build the view - var view = this.buildItemView(item, ItemView, itemViewOptions); - - // set up the child view event forwarding - this.addChildViewEventForwarding(view); - - // this view is about to be added - this.triggerMethod("before:item:added", view); - - // Store the child view itself so we can properly - // remove and/or close it later - this.children.add(view); - - // Render it and show it - this.renderItemView(view, index); - - // call the "show" method if the collection view - // has already been shown - if (this._isShown){ - Marionette.triggerMethod.call(view, "show"); - } - - // this view was added - this.triggerMethod("after:item:added", view); - }, - - // Set up the child view event forwarding. Uses an "itemview:" - // prefix in front of all forwarded events. - addChildViewEventForwarding: function(view){ - var prefix = Marionette.getOption(this, "itemViewEventPrefix"); - - // Forward all child item view events through the parent, - // prepending "itemview:" to the event name - this.listenTo(view, "all", function(){ - var args = slice(arguments); - args[0] = prefix + ":" + args[0]; - args.splice(1, 0, view); - - Marionette.triggerMethod.apply(this, args); - }, this); - }, - - // render the item view - renderItemView: function(view, index) { - view.render(); - this.appendHtml(this, view, index); - }, - - // Build an `itemView` for every model in the collection. - buildItemView: function(item, ItemViewType, itemViewOptions){ - var options = _.extend({model: item}, itemViewOptions); - return new ItemViewType(options); - }, - - // get the child view by item it holds, and remove it - removeItemView: function(item){ - var view = this.children.findByModel(item); - this.removeChildView(view); - this.checkEmpty(); - }, - - // Remove the child view and close it - removeChildView: function(view){ - - // shut down the child view properly, - // including events that the collection has from it - if (view){ - this.stopListening(view); - - // call 'close' or 'remove', depending on which is found - if (view.close) { view.close(); } - else if (view.remove) { view.remove(); } - - this.children.remove(view); - } - - this.triggerMethod("item:removed", view); - }, - - // helper to show the empty view if the collection is empty - checkEmpty: function() { - // check if we're empty now, and if we are, show the - // empty view - if (!this.collection || this.collection.length === 0){ - this.showEmptyView(); - } - }, - - // Append the HTML to the collection's `el`. - // Override this method to do something other - // then `.append`. - appendHtml: function(collectionView, itemView, index){ - collectionView.$el.append(itemView.el); - }, - - // Internal method to set up the `children` object for - // storing all of the child views - _initChildViewStorage: function(){ - this.children = new Backbone.ChildViewContainer(); - }, - - // Handle cleanup and other closing needs for - // the collection of views. - close: function(){ - if (this.isClosed){ return; } - - this.triggerMethod("collection:before:close"); - this.closeChildren(); - this.triggerMethod("collection:closed"); - - Marionette.View.prototype.close.apply(this, slice(arguments)); - }, - - // Close the child views that this collection view - // is holding on to, if any - closeChildren: function(){ - this.children.each(function(child){ - this.removeChildView(child); - }, this); - this.checkEmpty(); - } -}); - - -// Composite View -// -------------- - -// Used for rendering a branch-leaf, hierarchical structure. -// Extends directly from CollectionView and also renders an -// an item view as `modelView`, for the top leaf -Marionette.CompositeView = Marionette.CollectionView.extend({ - - // Setting up the inheritance chain which allows changes to - // Marionette.CollectionView.prototype.constructor which allows overriding - constructor: function(){ - Marionette.CollectionView.prototype.constructor.apply(this, slice(arguments)); - }, - - // Configured the initial events that the composite view - // binds to. Override this method to prevent the initial - // events, or to add your own initial events. - _initialEvents: function(){ - if (this.collection){ - this.listenTo(this.collection, "add", this.addChildView, this); - this.listenTo(this.collection, "remove", this.removeItemView, this); - this.listenTo(this.collection, "reset", this._renderChildren, this); - } - }, - - // Retrieve the `itemView` to be used when rendering each of - // the items in the collection. The default is to return - // `this.itemView` or Marionette.CompositeView if no `itemView` - // has been defined - getItemView: function(item){ - var itemView = Marionette.getOption(this, "itemView") || this.constructor; - - if (!itemView){ - throwError("An `itemView` must be specified", "NoItemViewError"); - } - - return itemView; - }, - - // Serialize the collection for the view. - // You can override the `serializeData` method in your own view - // definition, to provide custom serialization for your view's data. - serializeData: function(){ - var data = {}; - - if (this.model){ - data = this.model.toJSON(); - } - - return data; - }, - - // Renders the model once, and the collection once. Calling - // this again will tell the model's view to re-render itself - // but the collection will not re-render. - render: function(){ - this.isRendered = true; - this.isClosed = false; - this.resetItemViewContainer(); - - this.triggerBeforeRender(); - var html = this.renderModel(); - this.$el.html(html); - // the ui bindings is done here and not at the end of render since they - // will not be available until after the model is rendered, but should be - // available before the collection is rendered. - this.bindUIElements(); - this.triggerMethod("composite:model:rendered"); - - this._renderChildren(); - - this.triggerMethod("composite:rendered"); - this.triggerRendered(); - return this; - }, - - _renderChildren: function(){ - if (this.isRendered){ - Marionette.CollectionView.prototype._renderChildren.call(this); - this.triggerMethod("composite:collection:rendered"); - } - }, - - // Render an individual model, if we have one, as - // part of a composite view (branch / leaf). For example: - // a treeview. - renderModel: function(){ - var data = {}; - data = this.serializeData(); - data = this.mixinTemplateHelpers(data); - - var template = this.getTemplate(); - return Marionette.Renderer.render(template, data); - }, - - // Appends the `el` of itemView instances to the specified - // `itemViewContainer` (a jQuery selector). Override this method to - // provide custom logic of how the child item view instances have their - // HTML appended to the composite view instance. - appendHtml: function(cv, iv, index){ - var $container = this.getItemViewContainer(cv); - $container.append(iv.el); - }, - - // Internal method to ensure an `$itemViewContainer` exists, for the - // `appendHtml` method to use. - getItemViewContainer: function(containerView){ - if ("$itemViewContainer" in containerView){ - return containerView.$itemViewContainer; - } - - var container; - if (containerView.itemViewContainer){ - - var selector = _.result(containerView, "itemViewContainer"); - container = containerView.$(selector); - if (container.length <= 0) { - throwError("The specified `itemViewContainer` was not found: " + containerView.itemViewContainer, "ItemViewContainerMissingError"); - } - - } else { - container = containerView.$el; - } - - containerView.$itemViewContainer = container; - return container; - }, - - // Internal method to reset the `$itemViewContainer` on render - resetItemViewContainer: function(){ - if (this.$itemViewContainer){ - delete this.$itemViewContainer; - } - } -}); - - -// Layout -// ------ - -// Used for managing application layouts, nested layouts and -// multiple regions within an application or sub-application. -// -// A specialized view type that renders an area of HTML and then -// attaches `Region` instances to the specified `regions`. -// Used for composite view management and sub-application areas. -Marionette.Layout = Marionette.ItemView.extend({ - regionType: Marionette.Region, - - // Ensure the regions are available when the `initialize` method - // is called. - constructor: function (options) { - options = options || {}; - - this._firstRender = true; - this._initializeRegions(options); - - Marionette.ItemView.prototype.constructor.call(this, options); - }, - - // Layout's render will use the existing region objects the - // first time it is called. Subsequent calls will close the - // views that the regions are showing and then reset the `el` - // for the regions to the newly rendered DOM elements. - render: function(){ - - if (this._firstRender){ - // if this is the first render, don't do anything to - // reset the regions - this._firstRender = false; - } else if (this.isClosed){ - // a previously closed layout means we need to - // completely re-initialize the regions - this._initializeRegions(); - } else { - // If this is not the first render call, then we need to - // re-initializing the `el` for each region - this._reInitializeRegions(); - } - - var args = Array.prototype.slice.apply(arguments); - var result = Marionette.ItemView.prototype.render.apply(this, args); - - return result; - }, - - // Handle closing regions, and then close the view itself. - close: function () { - if (this.isClosed){ return; } - this.regionManager.close(); - var args = Array.prototype.slice.apply(arguments); - Marionette.ItemView.prototype.close.apply(this, args); - }, - - // Add a single region, by name, to the layout - addRegion: function(name, definition){ - var regions = {}; - regions[name] = definition; - return this.addRegions(regions)[name]; - }, - - // Add multiple regions as a {name: definition, name2: def2} object literal - addRegions: function(regions){ - this.regions = _.extend(this.regions || {}, regions); - return this._buildRegions(regions); - }, - - // Remove a single region from the Layout, by name - removeRegion: function(name){ - return this.regionManager.removeRegion(name); - }, - - // internal method to build regions - _buildRegions: function(regions){ - var that = this; - - var defaults = { - parentEl: function(){ return that.$el; } - }; - - return this.regionManager.addRegions(regions, defaults); - }, - - // Internal method to initialize the regions that have been defined in a - // `regions` attribute on this layout. - _initializeRegions: function (options) { - var regions; - this._initRegionManager(); - - if (_.isFunction(this.regions)) { - regions = this.regions(options); - } else { - regions = this.regions || {}; - } - - this.addRegions(regions); - }, - - // Internal method to re-initialize all of the regions by updating the `el` that - // they point to - _reInitializeRegions: function(){ - this.regionManager.closeRegions(); - this.regionManager.each(function(region){ - region.reset(); - }); - }, - - // Internal method to initialize the region manager - // and all regions in it - _initRegionManager: function(){ - this.regionManager = new Marionette.RegionManager(); - - this.listenTo(this.regionManager, "region:add", function(name, region){ - this[name] = region; - this.trigger("region:add", name, region); - }); - - this.listenTo(this.regionManager, "region:remove", function(name, region){ - delete this[name]; - this.trigger("region:remove", name, region); - }); - } -}); - - -// AppRouter -// --------- - -// Reduce the boilerplate code of handling route events -// and then calling a single method on another object. -// Have your routers configured to call the method on -// your object, directly. -// -// Configure an AppRouter with `appRoutes`. -// -// App routers can only take one `controller` object. -// It is recommended that you divide your controller -// objects in to smaller pieces of related functionality -// and have multiple routers / controllers, instead of -// just one giant router and controller. -// -// You can also add standard routes to an AppRouter. - -Marionette.AppRouter = Backbone.Router.extend({ - - constructor: function(options){ - Backbone.Router.prototype.constructor.apply(this, slice(arguments)); - - this.options = options; - - if (this.appRoutes){ - var controller = Marionette.getOption(this, "controller"); - this.processAppRoutes(controller, this.appRoutes); - } - }, - - // Internal method to process the `appRoutes` for the - // router, and turn them in to routes that trigger the - // specified method on the specified `controller`. - processAppRoutes: function(controller, appRoutes) { - var routeNames = _.keys(appRoutes).reverse(); // Backbone requires reverted order of routes - - _.each(routeNames, function(route) { - var methodName = appRoutes[route]; - var method = controller[methodName]; - - if (!method) { - throw new Error("Method '" + methodName + "' was not found on the controller"); - } - - this.route(route, methodName, _.bind(method, controller)); - }, this); - } -}); - - -// Application -// ----------- - -// Contain and manage the composite application as a whole. -// Stores and starts up `Region` objects, includes an -// event aggregator as `app.vent` -Marionette.Application = function(options){ - this._initRegionManager(); - this._initCallbacks = new Marionette.Callbacks(); - this.vent = new Backbone.Wreqr.EventAggregator(); - this.commands = new Backbone.Wreqr.Commands(); - this.reqres = new Backbone.Wreqr.RequestResponse(); - this.submodules = {}; - - _.extend(this, options); - - this.triggerMethod = Marionette.triggerMethod; -}; - -_.extend(Marionette.Application.prototype, Backbone.Events, { - // Command execution, facilitated by Backbone.Wreqr.Commands - execute: function(){ - var args = Array.prototype.slice.apply(arguments); - this.commands.execute.apply(this.commands, args); - }, - - // Request/response, facilitated by Backbone.Wreqr.RequestResponse - request: function(){ - var args = Array.prototype.slice.apply(arguments); - return this.reqres.request.apply(this.reqres, args); - }, - - // Add an initializer that is either run at when the `start` - // method is called, or run immediately if added after `start` - // has already been called. - addInitializer: function(initializer){ - this._initCallbacks.add(initializer); - }, - - // kick off all of the application's processes. - // initializes all of the regions that have been added - // to the app, and runs all of the initializer functions - start: function(options){ - this.triggerMethod("initialize:before", options); - this._initCallbacks.run(options, this); - this.triggerMethod("initialize:after", options); - - this.triggerMethod("start", options); - }, - - // Add regions to your app. - // Accepts a hash of named strings or Region objects - // addRegions({something: "#someRegion"}) - // addRegions({something: Region.extend({el: "#someRegion"}) }); - addRegions: function(regions){ - return this._regionManager.addRegions(regions); - }, - - // Removes a region from your app. - // Accepts the regions name - // removeRegion('myRegion') - removeRegion: function(region) { - this._regionManager.removeRegion(region); - }, - - // Create a module, attached to the application - module: function(moduleNames, moduleDefinition){ - // slice the args, and add this application object as the - // first argument of the array - var args = slice(arguments); - args.unshift(this); - - // see the Marionette.Module object for more information - return Marionette.Module.create.apply(Marionette.Module, args); - }, - - // Internal method to set up the region manager - _initRegionManager: function(){ - this._regionManager = new Marionette.RegionManager(); - - this.listenTo(this._regionManager, "region:add", function(name, region){ - this[name] = region; - }); - - this.listenTo(this._regionManager, "region:remove", function(name, region){ - delete this[name]; - }); - } -}); - -// Copy the `extend` function used by Backbone's classes -Marionette.Application.extend = Marionette.extend; - -// Module -// ------ - -// A simple module system, used to create privacy and encapsulation in -// Marionette applications -Marionette.Module = function(moduleName, app){ - this.moduleName = moduleName; - - // store sub-modules - this.submodules = {}; - - this._setupInitializersAndFinalizers(); - - // store the configuration for this module - this.app = app; - this.startWithParent = true; - - this.triggerMethod = Marionette.triggerMethod; -}; - -// Extend the Module prototype with events / listenTo, so that the module -// can be used as an event aggregator or pub/sub. -_.extend(Marionette.Module.prototype, Backbone.Events, { - - // Initializer for a specific module. Initializers are run when the - // module's `start` method is called. - addInitializer: function(callback){ - this._initializerCallbacks.add(callback); - }, - - // Finalizers are run when a module is stopped. They are used to teardown - // and finalize any variables, references, events and other code that the - // module had set up. - addFinalizer: function(callback){ - this._finalizerCallbacks.add(callback); - }, - - // Start the module, and run all of its initializers - start: function(options){ - // Prevent re-starting a module that is already started - if (this._isInitialized){ return; } - - // start the sub-modules (depth-first hierarchy) - _.each(this.submodules, function(mod){ - // check to see if we should start the sub-module with this parent - if (mod.startWithParent){ - mod.start(options); - } - }); - - // run the callbacks to "start" the current module - this.triggerMethod("before:start", options); - - this._initializerCallbacks.run(options, this); - this._isInitialized = true; - - this.triggerMethod("start", options); - }, - - // Stop this module by running its finalizers and then stop all of - // the sub-modules for this module - stop: function(){ - // if we are not initialized, don't bother finalizing - if (!this._isInitialized){ return; } - this._isInitialized = false; - - Marionette.triggerMethod.call(this, "before:stop"); - - // stop the sub-modules; depth-first, to make sure the - // sub-modules are stopped / finalized before parents - _.each(this.submodules, function(mod){ mod.stop(); }); - - // run the finalizers - this._finalizerCallbacks.run(undefined,this); - - // reset the initializers and finalizers - this._initializerCallbacks.reset(); - this._finalizerCallbacks.reset(); - - Marionette.triggerMethod.call(this, "stop"); - }, - - // Configure the module with a definition function and any custom args - // that are to be passed in to the definition function - addDefinition: function(moduleDefinition, customArgs){ - this._runModuleDefinition(moduleDefinition, customArgs); - }, - - // Internal method: run the module definition function with the correct - // arguments - _runModuleDefinition: function(definition, customArgs){ - if (!definition){ return; } - - // build the correct list of arguments for the module definition - var args = _.flatten([ - this, - this.app, - Backbone, - Marionette, - Marionette.$, _, - customArgs - ]); - - definition.apply(this, args); - }, - - // Internal method: set up new copies of initializers and finalizers. - // Calling this method will wipe out all existing initializers and - // finalizers. - _setupInitializersAndFinalizers: function(){ - this._initializerCallbacks = new Marionette.Callbacks(); - this._finalizerCallbacks = new Marionette.Callbacks(); - } -}); - -// Type methods to create modules -_.extend(Marionette.Module, { - - // Create a module, hanging off the app parameter as the parent object. - create: function(app, moduleNames, moduleDefinition){ - var module = app; - - // get the custom args passed in after the module definition and - // get rid of the module name and definition function - var customArgs = slice(arguments); - customArgs.splice(0, 3); - - // split the module names and get the length - moduleNames = moduleNames.split("."); - var length = moduleNames.length; - - // store the module definition for the last module in the chain - var moduleDefinitions = []; - moduleDefinitions[length-1] = moduleDefinition; - - // Loop through all the parts of the module definition - _.each(moduleNames, function(moduleName, i){ - var parentModule = module; - module = this._getModule(parentModule, moduleName, app); - this._addModuleDefinition(parentModule, module, moduleDefinitions[i], customArgs); - }, this); - - // Return the last module in the definition chain - return module; - }, - - _getModule: function(parentModule, moduleName, app, def, args){ - // Get an existing module of this name if we have one - var module = parentModule[moduleName]; - - if (!module){ - // Create a new module if we don't have one - module = new Marionette.Module(moduleName, app); - parentModule[moduleName] = module; - // store the module on the parent - parentModule.submodules[moduleName] = module; - } - - return module; - }, - - _addModuleDefinition: function(parentModule, module, def, args){ - var fn; - var startWithParent; - - if (_.isFunction(def)){ - // if a function is supplied for the module definition - fn = def; - startWithParent = true; - - } else if (_.isObject(def)){ - // if an object is supplied - fn = def.define; - startWithParent = def.startWithParent; - - } else { - // if nothing is supplied - startWithParent = true; - } - - // add module definition if needed - if (fn){ - module.addDefinition(fn, args); - } - - // `and` the two together, ensuring a single `false` will prevent it - // from starting with the parent - module.startWithParent = module.startWithParent && startWithParent; - - // setup auto-start if needed - if (module.startWithParent && !module.startWithParentIsConfigured){ - - // only configure this once - module.startWithParentIsConfigured = true; - - // add the module initializer config - parentModule.addInitializer(function(options){ - if (module.startWithParent){ - module.start(options); - } - }); - - } - - } -}); - - - window.Marionette = Marionette; - return Marionette; -})(this, Backbone, _); diff --git a/src/UI/JsLibraries/backbone.modelbinder.js b/src/UI/JsLibraries/backbone.modelbinder.js deleted file mode 100644 index 8dde7b055..000000000 --- a/src/UI/JsLibraries/backbone.modelbinder.js +++ /dev/null @@ -1,576 +0,0 @@ -// Backbone.ModelBinder v1.0.2 -// (c) 2013 Bart Wood -// Distributed Under MIT License - -(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['underscore', 'jquery', 'backbone'], factory); - } else { - // Browser globals - factory(_, $, Backbone); - } -}(function(_, $, Backbone){ - - if(!Backbone){ - throw 'Please include Backbone.js before Backbone.ModelBinder.js'; - } - - Backbone.ModelBinder = function(){ - _.bindAll.apply(_, [this].concat(_.functions(this))); - }; - - // Static setter for class level options - Backbone.ModelBinder.SetOptions = function(options){ - Backbone.ModelBinder.options = options; - }; - - // Current version of the library. - Backbone.ModelBinder.VERSION = '1.0.2'; - Backbone.ModelBinder.Constants = {}; - Backbone.ModelBinder.Constants.ModelToView = 'ModelToView'; - Backbone.ModelBinder.Constants.ViewToModel = 'ViewToModel'; - - _.extend(Backbone.ModelBinder.prototype, { - - bind:function (model, rootEl, attributeBindings, options) { - this.unbind(); - - this._model = model; - this._rootEl = rootEl; - this._setOptions(options); - - if (!this._model) this._throwException('model must be specified'); - if (!this._rootEl) this._throwException('rootEl must be specified'); - - if(attributeBindings){ - // Create a deep clone of the attribute bindings - this._attributeBindings = $.extend(true, {}, attributeBindings); - - this._initializeAttributeBindings(); - this._initializeElBindings(); - } - else { - this._initializeDefaultBindings(); - } - - this._bindModelToView(); - this._bindViewToModel(); - }, - - bindCustomTriggers: function (model, rootEl, triggers, attributeBindings, modelSetOptions) { - this._triggers = triggers; - this.bind(model, rootEl, attributeBindings, modelSetOptions) - }, - - unbind:function () { - this._unbindModelToView(); - this._unbindViewToModel(); - - if(this._attributeBindings){ - delete this._attributeBindings; - this._attributeBindings = undefined; - } - }, - - _setOptions: function(options){ - this._options = _.extend({ - boundAttribute: 'name' - }, Backbone.ModelBinder.options, options); - - // initialize default options - if(!this._options['modelSetOptions']){ - this._options['modelSetOptions'] = {}; - } - this._options['modelSetOptions'].changeSource = 'ModelBinder'; - - if(!this._options['changeTriggers']){ - this._options['changeTriggers'] = {'': 'change', '[contenteditable]': 'blur'}; - } - - if(!this._options['initialCopyDirection']){ - this._options['initialCopyDirection'] = Backbone.ModelBinder.Constants.ModelToView; - } - }, - - // Converts the input bindings, which might just be empty or strings, to binding objects - _initializeAttributeBindings:function () { - var attributeBindingKey, inputBinding, attributeBinding, elementBindingCount, elementBinding; - - for (attributeBindingKey in this._attributeBindings) { - inputBinding = this._attributeBindings[attributeBindingKey]; - - if (_.isString(inputBinding)) { - attributeBinding = {elementBindings: [{selector: inputBinding}]}; - } - else if (_.isArray(inputBinding)) { - attributeBinding = {elementBindings: inputBinding}; - } - else if(_.isObject(inputBinding)){ - attributeBinding = {elementBindings: [inputBinding]}; - } - else { - this._throwException('Unsupported type passed to Model Binder ' + attributeBinding); - } - - // Add a linkage from the element binding back to the attribute binding - for(elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++){ - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - elementBinding.attributeBinding = attributeBinding; - } - - attributeBinding.attributeName = attributeBindingKey; - this._attributeBindings[attributeBindingKey] = attributeBinding; - } - }, - - // If the bindings are not specified, the default binding is performed on the specified attribute, name by default - _initializeDefaultBindings: function(){ - var elCount, elsWithAttribute, matchedEl, name, attributeBinding; - - this._attributeBindings = {}; - elsWithAttribute = $('[' + this._options['boundAttribute'] + ']', this._rootEl); - - for(elCount = 0; elCount < elsWithAttribute.length; elCount++){ - matchedEl = elsWithAttribute[elCount]; - name = $(matchedEl).attr(this._options['boundAttribute']); - - // For elements like radio buttons we only want a single attribute binding with possibly multiple element bindings - if(!this._attributeBindings[name]){ - attributeBinding = {attributeName: name}; - attributeBinding.elementBindings = [{attributeBinding: attributeBinding, boundEls: [matchedEl]}]; - this._attributeBindings[name] = attributeBinding; - } - else{ - this._attributeBindings[name].elementBindings.push({attributeBinding: this._attributeBindings[name], boundEls: [matchedEl]}); - } - } - }, - - _initializeElBindings:function () { - var bindingKey, attributeBinding, bindingCount, elementBinding, foundEls, elCount, el; - for (bindingKey in this._attributeBindings) { - attributeBinding = this._attributeBindings[bindingKey]; - - for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { - elementBinding = attributeBinding.elementBindings[bindingCount]; - if (elementBinding.selector === '') { - foundEls = $(this._rootEl); - } - else { - foundEls = $(elementBinding.selector, this._rootEl); - } - - if (foundEls.length === 0) { - this._throwException('Bad binding found. No elements returned for binding selector ' + elementBinding.selector); - } - else { - elementBinding.boundEls = []; - for (elCount = 0; elCount < foundEls.length; elCount++) { - el = foundEls[elCount]; - elementBinding.boundEls.push(el); - } - } - } - } - }, - - _bindModelToView: function () { - this._model.on('change', this._onModelChange, this); - - if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ModelToView){ - this.copyModelAttributesToView(); - } - }, - - // attributesToCopy is an optional parameter - if empty, all attributes - // that are bound will be copied. Otherwise, only attributeBindings specified - // in the attributesToCopy are copied. - copyModelAttributesToView: function(attributesToCopy){ - var attributeName, attributeBinding; - - for (attributeName in this._attributeBindings) { - if(attributesToCopy === undefined || _.indexOf(attributesToCopy, attributeName) !== -1){ - attributeBinding = this._attributeBindings[attributeName]; - this._copyModelToView(attributeBinding); - } - } - }, - - copyViewValuesToModel: function(){ - var bindingKey, attributeBinding, bindingCount, elementBinding, elCount, el; - for (bindingKey in this._attributeBindings) { - attributeBinding = this._attributeBindings[bindingKey]; - - for (bindingCount = 0; bindingCount < attributeBinding.elementBindings.length; bindingCount++) { - elementBinding = attributeBinding.elementBindings[bindingCount]; - - if(this._isBindingUserEditable(elementBinding)){ - if(this._isBindingRadioGroup(elementBinding)){ - el = this._getRadioButtonGroupCheckedEl(elementBinding); - if(el){ - this._copyViewToModel(elementBinding, el); - } - } - else { - for(elCount = 0; elCount < elementBinding.boundEls.length; elCount++){ - el = $(elementBinding.boundEls[elCount]); - if(this._isElUserEditable(el)){ - this._copyViewToModel(elementBinding, el); - } - } - } - } - } - } - }, - - _unbindModelToView: function(){ - if(this._model){ - this._model.off('change', this._onModelChange); - this._model = undefined; - } - }, - - _bindViewToModel: function () { - _.each(this._options['changeTriggers'], function (event, selector) { - $(this._rootEl).delegate(selector, event, this._onElChanged); - }, this); - - if(this._options['initialCopyDirection'] === Backbone.ModelBinder.Constants.ViewToModel){ - this.copyViewValuesToModel(); - } - }, - - _unbindViewToModel: function () { - if(this._options && this._options['changeTriggers']){ - _.each(this._options['changeTriggers'], function (event, selector) { - $(this._rootEl).undelegate(selector, event, this._onElChanged); - }, this); - } - }, - - _onElChanged:function (event) { - var el, elBindings, elBindingCount, elBinding; - - el = $(event.target)[0]; - elBindings = this._getElBindings(el); - - for(elBindingCount = 0; elBindingCount < elBindings.length; elBindingCount++){ - elBinding = elBindings[elBindingCount]; - if (this._isBindingUserEditable(elBinding)) { - this._copyViewToModel(elBinding, el); - } - } - }, - - _isBindingUserEditable: function(elBinding){ - return elBinding.elAttribute === undefined || - elBinding.elAttribute === 'text' || - elBinding.elAttribute === 'html'; - }, - - _isElUserEditable: function(el){ - var isContentEditable = el.attr('contenteditable'); - return isContentEditable || el.is('input') || el.is('select') || el.is('textarea'); - }, - - _isBindingRadioGroup: function(elBinding){ - var elCount, el; - var isAllRadioButtons = elBinding.boundEls.length > 0; - for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ - el = $(elBinding.boundEls[elCount]); - if(el.attr('type') !== 'radio'){ - isAllRadioButtons = false; - break; - } - } - - return isAllRadioButtons; - }, - - _getRadioButtonGroupCheckedEl: function(elBinding){ - var elCount, el; - for(elCount = 0; elCount < elBinding.boundEls.length; elCount++){ - el = $(elBinding.boundEls[elCount]); - if(el.attr('type') === 'radio' && el.attr('checked')){ - return el; - } - } - - return undefined; - }, - - _getElBindings:function (findEl) { - var attributeName, attributeBinding, elementBindingCount, elementBinding, boundElCount, boundEl; - var elBindings = []; - - for (attributeName in this._attributeBindings) { - attributeBinding = this._attributeBindings[attributeName]; - - for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - - for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { - boundEl = elementBinding.boundEls[boundElCount]; - - if (boundEl === findEl) { - elBindings.push(elementBinding); - } - } - } - } - - return elBindings; - }, - - _onModelChange:function () { - var changedAttribute, attributeBinding; - - for (changedAttribute in this._model.changedAttributes()) { - attributeBinding = this._attributeBindings[changedAttribute]; - - if (attributeBinding) { - this._copyModelToView(attributeBinding); - } - } - }, - - _copyModelToView:function (attributeBinding) { - var elementBindingCount, elementBinding, boundElCount, boundEl, value, convertedValue; - - value = this._model.get(attributeBinding.attributeName); - - for (elementBindingCount = 0; elementBindingCount < attributeBinding.elementBindings.length; elementBindingCount++) { - elementBinding = attributeBinding.elementBindings[elementBindingCount]; - - for (boundElCount = 0; boundElCount < elementBinding.boundEls.length; boundElCount++) { - boundEl = elementBinding.boundEls[boundElCount]; - - if(!boundEl._isSetting){ - convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); - this._setEl($(boundEl), elementBinding, convertedValue); - } - } - } - }, - - _setEl: function (el, elementBinding, convertedValue) { - if (elementBinding.elAttribute) { - this._setElAttribute(el, elementBinding, convertedValue); - } - else { - this._setElValue(el, convertedValue); - } - }, - - _setElAttribute:function (el, elementBinding, convertedValue) { - switch (elementBinding.elAttribute) { - case 'html': - el.html(convertedValue); - break; - case 'text': - el.text(convertedValue); - break; - case 'enabled': - el.prop('disabled', !convertedValue); - break; - case 'displayed': - el[convertedValue ? 'show' : 'hide'](); - break; - case 'hidden': - el[convertedValue ? 'hide' : 'show'](); - break; - case 'css': - el.css(elementBinding.cssAttribute, convertedValue); - break; - case 'class': - var previousValue = this._model.previous(elementBinding.attributeBinding.attributeName); - var currentValue = this._model.get(elementBinding.attributeBinding.attributeName); - // is current value is now defined then remove the class the may have been set for the undefined value - if(!_.isUndefined(previousValue) || !_.isUndefined(currentValue)){ - previousValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, previousValue); - el.removeClass(previousValue); - } - - if(convertedValue){ - el.addClass(convertedValue); - } - break; - default: - el.attr(elementBinding.elAttribute, convertedValue); - } - }, - - _setElValue:function (el, convertedValue) { - if(el.attr('type')){ - switch (el.attr('type')) { - case 'radio': - if (el.val() === convertedValue) { - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked') || _.defer(function() { el.trigger('change'); }); - el.prop('checked', true); - } - else { - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked', false); - } - break; - case 'checkbox': - // must defer the change trigger or the change will actually fire with the old value - el.prop('checked') === !!convertedValue || _.defer(function() { el.trigger('change') }); - el.prop('checked', !!convertedValue); - break; - case 'file': - break; - default: - el.val(convertedValue); - } - } - else if(el.is('input') || el.is('select') || el.is('textarea')){ - el.val(convertedValue || (convertedValue === 0 ? '0' : '')); - } - else { - el.text(convertedValue || (convertedValue === 0 ? '0' : '')); - } - }, - - _copyViewToModel: function (elementBinding, el) { - var result, value, convertedValue; - - if (!el._isSetting) { - - el._isSetting = true; - result = this._setModel(elementBinding, $(el)); - el._isSetting = false; - - if(result && elementBinding.converter){ - value = this._model.get(elementBinding.attributeBinding.attributeName); - convertedValue = this._getConvertedValue(Backbone.ModelBinder.Constants.ModelToView, elementBinding, value); - this._setEl($(el), elementBinding, convertedValue); - } - } - }, - - _getElValue: function(elementBinding, el){ - switch (el.attr('type')) { - case 'checkbox': - return el.prop('checked') ? true : false; - default: - if(el.attr('contenteditable') !== undefined){ - return el.html(); - } - else { - return el.val(); - } - } - }, - - _setModel: function (elementBinding, el) { - var data = {}; - var elVal = this._getElValue(elementBinding, el); - elVal = this._getConvertedValue(Backbone.ModelBinder.Constants.ViewToModel, elementBinding, elVal); - data[elementBinding.attributeBinding.attributeName] = elVal; - return this._model.set(data, this._options['modelSetOptions']); - }, - - _getConvertedValue: function (direction, elementBinding, value) { - if (elementBinding.converter) { - value = elementBinding.converter(direction, value, elementBinding.attributeBinding.attributeName, this._model, elementBinding.boundEls); - } - - return value; - }, - - _throwException: function(message){ - if(this._options.suppressThrows){ - if(console && console.error){ - console.error(message); - } - } - else { - throw message; - } - } - }); - - Backbone.ModelBinder.CollectionConverter = function(collection){ - this._collection = collection; - - if(!this._collection){ - throw 'Collection must be defined'; - } - _.bindAll(this, 'convert'); - }; - - _.extend(Backbone.ModelBinder.CollectionConverter.prototype, { - convert: function(direction, value){ - if (direction === Backbone.ModelBinder.Constants.ModelToView) { - return value ? value.id : undefined; - } - else { - return this._collection.get(value); - } - } - }); - - // A static helper function to create a default set of bindings that you can customize before calling the bind() function - // rootEl - where to find all of the bound elements - // attributeType - probably 'name' or 'id' in most cases - // converter(optional) - the default converter you want applied to all your bindings - // elAttribute(optional) - the default elAttribute you want applied to all your bindings - Backbone.ModelBinder.createDefaultBindings = function(rootEl, attributeType, converter, elAttribute){ - var foundEls, elCount, foundEl, attributeName; - var bindings = {}; - - foundEls = $('[' + attributeType + ']', rootEl); - - for(elCount = 0; elCount < foundEls.length; elCount++){ - foundEl = foundEls[elCount]; - attributeName = $(foundEl).attr(attributeType); - - if(!bindings[attributeName]){ - var attributeBinding = {selector: '[' + attributeType + '="' + attributeName + '"]'}; - bindings[attributeName] = attributeBinding; - - if(converter){ - bindings[attributeName].converter = converter; - } - - if(elAttribute){ - bindings[attributeName].elAttribute = elAttribute; - } - } - } - - return bindings; - }; - - // Helps you to combine 2 sets of bindings - Backbone.ModelBinder.combineBindings = function(destination, source){ - _.each(source, function(value, key){ - var elementBinding = {selector: value.selector}; - - if(value.converter){ - elementBinding.converter = value.converter; - } - - if(value.elAttribute){ - elementBinding.elAttribute = value.elAttribute; - } - - if(!destination[key]){ - destination[key] = elementBinding; - } - else { - destination[key] = [destination[key], elementBinding]; - } - }); - - return destination; - }; - - - return Backbone.ModelBinder; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.pageable.js b/src/UI/JsLibraries/backbone.pageable.js deleted file mode 100644 index f6cdbcacd..000000000 --- a/src/UI/JsLibraries/backbone.pageable.js +++ /dev/null @@ -1,1345 +0,0 @@ -/* - backbone-pageable 1.4.1 - http://github.com/wyuenho/backbone-pageable - - Copyright (c) 2013 Jimmy Yuen Ho Wong - Licensed under the MIT @license. -*/ - -(function (factory) { - - // CommonJS - if (typeof exports == "object") { - module.exports = factory(require("underscore"), require("backbone")); - } - // AMD - else if (typeof define == "function" && define.amd) { - define(["underscore", "backbone"], factory); - } - // Browser - else if (typeof _ !== "undefined" && typeof Backbone !== "undefined") { - var oldPageableCollection = Backbone.PageableCollection; - var PageableCollection = factory(_, Backbone); - - /** - __BROWSER ONLY__ - - If you already have an object named `PageableCollection` attached to the - `Backbone` module, you can use this to return a local reference to this - Backbone.PageableCollection class and reset the name - Backbone.PageableCollection to its previous definition. - - // The left hand side gives you a reference to this - // Backbone.PageableCollection implementation, the right hand side - // resets Backbone.PageableCollection to your other - // Backbone.PageableCollection. - var PageableCollection = Backbone.PageableCollection.noConflict(); - - @static - @member Backbone.PageableCollection - @return {Backbone.PageableCollection} - */ - Backbone.PageableCollection.noConflict = function () { - Backbone.PageableCollection = oldPageableCollection; - return PageableCollection; - }; - } - -}(function (_, Backbone) { - - "use strict"; - - var _extend = _.extend; - var _omit = _.omit; - var _clone = _.clone; - var _each = _.each; - var _pick = _.pick; - var _contains = _.contains; - var _isEmpty = _.isEmpty; - var _pairs = _.pairs; - var _invert = _.invert; - var _isArray = _.isArray; - var _isFunction = _.isFunction; - var _isObject = _.isObject; - var _keys = _.keys; - var _isUndefined = _.isUndefined; - var _result = _.result; - var ceil = Math.ceil; - var floor = Math.floor; - var max = Math.max; - - var BBColProto = Backbone.Collection.prototype; - - function finiteInt (val, name) { - if (!_.isNumber(val) || _.isNaN(val) || !_.isFinite(val) || ~~val !== val) { - throw new TypeError("`" + name + "` must be a finite integer"); - } - return val; - } - - function queryStringToParams (qs) { - var kvp, k, v, ls, params = {}, decode = decodeURIComponent; - var kvps = qs.split('&'); - for (var i = 0, l = kvps.length; i < l; i++) { - var param = kvps[i]; - kvp = param.split('='), k = kvp[0], v = kvp[1] || true; - k = decode(k), v = decode(v), ls = params[k]; - if (_isArray(ls)) ls.push(v); - else if (ls) params[k] = [ls, v]; - else params[k] = v; - } - return params; - } - - // hack to make sure the whatever event handlers for this event is run - // before func is, and the event handlers that func will trigger. - function runOnceAtLastHandler (col, event, func) { - var eventHandlers = col._events[event]; - if (eventHandlers && eventHandlers.length) { - var lastHandler = eventHandlers[eventHandlers.length - 1]; - var oldCallback = lastHandler.callback; - lastHandler.callback = function () { - try { - oldCallback.apply(this, arguments); - func(); - } - catch (e) { - throw e; - } - finally { - lastHandler.callback = oldCallback; - } - }; - } - else func(); - } - - var PARAM_TRIM_RE = /[\s'"]/g; - var URL_TRIM_RE = /[<>\s'"]/g; - - /** - Drop-in replacement for Backbone.Collection. Supports server-side and - client-side pagination and sorting. Client-side mode also support fully - multi-directional synchronization of changes between pages. - - @class Backbone.PageableCollection - @extends Backbone.Collection - */ - var PageableCollection = Backbone.PageableCollection = Backbone.Collection.extend({ - - /** - The container object to store all pagination states. - - You can override the default state by extending this class or specifying - them in an `options` hash to the constructor. - - @property {Object} state - - @property {0|1} [state.firstPage=1] The first page index. Set to 0 if - your server API uses 0-based indices. You should only override this value - during extension, initialization or reset by the server after - fetching. This value should be read only at other times. - - @property {number} [state.lastPage=null] The last page index. This value - is __read only__ and it's calculated based on whether `firstPage` is 0 or - 1, during bootstrapping, fetching and resetting. Please don't change this - value under any circumstances. - - @property {number} [state.currentPage=null] The current page index. You - should only override this value during extension, initialization or reset - by the server after fetching. This value should be read only at other - times. Can be a 0-based or 1-based index, depending on whether - `firstPage` is 0 or 1. If left as default, it will be set to `firstPage` - on initialization. - - @property {number} [state.pageSize=25] How many records to show per - page. This value is __read only__ after initialization, if you want to - change the page size after initialization, you must call #setPageSize. - - @property {number} [state.totalPages=null] How many pages there are. This - value is __read only__ and it is calculated from `totalRecords`. - - @property {number} [state.totalRecords=null] How many records there - are. This value is __required__ under server mode. This value is optional - for client mode as the number will be the same as the number of models - during bootstrapping and during fetching, either supplied by the server - in the metadata, or calculated from the size of the response. - - @property {string} [state.sortKey=null] The model attribute to use for - sorting. - - @property {-1|0|1} [state.order=-1] The order to use for sorting. Specify - -1 for ascending order or 1 for descending order. If 0, no client side - sorting will be done and the order query parameter will not be sent to - the server during a fetch. - */ - state: { - firstPage: 1, - lastPage: null, - currentPage: null, - pageSize: 25, - totalPages: null, - totalRecords: null, - sortKey: null, - order: -1 - }, - - /** - @property {"server"|"client"|"infinite"} [mode="server"] The mode of - operations for this collection. `"server"` paginates on the server-side, - `"client"` paginates on the client-side and `"infinite"` paginates on the - server-side for APIs that do not support `totalRecords`. - */ - mode: "server", - - /** - A translation map to convert Backbone.PageableCollection state attributes - to the query parameters accepted by your server API. - - You can override the default state by extending this class or specifying - them in `options.queryParams` object hash to the constructor. - - @property {Object} queryParams - @property {string} [queryParams.currentPage="page"] - @property {string} [queryParams.pageSize="per_page"] - @property {string} [queryParams.totalPages="total_pages"] - @property {string} [queryParams.totalRecords="total_entries"] - @property {string} [queryParams.sortKey="sort_by"] - @property {string} [queryParams.order="order"] - @property {string} [queryParams.directions={"-1": "asc", "1": "desc"}] A - map for translating a Backbone.PageableCollection#state.order constant to - the ones your server API accepts. - */ - queryParams: { - currentPage: "page", - pageSize: "per_page", - totalPages: "total_pages", - totalRecords: "total_entries", - sortKey: "sort_by", - order: "order", - directions: { - "-1": "asc", - "1": "desc" - } - }, - - /** - __CLIENT MODE ONLY__ - - This collection is the internal storage for the bootstrapped or fetched - models. You can use this if you want to operate on all the pages. - - @property {Backbone.Collection} fullCollection - */ - - /** - Given a list of models or model attributues, bootstraps the full - collection in client mode or infinite mode, or just the page you want in - server mode. - - If you want to initialize a collection to a different state than the - default, you can specify them in `options.state`. Any state parameters - supplied will be merged with the default. If you want to change the - default mapping from #state keys to your server API's query parameter - names, you can specifiy an object hash in `option.queryParams`. Likewise, - any mapping provided will be merged with the default. Lastly, all - Backbone.Collection constructor options are also accepted. - - See: - - - Backbone.PageableCollection#state - - Backbone.PageableCollection#queryParams - - [Backbone.Collection#initialize](http://backbonejs.org/#Collection-constructor) - - @param {Array.<Object>} [models] - - @param {Object} [options] - - @param {function(*, *): number} [options.comparator] If specified, this - comparator is set to the current page under server mode, or the #fullCollection - otherwise. - - @param {boolean} [options.full] If `false` and either a - `options.comparator` or `sortKey` is defined, the comparator is attached - to the current page. Default is `true` under client or infinite mode and - the comparator will be attached to the #fullCollection. - - @param {Object} [options.state] The state attributes overriding the defaults. - - @param {string} [options.state.sortKey] The model attribute to use for - sorting. If specified instead of `options.comparator`, a comparator will - be automatically created using this value, and optionally a sorting order - specified in `options.state.order`. The comparator is then attached to - the new collection instance. - - @param {-1|1} [options.state.order] The order to use for sorting. Specify - -1 for ascending order and 1 for descending order. - - @param {Object} [options.queryParam] - */ - constructor: function (models, options) { - - BBColProto.constructor.apply(this, arguments); - - options = options || {}; - - var mode = this.mode = options.mode || this.mode || PageableProto.mode; - - var queryParams = _extend({}, PageableProto.queryParams, this.queryParams, - options.queryParams || {}); - - queryParams.directions = _extend({}, - PageableProto.queryParams.directions, - this.queryParams.directions, - queryParams.directions || {}); - - this.queryParams = queryParams; - - var state = this.state = _extend({}, PageableProto.state, this.state, - options.state || {}); - - state.currentPage = state.currentPage == null ? - state.firstPage : - state.currentPage; - - if (!_isArray(models)) models = models ? [models] : []; - - if (mode != "server" && state.totalRecords == null && !_isEmpty(models)) { - state.totalRecords = models.length; - } - - this.switchMode(mode, _extend({fetch: false, - resetState: false, - models: models}, options)); - - var comparator = options.comparator; - - if (state.sortKey && !comparator) { - this.setSorting(state.sortKey, state.order, options); - } - - if (mode != "server") { - var fullCollection = this.fullCollection; - - if (comparator && options.full) { - this.comparator = null; - fullCollection.comparator = comparator; - } - - if (options.full) fullCollection.sort(); - - // make sure the models in the current page and full collection have the - // same references - if (models && !_isEmpty(models)) { - this.reset([].slice.call(models), _extend({silent: true}, options)); - this.getPage(state.currentPage); - models.splice.apply(models, [0, models.length].concat(this.models)); - } - } - - this._initState = _clone(this.state); - }, - - /** - Makes a Backbone.Collection that contains all the pages. - - @private - @param {Array.<Object|Backbone.Model>} models - @param {Object} options Options for Backbone.Collection constructor. - @return {Backbone.Collection} - */ - _makeFullCollection: function (models, options) { - - var properties = ["url", "model", "sync", "comparator"]; - var thisProto = this.constructor.prototype; - var i, length, prop; - - var proto = {}; - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (!_isUndefined(thisProto[prop])) { - proto[prop] = thisProto[prop]; - } - } - - var fullCollection = new (Backbone.Collection.extend(proto))(models, options); - - for (i = 0, length = properties.length; i < length; i++) { - prop = properties[i]; - if (this[prop] !== thisProto[prop]) { - fullCollection[prop] = this[prop]; - } - } - - return fullCollection; - }, - - /** - Factory method that returns a Backbone event handler that responses to - the `add`, `remove`, `reset`, and the `sort` events. The returned event - handler will synchronize the current page collection and the full - collection's models. - - @private - - @param {Backbone.PageableCollection} pageCol - @param {Backbone.Collection} fullCol - - @return {function(string, Backbone.Model, Backbone.Collection, Object)} - Collection event handler - */ - _makeCollectionEventHandler: function (pageCol, fullCol) { - - return function collectionEventHandler (event, model, collection, options) { - - var handlers = pageCol._handlers; - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - pageCol.off(event, handler); - fullCol.off(event, handler); - }); - - var state = _clone(pageCol.state); - var firstPage = state.firstPage; - var currentPage = firstPage === 0 ? - state.currentPage : - state.currentPage - 1; - var pageSize = state.pageSize; - var pageStart = currentPage * pageSize, pageEnd = pageStart + pageSize; - - if (event == "add") { - var pageIndex, fullIndex, addAt, colToAdd, options = options || {}; - if (collection == fullCol) { - fullIndex = fullCol.indexOf(model); - if (fullIndex >= pageStart && fullIndex < pageEnd) { - colToAdd = pageCol; - pageIndex = addAt = fullIndex - pageStart; - } - } - else { - pageIndex = pageCol.indexOf(model); - fullIndex = pageStart + pageIndex; - colToAdd = fullCol; - var addAt = !_isUndefined(options.at) ? - options.at + pageStart : - fullIndex; - } - - ++state.totalRecords; - pageCol.state = pageCol._checkState(state); - - if (colToAdd) { - colToAdd.add(model, _extend({}, options || {}, {at: addAt})); - var modelToRemove = pageIndex >= pageSize ? - model : - !_isUndefined(options.at) && addAt < pageEnd && pageCol.length > pageSize ? - pageCol.at(pageSize) : - null; - if (modelToRemove) { - var popOptions = {onAdd: true}; - runOnceAtLastHandler(collection, event, function () { - pageCol.remove(modelToRemove, popOptions); - }); - } - } - } - - // remove the model from the other collection as well - if (event == "remove") { - if (!options.onAdd) { - // decrement totalRecords and update totalPages and lastPage - if (!--state.totalRecords) { - state.totalRecords = null; - state.totalPages = null; - } - else { - var totalPages = state.totalPages = ceil(state.totalRecords / pageSize); - state.lastPage = firstPage === 0 ? totalPages - 1 : totalPages || firstPage; - if (state.currentPage > totalPages) state.currentPage = state.lastPage; - } - pageCol.state = pageCol._checkState(state); - - var nextModel, removedIndex = options.index; - if (collection == pageCol) { - if (nextModel = fullCol.at(pageEnd)) { - runOnceAtLastHandler(pageCol, event, function () { - pageCol.push(nextModel); - }); - } - fullCol.remove(model); - } - else if (removedIndex >= pageStart && removedIndex < pageEnd) { - pageCol.remove(model); - var at = removedIndex + 1 - nextModel = fullCol.at(at) || fullCol.last(); - if (nextModel) pageCol.add(nextModel, {at: at}); - } - } - else delete options.onAdd; - } - - if (event == "reset") { - options = collection; - collection = model; - - // Reset that's not a result of getPage - if (collection == pageCol && options.from == null && - options.to == null) { - var head = fullCol.models.slice(0, pageStart); - var tail = fullCol.models.slice(pageStart + pageCol.models.length); - fullCol.reset(head.concat(pageCol.models).concat(tail), options); - } - else if (collection == fullCol) { - if (!(state.totalRecords = fullCol.models.length)) { - state.totalRecords = null; - state.totalPages = null; - } - if (pageCol.mode == "client") { - state.lastPage = state.currentPage = state.firstPage; - } - pageCol.state = pageCol._checkState(state); - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - if (event == "sort") { - options = collection; - collection = model; - if (collection === fullCol) { - pageCol.reset(fullCol.models.slice(pageStart, pageEnd), - _extend({}, options, {parse: false})); - } - } - - _each(_keys(handlers), function (event) { - var handler = handlers[event]; - _each([pageCol, fullCol], function (col) { - col.on(event, handler); - var callbacks = col._events[event] || []; - callbacks.unshift(callbacks.pop()); - }); - }); - }; - }, - - /** - Sanity check this collection's pagination states. Only perform checks - when all the required pagination state values are defined and not null. - If `totalPages` is undefined or null, it is set to `totalRecords` / - `pageSize`. `lastPage` is set according to whether `firstPage` is 0 or 1 - when no error occurs. - - @private - - @throws {TypeError} If `totalRecords`, `pageSize`, `currentPage` or - `firstPage` is not a finite integer. - - @throws {RangeError} If `pageSize`, `currentPage` or `firstPage` is out - of bounds. - - @return {Object} Returns the `state` object if no error was found. - */ - _checkState: function (state) { - - var mode = this.mode; - var links = this.links; - var totalRecords = state.totalRecords; - var pageSize = state.pageSize; - var currentPage = state.currentPage; - var firstPage = state.firstPage; - var totalPages = state.totalPages; - - if (totalRecords != null && pageSize != null && currentPage != null && - firstPage != null && (mode == "infinite" ? links : true)) { - - totalRecords = finiteInt(totalRecords, "totalRecords"); - pageSize = finiteInt(pageSize, "pageSize"); - currentPage = finiteInt(currentPage, "currentPage"); - firstPage = finiteInt(firstPage, "firstPage"); - - if (pageSize < 1) { - throw new RangeError("`pageSize` must be >= 1"); - } - - totalPages = state.totalPages = ceil(totalRecords / pageSize); - - if (firstPage < 0 || firstPage > 1) { - throw new RangeError("`firstPage must be 0 or 1`"); - } - - state.lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - - if (mode == "infinite") { - if (!links[currentPage + '']) { - throw new RangeError("No link found for page " + currentPage); - } - } - else if (currentPage < firstPage || - (totalPages > 0 && - (firstPage ? currentPage > totalPages : currentPage >= totalPages))) { - var op = firstPage ? ">=" : ">"; - - throw new RangeError("`currentPage` must be firstPage <= currentPage " + - (firstPage ? ">" : ">=") + - " totalPages if " + firstPage + "-based. Got " + - currentPage + '.'); - } - } - - return state; - }, - - /** - Change the page size of this collection. - - Under most if not all circumstances, you should call this method to - change the page size of a pageable collection because it will keep the - pagination state sane. By default, the method will recalculate the - current page number to one that will retain the current page's models - when increasing the page size. When decreasing the page size, this method - will retain the last models to the current page that will fit into the - smaller page size. - - If `options.first` is true, changing the page size will also reset the - current page back to the first page instead of trying to be smart. - - For server mode operations, changing the page size will trigger a #fetch - and subsequently a `reset` event. - - For client mode operations, changing the page size will `reset` the - current page by recalculating the current page boundary on the client - side. - - If `options.fetch` is true, a fetch can be forced if the collection is in - client mode. - - @param {number} pageSize The new page size to set to #state. - @param {Object} [options] {@link #fetch} options. - @param {boolean} [options.first=false] Reset the current page number to - the first page if `true`. - @param {boolean} [options.fetch] If `true`, force a fetch in client mode. - - @throws {TypeError} If `pageSize` is not a finite integer. - @throws {RangeError} If `pageSize` is less than 1. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - setPageSize: function (pageSize, options) { - pageSize = finiteInt(pageSize, "pageSize"); - - options = options || {first: false}; - - var state = this.state; - var totalPages = ceil(state.totalRecords / pageSize); - var currentPage = totalPages ? - max(state.firstPage, - floor(totalPages * - (state.firstPage ? - state.currentPage : - state.currentPage + 1) / - state.totalPages)) : - state.firstPage; - - state = this.state = this._checkState(_extend({}, state, { - pageSize: pageSize, - currentPage: options.first ? state.firstPage : currentPage, - totalPages: totalPages - })); - - return this.getPage(state.currentPage, _omit(options, ["first"])); - }, - - /** - Switching between client, server and infinite mode. - - If switching from client to server mode, the #fullCollection is emptied - first and then deleted and a fetch is immediately issued for the current - page from the server. Pass `false` to `options.fetch` to skip fetching. - - If switching to infinite mode, and if `options.models` is given for an - array of models, #links will be populated with a URL per page, using the - default URL for this collection. - - If switching from server to client mode, all of the pages are immediately - refetched. If you have too many pages, you can pass `false` to - `options.fetch` to skip fetching. - - If switching to any mode from infinite mode, the #links will be deleted. - - @param {"server"|"client"|"infinite"} [mode] The mode to switch to. - - @param {Object} [options] - - @param {boolean} [options.fetch=true] If `false`, no fetching is done. - - @param {boolean} [options.resetState=true] If 'false', the state is not - reset, but checked for sanity instead. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this if `options.fetch` is `false`. - */ - switchMode: function (mode, options) { - - if (!_contains(["server", "client", "infinite"], mode)) { - throw new TypeError('`mode` must be one of "server", "client" or "infinite"'); - } - - options = options || {fetch: true, resetState: true}; - - var state = this.state = options.resetState ? - _clone(this._initState) : - this._checkState(_extend({}, this.state)); - - this.mode = mode; - - var self = this; - var fullCollection = this.fullCollection; - var handlers = this._handlers = this._handlers || {}, handler; - if (mode != "server" && !fullCollection) { - fullCollection = this._makeFullCollection(options.models || [], options); - fullCollection.pageableCollection = this; - this.fullCollection = fullCollection; - var allHandler = this._makeCollectionEventHandler(this, fullCollection); - _each(["add", "remove", "reset", "sort"], function (event) { - handlers[event] = handler = _.bind(allHandler, {}, event); - self.on(event, handler); - fullCollection.on(event, handler); - }); - fullCollection.comparator = this._fullComparator; - } - else if (mode == "server" && fullCollection) { - _each(_keys(handlers), function (event) { - handler = handlers[event]; - self.off(event, handler); - fullCollection.off(event, handler); - }); - delete this._handlers; - this._fullComparator = fullCollection.comparator; - delete this.fullCollection; - } - - if (mode == "infinite") { - var links = this.links = {}; - var firstPage = state.firstPage; - var totalPages = ceil(state.totalRecords / state.pageSize); - var lastPage = firstPage === 0 ? max(0, totalPages - 1) : totalPages || firstPage; - for (var i = state.firstPage; i <= lastPage; i++) { - links[i] = this.url; - } - } - else if (this.links) delete this.links; - - return options.fetch ? - this.fetch(_omit(options, "fetch", "resetState")) : - this; - }, - - /** - @return {boolean} `true` if this collection can page backward, `false` - otherwise. - */ - hasPrevious: function () { - var state = this.state; - var currentPage = state.currentPage; - if (this.mode != "infinite") return currentPage > state.firstPage; - return !!this.links[currentPage - 1]; - }, - - /** - @return {boolean} `true` if this collection can page forward, `false` - otherwise. - */ - hasNext: function () { - var state = this.state; - var currentPage = this.state.currentPage; - if (this.mode != "infinite") return currentPage < state.lastPage; - return !!this.links[currentPage + 1]; - }, - - /** - Fetch the first page in server mode, or reset the current page of this - collection to the first page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getFirstPage: function (options) { - return this.getPage("first", options); - }, - - /** - Fetch the previous page in server mode, or reset the current page of this - collection to the previous page in client or infinite mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPreviousPage: function (options) { - return this.getPage("prev", options); - }, - - /** - Fetch the next page in server mode, or reset the current page of this - collection to the next page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getNextPage: function (options) { - return this.getPage("next", options); - }, - - /** - Fetch the last page in server mode, or reset the current page of this - collection to the last page in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getLastPage: function (options) { - return this.getPage("last", options); - }, - - /** - Given a page index, set #state.currentPage to that index. If this - collection is in server mode, fetch the page using the updated state, - otherwise, reset the current page of this collection to the page - specified by `index` in client mode. If `options.fetch` is true, a fetch - can be forced in client mode before resetting the current page. Under - infinite mode, if the index is less than the current page, a reset is - done as in client mode. If the index is greater than the current page - number, a fetch is made with the results **appended** to #fullCollection. - The current page will then be reset after fetching. - - @param {number|string} index The page index to go to, or the page name to - look up from #links in infinite mode. - @param {Object} [options] {@link #fetch} options or - [reset](http://backbonejs.org/#Collection-reset) options for client mode - when `options.fetch` is `false`. - @param {boolean} [options.fetch=false] If true, force a {@link #fetch} in - client mode. - - @throws {TypeError} If `index` is not a finite integer under server or - client mode, or does not yield a URL from #links under infinite mode. - - @throws {RangeError} If `index` is out of bounds. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPage: function (index, options) { - - var mode = this.mode, fullCollection = this.fullCollection; - - options = options || {fetch: false}; - - var state = this.state, - firstPage = state.firstPage, - currentPage = state.currentPage, - lastPage = state.lastPage, - pageSize = state.pageSize; - - var pageNum = index; - switch (index) { - case "first": pageNum = firstPage; break; - case "prev": pageNum = currentPage - 1; break; - case "next": pageNum = currentPage + 1; break; - case "last": pageNum = lastPage; break; - default: pageNum = finiteInt(index, "index"); - } - - this.state = this._checkState(_extend({}, state, {currentPage: pageNum})); - - options.from = currentPage, options.to = pageNum; - - var pageStart = (firstPage === 0 ? pageNum : pageNum - 1) * pageSize; - var pageModels = fullCollection && fullCollection.length ? - fullCollection.models.slice(pageStart, pageStart + pageSize) : - []; - if ((mode == "client" || (mode == "infinite" && !_isEmpty(pageModels))) && - !options.fetch) { - this.reset(pageModels, _omit(options, "fetch")); - return this; - } - - if (mode == "infinite") options.url = this.links[pageNum]; - - return this.fetch(_omit(options, "fetch")); - }, - - /** - Fetch the page for the provided item offset in server mode, or reset the current page of this - collection to the page for the provided item offset in client mode. - - @param {Object} options {@link #getPage} options. - - @chainable - @return {XMLHttpRequest|Backbone.PageableCollection} The XMLHttpRequest - from fetch or this. - */ - getPageByOffset: function (offset, options) { - if (offset < 0) { - throw new RangeError("`offset must be > 0`"); - } - offset = finiteInt(offset); - - var page = floor(offset / this.state.pageSize); - if (this.state.firstPage !== 0) page++; - if (page > this.state.lastPage) page = this.state.lastPage; - return this.getPage(page, options); - }, - - /** - Overidden to make `getPage` compatible with Zepto. - - @param {string} method - @param {Backbone.Model|Backbone.Collection} model - @param {Object} [options] - - @return {XMLHttpRequest} - */ - sync: function (method, model, options) { - var self = this; - if (self.mode == "infinite") { - var success = options.success; - var currentPage = self.state.currentPage; - options.success = function (resp, status, xhr) { - var links = self.links; - var newLinks = self.parseLinks(resp, _extend({xhr: xhr}, options)); - if (newLinks.first) links[self.state.firstPage] = newLinks.first; - if (newLinks.prev) links[currentPage - 1] = newLinks.prev; - if (newLinks.next) links[currentPage + 1] = newLinks.next; - if (success) success(resp, status, xhr); - }; - } - - return (BBColProto.sync || Backbone.sync).call(self, method, model, options); - }, - - /** - Parse pagination links from the server response. Only valid under - infinite mode. - - Given a response body and a XMLHttpRequest object, extract pagination - links from them for infinite paging. - - This default implementation parses the RFC 5988 `Link` header and extract - 3 links from it - `first`, `prev`, `next`. If a `previous` link is found, - it will be found in the `prev` key in the returned object hash. Any - subclasses overriding this method __must__ return an object hash having - only the keys above. If `first` is missing, the collection's default URL - is assumed to be the `first` URL. If `prev` or `next` is missing, it is - assumed to be `null`. An empty object hash must be returned if there are - no links found. If either the response or the header contains information - pertaining to the total number of records on the server, #state.totalRecords - must be set to that number. The default implementation uses the `last` - link from the header to calculate it. - - @param {*} resp The deserialized response body. - @param {Object} [options] - @param {XMLHttpRequest} [options.xhr] The XMLHttpRequest object for this - response. - @return {Object} - */ - parseLinks: function (resp, options) { - var links = {}; - var linkHeader = options.xhr.getResponseHeader("Link"); - if (linkHeader) { - var relations = ["first", "prev", "previous", "next", "last"]; - _each(linkHeader.split(","), function (linkValue) { - var linkParts = linkValue.split(";"); - var url = linkParts[0].replace(URL_TRIM_RE, ''); - var params = linkParts.slice(1); - _each(params, function (param) { - var paramParts = param.split("="); - var key = paramParts[0].replace(PARAM_TRIM_RE, ''); - var value = paramParts[1].replace(PARAM_TRIM_RE, ''); - if (key == "rel" && _contains(relations, value)) { - if (value == "previous") links.prev = url; - else links[value] = url; - } - }); - }); - - var last = links.last || '', qsi, qs; - if (qs = (qsi = last.indexOf('?')) ? last.slice(qsi + 1) : '') { - var params = queryStringToParams(qs); - - var state = _clone(this.state); - var queryParams = this.queryParams; - var pageSize = state.pageSize; - - var totalRecords = params[queryParams.totalRecords] * 1; - var pageNum = params[queryParams.currentPage] * 1; - var totalPages = params[queryParams.totalPages]; - - if (!totalRecords) { - if (pageNum) totalRecords = (state.firstPage === 0 ? - pageNum + 1 : - pageNum) * pageSize; - else if (totalPages) totalRecords = totalPages * pageSize; - } - - if (totalRecords) state.totalRecords = totalRecords; - - this.state = this._checkState(state); - } - } - - delete links.last; - - return links; - }, - - /** - Parse server response data. - - This default implementation assumes the response data is in one of two - structures: - - [ - {}, // Your new pagination state - [{}, ...] // An array of JSON objects - ] - - Or, - - [{}] // An array of JSON objects - - The first structure is the preferred form because the pagination states - may have been updated on the server side, sending them down again allows - this collection to update its states. If the response has a pagination - state object, it is checked for errors. - - The second structure is the - [Backbone.Collection#parse](http://backbonejs.org/#Collection-parse) - default. - - **Note:** this method has been further simplified since 1.1.7. While - existing #parse implementations will continue to work, new code is - encouraged to override #parseState and #parseRecords instead. - - @param {Object} resp The deserialized response data from the server. - @param {Object} the options for the ajax request - - @return {Array.<Object>} An array of model objects - */ - parse: function (resp, options) { - var newState = this.parseState(resp, _clone(this.queryParams), _clone(this.state), options); - if (newState) this.state = this._checkState(_extend({}, this.state, newState)); - return this.parseRecords(resp, options); - }, - - /** - Parse server response for server pagination state updates. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, a state object is - returned by mapping the server state keys to this pageable collection - instance's query parameter keys using `queryParams`. - - It is __NOT__ neccessary to return a full state object complete with all - the mappings defined in #queryParams. Any state object resulted is merged - with a copy of the current pageable collection state and checked for - sanity before actually updating. Most of the time, simply providing a new - `totalRecords` value is enough to trigger a full pagination state - recalculation. - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: resp.total_entries}; - } - - If you want to use header fields use: - - parseState: function (resp, queryParams, state, options) { - return {totalRecords: options.xhr.getResponseHeader("X-total")}; - } - - This method __MUST__ return a new state object instead of directly - modifying the #state object. The behavior of directly modifying #state is - undefined. - - @param {Object} resp The deserialized response data from the server. - @param {Object} queryParams A copy of #queryParams. - @param {Object} state A copy of #state. - @param {Object} [options] The options passed through from - `parse`. (backbone >= 0.9.10 only) - - @return {Object} A new (partial) state object. - */ - parseState: function (resp, queryParams, state, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - - var newState = _clone(state); - var serverState = resp[0]; - - _each(_pairs(_omit(queryParams, "directions")), function (kvp) { - var k = kvp[0], v = kvp[1]; - var serverVal = serverState[v]; - if (!_isUndefined(serverVal) && !_.isNull(serverVal)) newState[k] = serverState[v]; - }); - - if (serverState.order) { - newState.order = _invert(queryParams.directions)[serverState.order] * 1; - } - - return newState; - } - }, - - /** - Parse server response for an array of model objects. - - This default implementation first checks whether the response has any - state object as documented in #parse. If it exists, the array of model - objects is assumed to be the second element, otherwise the entire - response is returned directly. - - @param {Object} resp The deserialized response data from the server. - @param {Object} [options] The options passed through from the - `parse`. (backbone >= 0.9.10 only) - - @return {Array.<Object>} An array of model objects - */ - parseRecords: function (resp, options) { - if (resp && resp.length === 2 && _isObject(resp[0]) && _isArray(resp[1])) { - return resp[1]; - } - - return resp; - }, - - /** - Fetch a page from the server in server mode, or all the pages in client - mode. Under infinite mode, the current page is refetched by default and - then reset. - - The query string is constructed by translating the current pagination - state to your server API query parameter using #queryParams. The current - page will reset after fetch. - - @param {Object} [options] Accepts all - [Backbone.Collection#fetch](http://backbonejs.org/#Collection-fetch) - options. - - @return {XMLHttpRequest} - */ - fetch: function (options) { - - options = options || {}; - - var state = this._checkState(this.state); - - var mode = this.mode; - - if (mode == "infinite" && !options.url) { - options.url = this.links[state.currentPage]; - } - - var data = options.data || {}; - - // dedup query params - var url = _result(options, "url") || _result(this, "url") || ''; - var qsi = url.indexOf('?'); - if (qsi != -1) { - _extend(data, queryStringToParams(url.slice(qsi + 1))); - url = url.slice(0, qsi); - } - - options.url = url; - options.data = data; - - // map params except directions - var queryParams = this.mode == "client" ? - _pick(this.queryParams, "sortKey", "order") : - _omit(_pick(this.queryParams, _keys(PageableProto.queryParams)), - "directions"); - - var i, kvp, k, v, kvps = _pairs(queryParams), thisCopy = _clone(this); - for (i = 0; i < kvps.length; i++) { - kvp = kvps[i], k = kvp[0], v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (state[k] != null && v != null) { - data[v] = state[k]; - } - } - - // fix up sorting parameters - if (state.sortKey && state.order) { - data[queryParams.order] = this.queryParams.directions[state.order + ""]; - } - else if (!state.sortKey) delete data[queryParams.order]; - - // map extra query parameters - var extraKvps = _pairs(_omit(this.queryParams, - _keys(PageableProto.queryParams))); - for (i = 0; i < extraKvps.length; i++) { - kvp = extraKvps[i]; - v = kvp[1]; - v = _isFunction(v) ? v.call(thisCopy) : v; - if (v != null) data[kvp[0]] = v; - } - - if (mode != "server") { - var self = this, fullCol = this.fullCollection; - var success = options.success; - options.success = function (col, resp, opts) { - - // make sure the caller's intent is obeyed - opts = opts || {}; - if (_isUndefined(options.silent)) delete opts.silent; - else opts.silent = options.silent; - - var models = col.models; - if (mode == "client") fullCol.reset(models, opts); - else fullCol.add(models, _extend({at: fullCol.length}, opts)); - - if (success) success(col, resp, opts); - }; - - // silent the first reset from backbone - return BBColProto.fetch.call(self, _extend({}, options, {silent: true})); - } - - return BBColProto.fetch.call(this, options); - }, - - /** - Convenient method for making a `comparator` sorted by a model attribute - identified by `sortKey` and ordered by `order`. - - Like a Backbone.Collection, a Backbone.PageableCollection will maintain - the __current page__ in sorted order on the client side if a `comparator` - is attached to it. If the collection is in client mode, you can attach a - comparator to #fullCollection to have all the pages reflect the global - sorting order by specifying an option `full` to `true`. You __must__ call - `sort` manually or #fullCollection.sort after calling this method to - force a resort. - - While you can use this method to sort the current page in server mode, - the sorting order may not reflect the global sorting order due to the - additions or removals of the records on the server since the last - fetch. If you want the most updated page in a global sorting order, it is - recommended that you set #state.sortKey and optionally #state.order, and - then call #fetch. - - @protected - - @param {string} [sortKey=this.state.sortKey] See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {(function(Backbone.Model, string): Object) | string} [sortValue] See #setSorting. - - See [Backbone.Collection.comparator](http://backbonejs.org/#Collection-comparator). - */ - _makeComparator: function (sortKey, order, sortValue) { - var state = this.state; - - sortKey = sortKey || state.sortKey; - order = order || state.order; - - if (!sortKey || !order) return; - - if (!sortValue) sortValue = function (model, attr) { - return model.get(attr); - }; - - return function (left, right) { - var l = sortValue(left, sortKey), r = sortValue(right, sortKey), t; - if (order === 1) t = l, l = r, r = t; - if (l === r) return 0; - else if (l < r) return -1; - return 1; - }; - }, - - /** - Adjusts the sorting for this pageable collection. - - Given a `sortKey` and an `order`, sets `state.sortKey` and - `state.order`. A comparator can be applied on the client side to sort in - the order defined if `options.side` is `"client"`. By default the - comparator is applied to the #fullCollection. Set `options.full` to - `false` to apply a comparator to the current page under any mode. Setting - `sortKey` to `null` removes the comparator from both the current page and - the full collection. - - If a `sortValue` function is given, it will be passed the `(model, - sortKey)` arguments and is used to extract a value from the model during - comparison sorts. If `sortValue` is not given, `model.get(sortKey)` is - used for sorting. - - @chainable - - @param {string} sortKey See `state.sortKey`. - @param {number} [order=this.state.order] See `state.order`. - @param {Object} [options] - @param {"server"|"client"} [options.side] By default, `"client"` if - `mode` is `"client"`, `"server"` otherwise. - @param {boolean} [options.full=true] - @param {(function(Backbone.Model, string): Object) | string} [options.sortValue] - */ - setSorting: function (sortKey, order, options) { - - var state = this.state; - - state.sortKey = sortKey; - state.order = order = order || state.order; - - var fullCollection = this.fullCollection; - - var delComp = false, delFullComp = false; - - if (!sortKey) delComp = delFullComp = true; - - var mode = this.mode; - options = _extend({side: mode == "client" ? mode : "server", full: true}, - options); - - var comparator = this._makeComparator(sortKey, order, options.sortValue); - - var full = options.full, side = options.side; - - if (side == "client") { - if (full) { - if (fullCollection) fullCollection.comparator = comparator; - delComp = true; - } - else { - this.comparator = comparator; - delFullComp = true; - } - } - else if (side == "server" && !full) { - this.comparator = comparator; - } - - if (delComp) this.comparator = null; - if (delFullComp && fullCollection) fullCollection.comparator = null; - - return this; - } - - }); - - var PageableProto = PageableCollection.prototype; - - return PageableCollection; - -})); diff --git a/src/UI/JsLibraries/backbone.validation.js b/src/UI/JsLibraries/backbone.validation.js deleted file mode 100644 index d81836168..000000000 --- a/src/UI/JsLibraries/backbone.validation.js +++ /dev/null @@ -1,606 +0,0 @@ -// Backbone.Validation v0.8.1 -// -// Copyright (c) 2011-2013 Thomas Pedersen -// Distributed under MIT License -// -// Documentation and full license available at: -// http://thedersen.com/projects/backbone-validation -Backbone.Validation = (function(_){ - 'use strict'; - - // Default options - // --------------- - - var defaultOptions = { - forceUpdate: false, - selector: 'name', - labelFormatter: 'sentenceCase', - valid: Function.prototype, - invalid: Function.prototype - }; - - - // Helper functions - // ---------------- - - // Formatting functions used for formatting error messages - var formatFunctions = { - // Uses the configured label formatter to format the attribute name - // to make it more readable for the user - formatLabel: function(attrName, model) { - return defaultLabelFormatters[defaultOptions.labelFormatter](attrName, model); - }, - - // Replaces nummeric placeholders like {0} in a string with arguments - // passed to the function - format: function() { - var args = Array.prototype.slice.call(arguments), - text = args.shift(); - return text.replace(/\{(\d+)\}/g, function(match, number) { - return typeof args[number] !== 'undefined' ? args[number] : match; - }); - } - }; - - // Flattens an object - // eg: - // - // var o = { - // address: { - // street: 'Street', - // zip: 1234 - // } - // }; - // - // becomes: - // - // var o = { - // 'address.street': 'Street', - // 'address.zip': 1234 - // }; - var flatten = function (obj, into, prefix) { - into = into || {}; - prefix = prefix || ''; - - _.each(obj, function(val, key) { - if(obj.hasOwnProperty(key)) { - if (val && typeof val === 'object' && !( - val instanceof Array || - val instanceof Date || - val instanceof RegExp || - val instanceof Backbone.Model || - val instanceof Backbone.Collection) - ) { - flatten(val, into, prefix + key + '.'); - } - else { - into[prefix + key] = val; - } - } - }); - - return into; - }; - - // Validation - // ---------- - - var Validation = (function(){ - - // Returns an object with undefined properties for all - // attributes on the model that has defined one or more - // validation rules. - var getValidatedAttrs = function(model) { - return _.reduce(_.keys(model.validation || {}), function(memo, key) { - memo[key] = void 0; - return memo; - }, {}); - }; - - // Looks on the model for validations for a specified - // attribute. Returns an array of any validators defined, - // or an empty array if none is defined. - var getValidators = function(model, attr) { - var attrValidationSet = model.validation ? model.validation[attr] || {} : {}; - - // If the validator is a function or a string, wrap it in a function validator - if (_.isFunction(attrValidationSet) || _.isString(attrValidationSet)) { - attrValidationSet = { - fn: attrValidationSet - }; - } - - // Stick the validator object into an array - if(!_.isArray(attrValidationSet)) { - attrValidationSet = [attrValidationSet]; - } - - // Reduces the array of validators into a new array with objects - // with a validation method to call, the value to validate against - // and the specified error message, if any - return _.reduce(attrValidationSet, function(memo, attrValidation) { - _.each(_.without(_.keys(attrValidation), 'msg'), function(validator) { - memo.push({ - fn: defaultValidators[validator], - val: attrValidation[validator], - msg: attrValidation.msg - }); - }); - return memo; - }, []); - }; - - // Validates an attribute against all validators defined - // for that attribute. If one or more errors are found, - // the first error message is returned. - // If the attribute is valid, an empty string is returned. - var validateAttr = function(model, attr, value, computed) { - // Reduces the array of validators to an error message by - // applying all the validators and returning the first error - // message, if any. - return _.reduce(getValidators(model, attr), function(memo, validator){ - // Pass the format functions plus the default - // validators as the context to the validator - var ctx = _.extend({}, formatFunctions, defaultValidators), - result = validator.fn.call(ctx, value, attr, validator.val, model, computed); - - if(result === false || memo === false) { - return false; - } - if (result && !memo) { - return validator.msg || result; - } - return memo; - }, ''); - }; - - // Loops through the model's attributes and validates them all. - // Returns and object containing names of invalid attributes - // as well as error messages. - var validateModel = function(model, attrs) { - var error, - invalidAttrs = {}, - isValid = true, - computed = _.clone(attrs), - flattened = flatten(attrs); - - _.each(flattened, function(val, attr) { - error = validateAttr(model, attr, val, computed); - if (error) { - invalidAttrs[attr] = error; - isValid = false; - } - }); - - return { - invalidAttrs: invalidAttrs, - isValid: isValid - }; - }; - - // Contains the methods that are mixed in on the model when binding - var mixin = function(view, options) { - return { - - // Check whether or not a value passes validation - // without updating the model - preValidate: function(attr, value) { - return validateAttr(this, attr, value, _.extend({}, this.attributes)); - }, - - // Check to see if an attribute, an array of attributes or the - // entire model is valid. Passing true will force a validation - // of the model. - isValid: function(option) { - var flattened = flatten(this.attributes); - - if(_.isString(option)){ - return !validateAttr(this, option, flattened[option], _.extend({}, this.attributes)); - } - if(_.isArray(option)){ - return _.reduce(option, function(memo, attr) { - return memo && !validateAttr(this, attr, flattened[attr], _.extend({}, this.attributes)); - }, true, this); - } - if(option === true) { - this.validate(); - } - return this.validation ? this._isValid : true; - }, - - // This is called by Backbone when it needs to perform validation. - // You can call it manually without any parameters to validate the - // entire model. - validate: function(attrs, setOptions){ - var model = this, - validateAll = !attrs, - opt = _.extend({}, options, setOptions), - validatedAttrs = getValidatedAttrs(model), - allAttrs = _.extend({}, validatedAttrs, model.attributes, attrs), - changedAttrs = flatten(attrs || allAttrs), - - result = validateModel(model, allAttrs); - - model._isValid = result.isValid; - - // After validation is performed, loop through all changed attributes - // and call the valid callbacks so the view is updated. - _.each(validatedAttrs, function(val, attr){ - var invalid = result.invalidAttrs.hasOwnProperty(attr); - if(!invalid){ - opt.valid(view, attr, opt.selector); - } - }); - - // After validation is performed, loop through all changed attributes - // and call the invalid callback so the view is updated. - _.each(validatedAttrs, function(val, attr){ - var invalid = result.invalidAttrs.hasOwnProperty(attr), - changed = changedAttrs.hasOwnProperty(attr); - - if(invalid && (changed || validateAll)){ - opt.invalid(view, attr, result.invalidAttrs[attr], opt.selector); - } - }); - - // Trigger validated events. - // Need to defer this so the model is actually updated before - // the event is triggered. - _.defer(function() { - model.trigger('validated', model._isValid, model, result.invalidAttrs); - model.trigger('validated:' + (model._isValid ? 'valid' : 'invalid'), model, result.invalidAttrs); - }); - - // Return any error messages to Backbone, unless the forceUpdate flag is set. - // Then we do not return anything and fools Backbone to believe the validation was - // a success. That way Backbone will update the model regardless. - if (!opt.forceUpdate && _.intersection(_.keys(result.invalidAttrs), _.keys(changedAttrs)).length > 0) { - return result.invalidAttrs; - } - } - }; - }; - - // Helper to mix in validation on a model - var bindModel = function(view, model, options) { - _.extend(model, mixin(view, options)); - }; - - // Removes the methods added to a model - var unbindModel = function(model) { - delete model.validate; - delete model.preValidate; - delete model.isValid; - }; - - // Mix in validation on a model whenever a model is - // added to a collection - var collectionAdd = function(model) { - bindModel(this.view, model, this.options); - }; - - // Remove validation from a model whenever a model is - // removed from a collection - var collectionRemove = function(model) { - unbindModel(model); - }; - - // Returns the public methods on Backbone.Validation - return { - - // Current version of the library - version: '0.8.1', - - // Called to configure the default options - configure: function(options) { - _.extend(defaultOptions, options); - }, - - // Hooks up validation on a view with a model - // or collection - bind: function(view, options) { - var model = view.model, - collection = view.collection; - - options = _.extend({}, defaultOptions, defaultCallbacks, options); - - if(typeof model === 'undefined' && typeof collection === 'undefined'){ - throw 'Before you execute the binding your view must have a model or a collection.\n' + - 'See http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.'; - } - - if(model) { - bindModel(view, model, options); - } - else if(collection) { - collection.each(function(model){ - bindModel(view, model, options); - }); - collection.bind('add', collectionAdd, {view: view, options: options}); - collection.bind('remove', collectionRemove); - } - }, - - // Removes validation from a view with a model - // or collection - unbind: function(view) { - var model = view.model, - collection = view.collection; - - if(model) { - unbindModel(view.model); - } - if(collection) { - collection.each(function(model){ - unbindModel(model); - }); - collection.unbind('add', collectionAdd); - collection.unbind('remove', collectionRemove); - } - }, - - // Used to extend the Backbone.Model.prototype - // with validation - mixin: mixin(null, defaultOptions) - }; - }()); - - - // Callbacks - // --------- - - var defaultCallbacks = Validation.callbacks = { - - // Gets called when a previously invalid field in the - // view becomes valid. Removes any error message. - // Should be overridden with custom functionality. - valid: function(view, attr, selector) { - view.$('[' + selector + '~="' + attr + '"]') - .removeClass('invalid') - .removeAttr('data-error'); - }, - - // Gets called when a field in the view becomes invalid. - // Adds a error message. - // Should be overridden with custom functionality. - invalid: function(view, attr, error, selector) { - view.$('[' + selector + '~="' + attr + '"]') - .addClass('invalid') - .attr('data-error', error); - } - }; - - - // Patterns - // -------- - - var defaultPatterns = Validation.patterns = { - // Matches any digit(s) (i.e. 0-9) - digits: /^\d+$/, - - // Matched any number (e.g. 100.000) - number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/, - - // Matches a valid email address (e.g. mail@example.com) - email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i, - - // Mathes any valid url (e.g. http://www.xample.com) - url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i - }; - - - // Error messages - // -------------- - - // Error message for the build in validators. - // {x} gets swapped out with arguments form the validator. - var defaultMessages = Validation.messages = { - required: '{0} is required', - acceptance: '{0} must be accepted', - min: '{0} must be greater than or equal to {1}', - max: '{0} must be less than or equal to {1}', - range: '{0} must be between {1} and {2}', - length: '{0} must be {1} characters', - minLength: '{0} must be at least {1} characters', - maxLength: '{0} must be at most {1} characters', - rangeLength: '{0} must be between {1} and {2} characters', - oneOf: '{0} must be one of: {1}', - equalTo: '{0} must be the same as {1}', - pattern: '{0} must be a valid {1}' - }; - - // Label formatters - // ---------------- - - // Label formatters are used to convert the attribute name - // to a more human friendly label when using the built in - // error messages. - // Configure which one to use with a call to - // - // Backbone.Validation.configure({ - // labelFormatter: 'label' - // }); - var defaultLabelFormatters = Validation.labelFormatters = { - - // Returns the attribute name with applying any formatting - none: function(attrName) { - return attrName; - }, - - // Converts attributeName or attribute_name to Attribute name - sentenceCase: function(attrName) { - return attrName.replace(/(?:^\w|[A-Z]|\b\w)/g, function(match, index) { - return index === 0 ? match.toUpperCase() : ' ' + match.toLowerCase(); - }).replace(/_/g, ' '); - }, - - // Looks for a label configured on the model and returns it - // - // var Model = Backbone.Model.extend({ - // validation: { - // someAttribute: { - // required: true - // } - // }, - // - // labels: { - // someAttribute: 'Custom label' - // } - // }); - label: function(attrName, model) { - return (model.labels && model.labels[attrName]) || defaultLabelFormatters.sentenceCase(attrName, model); - } - }; - - - // Built in validators - // ------------------- - - var defaultValidators = Validation.validators = (function(){ - // Use native trim when defined - var trim = String.prototype.trim ? - function(text) { - return text === null ? '' : String.prototype.trim.call(text); - } : - function(text) { - var trimLeft = /^\s+/, - trimRight = /\s+$/; - - return text === null ? '' : text.toString().replace(trimLeft, '').replace(trimRight, ''); - }; - - // Determines whether or not a value is a number - var isNumber = function(value){ - return _.isNumber(value) || (_.isString(value) && value.match(defaultPatterns.number)); - }; - - // Determines whether or not a value is empty - var hasValue = function(value) { - return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === '') || (_.isArray(value) && _.isEmpty(value))); - }; - - return { - // Function validator - // Lets you implement a custom function used for validation - fn: function(value, attr, fn, model, computed) { - if(_.isString(fn)){ - fn = model[fn]; - } - return fn.call(model, value, attr, computed); - }, - - // Required validator - // Validates if the attribute is required or not - required: function(value, attr, required, model, computed) { - var isRequired = _.isFunction(required) ? required.call(model, value, attr, computed) : required; - if(!isRequired && !hasValue(value)) { - return false; // overrides all other validators - } - if (isRequired && !hasValue(value)) { - return this.format(defaultMessages.required, this.formatLabel(attr, model)); - } - }, - - // Acceptance validator - // Validates that something has to be accepted, e.g. terms of use - // `true` or 'true' are valid - acceptance: function(value, attr, accept, model) { - if(value !== 'true' && (!_.isBoolean(value) || value === false)) { - return this.format(defaultMessages.acceptance, this.formatLabel(attr, model)); - } - }, - - // Min validator - // Validates that the value has to be a number and equal to or greater than - // the min value specified - min: function(value, attr, minValue, model) { - if (!isNumber(value) || value < minValue) { - return this.format(defaultMessages.min, this.formatLabel(attr, model), minValue); - } - }, - - // Max validator - // Validates that the value has to be a number and equal to or less than - // the max value specified - max: function(value, attr, maxValue, model) { - if (!isNumber(value) || value > maxValue) { - return this.format(defaultMessages.max, this.formatLabel(attr, model), maxValue); - } - }, - - // Range validator - // Validates that the value has to be a number and equal to or between - // the two numbers specified - range: function(value, attr, range, model) { - if(!isNumber(value) || value < range[0] || value > range[1]) { - return this.format(defaultMessages.range, this.formatLabel(attr, model), range[0], range[1]); - } - }, - - // Length validator - // Validates that the value has to be a string with length equal to - // the length value specified - length: function(value, attr, length, model) { - if (!hasValue(value) || trim(value).length !== length) { - return this.format(defaultMessages.length, this.formatLabel(attr, model), length); - } - }, - - // Min length validator - // Validates that the value has to be a string with length equal to or greater than - // the min length value specified - minLength: function(value, attr, minLength, model) { - if (!hasValue(value) || trim(value).length < minLength) { - return this.format(defaultMessages.minLength, this.formatLabel(attr, model), minLength); - } - }, - - // Max length validator - // Validates that the value has to be a string with length equal to or less than - // the max length value specified - maxLength: function(value, attr, maxLength, model) { - if (!hasValue(value) || trim(value).length > maxLength) { - return this.format(defaultMessages.maxLength, this.formatLabel(attr, model), maxLength); - } - }, - - // Range length validator - // Validates that the value has to be a string and equal to or between - // the two numbers specified - rangeLength: function(value, attr, range, model) { - if(!hasValue(value) || trim(value).length < range[0] || trim(value).length > range[1]) { - return this.format(defaultMessages.rangeLength, this.formatLabel(attr, model), range[0], range[1]); - } - }, - - // One of validator - // Validates that the value has to be equal to one of the elements in - // the specified array. Case sensitive matching - oneOf: function(value, attr, values, model) { - if(!_.include(values, value)){ - return this.format(defaultMessages.oneOf, this.formatLabel(attr, model), values.join(', ')); - } - }, - - // Equal to validator - // Validates that the value has to be equal to the value of the attribute - // with the name specified - equalTo: function(value, attr, equalTo, model, computed) { - if(value !== computed[equalTo]) { - return this.format(defaultMessages.equalTo, this.formatLabel(attr, model), this.formatLabel(equalTo, model)); - } - }, - - // Pattern validator - // Validates that the value has to match the pattern specified. - // Can be a regular expression or the name of one of the built in patterns - pattern: function(value, attr, pattern, model) { - if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) { - return this.format(defaultMessages.pattern, this.formatLabel(attr, model), pattern); - } - } - }; - }()); - - return Validation; -}(_)); \ No newline at end of file diff --git a/src/UI/JsLibraries/backbone.wreqr.js b/src/UI/JsLibraries/backbone.wreqr.js deleted file mode 100644 index d8aa9bc9b..000000000 --- a/src/UI/JsLibraries/backbone.wreqr.js +++ /dev/null @@ -1,276 +0,0 @@ -(function (root, factory) { - if (typeof exports === 'object') { - - var underscore = require('underscore'); - var backbone = require('backbone'); - - module.exports = factory(underscore, backbone); - - } else if (typeof define === 'function' && define.amd) { - - define(['underscore', 'backbone'], factory); - - } -}(this, function (_, Backbone) { - 'use strict'; - - Backbone.Wreqr = (function(Backbone, Marionette, _){ - 'use strict'; - var Wreqr = {}; - - // Handlers -// -------- -// A registry of functions to call, given a name - -Wreqr.Handlers = (function(Backbone, _){ - 'use strict'; - - // Constructor - // ----------- - - var Handlers = function(options){ - this.options = options; - this._wreqrHandlers = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - Handlers.extend = Backbone.Model.extend; - - // Instance Members - // ---------------- - - _.extend(Handlers.prototype, Backbone.Events, { - - // Add multiple handlers using an object literal configuration - setHandlers: function(handlers){ - _.each(handlers, function(handler, name){ - var context = null; - - if (_.isObject(handler) && !_.isFunction(handler)){ - context = handler.context; - handler = handler.callback; - } - - this.setHandler(name, handler, context); - }, this); - }, - - // Add a handler for the given name, with an - // optional context to run the handler within - setHandler: function(name, handler, context){ - var config = { - callback: handler, - context: context - }; - - this._wreqrHandlers[name] = config; - - this.trigger("handler:add", name, handler, context); - }, - - // Determine whether or not a handler is registered - hasHandler: function(name){ - return !! this._wreqrHandlers[name]; - }, - - // Get the currently registered handler for - // the specified name. Throws an exception if - // no handler is found. - getHandler: function(name){ - var config = this._wreqrHandlers[name]; - - if (!config){ - throw new Error("Handler not found for '" + name + "'"); - } - - return function(){ - var args = Array.prototype.slice.apply(arguments); - return config.callback.apply(config.context, args); - }; - }, - - // Remove a handler for the specified name - removeHandler: function(name){ - delete this._wreqrHandlers[name]; - }, - - // Remove all handlers from this registry - removeAllHandlers: function(){ - this._wreqrHandlers = {}; - } - }); - - return Handlers; -})(Backbone, _); - - // Wreqr.CommandStorage -// -------------------- -// -// Store and retrieve commands for execution. -Wreqr.CommandStorage = (function(){ - 'use strict'; - - // Constructor function - var CommandStorage = function(options){ - this.options = options; - this._commands = {}; - - if (_.isFunction(this.initialize)){ - this.initialize(options); - } - }; - - // Instance methods - _.extend(CommandStorage.prototype, Backbone.Events, { - - // Get an object literal by command name, that contains - // the `commandName` and the `instances` of all commands - // represented as an array of arguments to process - getCommands: function(commandName){ - var commands = this._commands[commandName]; - - // we don't have it, so add it - if (!commands){ - - // build the configuration - commands = { - command: commandName, - instances: [] - }; - - // store it - this._commands[commandName] = commands; - } - - return commands; - }, - - // Add a command by name, to the storage and store the - // args for the command - addCommand: function(commandName, args){ - var command = this.getCommands(commandName); - command.instances.push(args); - }, - - // Clear all commands for the given `commandName` - clearCommands: function(commandName){ - var command = this.getCommands(commandName); - command.instances = []; - } - }); - - return CommandStorage; -})(); - - // Wreqr.Commands -// -------------- -// -// A simple command pattern implementation. Register a command -// handler and execute it. -Wreqr.Commands = (function(Wreqr){ - 'use strict'; - - return Wreqr.Handlers.extend({ - // default storage type - storageType: Wreqr.CommandStorage, - - constructor: function(options){ - this.options = options || {}; - - this._initializeStorage(this.options); - this.on("handler:add", this._executeCommands, this); - - var args = Array.prototype.slice.call(arguments); - Wreqr.Handlers.prototype.constructor.apply(this, args); - }, - - // Execute a named command with the supplied args - execute: function(name, args){ - name = arguments[0]; - args = Array.prototype.slice.call(arguments, 1); - - if (this.hasHandler(name)){ - this.getHandler(name).apply(this, args); - } else { - this.storage.addCommand(name, args); - } - - }, - - // Internal method to handle bulk execution of stored commands - _executeCommands: function(name, handler, context){ - var command = this.storage.getCommands(name); - - // loop through and execute all the stored command instances - _.each(command.instances, function(args){ - handler.apply(context, args); - }); - - this.storage.clearCommands(name); - }, - - // Internal method to initialize storage either from the type's - // `storageType` or the instance `options.storageType`. - _initializeStorage: function(options){ - var storage; - - var StorageType = options.storageType || this.storageType; - if (_.isFunction(StorageType)){ - storage = new StorageType(); - } else { - storage = StorageType; - } - - this.storage = storage; - } - }); - -})(Wreqr); - - // Wreqr.RequestResponse -// --------------------- -// -// A simple request/response implementation. Register a -// request handler, and return a response from it -Wreqr.RequestResponse = (function(Wreqr){ - 'use strict'; - - return Wreqr.Handlers.extend({ - request: function(){ - var name = arguments[0]; - var args = Array.prototype.slice.call(arguments, 1); - - return this.getHandler(name).apply(this, args); - } - }); - -})(Wreqr); - - // Event Aggregator -// ---------------- -// A pub-sub object that can be used to decouple various parts -// of an application through event-driven architecture. - -Wreqr.EventAggregator = (function(Backbone, _){ - 'use strict'; - var EA = function(){}; - - // Copy the `extend` function used by Backbone's classes - EA.extend = Backbone.Model.extend; - - // Copy the basic Backbone.Events on to the event aggregator - _.extend(EA.prototype, Backbone.Events); - - return EA; -})(Backbone, _); - - - return Wreqr; -})(Backbone, Backbone.Marionette, _); - - return Backbone.Wreqr; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/bootstrap.js b/src/UI/JsLibraries/bootstrap.js deleted file mode 100644 index 5debfd7de..000000000 --- a/src/UI/JsLibraries/bootstrap.js +++ /dev/null @@ -1,2363 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under the MIT license - */ - -if (typeof jQuery === 'undefined') { - throw new Error('Bootstrap\'s JavaScript requires jQuery') -} - -+function ($) { - 'use strict'; - var version = $.fn.jquery.split(' ')[0].split('.') - if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { - throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') - } -}(jQuery); - -/* ======================================================================== - * Bootstrap: transition.js v3.3.5 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false - var $el = this - $(this).one('bsTransitionEnd', function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - - if (!$.support.transition) return - - $.event.special.bsTransitionEnd = { - bindType: $.support.transition.end, - delegateType: $.support.transition.end, - handle: function (e) { - if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) - } - } - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: alert.js v3.3.5 - * http://getbootstrap.com/javascript/#alerts - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // ALERT CLASS DEFINITION - // ====================== - - var dismiss = '[data-dismiss="alert"]' - var Alert = function (el) { - $(el).on('click', dismiss, this.close) - } - - Alert.VERSION = '3.3.5' - - Alert.TRANSITION_DURATION = 150 - - Alert.prototype.close = function (e) { - var $this = $(this) - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = $(selector) - - if (e) e.preventDefault() - - if (!$parent.length) { - $parent = $this.closest('.alert') - } - - $parent.trigger(e = $.Event('close.bs.alert')) - - if (e.isDefaultPrevented()) return - - $parent.removeClass('in') - - function removeElement() { - // detach from parent, fire event then clean up data - $parent.detach().trigger('closed.bs.alert').remove() - } - - $.support.transition && $parent.hasClass('fade') ? - $parent - .one('bsTransitionEnd', removeElement) - .emulateTransitionEnd(Alert.TRANSITION_DURATION) : - removeElement() - } - - - // ALERT PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.alert') - - if (!data) $this.data('bs.alert', (data = new Alert(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.alert - - $.fn.alert = Plugin - $.fn.alert.Constructor = Alert - - - // ALERT NO CONFLICT - // ================= - - $.fn.alert.noConflict = function () { - $.fn.alert = old - return this - } - - - // ALERT DATA-API - // ============== - - $(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: button.js v3.3.5 - * http://getbootstrap.com/javascript/#buttons - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // BUTTON PUBLIC CLASS DEFINITION - // ============================== - - var Button = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Button.DEFAULTS, options) - this.isLoading = false - } - - Button.VERSION = '3.3.5' - - Button.DEFAULTS = { - loadingText: 'loading...' - } - - Button.prototype.setState = function (state) { - var d = 'disabled' - var $el = this.$element - var val = $el.is('input') ? 'val' : 'html' - var data = $el.data() - - state += 'Text' - - if (data.resetText == null) $el.data('resetText', $el[val]()) - - // push to event loop to allow forms to submit - setTimeout($.proxy(function () { - $el[val](data[state] == null ? this.options[state] : data[state]) - - if (state == 'loadingText') { - this.isLoading = true - $el.addClass(d).attr(d, d) - } else if (this.isLoading) { - this.isLoading = false - $el.removeClass(d).removeAttr(d) - } - }, this), 0) - } - - Button.prototype.toggle = function () { - var changed = true - var $parent = this.$element.closest('[data-toggle="buttons"]') - - if ($parent.length) { - var $input = this.$element.find('input') - if ($input.prop('type') == 'radio') { - if ($input.prop('checked')) changed = false - $parent.find('.active').removeClass('active') - this.$element.addClass('active') - } else if ($input.prop('type') == 'checkbox') { - if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false - this.$element.toggleClass('active') - } - $input.prop('checked', this.$element.hasClass('active')) - if (changed) $input.trigger('change') - } else { - this.$element.attr('aria-pressed', !this.$element.hasClass('active')) - this.$element.toggleClass('active') - } - } - - - // BUTTON PLUGIN DEFINITION - // ======================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.button') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.button', (data = new Button(this, options))) - - if (option == 'toggle') data.toggle() - else if (option) data.setState(option) - }) - } - - var old = $.fn.button - - $.fn.button = Plugin - $.fn.button.Constructor = Button - - - // BUTTON NO CONFLICT - // ================== - - $.fn.button.noConflict = function () { - $.fn.button = old - return this - } - - - // BUTTON DATA-API - // =============== - - $(document) - .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { - var $btn = $(e.target) - if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn') - Plugin.call($btn, 'toggle') - if (!($(e.target).is('input[type="radio"]') || $(e.target).is('input[type="checkbox"]'))) e.preventDefault() - }) - .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { - $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: carousel.js v3.3.5 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = null - this.sliding = null - this.interval = null - this.$active = null - this.$items = null - - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) - - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) - } - - Carousel.VERSION = '3.3.5' - - Carousel.TRANSITION_DURATION = 600 - - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true - } - - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } - - e.preventDefault() - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.item') - return this.$items.index(item || this.$active) - } - - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this - - if ($next.hasClass('active')) return (this.sliding = false) - - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - - this.sliding = true - - isCycling && this.pause() - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - var old = $.fn.carousel - - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - Plugin.call($target, options) - - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - } - - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: collapse.js v3.3.5 - * http://getbootstrap.com/javascript/#collapse - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // COLLAPSE PUBLIC CLASS DEFINITION - // ================================ - - var Collapse = function (element, options) { - this.$element = $(element) - this.options = $.extend({}, Collapse.DEFAULTS, options) - this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + - '[data-toggle="collapse"][data-target="#' + element.id + '"]') - this.transitioning = null - - if (this.options.parent) { - this.$parent = this.getParent() - } else { - this.addAriaAndCollapsedClass(this.$element, this.$trigger) - } - - if (this.options.toggle) this.toggle() - } - - Collapse.VERSION = '3.3.5' - - Collapse.TRANSITION_DURATION = 350 - - Collapse.DEFAULTS = { - toggle: true - } - - Collapse.prototype.dimension = function () { - var hasWidth = this.$element.hasClass('width') - return hasWidth ? 'width' : 'height' - } - - Collapse.prototype.show = function () { - if (this.transitioning || this.$element.hasClass('in')) return - - var activesData - var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') - - if (actives && actives.length) { - activesData = actives.data('bs.collapse') - if (activesData && activesData.transitioning) return - } - - var startEvent = $.Event('show.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - if (actives && actives.length) { - Plugin.call(actives, 'hide') - activesData || actives.data('bs.collapse', null) - } - - var dimension = this.dimension() - - this.$element - .removeClass('collapse') - .addClass('collapsing')[dimension](0) - .attr('aria-expanded', true) - - this.$trigger - .removeClass('collapsed') - .attr('aria-expanded', true) - - this.transitioning = 1 - - var complete = function () { - this.$element - .removeClass('collapsing') - .addClass('collapse in')[dimension]('') - this.transitioning = 0 - this.$element - .trigger('shown.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - var scrollSize = $.camelCase(['scroll', dimension].join('-')) - - this.$element - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) - } - - Collapse.prototype.hide = function () { - if (this.transitioning || !this.$element.hasClass('in')) return - - var startEvent = $.Event('hide.bs.collapse') - this.$element.trigger(startEvent) - if (startEvent.isDefaultPrevented()) return - - var dimension = this.dimension() - - this.$element[dimension](this.$element[dimension]())[0].offsetHeight - - this.$element - .addClass('collapsing') - .removeClass('collapse in') - .attr('aria-expanded', false) - - this.$trigger - .addClass('collapsed') - .attr('aria-expanded', false) - - this.transitioning = 1 - - var complete = function () { - this.transitioning = 0 - this.$element - .removeClass('collapsing') - .addClass('collapse') - .trigger('hidden.bs.collapse') - } - - if (!$.support.transition) return complete.call(this) - - this.$element - [dimension](0) - .one('bsTransitionEnd', $.proxy(complete, this)) - .emulateTransitionEnd(Collapse.TRANSITION_DURATION) - } - - Collapse.prototype.toggle = function () { - this[this.$element.hasClass('in') ? 'hide' : 'show']() - } - - Collapse.prototype.getParent = function () { - return $(this.options.parent) - .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') - .each($.proxy(function (i, element) { - var $element = $(element) - this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) - }, this)) - .end() - } - - Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { - var isOpen = $element.hasClass('in') - - $element.attr('aria-expanded', isOpen) - $trigger - .toggleClass('collapsed', !isOpen) - .attr('aria-expanded', isOpen) - } - - function getTargetFromTrigger($trigger) { - var href - var target = $trigger.attr('data-target') - || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 - - return $(target) - } - - - // COLLAPSE PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.collapse') - var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false - if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.collapse - - $.fn.collapse = Plugin - $.fn.collapse.Constructor = Collapse - - - // COLLAPSE NO CONFLICT - // ==================== - - $.fn.collapse.noConflict = function () { - $.fn.collapse = old - return this - } - - - // COLLAPSE DATA-API - // ================= - - $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { - var $this = $(this) - - if (!$this.attr('data-target')) e.preventDefault() - - var $target = getTargetFromTrigger($this) - var data = $target.data('bs.collapse') - var option = data ? 'toggle' : $this.data() - - Plugin.call($target, option) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: dropdown.js v3.3.5 - * http://getbootstrap.com/javascript/#dropdowns - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // DROPDOWN CLASS DEFINITION - // ========================= - - var backdrop = '.dropdown-backdrop' - var toggle = '[data-toggle="dropdown"]' - var Dropdown = function (element) { - $(element).on('click.bs.dropdown', this.toggle) - } - - Dropdown.VERSION = '3.3.5' - - function getParent($this) { - var selector = $this.attr('data-target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - var $parent = selector && $(selector) - - return $parent && $parent.length ? $parent : $this.parent() - } - - function clearMenus(e) { - if (e && e.which === 3) return - $(backdrop).remove() - $(toggle).each(function () { - var $this = $(this) - var $parent = getParent($this) - var relatedTarget = { relatedTarget: this } - - if (!$parent.hasClass('open')) return - - if (e && e.type == 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) return - - $parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this.attr('aria-expanded', 'false') - $parent.removeClass('open').trigger('hidden.bs.dropdown', relatedTarget) - }) - } - - Dropdown.prototype.toggle = function (e) { - var $this = $(this) - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) { - // if mobile we use a backdrop because click events don't delegate - $(document.createElement('div')) - .addClass('dropdown-backdrop') - .insertAfter($(this)) - .on('click', clearMenus) - } - - var relatedTarget = { relatedTarget: this } - $parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget)) - - if (e.isDefaultPrevented()) return - - $this - .trigger('focus') - .attr('aria-expanded', 'true') - - $parent - .toggleClass('open') - .trigger('shown.bs.dropdown', relatedTarget) - } - - return false - } - - Dropdown.prototype.keydown = function (e) { - if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return - - var $this = $(this) - - e.preventDefault() - e.stopPropagation() - - if ($this.is('.disabled, :disabled')) return - - var $parent = getParent($this) - var isActive = $parent.hasClass('open') - - if (!isActive && e.which != 27 || isActive && e.which == 27) { - if (e.which == 27) $parent.find(toggle).trigger('focus') - return $this.trigger('click') - } - - var desc = ' li:not(.disabled):visible a' - var $items = $parent.find('.dropdown-menu' + desc) - - if (!$items.length) return - - var index = $items.index(e.target) - - if (e.which == 38 && index > 0) index-- // up - if (e.which == 40 && index < $items.length - 1) index++ // down - if (!~index) index = 0 - - $items.eq(index).trigger('focus') - } - - - // DROPDOWN PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.dropdown') - - if (!data) $this.data('bs.dropdown', (data = new Dropdown(this))) - if (typeof option == 'string') data[option].call($this) - }) - } - - var old = $.fn.dropdown - - $.fn.dropdown = Plugin - $.fn.dropdown.Constructor = Dropdown - - - // DROPDOWN NO CONFLICT - // ==================== - - $.fn.dropdown.noConflict = function () { - $.fn.dropdown = old - return this - } - - - // APPLY TO STANDARD DROPDOWN ELEMENTS - // =================================== - - $(document) - .on('click.bs.dropdown.data-api', clearMenus) - .on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) - .on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle) - .on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown) - .on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: modal.js v3.3.5 - * http://getbootstrap.com/javascript/#modals - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // MODAL CLASS DEFINITION - // ====================== - - var Modal = function (element, options) { - this.options = options - this.$body = $(document.body) - this.$element = $(element) - this.$dialog = this.$element.find('.modal-dialog') - this.$backdrop = null - this.isShown = null - this.originalBodyPad = null - this.scrollbarWidth = 0 - this.ignoreBackdropClick = false - - if (this.options.remote) { - this.$element - .find('.modal-content') - .load(this.options.remote, $.proxy(function () { - this.$element.trigger('loaded.bs.modal') - }, this)) - } - } - - Modal.VERSION = '3.3.5' - - Modal.TRANSITION_DURATION = 300 - Modal.BACKDROP_TRANSITION_DURATION = 150 - - Modal.DEFAULTS = { - backdrop: true, - keyboard: true, - show: true - } - - Modal.prototype.toggle = function (_relatedTarget) { - return this.isShown ? this.hide() : this.show(_relatedTarget) - } - - Modal.prototype.show = function (_relatedTarget) { - var that = this - var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) - - this.$element.trigger(e) - - if (this.isShown || e.isDefaultPrevented()) return - - this.isShown = true - - this.checkScrollbar() - this.setScrollbar() - this.$body.addClass('modal-open') - - this.escape() - this.resize() - - this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) - - this.$dialog.on('mousedown.dismiss.bs.modal', function () { - that.$element.one('mouseup.dismiss.bs.modal', function (e) { - if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true - }) - }) - - this.backdrop(function () { - var transition = $.support.transition && that.$element.hasClass('fade') - - if (!that.$element.parent().length) { - that.$element.appendTo(that.$body) // don't move modals dom position - } - - that.$element - .show() - .scrollTop(0) - - that.adjustDialog() - - if (transition) { - that.$element[0].offsetWidth // force reflow - } - - that.$element.addClass('in') - - that.enforceFocus() - - var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) - - transition ? - that.$dialog // wait for modal to slide in - .one('bsTransitionEnd', function () { - that.$element.trigger('focus').trigger(e) - }) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - that.$element.trigger('focus').trigger(e) - }) - } - - Modal.prototype.hide = function (e) { - if (e) e.preventDefault() - - e = $.Event('hide.bs.modal') - - this.$element.trigger(e) - - if (!this.isShown || e.isDefaultPrevented()) return - - this.isShown = false - - this.escape() - this.resize() - - $(document).off('focusin.bs.modal') - - this.$element - .removeClass('in') - .off('click.dismiss.bs.modal') - .off('mouseup.dismiss.bs.modal') - - this.$dialog.off('mousedown.dismiss.bs.modal') - - $.support.transition && this.$element.hasClass('fade') ? - this.$element - .one('bsTransitionEnd', $.proxy(this.hideModal, this)) - .emulateTransitionEnd(Modal.TRANSITION_DURATION) : - this.hideModal() - } - - Modal.prototype.enforceFocus = function () { - $(document) - .off('focusin.bs.modal') // guard against infinite focus loop - .on('focusin.bs.modal', $.proxy(function (e) { - if (this.$element[0] !== e.target && !this.$element.has(e.target).length) { - this.$element.trigger('focus') - } - }, this)) - } - - Modal.prototype.escape = function () { - if (this.isShown && this.options.keyboard) { - this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { - e.which == 27 && this.hide() - }, this)) - } else if (!this.isShown) { - this.$element.off('keydown.dismiss.bs.modal') - } - } - - Modal.prototype.resize = function () { - if (this.isShown) { - $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) - } else { - $(window).off('resize.bs.modal') - } - } - - Modal.prototype.hideModal = function () { - var that = this - this.$element.hide() - this.backdrop(function () { - that.$body.removeClass('modal-open') - that.resetAdjustments() - that.resetScrollbar() - that.$element.trigger('hidden.bs.modal') - }) - } - - Modal.prototype.removeBackdrop = function () { - this.$backdrop && this.$backdrop.remove() - this.$backdrop = null - } - - Modal.prototype.backdrop = function (callback) { - var that = this - var animate = this.$element.hasClass('fade') ? 'fade' : '' - - if (this.isShown && this.options.backdrop) { - var doAnimate = $.support.transition && animate - - this.$backdrop = $(document.createElement('div')) - .addClass('modal-backdrop ' + animate) - .appendTo(this.$body) - - this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { - if (this.ignoreBackdropClick) { - this.ignoreBackdropClick = false - return - } - if (e.target !== e.currentTarget) return - this.options.backdrop == 'static' - ? this.$element[0].focus() - : this.hide() - }, this)) - - if (doAnimate) this.$backdrop[0].offsetWidth // force reflow - - this.$backdrop.addClass('in') - - if (!callback) return - - doAnimate ? - this.$backdrop - .one('bsTransitionEnd', callback) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callback() - - } else if (!this.isShown && this.$backdrop) { - this.$backdrop.removeClass('in') - - var callbackRemove = function () { - that.removeBackdrop() - callback && callback() - } - $.support.transition && this.$element.hasClass('fade') ? - this.$backdrop - .one('bsTransitionEnd', callbackRemove) - .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : - callbackRemove() - - } else if (callback) { - callback() - } - } - - // these following methods are used to handle overflowing modals - - Modal.prototype.handleUpdate = function () { - this.adjustDialog() - } - - Modal.prototype.adjustDialog = function () { - var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight - - this.$element.css({ - paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', - paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' - }) - } - - Modal.prototype.resetAdjustments = function () { - this.$element.css({ - paddingLeft: '', - paddingRight: '' - }) - } - - Modal.prototype.checkScrollbar = function () { - var fullWindowWidth = window.innerWidth - if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 - var documentElementRect = document.documentElement.getBoundingClientRect() - fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) - } - this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth - this.scrollbarWidth = this.measureScrollbar() - } - - Modal.prototype.setScrollbar = function () { - var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) - this.originalBodyPad = document.body.style.paddingRight || '' - if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) - } - - Modal.prototype.resetScrollbar = function () { - this.$body.css('padding-right', this.originalBodyPad) - } - - Modal.prototype.measureScrollbar = function () { // thx walsh - var scrollDiv = document.createElement('div') - scrollDiv.className = 'modal-scrollbar-measure' - this.$body.append(scrollDiv) - var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth - this.$body[0].removeChild(scrollDiv) - return scrollbarWidth - } - - - // MODAL PLUGIN DEFINITION - // ======================= - - function Plugin(option, _relatedTarget) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.modal') - var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) - - if (!data) $this.data('bs.modal', (data = new Modal(this, options))) - if (typeof option == 'string') data[option](_relatedTarget) - else if (options.show) data.show(_relatedTarget) - }) - } - - var old = $.fn.modal - - $.fn.modal = Plugin - $.fn.modal.Constructor = Modal - - - // MODAL NO CONFLICT - // ================= - - $.fn.modal.noConflict = function () { - $.fn.modal = old - return this - } - - - // MODAL DATA-API - // ============== - - $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { - var $this = $(this) - var href = $this.attr('href') - var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 - var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) - - if ($this.is('a')) e.preventDefault() - - $target.one('show.bs.modal', function (showEvent) { - if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown - $target.one('hidden.bs.modal', function () { - $this.is(':visible') && $this.trigger('focus') - }) - }) - Plugin.call($target, option, this) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tooltip.js v3.3.5 - * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = null - this.options = null - this.enabled = null - this.timeout = null - this.hoverState = null - this.$element = null - this.inState = null - - this.init('tooltip', element, options) - } - - Tooltip.VERSION = '3.3.5' - - Tooltip.TRANSITION_DURATION = 150 - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false, - viewport: { - selector: 'body', - padding: 0 - } - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport)) - this.inState = { click: false, hover: false, focus: false } - - if (this.$element[0] instanceof document.constructor && !this.options.selector) { - throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!') - } - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true - } - - if (self.tip().hasClass('in') || self.hoverState == 'in') { - self.hoverState = 'in' - return - } - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.isInStateTrue = function () { - for (var key in this.inState) { - if (this.inState[key]) return true - } - - return false - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget).data('bs.' + this.type) - - if (!self) { - self = new this.constructor(obj.currentTarget, this.getDelegateOptions()) - $(obj.currentTarget).data('bs.' + this.type, self) - } - - if (obj instanceof $.Event) { - self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false - } - - if (self.isInStateTrue()) return - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0]) - if (e.isDefaultPrevented() || !inDom) return - var that = this - - var $tip = this.tip() - - var tipId = this.getUID(this.type) - - this.setContent() - $tip.attr('id', tipId) - this.$element.attr('aria-describedby', tipId) - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - .data('bs.' + this.type, this) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - this.$element.trigger('inserted.bs.' + this.type) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var orgPlacement = placement - var viewportDim = this.getPosition(this.$viewport) - - placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top' : - placement == 'top' && pos.top - actualHeight < viewportDim.top ? 'bottom' : - placement == 'right' && pos.right + actualWidth > viewportDim.width ? 'left' : - placement == 'left' && pos.left - actualWidth < viewportDim.left ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - - var complete = function () { - var prevHoverState = that.hoverState - that.$element.trigger('shown.bs.' + that.type) - that.hoverState = null - - if (prevHoverState == 'out') that.leave(that) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top += marginTop - offset.left += marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - offset.top = offset.top + height - actualHeight - } - - var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight) - - if (delta.left) offset.left += delta.left - else offset.top += delta.top - - var isVertical = /top|bottom/.test(placement) - var arrowDelta = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight - var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight' - - $tip.offset(offset) - this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) { - this.arrow() - .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%') - .css(isVertical ? 'top' : 'left', '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function (callback) { - var that = this - var $tip = $(this.$tip) - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element - .removeAttr('aria-describedby') - .trigger('hidden.bs.' + that.type) - callback && callback() - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && $tip.hasClass('fade') ? - $tip - .one('bsTransitionEnd', complete) - .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function ($element) { - $element = $element || this.$element - - var el = $element[0] - var isBody = el.tagName == 'BODY' - - var elRect = el.getBoundingClientRect() - if (elRect.width == null) { - // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093 - elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top }) - } - var elOffset = isBody ? { top: 0, left: 0 } : $element.offset() - var scroll = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() } - var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null - - return $.extend({}, elRect, scroll, outerDims, elOffset) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - - } - - Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) { - var delta = { top: 0, left: 0 } - if (!this.$viewport) return delta - - var viewportPadding = this.options.viewport && this.options.viewport.padding || 0 - var viewportDimensions = this.getPosition(this.$viewport) - - if (/right|left/.test(placement)) { - var topEdgeOffset = pos.top - viewportPadding - viewportDimensions.scroll - var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight - if (topEdgeOffset < viewportDimensions.top) { // top overflow - delta.top = viewportDimensions.top - topEdgeOffset - } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow - delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset - } - } else { - var leftEdgeOffset = pos.left - viewportPadding - var rightEdgeOffset = pos.left + viewportPadding + actualWidth - if (leftEdgeOffset < viewportDimensions.left) { // left overflow - delta.left = viewportDimensions.left - leftEdgeOffset - } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow - delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset - } - } - - return delta - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.getUID = function (prefix) { - do prefix += ~~(Math.random() * 1000000) - while (document.getElementById(prefix)) - return prefix - } - - Tooltip.prototype.tip = function () { - if (!this.$tip) { - this.$tip = $(this.options.template) - if (this.$tip.length != 1) { - throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!') - } - } - return this.$tip - } - - Tooltip.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')) - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = this - if (e) { - self = $(e.currentTarget).data('bs.' + this.type) - if (!self) { - self = new this.constructor(e.currentTarget, this.getDelegateOptions()) - $(e.currentTarget).data('bs.' + this.type, self) - } - } - - if (e) { - self.inState.click = !self.inState.click - if (self.isInStateTrue()) self.enter(self) - else self.leave(self) - } else { - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - } - - Tooltip.prototype.destroy = function () { - var that = this - clearTimeout(this.timeout) - this.hide(function () { - that.$element.off('.' + that.type).removeData('bs.' + that.type) - if (that.$tip) { - that.$tip.detach() - } - that.$tip = null - that.$arrow = null - that.$viewport = null - }) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tooltip - - $.fn.tooltip = Plugin - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: popover.js v3.3.5 - * http://getbootstrap.com/javascript/#popovers - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // POPOVER PUBLIC CLASS DEFINITION - // =============================== - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - - Popover.VERSION = '3.3.5' - - Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { - placement: 'right', - trigger: 'click', - content: '', - template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>' - }) - - - // NOTE: POPOVER EXTENDS tooltip.js - // ================================ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) - - Popover.prototype.constructor = Popover - - Popover.prototype.getDefaults = function () { - return Popover.DEFAULTS - } - - Popover.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - var content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content').children().detach().end()[ // we use append for html objects to maintain js events - this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' - ](content) - - $tip.removeClass('fade top bottom left right in') - - // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do - // this manually by checking the contents. - if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() - } - - Popover.prototype.hasContent = function () { - return this.getTitle() || this.getContent() - } - - Popover.prototype.getContent = function () { - var $e = this.$element - var o = this.options - - return $e.attr('data-content') - || (typeof o.content == 'function' ? - o.content.call($e[0]) : - o.content) - } - - Popover.prototype.arrow = function () { - return (this.$arrow = this.$arrow || this.tip().find('.arrow')) - } - - - // POPOVER PLUGIN DEFINITION - // ========================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.popover') - var options = typeof option == 'object' && option - - if (!data && /destroy|hide/.test(option)) return - if (!data) $this.data('bs.popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.popover - - $.fn.popover = Plugin - $.fn.popover.Constructor = Popover - - - // POPOVER NO CONFLICT - // =================== - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: scrollspy.js v3.3.5 - * http://getbootstrap.com/javascript/#scrollspy - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // SCROLLSPY CLASS DEFINITION - // ========================== - - function ScrollSpy(element, options) { - this.$body = $(document.body) - this.$scrollElement = $(element).is(document.body) ? $(window) : $(element) - this.options = $.extend({}, ScrollSpy.DEFAULTS, options) - this.selector = (this.options.target || '') + ' .nav li > a' - this.offsets = [] - this.targets = [] - this.activeTarget = null - this.scrollHeight = 0 - - this.$scrollElement.on('scroll.bs.scrollspy', $.proxy(this.process, this)) - this.refresh() - this.process() - } - - ScrollSpy.VERSION = '3.3.5' - - ScrollSpy.DEFAULTS = { - offset: 10 - } - - ScrollSpy.prototype.getScrollHeight = function () { - return this.$scrollElement[0].scrollHeight || Math.max(this.$body[0].scrollHeight, document.documentElement.scrollHeight) - } - - ScrollSpy.prototype.refresh = function () { - var that = this - var offsetMethod = 'offset' - var offsetBase = 0 - - this.offsets = [] - this.targets = [] - this.scrollHeight = this.getScrollHeight() - - if (!$.isWindow(this.$scrollElement[0])) { - offsetMethod = 'position' - offsetBase = this.$scrollElement.scrollTop() - } - - this.$body - .find(this.selector) - .map(function () { - var $el = $(this) - var href = $el.data('target') || $el.attr('href') - var $href = /^#./.test(href) && $(href) - - return ($href - && $href.length - && $href.is(':visible') - && [[$href[offsetMethod]().top + offsetBase, href]]) || null - }) - .sort(function (a, b) { return a[0] - b[0] }) - .each(function () { - that.offsets.push(this[0]) - that.targets.push(this[1]) - }) - } - - ScrollSpy.prototype.process = function () { - var scrollTop = this.$scrollElement.scrollTop() + this.options.offset - var scrollHeight = this.getScrollHeight() - var maxScroll = this.options.offset + scrollHeight - this.$scrollElement.height() - var offsets = this.offsets - var targets = this.targets - var activeTarget = this.activeTarget - var i - - if (this.scrollHeight != scrollHeight) { - this.refresh() - } - - if (scrollTop >= maxScroll) { - return activeTarget != (i = targets[targets.length - 1]) && this.activate(i) - } - - if (activeTarget && scrollTop < offsets[0]) { - this.activeTarget = null - return this.clear() - } - - for (i = offsets.length; i--;) { - activeTarget != targets[i] - && scrollTop >= offsets[i] - && (offsets[i + 1] === undefined || scrollTop < offsets[i + 1]) - && this.activate(targets[i]) - } - } - - ScrollSpy.prototype.activate = function (target) { - this.activeTarget = target - - this.clear() - - var selector = this.selector + - '[data-target="' + target + '"],' + - this.selector + '[href="' + target + '"]' - - var active = $(selector) - .parents('li') - .addClass('active') - - if (active.parent('.dropdown-menu').length) { - active = active - .closest('li.dropdown') - .addClass('active') - } - - active.trigger('activate.bs.scrollspy') - } - - ScrollSpy.prototype.clear = function () { - $(this.selector) - .parentsUntil(this.options.target, '.active') - .removeClass('active') - } - - - // SCROLLSPY PLUGIN DEFINITION - // =========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.scrollspy') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.scrollspy', (data = new ScrollSpy(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.scrollspy - - $.fn.scrollspy = Plugin - $.fn.scrollspy.Constructor = ScrollSpy - - - // SCROLLSPY NO CONFLICT - // ===================== - - $.fn.scrollspy.noConflict = function () { - $.fn.scrollspy = old - return this - } - - - // SCROLLSPY DATA-API - // ================== - - $(window).on('load.bs.scrollspy.data-api', function () { - $('[data-spy="scroll"]').each(function () { - var $spy = $(this) - Plugin.call($spy, $spy.data()) - }) - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tab.js v3.3.5 - * http://getbootstrap.com/javascript/#tabs - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TAB CLASS DEFINITION - // ==================== - - var Tab = function (element) { - // jscs:disable requireDollarBeforejQueryAssignment - this.element = $(element) - // jscs:enable requireDollarBeforejQueryAssignment - } - - Tab.VERSION = '3.3.5' - - Tab.TRANSITION_DURATION = 150 - - Tab.prototype.show = function () { - var $this = this.element - var $ul = $this.closest('ul:not(.dropdown-menu)') - var selector = $this.data('target') - - if (!selector) { - selector = $this.attr('href') - selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 - } - - if ($this.parent('li').hasClass('active')) return - - var $previous = $ul.find('.active:last a') - var hideEvent = $.Event('hide.bs.tab', { - relatedTarget: $this[0] - }) - var showEvent = $.Event('show.bs.tab', { - relatedTarget: $previous[0] - }) - - $previous.trigger(hideEvent) - $this.trigger(showEvent) - - if (showEvent.isDefaultPrevented() || hideEvent.isDefaultPrevented()) return - - var $target = $(selector) - - this.activate($this.closest('li'), $ul) - this.activate($target, $target.parent(), function () { - $previous.trigger({ - type: 'hidden.bs.tab', - relatedTarget: $this[0] - }) - $this.trigger({ - type: 'shown.bs.tab', - relatedTarget: $previous[0] - }) - }) - } - - Tab.prototype.activate = function (element, container, callback) { - var $active = container.find('> .active') - var transition = callback - && $.support.transition - && ($active.length && $active.hasClass('fade') || !!container.find('> .fade').length) - - function next() { - $active - .removeClass('active') - .find('> .dropdown-menu > .active') - .removeClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', false) - - element - .addClass('active') - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - - if (transition) { - element[0].offsetWidth // reflow for transition - element.addClass('in') - } else { - element.removeClass('fade') - } - - if (element.parent('.dropdown-menu').length) { - element - .closest('li.dropdown') - .addClass('active') - .end() - .find('[data-toggle="tab"]') - .attr('aria-expanded', true) - } - - callback && callback() - } - - $active.length && transition ? - $active - .one('bsTransitionEnd', next) - .emulateTransitionEnd(Tab.TRANSITION_DURATION) : - next() - - $active.removeClass('in') - } - - - // TAB PLUGIN DEFINITION - // ===================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tab') - - if (!data) $this.data('bs.tab', (data = new Tab(this))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.tab - - $.fn.tab = Plugin - $.fn.tab.Constructor = Tab - - - // TAB NO CONFLICT - // =============== - - $.fn.tab.noConflict = function () { - $.fn.tab = old - return this - } - - - // TAB DATA-API - // ============ - - var clickHandler = function (e) { - e.preventDefault() - Plugin.call($(this), 'show') - } - - $(document) - .on('click.bs.tab.data-api', '[data-toggle="tab"]', clickHandler) - .on('click.bs.tab.data-api', '[data-toggle="pill"]', clickHandler) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: affix.js v3.3.5 - * http://getbootstrap.com/javascript/#affix - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // AFFIX CLASS DEFINITION - // ====================== - - var Affix = function (element, options) { - this.options = $.extend({}, Affix.DEFAULTS, options) - - this.$target = $(this.options.target) - .on('scroll.bs.affix.data-api', $.proxy(this.checkPosition, this)) - .on('click.bs.affix.data-api', $.proxy(this.checkPositionWithEventLoop, this)) - - this.$element = $(element) - this.affixed = null - this.unpin = null - this.pinnedOffset = null - - this.checkPosition() - } - - Affix.VERSION = '3.3.5' - - Affix.RESET = 'affix affix-top affix-bottom' - - Affix.DEFAULTS = { - offset: 0, - target: window - } - - Affix.prototype.getState = function (scrollHeight, height, offsetTop, offsetBottom) { - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - var targetHeight = this.$target.height() - - if (offsetTop != null && this.affixed == 'top') return scrollTop < offsetTop ? 'top' : false - - if (this.affixed == 'bottom') { - if (offsetTop != null) return (scrollTop + this.unpin <= position.top) ? false : 'bottom' - return (scrollTop + targetHeight <= scrollHeight - offsetBottom) ? false : 'bottom' - } - - var initializing = this.affixed == null - var colliderTop = initializing ? scrollTop : position.top - var colliderHeight = initializing ? targetHeight : height - - if (offsetTop != null && scrollTop <= offsetTop) return 'top' - if (offsetBottom != null && (colliderTop + colliderHeight >= scrollHeight - offsetBottom)) return 'bottom' - - return false - } - - Affix.prototype.getPinnedOffset = function () { - if (this.pinnedOffset) return this.pinnedOffset - this.$element.removeClass(Affix.RESET).addClass('affix') - var scrollTop = this.$target.scrollTop() - var position = this.$element.offset() - return (this.pinnedOffset = position.top - scrollTop) - } - - Affix.prototype.checkPositionWithEventLoop = function () { - setTimeout($.proxy(this.checkPosition, this), 1) - } - - Affix.prototype.checkPosition = function () { - if (!this.$element.is(':visible')) return - - var height = this.$element.height() - var offset = this.options.offset - var offsetTop = offset.top - var offsetBottom = offset.bottom - var scrollHeight = Math.max($(document).height(), $(document.body).height()) - - if (typeof offset != 'object') offsetBottom = offsetTop = offset - if (typeof offsetTop == 'function') offsetTop = offset.top(this.$element) - if (typeof offsetBottom == 'function') offsetBottom = offset.bottom(this.$element) - - var affix = this.getState(scrollHeight, height, offsetTop, offsetBottom) - - if (this.affixed != affix) { - if (this.unpin != null) this.$element.css('top', '') - - var affixType = 'affix' + (affix ? '-' + affix : '') - var e = $.Event(affixType + '.bs.affix') - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - this.affixed = affix - this.unpin = affix == 'bottom' ? this.getPinnedOffset() : null - - this.$element - .removeClass(Affix.RESET) - .addClass(affixType) - .trigger(affixType.replace('affix', 'affixed') + '.bs.affix') - } - - if (affix == 'bottom') { - this.$element.offset({ - top: scrollHeight - height - offsetBottom - }) - } - } - - - // AFFIX PLUGIN DEFINITION - // ======================= - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.affix') - var options = typeof option == 'object' && option - - if (!data) $this.data('bs.affix', (data = new Affix(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.affix - - $.fn.affix = Plugin - $.fn.affix.Constructor = Affix - - - // AFFIX NO CONFLICT - // ================= - - $.fn.affix.noConflict = function () { - $.fn.affix = old - return this - } - - - // AFFIX DATA-API - // ============== - - $(window).on('load', function () { - $('[data-spy="affix"]').each(function () { - var $spy = $(this) - var data = $spy.data() - - data.offset = data.offset || {} - - if (data.offsetBottom != null) data.offset.bottom = data.offsetBottom - if (data.offsetTop != null) data.offset.top = data.offsetTop - - Plugin.call($spy, data) - }) - }) - -}(jQuery); diff --git a/src/UI/JsLibraries/bootstrap.tagsinput.js b/src/UI/JsLibraries/bootstrap.tagsinput.js deleted file mode 100644 index 93e7548a4..000000000 --- a/src/UI/JsLibraries/bootstrap.tagsinput.js +++ /dev/null @@ -1,617 +0,0 @@ -(function ($) { - "use strict"; - - var defaultOptions = { - tagClass: function(item) { - return 'label label-info'; - }, - itemValue: function(item) { - return item ? item.toString() : item; - }, - itemText: function(item) { - return this.itemValue(item); - }, - freeInput: true, - addOnBlur: true, - maxTags: undefined, - maxChars: undefined, - confirmKeys: [13, 44], - onTagExists: function(item, $tag) { - $tag.hide().fadeIn(); - }, - trimValue: false, - allowDuplicates: false - }; - - /** - * Constructor function - */ - function TagsInput(element, options) { - this.itemsArray = []; - - this.$element = $(element); - this.$element.hide(); - - this.isSelect = (element.tagName === 'SELECT'); - this.multiple = (this.isSelect && element.hasAttribute('multiple')); - this.objectItems = options && options.itemValue; - this.placeholderText = element.hasAttribute('placeholder') ? this.$element.attr('placeholder') : ''; - this.inputSize = Math.max(1, this.placeholderText.length); - - this.$container = $('<div class="bootstrap-tagsinput"></div>'); - this.$input = $('<input type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container); - - this.$element.after(this.$container); - -// var inputWidth = (this.inputSize < 3 ? 3 : this.inputSize) + "em"; -// this.$input.get(0).style.cssText = "width: " + inputWidth + " !important;"; - this.build(options); - } - - TagsInput.prototype = { - constructor: TagsInput, - - /** - * Adds the given item as a new tag. Pass true to dontPushVal to prevent - * updating the elements val() - */ - add: function(item, dontPushVal) { - var self = this; - - if (self.options.maxTags && self.itemsArray.length >= self.options.maxTags) - return; - - // Ignore falsey values, except false - if (item !== false && !item) - return; - - // Trim value - if (typeof item === "string" && self.options.trimValue) { - item = $.trim(item); - } - - // Throw an error when trying to add an object while the itemValue option was not set - if (typeof item === "object" && !self.objectItems) - throw("Can't add objects when itemValue option is not set"); - - // Ignore strings only containg whitespace - if (item.toString().match(/^\s*$/)) - return; - - // If SELECT but not multiple, remove current tag - if (self.isSelect && !self.multiple && self.itemsArray.length > 0) - self.remove(self.itemsArray[0]); - - if (typeof item === "string" && this.$element[0].tagName === 'INPUT') { - var items = item.split(','); - if (items.length > 1) { - for (var i = 0; i < items.length; i++) { - this.add(items[i], true); - } - - if (!dontPushVal) - self.pushVal(); - return; - } - } - - var itemValue = self.options.itemValue(item), - itemText = self.options.itemText(item), - tagClass = self.options.tagClass(item); - - // Ignore items allready added - var existing = $.grep(self.itemsArray, function(item) { return self.options.itemValue(item) === itemValue; } )[0]; - if (existing && !self.options.allowDuplicates) { - // Invoke onTagExists - if (self.options.onTagExists) { - var $existingTag = $(".tag", self.$container).filter(function() { return $(this).data("item") === existing; }); - self.options.onTagExists(item, $existingTag); - } - return; - } - - // if length greater than limit - if (self.items().toString().length + item.length + 1 > self.options.maxInputLength) - return; - - // raise beforeItemAdd arg - var beforeItemAddEvent = $.Event('beforeItemAdd', { item: item, cancel: false }); - self.$element.trigger(beforeItemAddEvent); - if (beforeItemAddEvent.cancel) - return; - - // register item in internal array and map - self.itemsArray.push(item); - - // add a tag element - var $tag = $('<span class="tag ' + htmlEncode(tagClass) + '">' + htmlEncode(itemText) + '<span data-role="remove"></span></span>'); - $tag.data('item', item); - self.findInputWrapper().before($tag); - $tag.after(' '); - - // add <option /> if item represents a value not present in one of the <select />'s options - if (self.isSelect && !$('option[value="' + encodeURIComponent(itemValue) + '"]',self.$element)[0]) { - var $option = $('<option selected>' + htmlEncode(itemText) + '</option>'); - $option.data('item', item); - $option.attr('value', itemValue); - self.$element.append($option); - } - - if (!dontPushVal) - self.pushVal(); - - // Add class when reached maxTags - if (self.options.maxTags === self.itemsArray.length || self.items().toString().length === self.options.maxInputLength) - self.$container.addClass('bootstrap-tagsinput-max'); - - self.$element.trigger($.Event('itemAdded', { item: item })); - }, - - /** - * Removes the given item. Pass true to dontPushVal to prevent updating the - * elements val() - */ - remove: function(item, dontPushVal) { - var self = this; - - if (self.objectItems) { - if (typeof item === "object") - item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == self.options.itemValue(item); } ); - else - item = $.grep(self.itemsArray, function(other) { return self.options.itemValue(other) == item; } ); - - item = item[item.length-1]; - } - - if (item) { - var beforeItemRemoveEvent = $.Event('beforeItemRemove', { item: item, cancel: false }); - self.$element.trigger(beforeItemRemoveEvent); - if (beforeItemRemoveEvent.cancel) - return; - - $('.tag', self.$container).filter(function() { return $(this).data('item') === item; }).remove(); - $('option', self.$element).filter(function() { return $(this).data('item') === item; }).remove(); - if($.inArray(item, self.itemsArray) !== -1) - self.itemsArray.splice($.inArray(item, self.itemsArray), 1); - } - - if (!dontPushVal) - self.pushVal(); - - // Remove class when reached maxTags - if (self.options.maxTags > self.itemsArray.length) - self.$container.removeClass('bootstrap-tagsinput-max'); - - self.$element.trigger($.Event('itemRemoved', { item: item })); - }, - - /** - * Removes all items - */ - removeAll: function() { - var self = this; - - $('.tag', self.$container).remove(); - $('option', self.$element).remove(); - - while(self.itemsArray.length > 0) - self.itemsArray.pop(); - - self.pushVal(); - }, - - /** - * Refreshes the tags so they match the text/value of their corresponding - * item. - */ - refresh: function() { - var self = this; - $('.tag', self.$container).each(function() { - var $tag = $(this), - item = $tag.data('item'), - itemValue = self.options.itemValue(item), - itemText = self.options.itemText(item), - tagClass = self.options.tagClass(item); - - // Update tag's class and inner text - $tag.attr('class', null); - $tag.addClass('tag ' + htmlEncode(tagClass)); - $tag.contents().filter(function() { - return this.nodeType == 3; - })[0].nodeValue = htmlEncode(itemText); - - if (self.isSelect) { - var option = $('option', self.$element).filter(function() { return $(this).data('item') === item; }); - option.attr('value', itemValue); - } - }); - }, - - /** - * Returns the items added as tags - */ - items: function() { - return this.itemsArray; - }, - - /** - * Assembly value by retrieving the value of each item, and set it on the - * element. - */ - pushVal: function() { - var self = this, - val = $.map(self.items(), function(item) { - return self.options.itemValue(item).toString(); - }); - - self.$element.val(val, true).trigger('change'); - }, - - /** - * Initializes the tags input behaviour on the element - */ - build: function(options) { - var self = this; - - self.options = $.extend({}, defaultOptions, options); - // When itemValue is set, freeInput should always be false - if (self.objectItems) - self.options.freeInput = false; - - makeOptionItemFunction(self.options, 'itemValue'); - makeOptionItemFunction(self.options, 'itemText'); - makeOptionFunction(self.options, 'tagClass'); - - // Typeahead Bootstrap version 2.3.2 - if (self.options.typeahead) { - var typeahead = self.options.typeahead || {}; - - makeOptionFunction(typeahead, 'source'); - - self.$input.typeahead($.extend({}, typeahead, { - source: function (query, process) { - function processItems(items) { - var texts = []; - - for (var i = 0; i < items.length; i++) { - var text = self.options.itemText(items[i]); - map[text] = items[i]; - texts.push(text); - } - process(texts); - } - - this.map = {}; - var map = this.map, - data = typeahead.source(query); - - if ($.isFunction(data.success)) { - // support for Angular callbacks - data.success(processItems); - } else if ($.isFunction(data.then)) { - // support for Angular promises - data.then(processItems); - } else { - // support for functions and jquery promises - $.when(data) - .then(processItems); - } - }, - updater: function (text) { - self.add(this.map[text]); - }, - matcher: function (text) { - return (text.toLowerCase().indexOf(this.query.trim().toLowerCase()) !== -1); - }, - sorter: function (texts) { - return texts.sort(); - }, - highlighter: function (text) { - var regex = new RegExp( '(' + this.query + ')', 'gi' ); - return text.replace( regex, "<strong>$1</strong>" ); - } - })); - } - - // typeahead.js - if (self.options.typeaheadjs) { - var typeaheadjs = self.options.typeaheadjs || {}; - - self.$input.typeahead(null, typeaheadjs).on('typeahead:selected', $.proxy(function (obj, datum) { - if (typeaheadjs.valueKey) - self.add(datum[typeaheadjs.valueKey]); - else - self.add(datum); - self.$input.typeahead('val', ''); - }, self)); - } - - self.$container.on('click', $.proxy(function(event) { - if (! self.$element.attr('disabled')) { - self.$input.removeAttr('disabled'); - } - self.$input.focus(); - }, self)); - - if (self.options.addOnBlur && self.options.freeInput) { - self.$input.on('focusout', $.proxy(function(event) { - // HACK: only process on focusout when no typeahead opened, to - // avoid adding the typeahead text as tag - if ($('.typeahead, .twitter-typeahead', self.$container).length === 0) { - self.add(self.$input.val()); - self.$input.val(''); - } - }, self)); - } - - - self.$container.on('keydown', 'input', $.proxy(function(event) { - var $input = $(event.target), - $inputWrapper = self.findInputWrapper(); - - if (self.$element.attr('disabled')) { - self.$input.attr('disabled', 'disabled'); - return; - } - - switch (event.which) { - // BACKSPACE - case 8: - if (doGetCaretPosition($input[0]) === 0) { - var prev = $inputWrapper.prev(); - if (prev) { - self.remove(prev.data('item')); - } - } - break; - - // DELETE - case 46: - if (doGetCaretPosition($input[0]) === 0) { - var next = $inputWrapper.next(); - if (next) { - self.remove(next.data('item')); - } - } - break; - - // LEFT ARROW - case 37: - // Try to move the input before the previous tag - var $prevTag = $inputWrapper.prev(); - if ($input.val().length === 0 && $prevTag[0]) { - $prevTag.before($inputWrapper); - $input.focus(); - } - break; - // RIGHT ARROW - case 39: - // Try to move the input after the next tag - var $nextTag = $inputWrapper.next(); - if ($input.val().length === 0 && $nextTag[0]) { - $nextTag.after($inputWrapper); - $input.focus(); - } - break; - default: - // ignore - } - - // Reset internal input's size - var textLength = $input.val().length, - wordSpace = Math.ceil(textLength / 5), - size = textLength + wordSpace + 1; - $input.attr('size', Math.max(this.inputSize, $input.val().length)); - }, self)); - - self.$container.on('keypress', 'input', $.proxy(function(event) { - var $input = $(event.target); - - if (self.$element.attr('disabled')) { - self.$input.attr('disabled', 'disabled'); - return; - } - - var text = $input.val(), - maxLengthReached = self.options.maxChars && text.length >= self.options.maxChars; - if (self.options.freeInput && (keyCombinationInList(event, self.options.confirmKeys) || maxLengthReached)) { - self.add(maxLengthReached ? text.substr(0, self.options.maxChars) : text); - $input.val(''); - event.preventDefault(); - } - - // Reset internal input's size - var textLength = $input.val().length, - wordSpace = Math.ceil(textLength / 5), - size = textLength + wordSpace + 1; - $input.attr('size', Math.max(this.inputSize, $input.val().length)); - }, self)); - - // Remove icon clicked - self.$container.on('click', '[data-role=remove]', $.proxy(function(event) { - if (self.$element.attr('disabled')) { - return; - } - self.remove($(event.target).closest('.tag').data('item')); - }, self)); - - // Only add existing value as tags when using strings as tags - if (self.options.itemValue === defaultOptions.itemValue) { - if (self.$element[0].tagName === 'INPUT') { - self.add(self.$element.val()); - } else { - $('option', self.$element).each(function() { - self.add($(this).attr('value'), true); - }); - } - } - }, - - /** - * Removes all tagsinput behaviour and unregsiter all event handlers - */ - destroy: function() { - var self = this; - - // Unbind events - self.$container.off('keypress', 'input'); - self.$container.off('click', '[role=remove]'); - - self.$container.remove(); - self.$element.removeData('tagsinput'); - self.$element.show(); - }, - - /** - * Sets focus on the tagsinput - */ - focus: function() { - this.$input.focus(); - }, - - /** - * Returns the internal input element - */ - input: function() { - return this.$input; - }, - - /** - * Returns the element which is wrapped around the internal input. This - * is normally the $container, but typeahead.js moves the $input element. - */ - findInputWrapper: function() { - var elt = this.$input[0], - container = this.$container[0]; - while(elt && elt.parentNode !== container) - elt = elt.parentNode; - - return $(elt); - } - }; - - /** - * Register JQuery plugin - */ - $.fn.tagsinput = function(arg1, arg2) { - var results = []; - - this.each(function() { - var tagsinput = $(this).data('tagsinput'); - // Initialize a new tags input - if (!tagsinput) { - tagsinput = new TagsInput(this, arg1); - $(this).data('tagsinput', tagsinput); - results.push(tagsinput); - - if (this.tagName === 'SELECT') { - $('option', $(this)).attr('selected', 'selected'); - } - - // Init tags from $(this).val() - $(this).val($(this).val()); - } else if (!arg1 && !arg2) { - // tagsinput already exists - // no function, trying to init - results.push(tagsinput); - } else if(tagsinput[arg1] !== undefined) { - // Invoke function on existing tags input - var retVal = tagsinput[arg1](arg2); - if (retVal !== undefined) - results.push(retVal); - } - }); - - if ( typeof arg1 == 'string') { - // Return the results from the invoked function calls - return results.length > 1 ? results : results[0]; - } else { - return results; - } - }; - - $.fn.tagsinput.Constructor = TagsInput; - - /** - * Most options support both a string or number as well as a function as - * option value. This function makes sure that the option with the given - * key in the given options is wrapped in a function - */ - function makeOptionItemFunction(options, key) { - if (typeof options[key] !== 'function') { - var propertyName = options[key]; - options[key] = function(item) { return item[propertyName]; }; - } - } - function makeOptionFunction(options, key) { - if (typeof options[key] !== 'function') { - var value = options[key]; - options[key] = function() { return value; }; - } - } - /** - * HtmlEncodes the given value - */ - var htmlEncodeContainer = $('<div />'); - function htmlEncode(value) { - if (value) { - return htmlEncodeContainer.text(value).html(); - } else { - return ''; - } - } - - /** - * Returns the position of the caret in the given input field - * http://flightschool.acylt.com/devnotes/caret-position-woes/ - */ - function doGetCaretPosition(oField) { - var iCaretPos = 0; - if (document.selection) { - oField.focus (); - var oSel = document.selection.createRange(); - oSel.moveStart ('character', -oField.value.length); - iCaretPos = oSel.text.length; - } else if (oField.selectionStart || oField.selectionStart == '0') { - iCaretPos = oField.selectionStart; - } - return (iCaretPos); - } - - /** - * Returns boolean indicates whether user has pressed an expected key combination. - * @param object keyPressEvent: JavaScript event object, refer - * http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html - * @param object lookupList: expected key combinations, as in: - * [13, {which: 188, shiftKey: true}] - */ - function keyCombinationInList(keyPressEvent, lookupList) { - var found = false; - $.each(lookupList, function (index, keyCombination) { - if (typeof (keyCombination) === 'number' && keyPressEvent.which === keyCombination) { - found = true; - return false; - } - - if (keyPressEvent.which === keyCombination.which) { - var alt = !keyCombination.hasOwnProperty('altKey') || keyPressEvent.altKey === keyCombination.altKey, - shift = !keyCombination.hasOwnProperty('shiftKey') || keyPressEvent.shiftKey === keyCombination.shiftKey, - ctrl = !keyCombination.hasOwnProperty('ctrlKey') || keyPressEvent.ctrlKey === keyCombination.ctrlKey; - if (alt && shift && ctrl) { - found = true; - return false; - } - } - }); - - return found; - } - - /** - * Initialize tagsinput behaviour on inputs and selects which have - * data-role=tagsinput - */ - $(function() { - $("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput(); - }); -})(window.jQuery); diff --git a/src/UI/JsLibraries/filesize.js b/src/UI/JsLibraries/filesize.js deleted file mode 100644 index 74f600064..000000000 --- a/src/UI/JsLibraries/filesize.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * filesize - * - * @author Jason Mulligan <jason.mulligan@avoidwork.com> - * @copyright 2013 Jason Mulligan - * @license BSD-3 <https://raw.github.com/avoidwork/filesize.js/master/LICENSE> - * @link http://filesizejs.com - * @module filesize - * @version 2.0.0 - */ -( function ( global ) { -"use strict"; - -var bit = /b$/, - bite = /^B$/, - radix = 10, - right = /\.(.*)/, - zero = /^0$/; - -/** - * filesize - * - * @method filesize - * @param {Mixed} arg String, Int or Float to transform - * @param {Object} descriptor [Optional] Flags - * @return {String} Readable file size String - */ -function filesize ( arg, descriptor ) { - var result = "", - skip = false, - i = 6, - base, bits, neg, num, round, size, sizes, unix, spacer, suffix, z; - - if ( isNaN( arg ) ) { - throw new Error( "Invalid arguments" ); - } - - descriptor = descriptor || {}; - bits = ( descriptor.bits === true ); - unix = ( descriptor.unix === true ); - base = descriptor.base !== undefined ? descriptor.base : unix ? 2 : 10; - round = descriptor.round !== undefined ? descriptor.round : unix ? 1 : 2; - spacer = descriptor.spacer !== undefined ? descriptor.spacer : unix ? "" : " "; - num = Number( arg ); - neg = ( num < 0 ); - - // Flipping a negative number to determine the size - if ( neg ) { - num = -num; - } - - // Zero is now a special case because bytes divide by 1 - if ( num === 0 ) { - if ( unix ) { - result = "0"; - } - else { - result = "0" + spacer + "B"; - } - } - else { - sizes = options[base][bits ? "bits" : "bytes"]; - - while ( i-- ) { - size = sizes[i][1]; - suffix = sizes[i][0]; - - if ( num >= size ) { - // Treating bytes as cardinal - if ( bite.test( suffix ) ) { - skip = true; - round = 0; - } - - result = ( num / size ).toFixed( round ); - - if ( !skip && unix ) { - if ( bits && bit.test( suffix ) ) { - suffix = suffix.toLowerCase(); - } - - suffix = suffix.charAt( 0 ); - z = right.exec( result ); - - if ( !bits && suffix === "k" ) { - suffix = "K"; - } - - if ( z !== null && z[1] !== undefined && zero.test( z[1] ) ) { - result = parseInt( result, radix ); - } - - result += spacer + suffix; - } - else if ( !unix ) { - result += spacer + suffix; - } - - break; - } - } - } - - // Decorating a 'diff' - if ( neg ) { - result = "-" + result; - } - - return result; -} - -/** - * Size options - * - * @type {Object} - */ -var options = { - 2 : { - bits : [["B", 1], ["kb", 128], ["Mb", 131072], ["Gb", 134217728], ["Tb", 137438953472], ["Pb", 140737488355328]], - bytes : [["B", 1], ["kB", 1024], ["MB", 1048576], ["GB", 1073741824], ["TB", 1099511627776], ["PB", 1125899906842624]] - }, - 10 : { - bits : [["B", 1], ["kb", 125], ["Mb", 125000], ["Gb", 125000000], ["Tb", 125000000000], ["Pb", 125000000000000]], - bytes : [["B", 1], ["kB", 1000], ["MB", 1000000], ["GB", 1000000000], ["TB", 1000000000000], ["PB", 1000000000000000]] - } -}; - -// CommonJS, AMD, script tag -if ( typeof exports !== "undefined" ) { - module.exports = filesize; -} -else if ( typeof define === "function" ) { - define( function () { - return filesize; - } ); -} -else { - global.filesize = filesize; -} - -} )( this ); diff --git a/src/UI/JsLibraries/fullcalendar.js b/src/UI/JsLibraries/fullcalendar.js deleted file mode 100644 index d4c97f0dc..000000000 --- a/src/UI/JsLibraries/fullcalendar.js +++ /dev/null @@ -1,15591 +0,0 @@ -/*! - * FullCalendar v3.4.0 - * Docs & License: https://fullcalendar.io/ - * (c) 2017 Adam Shaw - */ - -(function(factory) { - if (typeof define === 'function' && define.amd) { - define([ 'jquery', 'moment' ], factory); - } - else if (typeof exports === 'object') { // Node/CommonJS - module.exports = factory(require('jquery'), require('moment')); - } - else { - factory(jQuery, moment); - } -})(function($, moment) { - -;; - -var FC = $.fullCalendar = { - version: "3.4.0", - // When introducing internal API incompatibilities (where fullcalendar plugins would break), - // the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0) - // and the below integer should be incremented. - internalApiVersion: 9 -}; -var fcViews = FC.views = {}; - - -$.fn.fullCalendar = function(options) { - var args = Array.prototype.slice.call(arguments, 1); // for a possible method call - var res = this; // what this function will return (this jQuery object by default) - - this.each(function(i, _element) { // loop each DOM element involved - var element = $(_element); - var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) - var singleRes; // the returned value of this single method call - - // a method call - if (typeof options === 'string') { - if (calendar && $.isFunction(calendar[options])) { - singleRes = calendar[options].apply(calendar, args); - if (!i) { - res = singleRes; // record the first method call result - } - if (options === 'destroy') { // for the destroy method, must remove Calendar object data - element.removeData('fullCalendar'); - } - } - } - // a new calendar initialization - else if (!calendar) { // don't initialize twice - calendar = new Calendar(element, options); - element.data('fullCalendar', calendar); - calendar.render(); - } - }); - - return res; -}; - - -var complexOptions = [ // names of options that are objects whose properties should be combined - 'header', - 'footer', - 'buttonText', - 'buttonIcons', - 'themeButtonIcons' -]; - - -// Merges an array of option objects into a single object -function mergeOptions(optionObjs) { - return mergeProps(optionObjs, complexOptions); -} - -;; - -// exports -FC.intersectRanges = intersectRanges; -FC.applyAll = applyAll; -FC.debounce = debounce; -FC.isInt = isInt; -FC.htmlEscape = htmlEscape; -FC.cssToStr = cssToStr; -FC.proxy = proxy; -FC.capitaliseFirstLetter = capitaliseFirstLetter; - - -/* FullCalendar-specific DOM Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left -// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. -function compensateScroll(rowEls, scrollbarWidths) { - if (scrollbarWidths.left) { - rowEls.css({ - 'border-left-width': 1, - 'margin-left': scrollbarWidths.left - 1 - }); - } - if (scrollbarWidths.right) { - rowEls.css({ - 'border-right-width': 1, - 'margin-right': scrollbarWidths.right - 1 - }); - } -} - - -// Undoes compensateScroll and restores all borders/margins -function uncompensateScroll(rowEls) { - rowEls.css({ - 'margin-left': '', - 'margin-right': '', - 'border-left-width': '', - 'border-right-width': '' - }); -} - - -// Make the mouse cursor express that an event is not allowed in the current area -function disableCursor() { - $('body').addClass('fc-not-allowed'); -} - - -// Returns the mouse cursor to its original look -function enableCursor() { - $('body').removeClass('fc-not-allowed'); -} - - -// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. -// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering -// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and -// reduces the available height. -function distributeHeight(els, availableHeight, shouldRedistribute) { - - // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, - // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. - - var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element - var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* - var flexEls = []; // elements that are allowed to expand. array of DOM nodes - var flexOffsets = []; // amount of vertical space it takes up - var flexHeights = []; // actual css height - var usedHeight = 0; - - undistributeHeight(els); // give all elements their natural height - - // find elements that are below the recommended height (expandable). - // important to query for heights in a single first pass (to avoid reflow oscillation). - els.each(function(i, el) { - var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = $(el).outerHeight(true); - - if (naturalOffset < minOffset) { - flexEls.push(el); - flexOffsets.push(naturalOffset); - flexHeights.push($(el).height()); - } - else { - // this element stretches past recommended height (non-expandable). mark the space as occupied. - usedHeight += naturalOffset; - } - }); - - // readjust the recommended height to only consider the height available to non-maxed-out rows. - if (shouldRedistribute) { - availableHeight -= usedHeight; - minOffset1 = Math.floor(availableHeight / flexEls.length); - minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* - } - - // assign heights to all expandable elements - $(flexEls).each(function(i, el) { - var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; - var naturalOffset = flexOffsets[i]; - var naturalHeight = flexHeights[i]; - var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding - - if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things - $(el).height(newHeight); - } - }); -} - - -// Undoes distrubuteHeight, restoring all els to their natural height -function undistributeHeight(els) { - els.height(''); -} - - -// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the -// cells to be that width. -// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline -function matchCellWidths(els) { - var maxInnerWidth = 0; - - els.find('> *').each(function(i, innerEl) { - var innerWidth = $(innerEl).outerWidth(); - if (innerWidth > maxInnerWidth) { - maxInnerWidth = innerWidth; - } - }); - - maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance - - els.width(maxInnerWidth); - - return maxInnerWidth; -} - - -// Given one element that resides inside another, -// Subtracts the height of the inner element from the outer element. -function subtractInnerElHeight(outerEl, innerEl) { - var both = outerEl.add(innerEl); - var diff; - - // effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked - both.css({ - position: 'relative', // cause a reflow, which will force fresh dimension recalculation - left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll - }); - diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions - both.css({ position: '', left: '' }); // undo hack - - return diff; -} - - -/* Element Geom Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.getOuterRect = getOuterRect; -FC.getClientRect = getClientRect; -FC.getContentRect = getContentRect; -FC.getScrollbarWidths = getScrollbarWidths; - - -// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 -function getScrollParent(el) { - var position = el.css('position'), - scrollParent = el.parents().filter(function() { - var parent = $(this); - return (/(auto|scroll)/).test( - parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') - ); - }).eq(0); - - return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; -} - - -// Queries the outer bounding area of a jQuery element. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getOuterRect(el, origin) { - var offset = el.offset(); - var left = offset.left - (origin ? origin.left : 0); - var top = offset.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.outerWidth(), - top: top, - bottom: top + el.outerHeight() - }; -} - - -// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -// WARNING: given element can't have borders -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getClientRect(el, origin) { - var offset = el.offset(); - var scrollbarWidths = getScrollbarWidths(el); - var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0); - - return { - left: left, - right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars - top: top, - bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars - }; -} - - -// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. -// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). -// Origin is optional. -function getContentRect(el, origin) { - var offset = el.offset(); // just outside of border, margin not included - var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') - - (origin ? origin.left : 0); - var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') - - (origin ? origin.top : 0); - - return { - left: left, - right: left + el.width(), - top: top, - bottom: top + el.height() - }; -} - - -// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. -// WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger). -// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. -function getScrollbarWidths(el) { - var leftRightWidth = el[0].offsetWidth - el[0].clientWidth; - var bottomWidth = el[0].offsetHeight - el[0].clientHeight; - var widths; - - leftRightWidth = sanitizeScrollbarWidth(leftRightWidth); - bottomWidth = sanitizeScrollbarWidth(bottomWidth); - - widths = { left: 0, right: 0, top: 0, bottom: bottomWidth }; - - if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? - widths.left = leftRightWidth; - } - else { - widths.right = leftRightWidth; - } - - return widths; -} - - -// The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to -// retina displays, rounding, and IE11. Massage them into a usable value. -function sanitizeScrollbarWidth(width) { - width = Math.max(0, width); // no negatives - width = Math.round(width); - return width; -} - - -// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side - -var _isLeftRtlScrollbars = null; - -function getIsLeftRtlScrollbars() { // responsible for caching the computation - if (_isLeftRtlScrollbars === null) { - _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); - } - return _isLeftRtlScrollbars; -} - -function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it - var el = $('<div><div/></div>') - .css({ - position: 'absolute', - top: -1000, - left: 0, - border: 0, - padding: 0, - overflow: 'scroll', - direction: 'rtl' - }) - .appendTo('body'); - var innerEl = el.children(); - var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? - el.remove(); - return res; -} - - -// Retrieves a jQuery element's computed CSS value as a floating-point number. -// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. -function getCssFloat(el, prop) { - return parseFloat(el.css(prop)) || 0; -} - - -/* Mouse / Touch Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.preventDefault = preventDefault; - - -// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) -function isPrimaryMouseButton(ev) { - return ev.which == 1 && !ev.ctrlKey; -} - - -function getEvX(ev) { - var touches = ev.originalEvent.touches; - - // on mobile FF, pageX for touch events is present, but incorrect, - // so, look at touch coordinates first. - if (touches && touches.length) { - return touches[0].pageX; - } - - return ev.pageX; -} - - -function getEvY(ev) { - var touches = ev.originalEvent.touches; - - // on mobile FF, pageX for touch events is present, but incorrect, - // so, look at touch coordinates first. - if (touches && touches.length) { - return touches[0].pageY; - } - - return ev.pageY; -} - - -function getEvIsTouch(ev) { - return /^touch/.test(ev.type); -} - - -function preventSelection(el) { - el.addClass('fc-unselectable') - .on('selectstart', preventDefault); -} - - -function allowSelection(el) { - el.removeClass('fc-unselectable') - .off('selectstart', preventDefault); -} - - -// Stops a mouse/touch event from doing it's native browser action -function preventDefault(ev) { - ev.preventDefault(); -} - - -/* General Geometry Utils -----------------------------------------------------------------------------------------------------------------------*/ - -FC.intersectRects = intersectRects; - -// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false -function intersectRects(rect1, rect2) { - var res = { - left: Math.max(rect1.left, rect2.left), - right: Math.min(rect1.right, rect2.right), - top: Math.max(rect1.top, rect2.top), - bottom: Math.min(rect1.bottom, rect2.bottom) - }; - - if (res.left < res.right && res.top < res.bottom) { - return res; - } - return false; -} - - -// Returns a new point that will have been moved to reside within the given rectangle -function constrainPoint(point, rect) { - return { - left: Math.min(Math.max(point.left, rect.left), rect.right), - top: Math.min(Math.max(point.top, rect.top), rect.bottom) - }; -} - - -// Returns a point that is the center of the given rectangle -function getRectCenter(rect) { - return { - left: (rect.left + rect.right) / 2, - top: (rect.top + rect.bottom) / 2 - }; -} - - -// Subtracts point2's coordinates from point1's coordinates, returning a delta -function diffPoints(point1, point2) { - return { - left: point1.left - point2.left, - top: point1.top - point2.top - }; -} - - -/* Object Ordering by Field -----------------------------------------------------------------------------------------------------------------------*/ - -FC.parseFieldSpecs = parseFieldSpecs; -FC.compareByFieldSpecs = compareByFieldSpecs; -FC.compareByFieldSpec = compareByFieldSpec; -FC.flexibleCompare = flexibleCompare; - - -function parseFieldSpecs(input) { - var specs = []; - var tokens = []; - var i, token; - - if (typeof input === 'string') { - tokens = input.split(/\s*,\s*/); - } - else if (typeof input === 'function') { - tokens = [ input ]; - } - else if ($.isArray(input)) { - tokens = input; - } - - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - - if (typeof token === 'string') { - specs.push( - token.charAt(0) == '-' ? - { field: token.substring(1), order: -1 } : - { field: token, order: 1 } - ); - } - else if (typeof token === 'function') { - specs.push({ func: token }); - } - } - - return specs; -} - - -function compareByFieldSpecs(obj1, obj2, fieldSpecs) { - var i; - var cmp; - - for (i = 0; i < fieldSpecs.length; i++) { - cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]); - if (cmp) { - return cmp; - } - } - - return 0; -} - - -function compareByFieldSpec(obj1, obj2, fieldSpec) { - if (fieldSpec.func) { - return fieldSpec.func(obj1, obj2); - } - return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) * - (fieldSpec.order || 1); -} - - -function flexibleCompare(a, b) { - if (!a && !b) { - return 0; - } - if (b == null) { - return -1; - } - if (a == null) { - return 1; - } - if ($.type(a) === 'string' || $.type(b) === 'string') { - return String(a).localeCompare(String(b)); - } - return a - b; -} - - -/* FullCalendar-specific Misc Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -// Computes the intersection of the two ranges. Will return fresh date clones in a range. -// Returns undefined if no intersection. -// Expects all dates to be normalized to the same timezone beforehand. -// TODO: move to date section? -function intersectRanges(subjectRange, constraintRange) { - var subjectStart = subjectRange.start; - var subjectEnd = subjectRange.end; - var constraintStart = constraintRange.start; - var constraintEnd = constraintRange.end; - var segStart, segEnd; - var isStart, isEnd; - - if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all? - - if (subjectStart >= constraintStart) { - segStart = subjectStart.clone(); - isStart = true; - } - else { - segStart = constraintStart.clone(); - isStart = false; - } - - if (subjectEnd <= constraintEnd) { - segEnd = subjectEnd.clone(); - isEnd = true; - } - else { - segEnd = constraintEnd.clone(); - isEnd = false; - } - - return { - start: segStart, - end: segEnd, - isStart: isStart, - isEnd: isEnd - }; - } -} - - -/* Date Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -FC.computeGreatestUnit = computeGreatestUnit; -FC.divideRangeByDuration = divideRangeByDuration; -FC.divideDurationByDuration = divideDurationByDuration; -FC.multiplyDuration = multiplyDuration; -FC.durationHasTime = durationHasTime; - -var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; -var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending - - -// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. -// Moments will have their timezones normalized. -function diffDayTime(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), - ms: a.time() - b.time() // time-of-day from day start. disregards timezone - }); -} - - -// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations. -function diffDay(a, b) { - return moment.duration({ - days: a.clone().stripTime().diff(b.clone().stripTime(), 'days') - }); -} - - -// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. -function diffByUnit(a, b, unit) { - return moment.duration( - Math.round(a.diff(b, unit, true)), // returnFloat=true - unit - ); -} - - -// Computes the unit name of the largest whole-unit period of time. -// For example, 48 hours will be "days" whereas 49 hours will be "hours". -// Accepts start/end, a range object, or an original duration object. -function computeGreatestUnit(start, end) { - var i, unit; - var val; - - for (i = 0; i < unitsDesc.length; i++) { - unit = unitsDesc[i]; - val = computeRangeAs(unit, start, end); - - if (val >= 1 && isInt(val)) { - break; - } - } - - return unit; // will be "milliseconds" if nothing else matches -} - - -// like computeGreatestUnit, but has special abilities to interpret the source input for clues -function computeDurationGreatestUnit(duration, durationInput) { - var unit = computeGreatestUnit(duration); - - // prevent days:7 from being interpreted as a week - if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) { - unit = 'day'; - } - - return unit; -} - - -// Computes the number of units (like "hours") in the given range. -// Range can be a {start,end} object, separate start/end args, or a Duration. -// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling -// of month-diffing logic (which tends to vary from version to version). -function computeRangeAs(unit, start, end) { - - if (end != null) { // given start, end - return end.diff(start, unit, true); - } - else if (moment.isDuration(start)) { // given duration - return start.as(unit); - } - else { // given { start, end } range object - return start.end.diff(start.start, unit, true); - } -} - - -// Intelligently divides a range (specified by a start/end params) by a duration -function divideRangeByDuration(start, end, dur) { - var months; - - if (durationHasTime(dur)) { - return (end - start) / dur; - } - months = dur.asMonths(); - if (Math.abs(months) >= 1 && isInt(months)) { - return end.diff(start, 'months', true) / months; - } - return end.diff(start, 'days', true) / dur.asDays(); -} - - -// Intelligently divides one duration by another -function divideDurationByDuration(dur1, dur2) { - var months1, months2; - - if (durationHasTime(dur1) || durationHasTime(dur2)) { - return dur1 / dur2; - } - months1 = dur1.asMonths(); - months2 = dur2.asMonths(); - if ( - Math.abs(months1) >= 1 && isInt(months1) && - Math.abs(months2) >= 1 && isInt(months2) - ) { - return months1 / months2; - } - return dur1.asDays() / dur2.asDays(); -} - - -// Intelligently multiplies a duration by a number -function multiplyDuration(dur, n) { - var months; - - if (durationHasTime(dur)) { - return moment.duration(dur * n); - } - months = dur.asMonths(); - if (Math.abs(months) >= 1 && isInt(months)) { - return moment.duration({ months: months * n }); - } - return moment.duration({ days: dur.asDays() * n }); -} - - -function cloneRange(range) { - return { - start: range.start.clone(), - end: range.end.clone() - }; -} - - -// Trims the beginning and end of inner range to be completely within outerRange. -// Returns a new range object. -function constrainRange(innerRange, outerRange) { - innerRange = cloneRange(innerRange); - - if (outerRange.start) { - // needs to be inclusively before outerRange's end - innerRange.start = constrainDate(innerRange.start, outerRange); - } - - if (outerRange.end) { - innerRange.end = minMoment(innerRange.end, outerRange.end); - } - - return innerRange; -} - - -// If the given date is not within the given range, move it inside. -// (If it's past the end, make it one millisecond before the end). -// Always returns a new moment. -function constrainDate(date, range) { - date = date.clone(); - - if (range.start) { - date = maxMoment(date, range.start); - } - - if (range.end && date >= range.end) { - date = range.end.clone().subtract(1); - } - - return date; -} - - -function isDateWithinRange(date, range) { - return (!range.start || date >= range.start) && - (!range.end || date < range.end); -} - - -// TODO: deal with repeat code in intersectRanges -// constraintRange can have unspecified start/end, an open-ended range. -function doRangesIntersect(subjectRange, constraintRange) { - return (!constraintRange.start || subjectRange.end >= constraintRange.start) && - (!constraintRange.end || subjectRange.start < constraintRange.end); -} - - -function isRangeWithinRange(innerRange, outerRange) { - return (!outerRange.start || innerRange.start >= outerRange.start) && - (!outerRange.end || innerRange.end <= outerRange.end); -} - - -function isRangesEqual(range0, range1) { - return ((range0.start && range1.start && range0.start.isSame(range1.start)) || (!range0.start && !range1.start)) && - ((range0.end && range1.end && range0.end.isSame(range1.end)) || (!range0.end && !range1.end)); -} - - -// Returns the moment that's earlier in time. Always a copy. -function minMoment(mom1, mom2) { - return (mom1.isBefore(mom2) ? mom1 : mom2).clone(); -} - - -// Returns the moment that's later in time. Always a copy. -function maxMoment(mom1, mom2) { - return (mom1.isAfter(mom2) ? mom1 : mom2).clone(); -} - - -// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) -function durationHasTime(dur) { - return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); -} - - -function isNativeDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; -} - - -// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" -function isTimeString(str) { - return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); -} - - -/* Logging and Debug -----------------------------------------------------------------------------------------------------------------------*/ - -FC.log = function() { - var console = window.console; - - if (console && console.log) { - return console.log.apply(console, arguments); - } -}; - -FC.warn = function() { - var console = window.console; - - if (console && console.warn) { - return console.warn.apply(console, arguments); - } - else { - return FC.log.apply(FC, arguments); - } -}; - - -/* General Utilities -----------------------------------------------------------------------------------------------------------------------*/ - -var hasOwnPropMethod = {}.hasOwnProperty; - - -// Merges an array of objects into a single object. -// The second argument allows for an array of property names who's object values will be merged together. -function mergeProps(propObjs, complexProps) { - var dest = {}; - var i, name; - var complexObjs; - var j, val; - var props; - - if (complexProps) { - for (i = 0; i < complexProps.length; i++) { - name = complexProps[i]; - complexObjs = []; - - // collect the trailing object values, stopping when a non-object is discovered - for (j = propObjs.length - 1; j >= 0; j--) { - val = propObjs[j][name]; - - if (typeof val === 'object') { - complexObjs.unshift(val); - } - else if (val !== undefined) { - dest[name] = val; // if there were no objects, this value will be used - break; - } - } - - // if the trailing values were objects, use the merged value - if (complexObjs.length) { - dest[name] = mergeProps(complexObjs); - } - } - } - - // copy values into the destination, going from last to first - for (i = propObjs.length - 1; i >= 0; i--) { - props = propObjs[i]; - - for (name in props) { - if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign - dest[name] = props[name]; - } - } - } - - return dest; -} - - -// Create an object that has the given prototype. Just like Object.create -function createObject(proto) { - var f = function() {}; - f.prototype = proto; - return new f(); -} -FC.createObject = createObject; - - -function copyOwnProps(src, dest) { - for (var name in src) { - if (hasOwnProp(src, name)) { - dest[name] = src[name]; - } - } -} - - -function hasOwnProp(obj, name) { - return hasOwnPropMethod.call(obj, name); -} - - -// Is the given value a non-object non-function value? -function isAtomic(val) { - return /undefined|null|boolean|number|string/.test($.type(val)); -} - - -function applyAll(functions, thisObj, args) { - if ($.isFunction(functions)) { - functions = [ functions ]; - } - if (functions) { - var i; - var ret; - for (i=0; i<functions.length; i++) { - ret = functions[i].apply(thisObj, args) || ret; - } - return ret; - } -} - - -function firstDefined() { - for (var i=0; i<arguments.length; i++) { - if (arguments[i] !== undefined) { - return arguments[i]; - } - } -} - - -function htmlEscape(s) { - return (s + '').replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/'/g, ''') - .replace(/"/g, '"') - .replace(/\n/g, '<br />'); -} - - -function stripHtmlEntities(text) { - return text.replace(/&.*?;/g, ''); -} - - -// Given a hash of CSS properties, returns a string of CSS. -// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. -function cssToStr(cssProps) { - var statements = []; - - $.each(cssProps, function(name, val) { - if (val != null) { - statements.push(name + ':' + val); - } - }); - - return statements.join(';'); -} - - -// Given an object hash of HTML attribute names to values, -// generates a string that can be injected between < > in HTML -function attrsToStr(attrs) { - var parts = []; - - $.each(attrs, function(name, val) { - if (val != null) { - parts.push(name + '="' + htmlEscape(val) + '"'); - } - }); - - return parts.join(' '); -} - - -function capitaliseFirstLetter(str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - - -function compareNumbers(a, b) { // for .sort() - return a - b; -} - - -function isInt(n) { - return n % 1 === 0; -} - - -// Returns a method bound to the given object context. -// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with -// different contexts as identical when binding/unbinding events. -function proxy(obj, methodName) { - var method = obj[methodName]; - - return function() { - return method.apply(obj, arguments); - }; -} - - -// Returns a function, that, as long as it continues to be invoked, will not -// be triggered. The function will be called after it stops being called for -// N milliseconds. If `immediate` is passed, trigger the function on the -// leading edge, instead of the trailing. -// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 -function debounce(func, wait, immediate) { - var timeout, args, context, timestamp, result; - - var later = function() { - var last = +new Date() - timestamp; - if (last < wait) { - timeout = setTimeout(later, wait - last); - } - else { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - context = args = null; - } - } - }; - - return function() { - context = this; - args = arguments; - timestamp = +new Date(); - var callNow = immediate && !timeout; - if (!timeout) { - timeout = setTimeout(later, wait); - } - if (callNow) { - result = func.apply(context, args); - context = args = null; - } - return result; - }; -} - -;; - -/* -GENERAL NOTE on moments throughout the *entire rest* of the codebase: -All moments are assumed to be ambiguously-zoned unless otherwise noted, -with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*. -Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature. -*/ - -var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; -var ambigTimeOrZoneRegex = - /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; -var newMomentProto = moment.fn; // where we will attach our new methods -var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods - -// tell momentjs to transfer these properties upon clone -var momentProperties = moment.momentProperties; -momentProperties.push('_fullCalendar'); -momentProperties.push('_ambigTime'); -momentProperties.push('_ambigZone'); - - -// Creating -// ------------------------------------------------------------------------------------------------- - -// Creates a new moment, similar to the vanilla moment(...) constructor, but with -// extra features (ambiguous time, enhanced formatting). When given an existing moment, -// it will function as a clone (and retain the zone of the moment). Anything else will -// result in a moment in the local zone. -FC.moment = function() { - return makeMoment(arguments); -}; - -// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone. -FC.moment.utc = function() { - var mom = makeMoment(arguments, true); - - // Force it into UTC because makeMoment doesn't guarantee it - // (if given a pre-existing moment for example) - if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone - mom.utc(); - } - - return mom; -}; - -// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved. -// ISO8601 strings with no timezone offset will become ambiguously zoned. -FC.moment.parseZone = function() { - return makeMoment(arguments, true, true); -}; - -// Builds an enhanced moment from args. When given an existing moment, it clones. When given a -// native Date, or called with no arguments (the current time), the resulting moment will be local. -// Anything else needs to be "parsed" (a string or an array), and will be affected by: -// parseAsUTC - if there is no zone information, should we parse the input in UTC? -// parseZone - if there is zone information, should we force the zone of the moment? -function makeMoment(args, parseAsUTC, parseZone) { - var input = args[0]; - var isSingleString = args.length == 1 && typeof input === 'string'; - var isAmbigTime; - var isAmbigZone; - var ambigMatch; - var mom; - - if (moment.isMoment(input) || isNativeDate(input) || input === undefined) { - mom = moment.apply(null, args); - } - else { // "parsing" is required - isAmbigTime = false; - isAmbigZone = false; - - if (isSingleString) { - if (ambigDateOfMonthRegex.test(input)) { - // accept strings like '2014-05', but convert to the first of the month - input += '-01'; - args = [ input ]; // for when we pass it on to moment's constructor - isAmbigTime = true; - isAmbigZone = true; - } - else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { - isAmbigTime = !ambigMatch[5]; // no time part? - isAmbigZone = true; - } - } - else if ($.isArray(input)) { - // arrays have no timezone information, so assume ambiguous zone - isAmbigZone = true; - } - // otherwise, probably a string with a format - - if (parseAsUTC || isAmbigTime) { - mom = moment.utc.apply(moment, args); - } - else { - mom = moment.apply(null, args); - } - - if (isAmbigTime) { - mom._ambigTime = true; - mom._ambigZone = true; // ambiguous time always means ambiguous zone - } - else if (parseZone) { // let's record the inputted zone somehow - if (isAmbigZone) { - mom._ambigZone = true; - } - else if (isSingleString) { - mom.utcOffset(input); // if not a valid zone, will assign UTC - } - } - } - - mom._fullCalendar = true; // flag for extended functionality - - return mom; -} - - -// Week Number -// ------------------------------------------------------------------------------------------------- - - -// Returns the week number, considering the locale's custom week number calcuation -// `weeks` is an alias for `week` -newMomentProto.week = newMomentProto.weeks = function(input) { - var weekCalc = this._locale._fullCalendar_weekCalc; - - if (input == null && typeof weekCalc === 'function') { // custom function only works for getter - return weekCalc(this); - } - else if (weekCalc === 'ISO') { - return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter - } - - return oldMomentProto.week.apply(this, arguments); // local getter/setter -}; - - -// Time-of-day -// ------------------------------------------------------------------------------------------------- - -// GETTER -// Returns a Duration with the hours/minutes/seconds/ms values of the moment. -// If the moment has an ambiguous time, a duration of 00:00 will be returned. -// -// SETTER -// You can supply a Duration, a Moment, or a Duration-like argument. -// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. -newMomentProto.time = function(time) { - - // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. - // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. - if (!this._fullCalendar) { - return oldMomentProto.time.apply(this, arguments); - } - - if (time == null) { // getter - return moment.duration({ - hours: this.hours(), - minutes: this.minutes(), - seconds: this.seconds(), - milliseconds: this.milliseconds() - }); - } - else { // setter - - this._ambigTime = false; // mark that the moment now has a time - - if (!moment.isDuration(time) && !moment.isMoment(time)) { - time = moment.duration(time); - } - - // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). - // Only for Duration times, not Moment times. - var dayHours = 0; - if (moment.isDuration(time)) { - dayHours = Math.floor(time.asDays()) * 24; - } - - // We need to set the individual fields. - // Can't use startOf('day') then add duration. In case of DST at start of day. - return this.hours(dayHours + time.hours()) - .minutes(time.minutes()) - .seconds(time.seconds()) - .milliseconds(time.milliseconds()); - } -}; - -// Converts the moment to UTC, stripping out its time-of-day and timezone offset, -// but preserving its YMD. A moment with a stripped time will display no time -// nor timezone offset when .format() is called. -newMomentProto.stripTime = function() { - - if (!this._ambigTime) { - - this.utc(true); // keepLocalTime=true (for keeping *date* value) - - // set time to zero - this.set({ - hours: 0, - minutes: 0, - seconds: 0, - ms: 0 - }); - - // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), - // which clears all ambig flags. - this._ambigTime = true; - this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset - } - - return this; // for chaining -}; - -// Returns if the moment has a non-ambiguous time (boolean) -newMomentProto.hasTime = function() { - return !this._ambigTime; -}; - - -// Timezone -// ------------------------------------------------------------------------------------------------- - -// Converts the moment to UTC, stripping out its timezone offset, but preserving its -// YMD and time-of-day. A moment with a stripped timezone offset will display no -// timezone offset when .format() is called. -newMomentProto.stripZone = function() { - var wasAmbigTime; - - if (!this._ambigZone) { - - wasAmbigTime = this._ambigTime; - - this.utc(true); // keepLocalTime=true (for keeping date and time values) - - // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore - this._ambigTime = wasAmbigTime || false; - - // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), - // which clears the ambig flags. - this._ambigZone = true; - } - - return this; // for chaining -}; - -// Returns of the moment has a non-ambiguous timezone offset (boolean) -newMomentProto.hasZone = function() { - return !this._ambigZone; -}; - - -// implicitly marks a zone -newMomentProto.local = function(keepLocalTime) { - - // for when converting from ambiguously-zoned to local, - // keep the time values when converting from UTC -> local - oldMomentProto.local.call(this, this._ambigZone || keepLocalTime); - - // ensure non-ambiguous - // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals - this._ambigTime = false; - this._ambigZone = false; - - return this; // for chaining -}; - - -// implicitly marks a zone -newMomentProto.utc = function(keepLocalTime) { - - oldMomentProto.utc.call(this, keepLocalTime); - - // ensure non-ambiguous - // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals - this._ambigTime = false; - this._ambigZone = false; - - return this; -}; - - -// implicitly marks a zone (will probably get called upon .utc() and .local()) -newMomentProto.utcOffset = function(tzo) { - - if (tzo != null) { // setter - // these assignments needs to happen before the original zone method is called. - // I forget why, something to do with a browser crash. - this._ambigTime = false; - this._ambigZone = false; - } - - return oldMomentProto.utcOffset.apply(this, arguments); -}; - - -// Formatting -// ------------------------------------------------------------------------------------------------- - -newMomentProto.format = function() { - - if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? - return formatDate(this, arguments[0]); // our extended formatting - } - if (this._ambigTime) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss'); - } - if (this._fullCalendar) { // enhanced non-ambig moment? - // moment.format() doesn't ensure english, but we want to. - return oldMomentFormat(englishMoment(this)); - } - - return oldMomentProto.format.apply(this, arguments); -}; - -newMomentProto.toISOString = function() { - - if (this._ambigTime) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD'); - } - if (this._ambigZone) { - return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss'); - } - if (this._fullCalendar) { // enhanced non-ambig moment? - // depending on browser, moment might not output english. ensure english. - // https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22 - return oldMomentProto.toISOString.apply(englishMoment(this), arguments); - } - - return oldMomentProto.toISOString.apply(this, arguments); -}; - -function englishMoment(mom) { - if (mom.locale() !== 'en') { - return mom.clone().locale('en'); - } - return mom; -} - -;; -(function() { - -// exports -FC.formatDate = formatDate; -FC.formatRange = formatRange; -FC.oldMomentFormat = oldMomentFormat; -FC.queryMostGranularFormatUnit = queryMostGranularFormatUnit; - - -// Config -// --------------------------------------------------------------------------------------------------------------------- - -/* -Inserted between chunks in the fake ("intermediate") formatting string. -Important that it passes as whitespace (\s) because moment often identifies non-standalone months -via a regexp with an \s. -*/ -var PART_SEPARATOR = '\u000b'; // vertical tab - -/* -Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text, -but rather, a "special" token that has custom rendering (see specialTokens map). -*/ -var SPECIAL_TOKEN_MARKER = '\u001f'; // information separator 1 - -/* -Inserted at the beginning and end of a span of text that must have non-zero numeric characters. -Handling of these markers is done in a post-processing step at the very end of text rendering. -*/ -var MAYBE_MARKER = '\u001e'; // information separator 2 -var MAYBE_REGEXP = new RegExp(MAYBE_MARKER + '([^' + MAYBE_MARKER + ']*)' + MAYBE_MARKER, 'g'); // must be global - -/* -Addition formatting tokens we want recognized -*/ -var specialTokens = { - t: function(date) { // "a" or "p" - return oldMomentFormat(date, 'a').charAt(0); - }, - T: function(date) { // "A" or "P" - return oldMomentFormat(date, 'A').charAt(0); - } -}; - -/* -The first characters of formatting tokens for units that are 1 day or larger. -`value` is for ranking relative size (lower means bigger). -`unit` is a normalized unit, used for comparing moments. -*/ -var largeTokenMap = { - Y: { value: 1, unit: 'year' }, - M: { value: 2, unit: 'month' }, - W: { value: 3, unit: 'week' }, // ISO week - w: { value: 3, unit: 'week' }, // local week - D: { value: 4, unit: 'day' }, // day of month - d: { value: 4, unit: 'day' } // day of week -}; - - -// Single Date Formatting -// --------------------------------------------------------------------------------------------------------------------- - -/* -Formats `date` with a Moment formatting string, but allow our non-zero areas and special token -*/ -function formatDate(date, formatStr) { - return renderFakeFormatString( - getParsedFormatString(formatStr).fakeFormatString, - date - ); -} - -/* -Call this if you want Moment's original format method to be used -*/ -function oldMomentFormat(mom, formatStr) { - return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js -} - - -// Date Range Formatting -// ------------------------------------------------------------------------------------------------- -// TODO: make it work with timezone offset - -/* -Using a formatting string meant for a single date, generate a range string, like -"Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. -If the dates are the same as far as the format string is concerned, just return a single -rendering of one date, without any separator. -*/ -function formatRange(date1, date2, formatStr, separator, isRTL) { - var localeData; - - date1 = FC.moment.parseZone(date1); - date2 = FC.moment.parseZone(date2); - - localeData = date1.localeData(); - - // Expand localized format strings, like "LL" -> "MMMM D YYYY". - // BTW, this is not important for `formatDate` because it is impossible to put custom tokens - // or non-zero areas in Moment's localized format strings. - formatStr = localeData.longDateFormat(formatStr) || formatStr; - - return renderParsedFormat( - getParsedFormatString(formatStr), - date1, - date2, - separator || ' - ', - isRTL - ); -} - -/* -Renders a range with an already-parsed format string. -*/ -function renderParsedFormat(parsedFormat, date1, date2, separator, isRTL) { - var sameUnits = parsedFormat.sameUnits; - var unzonedDate1 = date1.clone().stripZone(); // for same-unit comparisons - var unzonedDate2 = date2.clone().stripZone(); // " - - var renderedParts1 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date1); - var renderedParts2 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date2); - - var leftI; - var leftStr = ''; - var rightI; - var rightStr = ''; - var middleI; - var middleStr1 = ''; - var middleStr2 = ''; - var middleStr = ''; - - // Start at the leftmost side of the formatting string and continue until you hit a token - // that is not the same between dates. - for ( - leftI = 0; - leftI < sameUnits.length && (!sameUnits[leftI] || unzonedDate1.isSame(unzonedDate2, sameUnits[leftI])); - leftI++ - ) { - leftStr += renderedParts1[leftI]; - } - - // Similarly, start at the rightmost side of the formatting string and move left - for ( - rightI = sameUnits.length - 1; - rightI > leftI && (!sameUnits[rightI] || unzonedDate1.isSame(unzonedDate2, sameUnits[rightI])); - rightI-- - ) { - // If current chunk is on the boundary of unique date-content, and is a special-case - // date-formatting postfix character, then don't consume it. Consider it unique date-content. - // TODO: make configurable - if (rightI - 1 === leftI && renderedParts1[rightI] === '.') { - break; - } - - rightStr = renderedParts1[rightI] + rightStr; - } - - // The area in the middle is different for both of the dates. - // Collect them distinctly so we can jam them together later. - for (middleI = leftI; middleI <= rightI; middleI++) { - middleStr1 += renderedParts1[middleI]; - middleStr2 += renderedParts2[middleI]; - } - - if (middleStr1 || middleStr2) { - if (isRTL) { - middleStr = middleStr2 + separator + middleStr1; - } - else { - middleStr = middleStr1 + separator + middleStr2; - } - } - - return processMaybeMarkers( - leftStr + middleStr + rightStr - ); -} - - -// Format String Parsing -// --------------------------------------------------------------------------------------------------------------------- - -var parsedFormatStrCache = {}; - -/* -Returns a parsed format string, leveraging a cache. -*/ -function getParsedFormatString(formatStr) { - return parsedFormatStrCache[formatStr] || - (parsedFormatStrCache[formatStr] = parseFormatString(formatStr)); -} - -/* -Parses a format string into the following: -- fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed. -- sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"), - that indicates how similar a range's start & end must be in order to share the same formatted text. - If not a token, then the value is null. - Always a flat array (not nested liked "chunks"). -*/ -function parseFormatString(formatStr) { - var chunks = chunkFormatString(formatStr); - - return { - fakeFormatString: buildFakeFormatString(chunks), - sameUnits: buildSameUnits(chunks) - }; -} - -/* -Break the formatting string into an array of chunks. -A 'maybe' chunk will have nested chunks. -*/ -function chunkFormatString(formatStr) { - var chunks = []; - var match; - - // TODO: more descrimination - // \4 is a backreference to the first character of a multi-character set. - var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; - - while ((match = chunker.exec(formatStr))) { - if (match[1]) { // a literal string inside [ ... ] - chunks.push.apply(chunks, // append - splitStringLiteral(match[1]) - ); - } - else if (match[2]) { // non-zero formatting inside ( ... ) - chunks.push({ maybe: chunkFormatString(match[2]) }); - } - else if (match[3]) { // a formatting token - chunks.push({ token: match[3] }); - } - else if (match[5]) { // an unenclosed literal string - chunks.push.apply(chunks, // append - splitStringLiteral(match[5]) - ); - } - } - - return chunks; -} - -/* -Potentially splits a literal-text string into multiple parts. For special cases. -*/ -function splitStringLiteral(s) { - if (s === '. ') { - return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date - } - else { - return [ s ]; - } -} - -/* -Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control -characters that will eventually be given to moment for formatting, and then post-processed. -*/ -function buildFakeFormatString(chunks) { - var parts = []; - var i, chunk; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (typeof chunk === 'string') { - parts.push('[' + chunk + ']'); - } - else if (chunk.token) { - if (chunk.token in specialTokens) { - parts.push( - SPECIAL_TOKEN_MARKER + // useful during post-processing - '[' + chunk.token + ']' // preserve as literal text - ); - } - else { - parts.push(chunk.token); // unprotected text implies a format string - } - } - else if (chunk.maybe) { - parts.push( - MAYBE_MARKER + // useful during post-processing - buildFakeFormatString(chunk.maybe) + - MAYBE_MARKER - ); - } - } - - return parts.join(PART_SEPARATOR); -} - -/* -Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate -in which regard two dates must be similar in order to share range formatting text. -The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat. -*/ -function buildSameUnits(chunks) { - var units = []; - var i, chunk; - var tokenInfo; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (chunk.token) { - tokenInfo = largeTokenMap[chunk.token.charAt(0)]; - units.push(tokenInfo ? tokenInfo.unit : 'second'); // default to a very strict same-second - } - else if (chunk.maybe) { - units.push.apply(units, // append - buildSameUnits(chunk.maybe) - ); - } - else { - units.push(null); - } - } - - return units; -} - - -// Rendering to text -// --------------------------------------------------------------------------------------------------------------------- - -/* -Formats a date with a fake format string, post-processes the control characters, then returns. -*/ -function renderFakeFormatString(fakeFormatString, date) { - return processMaybeMarkers( - renderFakeFormatStringParts(fakeFormatString, date).join('') - ); -} - -/* -Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers. -*/ -function renderFakeFormatStringParts(fakeFormatString, date) { - var parts = []; - var fakeRender = oldMomentFormat(date, fakeFormatString); - var fakeParts = fakeRender.split(PART_SEPARATOR); - var i, fakePart; - - for (i = 0; i < fakeParts.length; i++) { - fakePart = fakeParts[i]; - - if (fakePart.charAt(0) === SPECIAL_TOKEN_MARKER) { - parts.push( - // the literal string IS the token's name. - // call special token's registered function. - specialTokens[fakePart.substring(1)](date) - ); - } - else { - parts.push(fakePart); - } - } - - return parts; -} - -/* -Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string. -*/ -function processMaybeMarkers(s) { - return s.replace(MAYBE_REGEXP, function(m0, m1) { // regex assumed to have 'g' flag - if (m1.match(/[1-9]/)) { // any non-zero numeric characters? - return m1; - } - else { - return ''; - } - }); -} - - -// Misc Utils -// ------------------------------------------------------------------------------------------------- - -/* -Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string. -*/ -function queryMostGranularFormatUnit(formatStr) { - var chunks = chunkFormatString(formatStr); - var i, chunk; - var candidate; - var best; - - for (i = 0; i < chunks.length; i++) { - chunk = chunks[i]; - - if (chunk.token) { - candidate = largeTokenMap[chunk.token.charAt(0)]; - if (candidate) { - if (!best || candidate.value > best.value) { - best = candidate; - } - } - } - } - - if (best) { - return best.unit; - } - - return null; -}; - -})(); - -// quick local references -var formatDate = FC.formatDate; -var formatRange = FC.formatRange; -var oldMomentFormat = FC.oldMomentFormat; - -;; - -FC.Class = Class; // export - -// Class that all other classes will inherit from -function Class() { } - - -// Called on a class to create a subclass. -// Last argument contains instance methods. Any argument before the last are considered mixins. -Class.extend = function() { - var len = arguments.length; - var i; - var members; - - for (i = 0; i < len; i++) { - members = arguments[i]; - if (i < len - 1) { // not the last argument? - mixIntoClass(this, members); - } - } - - return extendClass(this, members || {}); // members will be undefined if no arguments -}; - - -// Adds new member variables/methods to the class's prototype. -// Can be called with another class, or a plain object hash containing new members. -Class.mixin = function(members) { - mixIntoClass(this, members); -}; - - -function extendClass(superClass, members) { - var subClass; - - // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist - if (hasOwnProp(members, 'constructor')) { - subClass = members.constructor; - } - if (typeof subClass !== 'function') { - subClass = members.constructor = function() { - superClass.apply(this, arguments); - }; - } - - // build the base prototype for the subclass, which is an new object chained to the superclass's prototype - subClass.prototype = createObject(superClass.prototype); - - // copy each member variable/method onto the the subclass's prototype - copyOwnProps(members, subClass.prototype); - - // copy over all class variables/methods to the subclass, such as `extend` and `mixin` - copyOwnProps(superClass, subClass); - - return subClass; -} - - -function mixIntoClass(theClass, members) { - copyOwnProps(members, theClass.prototype); -} -;; - -var Model = Class.extend(EmitterMixin, ListenerMixin, { - - _props: null, - _watchers: null, - _globalWatchArgs: null, - - constructor: function() { - this._watchers = {}; - this._props = {}; - this.applyGlobalWatchers(); - }, - - applyGlobalWatchers: function() { - var argSets = this._globalWatchArgs || []; - var i; - - for (i = 0; i < argSets.length; i++) { - this.watch.apply(this, argSets[i]); - } - }, - - has: function(name) { - return name in this._props; - }, - - get: function(name) { - if (name === undefined) { - return this._props; - } - - return this._props[name]; - }, - - set: function(name, val) { - var newProps; - - if (typeof name === 'string') { - newProps = {}; - newProps[name] = val === undefined ? null : val; - } - else { - newProps = name; - } - - this.setProps(newProps); - }, - - reset: function(newProps) { - var oldProps = this._props; - var changeset = {}; // will have undefined's to signal unsets - var name; - - for (name in oldProps) { - changeset[name] = undefined; - } - - for (name in newProps) { - changeset[name] = newProps[name]; - } - - this.setProps(changeset); - }, - - unset: function(name) { // accepts a string or array of strings - var newProps = {}; - var names; - var i; - - if (typeof name === 'string') { - names = [ name ]; - } - else { - names = name; - } - - for (i = 0; i < names.length; i++) { - newProps[names[i]] = undefined; - } - - this.setProps(newProps); - }, - - setProps: function(newProps) { - var changedProps = {}; - var changedCnt = 0; - var name, val; - - for (name in newProps) { - val = newProps[name]; - - // a change in value? - // if an object, don't check equality, because might have been mutated internally. - // TODO: eventually enforce immutability. - if ( - typeof val === 'object' || - val !== this._props[name] - ) { - changedProps[name] = val; - changedCnt++; - } - } - - if (changedCnt) { - - this.trigger('before:batchChange', changedProps); - - for (name in changedProps) { - val = changedProps[name]; - - this.trigger('before:change', name, val); - this.trigger('before:change:' + name, val); - } - - for (name in changedProps) { - val = changedProps[name]; - - if (val === undefined) { - delete this._props[name]; - } - else { - this._props[name] = val; - } - - this.trigger('change:' + name, val); - this.trigger('change', name, val); - } - - this.trigger('batchChange', changedProps); - } - }, - - watch: function(name, depList, startFunc, stopFunc) { - var _this = this; - - this.unwatch(name); - - this._watchers[name] = this._watchDeps(depList, function(deps) { - var res = startFunc.call(_this, deps); - - if (res && res.then) { - _this.unset(name); // put in an unset state while resolving - res.then(function(val) { - _this.set(name, val); - }); - } - else { - _this.set(name, res); - } - }, function() { - _this.unset(name); - - if (stopFunc) { - stopFunc.call(_this); - } - }); - }, - - unwatch: function(name) { - var watcher = this._watchers[name]; - - if (watcher) { - delete this._watchers[name]; - watcher.teardown(); - } - }, - - _watchDeps: function(depList, startFunc, stopFunc) { - var _this = this; - var queuedChangeCnt = 0; - var depCnt = depList.length; - var satisfyCnt = 0; - var values = {}; // what's passed as the `deps` arguments - var bindTuples = []; // array of [ eventName, handlerFunc ] arrays - var isCallingStop = false; - - function onBeforeDepChange(depName, val, isOptional) { - queuedChangeCnt++; - if (queuedChangeCnt === 1) { // first change to cause a "stop" ? - if (satisfyCnt === depCnt) { // all deps previously satisfied? - isCallingStop = true; - stopFunc(); - isCallingStop = false; - } - } - } - - function onDepChange(depName, val, isOptional) { - - if (val === undefined) { // unsetting a value? - - // required dependency that was previously set? - if (!isOptional && values[depName] !== undefined) { - satisfyCnt--; - } - - delete values[depName]; - } - else { // setting a value? - - // required dependency that was previously unset? - if (!isOptional && values[depName] === undefined) { - satisfyCnt++; - } - - values[depName] = val; - } - - queuedChangeCnt--; - if (!queuedChangeCnt) { // last change to cause a "start"? - - // now finally satisfied or satisfied all along? - if (satisfyCnt === depCnt) { - - // if the stopFunc initiated another value change, ignore it. - // it will be processed by another change event anyway. - if (!isCallingStop) { - startFunc(values); - } - } - } - } - - // intercept for .on() that remembers handlers - function bind(eventName, handler) { - _this.on(eventName, handler); - bindTuples.push([ eventName, handler ]); - } - - // listen to dependency changes - depList.forEach(function(depName) { - var isOptional = false; - - if (depName.charAt(0) === '?') { // TODO: more DRY - depName = depName.substring(1); - isOptional = true; - } - - bind('before:change:' + depName, function(val) { - onBeforeDepChange(depName, val, isOptional); - }); - - bind('change:' + depName, function(val) { - onDepChange(depName, val, isOptional); - }); - }); - - // process current dependency values - depList.forEach(function(depName) { - var isOptional = false; - - if (depName.charAt(0) === '?') { // TODO: more DRY - depName = depName.substring(1); - isOptional = true; - } - - if (_this.has(depName)) { - values[depName] = _this.get(depName); - satisfyCnt++; - } - else if (isOptional) { - satisfyCnt++; - } - }); - - // initially satisfied - if (satisfyCnt === depCnt) { - startFunc(values); - } - - return { - teardown: function() { - // remove all handlers - for (var i = 0; i < bindTuples.length; i++) { - _this.off(bindTuples[i][0], bindTuples[i][1]); - } - bindTuples = null; - - // was satisfied, so call stopFunc - if (satisfyCnt === depCnt) { - stopFunc(); - } - }, - flash: function() { - if (satisfyCnt === depCnt) { - stopFunc(); - startFunc(values); - } - } - }; - }, - - flash: function(name) { - var watcher = this._watchers[name]; - - if (watcher) { - watcher.flash(); - } - } - -}); - - -Model.watch = function(/* same arguments as this.watch() */) { - var proto = this.prototype; - - if (!proto._globalWatchArgs) { - proto._globalWatchArgs = []; - } - - proto._globalWatchArgs.push(arguments); -}; - - -FC.Model = Model; - - -;; - -var Promise = { - - construct: function(executor) { - var deferred = $.Deferred(); - var promise = deferred.promise(); - - if (typeof executor === 'function') { - executor( - function(val) { // resolve - deferred.resolve(val); - attachImmediatelyResolvingThen(promise, val); - }, - function() { // reject - deferred.reject(); - attachImmediatelyRejectingThen(promise); - } - ); - } - - return promise; - }, - - resolve: function(val) { - var deferred = $.Deferred().resolve(val); - var promise = deferred.promise(); - - attachImmediatelyResolvingThen(promise, val); - - return promise; - }, - - reject: function() { - var deferred = $.Deferred().reject(); - var promise = deferred.promise(); - - attachImmediatelyRejectingThen(promise); - - return promise; - } - -}; - - -function attachImmediatelyResolvingThen(promise, val) { - promise.then = function(onResolve) { - if (typeof onResolve === 'function') { - onResolve(val); - } - return promise; // for chaining - }; -} - - -function attachImmediatelyRejectingThen(promise) { - promise.then = function(onResolve, onReject) { - if (typeof onReject === 'function') { - onReject(); - } - return promise; // for chaining - }; -} - - -FC.Promise = Promise; - -;; - -var TaskQueue = Class.extend(EmitterMixin, { - - q: null, - isPaused: false, - isRunning: false, - - - constructor: function() { - this.q = []; - }, - - - queue: function(/* taskFunc, taskFunc... */) { - this.q.push.apply(this.q, arguments); // append - this.tryStart(); - }, - - - pause: function() { - this.isPaused = true; - }, - - - resume: function() { - this.isPaused = false; - this.tryStart(); - }, - - - tryStart: function() { - if (!this.isRunning && this.canRunNext()) { - this.isRunning = true; - this.trigger('start'); - this.runNext(); - } - }, - - - canRunNext: function() { - return !this.isPaused && this.q.length; - }, - - - runNext: function() { // does not check canRunNext - this.runTask(this.q.shift()); - }, - - - runTask: function(task) { - this.runTaskFunc(task); - }, - - - runTaskFunc: function(taskFunc) { - var _this = this; - var res = taskFunc(); - - if (res && res.then) { - res.then(done); - } - else { - done(); - } - - function done() { - if (_this.canRunNext()) { - _this.runNext(); - } - else { - _this.isRunning = false; - _this.trigger('stop'); - } - } - } - -}); - -FC.TaskQueue = TaskQueue; - -;; - -var RenderQueue = TaskQueue.extend({ - - waitsByNamespace: null, - waitNamespace: null, - waitId: null, - - - constructor: function(waitsByNamespace) { - TaskQueue.call(this); // super-constructor - - this.waitsByNamespace = waitsByNamespace || {}; - }, - - - queue: function(taskFunc, namespace, type) { - var task = { - func: taskFunc, - namespace: namespace, - type: type - }; - var waitMs; - - if (namespace) { - waitMs = this.waitsByNamespace[namespace]; - } - - if (this.waitNamespace) { - if (namespace === this.waitNamespace && waitMs != null) { - this.delayWait(waitMs); - } - else { - this.clearWait(); - this.tryStart(); - } - } - - if (this.compoundTask(task)) { // appended to queue? - - if (!this.waitNamespace && waitMs != null) { - this.startWait(namespace, waitMs); - } - else { - this.tryStart(); - } - } - }, - - - startWait: function(namespace, waitMs) { - this.waitNamespace = namespace; - this.spawnWait(waitMs); - }, - - - delayWait: function(waitMs) { - clearTimeout(this.waitId); - this.spawnWait(waitMs); - }, - - - spawnWait: function(waitMs) { - var _this = this; - - this.waitId = setTimeout(function() { - _this.waitNamespace = null; - _this.tryStart(); - }, waitMs); - }, - - - clearWait: function() { - if (this.waitNamespace) { - clearTimeout(this.waitId); - this.waitId = null; - this.waitNamespace = null; - } - }, - - - canRunNext: function() { - if (!TaskQueue.prototype.canRunNext.apply(this, arguments)) { - return false; - } - - // waiting for a certain namespace to stop receiving tasks? - if (this.waitNamespace) { - - // if there was a different namespace task in the meantime, - // that forces all previously-waiting tasks to suddenly execute. - // TODO: find a way to do this in constant time. - for (var q = this.q, i = 0; i < q.length; i++) { - if (q[i].namespace !== this.waitNamespace) { - return true; // allow execution - } - } - - return false; - } - - return true; - }, - - - runTask: function(task) { - this.runTaskFunc(task.func); - }, - - - compoundTask: function(newTask) { - var q = this.q; - var shouldAppend = true; - var i, task; - - if (newTask.namespace) { - - if (newTask.type === 'destroy' || newTask.type === 'init') { - - // remove all add/remove ops with same namespace, regardless of order - for (i = q.length - 1; i >= 0; i--) { - task = q[i]; - - if ( - task.namespace === newTask.namespace && - (task.type === 'add' || task.type === 'remove') - ) { - q.splice(i, 1); // remove task - } - } - - if (newTask.type === 'destroy') { - // eat away final init/destroy operation - if (q.length) { - task = q[q.length - 1]; // last task - - if (task.namespace === newTask.namespace) { - - // the init and our destroy cancel each other out - if (task.type === 'init') { - shouldAppend = false; - q.pop(); - } - // prefer to use the destroy operation that's already present - else if (task.type === 'destroy') { - shouldAppend = false; - } - } - } - } - else if (newTask.type === 'init') { - // eat away final init operation - if (q.length) { - task = q[q.length - 1]; // last task - - if ( - task.namespace === newTask.namespace && - task.type === 'init' - ) { - // our init operation takes precedence - q.pop(); - } - } - } - } - } - - if (shouldAppend) { - q.push(newTask); - } - - return shouldAppend; - } - -}); - -FC.RenderQueue = RenderQueue; - -;; - -var EmitterMixin = FC.EmitterMixin = { - - // jQuery-ification via $(this) allows a non-DOM object to have - // the same event handling capabilities (including namespaces). - - - on: function(types, handler) { - $(this).on(types, this._prepareIntercept(handler)); - return this; // for chaining - }, - - - one: function(types, handler) { - $(this).one(types, this._prepareIntercept(handler)); - return this; // for chaining - }, - - - _prepareIntercept: function(handler) { - // handlers are always called with an "event" object as their first param. - // sneak the `this` context and arguments into the extra parameter object - // and forward them on to the original handler. - var intercept = function(ev, extra) { - return handler.apply( - extra.context || this, - extra.args || [] - ); - }; - - // mimick jQuery's internal "proxy" system (risky, I know) - // causing all functions with the same .guid to appear to be the same. - // https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448 - // this is needed for calling .off with the original non-intercept handler. - if (!handler.guid) { - handler.guid = $.guid++; - } - intercept.guid = handler.guid; - - return intercept; - }, - - - off: function(types, handler) { - $(this).off(types, handler); - - return this; // for chaining - }, - - - trigger: function(types) { - var args = Array.prototype.slice.call(arguments, 1); // arguments after the first - - // pass in "extra" info to the intercept - $(this).triggerHandler(types, { args: args }); - - return this; // for chaining - }, - - - triggerWith: function(types, context, args) { - - // `triggerHandler` is less reliant on the DOM compared to `trigger`. - // pass in "extra" info to the intercept. - $(this).triggerHandler(types, { context: context, args: args }); - - return this; // for chaining - } - -}; - -;; - -/* -Utility methods for easily listening to events on another object, -and more importantly, easily unlistening from them. -*/ -var ListenerMixin = FC.ListenerMixin = (function() { - var guid = 0; - var ListenerMixin = { - - listenerId: null, - - /* - Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name. - The `callback` will be called with the `this` context of the object that .listenTo is being called on. - Can be called: - .listenTo(other, eventName, callback) - OR - .listenTo(other, { - eventName1: callback1, - eventName2: callback2 - }) - */ - listenTo: function(other, arg, callback) { - if (typeof arg === 'object') { // given dictionary of callbacks - for (var eventName in arg) { - if (arg.hasOwnProperty(eventName)) { - this.listenTo(other, eventName, arg[eventName]); - } - } - } - else if (typeof arg === 'string') { - other.on( - arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object - $.proxy(callback, this) // always use `this` context - // the usually-undesired jQuery guid behavior doesn't matter, - // because we always unbind via namespace - ); - } - }, - - /* - Causes the current object to stop listening to events on the `other` object. - `eventName` is optional. If omitted, will stop listening to ALL events on `other`. - */ - stopListeningTo: function(other, eventName) { - other.off((eventName || '') + '.' + this.getListenerNamespace()); - }, - - /* - Returns a string, unique to this object, to be used for event namespacing - */ - getListenerNamespace: function() { - if (this.listenerId == null) { - this.listenerId = guid++; - } - return '_listener' + this.listenerId; - } - - }; - return ListenerMixin; -})(); -;; - -/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------- -Options: - - className (string) - - content (HTML string or jQuery element set) - - parentEl - - top - - left - - right (the x coord of where the right edge should be. not a "CSS" right) - - autoHide (boolean) - - show (callback) - - hide (callback) -*/ - -var Popover = Class.extend(ListenerMixin, { - - isHidden: true, - options: null, - el: null, // the container element for the popover. generated by this object - margin: 10, // the space required between the popover and the edges of the scroll container - - - constructor: function(options) { - this.options = options || {}; - }, - - - // Shows the popover on the specified position. Renders it if not already - show: function() { - if (this.isHidden) { - if (!this.el) { - this.render(); - } - this.el.show(); - this.position(); - this.isHidden = false; - this.trigger('show'); - } - }, - - - // Hides the popover, through CSS, but does not remove it from the DOM - hide: function() { - if (!this.isHidden) { - this.el.hide(); - this.isHidden = true; - this.trigger('hide'); - } - }, - - - // Creates `this.el` and renders content inside of it - render: function() { - var _this = this; - var options = this.options; - - this.el = $('<div class="fc-popover"/>') - .addClass(options.className || '') - .css({ - // position initially to the top left to avoid creating scrollbars - top: 0, - left: 0 - }) - .append(options.content) - .appendTo(options.parentEl); - - // when a click happens on anything inside with a 'fc-close' className, hide the popover - this.el.on('click', '.fc-close', function() { - _this.hide(); - }); - - if (options.autoHide) { - this.listenTo($(document), 'mousedown', this.documentMousedown); - } - }, - - - // Triggered when the user clicks *anywhere* in the document, for the autoHide feature - documentMousedown: function(ev) { - // only hide the popover if the click happened outside the popover - if (this.el && !$(ev.target).closest(this.el).length) { - this.hide(); - } - }, - - - // Hides and unregisters any handlers - removeElement: function() { - this.hide(); - - if (this.el) { - this.el.remove(); - this.el = null; - } - - this.stopListeningTo($(document), 'mousedown'); - }, - - - // Positions the popover optimally, using the top/left/right options - position: function() { - var options = this.options; - var origin = this.el.offsetParent().offset(); - var width = this.el.outerWidth(); - var height = this.el.outerHeight(); - var windowEl = $(window); - var viewportEl = getScrollParent(this.el); - var viewportTop; - var viewportLeft; - var viewportOffset; - var top; // the "position" (not "offset") values for the popover - var left; // - - // compute top and left - top = options.top || 0; - if (options.left !== undefined) { - left = options.left; - } - else if (options.right !== undefined) { - left = options.right - width; // derive the left value from the right value - } - else { - left = 0; - } - - if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result - viewportEl = windowEl; - viewportTop = 0; // the window is always at the top left - viewportLeft = 0; // (and .offset() won't work if called here) - } - else { - viewportOffset = viewportEl.offset(); - viewportTop = viewportOffset.top; - viewportLeft = viewportOffset.left; - } - - // if the window is scrolled, it causes the visible area to be further down - viewportTop += windowEl.scrollTop(); - viewportLeft += windowEl.scrollLeft(); - - // constrain to the view port. if constrained by two edges, give precedence to top/left - if (options.viewportConstrain !== false) { - top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); - top = Math.max(top, viewportTop + this.margin); - left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); - left = Math.max(left, viewportLeft + this.margin); - } - - this.el.css({ - top: top - origin.top, - left: left - origin.left - }); - }, - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - // TODO: better code reuse for this. Repeat code - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - -}); - -;; - -/* -A cache for the left/right/top/bottom/width/height values for one or more elements. -Works with both offset (from topleft document) and position (from offsetParent). - -options: -- els -- isHorizontal -- isVertical -*/ -var CoordCache = FC.CoordCache = Class.extend({ - - els: null, // jQuery set (assumed to be siblings) - forcedOffsetParentEl: null, // options can override the natural offsetParent - origin: null, // {left,top} position of offsetParent of els - boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null - isHorizontal: false, // whether to query for left/right/width - isVertical: false, // whether to query for top/bottom/height - - // arrays of coordinates (offsets from topleft of document) - lefts: null, - rights: null, - tops: null, - bottoms: null, - - - constructor: function(options) { - this.els = $(options.els); - this.isHorizontal = options.isHorizontal; - this.isVertical = options.isVertical; - this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null; - }, - - - // Queries the els for coordinates and stores them. - // Call this method before using and of the get* methods below. - build: function() { - var offsetParentEl = this.forcedOffsetParentEl; - if (!offsetParentEl && this.els.length > 0) { - offsetParentEl = this.els.eq(0).offsetParent(); - } - - this.origin = offsetParentEl ? - offsetParentEl.offset() : - null; - - this.boundingRect = this.queryBoundingRect(); - - if (this.isHorizontal) { - this.buildElHorizontals(); - } - if (this.isVertical) { - this.buildElVerticals(); - } - }, - - - // Destroys all internal data about coordinates, freeing memory - clear: function() { - this.origin = null; - this.boundingRect = null; - this.lefts = null; - this.rights = null; - this.tops = null; - this.bottoms = null; - }, - - - // When called, if coord caches aren't built, builds them - ensureBuilt: function() { - if (!this.origin) { - this.build(); - } - }, - - - // Populates the left/right internal coordinate arrays - buildElHorizontals: function() { - var lefts = []; - var rights = []; - - this.els.each(function(i, node) { - var el = $(node); - var left = el.offset().left; - var width = el.outerWidth(); - - lefts.push(left); - rights.push(left + width); - }); - - this.lefts = lefts; - this.rights = rights; - }, - - - // Populates the top/bottom internal coordinate arrays - buildElVerticals: function() { - var tops = []; - var bottoms = []; - - this.els.each(function(i, node) { - var el = $(node); - var top = el.offset().top; - var height = el.outerHeight(); - - tops.push(top); - bottoms.push(top + height); - }); - - this.tops = tops; - this.bottoms = bottoms; - }, - - - // Given a left offset (from document left), returns the index of the el that it horizontally intersects. - // If no intersection is made, returns undefined. - getHorizontalIndex: function(leftOffset) { - this.ensureBuilt(); - - var lefts = this.lefts; - var rights = this.rights; - var len = lefts.length; - var i; - - for (i = 0; i < len; i++) { - if (leftOffset >= lefts[i] && leftOffset < rights[i]) { - return i; - } - } - }, - - - // Given a top offset (from document top), returns the index of the el that it vertically intersects. - // If no intersection is made, returns undefined. - getVerticalIndex: function(topOffset) { - this.ensureBuilt(); - - var tops = this.tops; - var bottoms = this.bottoms; - var len = tops.length; - var i; - - for (i = 0; i < len; i++) { - if (topOffset >= tops[i] && topOffset < bottoms[i]) { - return i; - } - } - }, - - - // Gets the left offset (from document left) of the element at the given index - getLeftOffset: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex]; - }, - - - // Gets the left position (from offsetParent left) of the element at the given index - getLeftPosition: function(leftIndex) { - this.ensureBuilt(); - return this.lefts[leftIndex] - this.origin.left; - }, - - - // Gets the right offset (from document left) of the element at the given index. - // This value is NOT relative to the document's right edge, like the CSS concept of "right" would be. - getRightOffset: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex]; - }, - - - // Gets the right position (from offsetParent left) of the element at the given index. - // This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be. - getRightPosition: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.origin.left; - }, - - - // Gets the width of the element at the given index - getWidth: function(leftIndex) { - this.ensureBuilt(); - return this.rights[leftIndex] - this.lefts[leftIndex]; - }, - - - // Gets the top offset (from document top) of the element at the given index - getTopOffset: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex]; - }, - - - // Gets the top position (from offsetParent top) of the element at the given position - getTopPosition: function(topIndex) { - this.ensureBuilt(); - return this.tops[topIndex] - this.origin.top; - }, - - // Gets the bottom offset (from the document top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomOffset: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex]; - }, - - - // Gets the bottom position (from the offsetParent top) of the element at the given index. - // This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be. - getBottomPosition: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.origin.top; - }, - - - // Gets the height of the element at the given index - getHeight: function(topIndex) { - this.ensureBuilt(); - return this.bottoms[topIndex] - this.tops[topIndex]; - }, - - - // Bounding Rect - // TODO: decouple this from CoordCache - - // Compute and return what the elements' bounding rectangle is, from the user's perspective. - // Right now, only returns a rectangle if constrained by an overflow:scroll element. - // Returns null if there are no elements - queryBoundingRect: function() { - var scrollParentEl; - - if (this.els.length > 0) { - scrollParentEl = getScrollParent(this.els.eq(0)); - - if (!scrollParentEl.is(document)) { - return getClientRect(scrollParentEl); - } - } - - return null; - }, - - isPointInBounds: function(leftOffset, topOffset) { - return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset); - }, - - isLeftInBounds: function(leftOffset) { - return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right); - }, - - isTopInBounds: function(topOffset) { - return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom); - } - -}); - -;; - -/* Tracks a drag's mouse movement, firing various handlers -----------------------------------------------------------------------------------------------------------------------*/ -// TODO: use Emitter - -var DragListener = FC.DragListener = Class.extend(ListenerMixin, { - - options: null, - subjectEl: null, - - // coordinates of the initial mousedown - originX: null, - originY: null, - - // the wrapping element that scrolls, or MIGHT scroll if there's overflow. - // TODO: do this for wrappers that have overflow:hidden as well. - scrollEl: null, - - isInteracting: false, - isDistanceSurpassed: false, - isDelayEnded: false, - isDragging: false, - isTouch: false, - isGeneric: false, // initiated by 'dragstart' (jqui) - - delay: null, - delayTimeoutId: null, - minDistance: null, - - shouldCancelTouchScroll: true, - scrollAlwaysKills: false, - - - constructor: function(options) { - this.options = options || {}; - }, - - - // Interaction (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - startInteraction: function(ev, extraOptions) { - - if (ev.type === 'mousedown') { - if (GlobalEmitter.get().shouldIgnoreMouse()) { - return; - } - else if (!isPrimaryMouseButton(ev)) { - return; - } - else { - ev.preventDefault(); // prevents native selection in most browsers - } - } - - if (!this.isInteracting) { - - // process options - extraOptions = extraOptions || {}; - this.delay = firstDefined(extraOptions.delay, this.options.delay, 0); - this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0); - this.subjectEl = this.options.subjectEl; - - preventSelection($('body')); - - this.isInteracting = true; - this.isTouch = getEvIsTouch(ev); - this.isGeneric = ev.type === 'dragstart'; - this.isDelayEnded = false; - this.isDistanceSurpassed = false; - - this.originX = getEvX(ev); - this.originY = getEvY(ev); - this.scrollEl = getScrollParent($(ev.target)); - - this.bindHandlers(); - this.initAutoScroll(); - this.handleInteractionStart(ev); - this.startDelay(ev); - - if (!this.minDistance) { - this.handleDistanceSurpassed(ev); - } - } - }, - - - handleInteractionStart: function(ev) { - this.trigger('interactionStart', ev); - }, - - - endInteraction: function(ev, isCancelled) { - if (this.isInteracting) { - this.endDrag(ev); - - if (this.delayTimeoutId) { - clearTimeout(this.delayTimeoutId); - this.delayTimeoutId = null; - } - - this.destroyAutoScroll(); - this.unbindHandlers(); - - this.isInteracting = false; - this.handleInteractionEnd(ev, isCancelled); - - allowSelection($('body')); - } - }, - - - handleInteractionEnd: function(ev, isCancelled) { - this.trigger('interactionEnd', ev, isCancelled || false); - }, - - - // Binding To DOM - // ----------------------------------------------------------------------------------------------------------------- - - - bindHandlers: function() { - // some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart, - // so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly. - var globalEmitter = GlobalEmitter.get(); - - if (this.isGeneric) { - this.listenTo($(document), { // might only work on iOS because of GlobalEmitter's bind :( - drag: this.handleMove, - dragstop: this.endInteraction - }); - } - else if (this.isTouch) { - this.listenTo(globalEmitter, { - touchmove: this.handleTouchMove, - touchend: this.endInteraction, - scroll: this.handleTouchScroll - }); - } - else { - this.listenTo(globalEmitter, { - mousemove: this.handleMouseMove, - mouseup: this.endInteraction - }); - } - - this.listenTo(globalEmitter, { - selectstart: preventDefault, // don't allow selection while dragging - contextmenu: preventDefault // long taps would open menu on Chrome dev tools - }); - }, - - - unbindHandlers: function() { - this.stopListeningTo(GlobalEmitter.get()); - this.stopListeningTo($(document)); // for isGeneric - }, - - - // Drag (high-level) - // ----------------------------------------------------------------------------------------------------------------- - - - // extraOptions ignored if drag already started - startDrag: function(ev, extraOptions) { - this.startInteraction(ev, extraOptions); // ensure interaction began - - if (!this.isDragging) { - this.isDragging = true; - this.handleDragStart(ev); - } - }, - - - handleDragStart: function(ev) { - this.trigger('dragStart', ev); - }, - - - handleMove: function(ev) { - var dx = getEvX(ev) - this.originX; - var dy = getEvY(ev) - this.originY; - var minDistance = this.minDistance; - var distanceSq; // current distance from the origin, squared - - if (!this.isDistanceSurpassed) { - distanceSq = dx * dx + dy * dy; - if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem - this.handleDistanceSurpassed(ev); - } - } - - if (this.isDragging) { - this.handleDrag(dx, dy, ev); - } - }, - - - // Called while the mouse is being moved and when we know a legitimate drag is taking place - handleDrag: function(dx, dy, ev) { - this.trigger('drag', dx, dy, ev); - this.updateAutoScroll(ev); // will possibly cause scrolling - }, - - - endDrag: function(ev) { - if (this.isDragging) { - this.isDragging = false; - this.handleDragEnd(ev); - } - }, - - - handleDragEnd: function(ev) { - this.trigger('dragEnd', ev); - }, - - - // Delay - // ----------------------------------------------------------------------------------------------------------------- - - - startDelay: function(initialEv) { - var _this = this; - - if (this.delay) { - this.delayTimeoutId = setTimeout(function() { - _this.handleDelayEnd(initialEv); - }, this.delay); - } - else { - this.handleDelayEnd(initialEv); - } - }, - - - handleDelayEnd: function(initialEv) { - this.isDelayEnded = true; - - if (this.isDistanceSurpassed) { - this.startDrag(initialEv); - } - }, - - - // Distance - // ----------------------------------------------------------------------------------------------------------------- - - - handleDistanceSurpassed: function(ev) { - this.isDistanceSurpassed = true; - - if (this.isDelayEnded) { - this.startDrag(ev); - } - }, - - - // Mouse / Touch - // ----------------------------------------------------------------------------------------------------------------- - - - handleTouchMove: function(ev) { - - // prevent inertia and touchmove-scrolling while dragging - if (this.isDragging && this.shouldCancelTouchScroll) { - ev.preventDefault(); - } - - this.handleMove(ev); - }, - - - handleMouseMove: function(ev) { - this.handleMove(ev); - }, - - - // Scrolling (unrelated to auto-scroll) - // ----------------------------------------------------------------------------------------------------------------- - - - handleTouchScroll: function(ev) { - // if the drag is being initiated by touch, but a scroll happens before - // the drag-initiating delay is over, cancel the drag - if (!this.isDragging || this.scrollAlwaysKills) { - this.endInteraction(ev, true); // isCancelled=true - } - }, - - - // Utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Triggers a callback. Calls a function in the option hash of the same name. - // Arguments beyond the first `name` are forwarded on. - trigger: function(name) { - if (this.options[name]) { - this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - // makes _methods callable by event name. TODO: kill this - if (this['_' + name]) { - this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1)); - } - } - - -}); - -;; -/* -this.scrollEl is set in DragListener -*/ -DragListener.mixin({ - - isAutoScroll: false, - - scrollBounds: null, // { top, bottom, left, right } - scrollTopVel: null, // pixels per second - scrollLeftVel: null, // pixels per second - scrollIntervalId: null, // ID of setTimeout for scrolling animation loop - - // defaults - scrollSensitivity: 30, // pixels from edge for scrolling to start - scrollSpeed: 200, // pixels per second, at maximum speed - scrollIntervalMs: 50, // millisecond wait between scroll increment - - - initAutoScroll: function() { - var scrollEl = this.scrollEl; - - this.isAutoScroll = - this.options.scroll && - scrollEl && - !scrollEl.is(window) && - !scrollEl.is(document); - - if (this.isAutoScroll) { - // debounce makes sure rapid calls don't happen - this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100)); - } - }, - - - destroyAutoScroll: function() { - this.endAutoScroll(); // kill any animation loop - - // remove the scroll handler if there is a scrollEl - if (this.isAutoScroll) { - this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :( - } - }, - - - // Computes and stores the bounding rectangle of scrollEl - computeScrollBounds: function() { - if (this.isAutoScroll) { - this.scrollBounds = getOuterRect(this.scrollEl); - // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars - } - }, - - - // Called when the dragging is in progress and scrolling should be updated - updateAutoScroll: function(ev) { - var sensitivity = this.scrollSensitivity; - var bounds = this.scrollBounds; - var topCloseness, bottomCloseness; - var leftCloseness, rightCloseness; - var topVel = 0; - var leftVel = 0; - - if (bounds) { // only scroll if scrollEl exists - - // compute closeness to edges. valid range is from 0.0 - 1.0 - topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity; - bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity; - leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity; - rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity; - - // translate vertical closeness into velocity. - // mouse must be completely in bounds for velocity to happen. - if (topCloseness >= 0 && topCloseness <= 1) { - topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up - } - else if (bottomCloseness >= 0 && bottomCloseness <= 1) { - topVel = bottomCloseness * this.scrollSpeed; - } - - // translate horizontal closeness into velocity - if (leftCloseness >= 0 && leftCloseness <= 1) { - leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left - } - else if (rightCloseness >= 0 && rightCloseness <= 1) { - leftVel = rightCloseness * this.scrollSpeed; - } - } - - this.setScrollVel(topVel, leftVel); - }, - - - // Sets the speed-of-scrolling for the scrollEl - setScrollVel: function(topVel, leftVel) { - - this.scrollTopVel = topVel; - this.scrollLeftVel = leftVel; - - this.constrainScrollVel(); // massages into realistic values - - // if there is non-zero velocity, and an animation loop hasn't already started, then START - if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { - this.scrollIntervalId = setInterval( - proxy(this, 'scrollIntervalFunc'), // scope to `this` - this.scrollIntervalMs - ); - } - }, - - - // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way - constrainScrollVel: function() { - var el = this.scrollEl; - - if (this.scrollTopVel < 0) { // scrolling up? - if (el.scrollTop() <= 0) { // already scrolled all the way up? - this.scrollTopVel = 0; - } - } - else if (this.scrollTopVel > 0) { // scrolling down? - if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? - this.scrollTopVel = 0; - } - } - - if (this.scrollLeftVel < 0) { // scrolling left? - if (el.scrollLeft() <= 0) { // already scrolled all the left? - this.scrollLeftVel = 0; - } - } - else if (this.scrollLeftVel > 0) { // scrolling right? - if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? - this.scrollLeftVel = 0; - } - } - }, - - - // This function gets called during every iteration of the scrolling animation loop - scrollIntervalFunc: function() { - var el = this.scrollEl; - var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by - - // change the value of scrollEl's scroll - if (this.scrollTopVel) { - el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); - } - if (this.scrollLeftVel) { - el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); - } - - this.constrainScrollVel(); // since the scroll values changed, recompute the velocities - - // if scrolled all the way, which causes the vels to be zero, stop the animation loop - if (!this.scrollTopVel && !this.scrollLeftVel) { - this.endAutoScroll(); - } - }, - - - // Kills any existing scrolling animation loop - endAutoScroll: function() { - if (this.scrollIntervalId) { - clearInterval(this.scrollIntervalId); - this.scrollIntervalId = null; - - this.handleScrollEnd(); - } - }, - - - // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) - handleDebouncedScroll: function() { - // recompute all coordinates, but *only* if this is *not* part of our scrolling animation - if (!this.scrollIntervalId) { - this.handleScrollEnd(); - } - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - } - -}); -;; - -/* Tracks mouse movements over a component and raises events about which hit the mouse is over. ------------------------------------------------------------------------------------------------------------------------- -options: -- subjectEl -- subjectCenter -*/ - -var HitDragListener = DragListener.extend({ - - component: null, // converts coordinates to hits - // methods: hitsNeeded, hitsNotNeeded, queryHit - - origHit: null, // the hit the mouse was over when listening started - hit: null, // the hit the mouse is over - coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions - - - constructor: function(component, options) { - DragListener.call(this, options); // call the super-constructor - - this.component = component; - }, - - - // Called when drag listening starts (but a real drag has not necessarily began). - // ev might be undefined if dragging was started manually. - handleInteractionStart: function(ev) { - var subjectEl = this.subjectEl; - var subjectRect; - var origPoint; - var point; - - this.component.hitsNeeded(); - this.computeScrollBounds(); // for autoscroll - - if (ev) { - origPoint = { left: getEvX(ev), top: getEvY(ev) }; - point = origPoint; - - // constrain the point to bounds of the element being dragged - if (subjectEl) { - subjectRect = getOuterRect(subjectEl); // used for centering as well - point = constrainPoint(point, subjectRect); - } - - this.origHit = this.queryHit(point.left, point.top); - - // treat the center of the subject as the collision point? - if (subjectEl && this.options.subjectCenter) { - - // only consider the area the subject overlaps the hit. best for large subjects. - // TODO: skip this if hit didn't supply left/right/top/bottom - if (this.origHit) { - subjectRect = intersectRects(this.origHit, subjectRect) || - subjectRect; // in case there is no intersection - } - - point = getRectCenter(subjectRect); - } - - this.coordAdjust = diffPoints(point, origPoint); // point - origPoint - } - else { - this.origHit = null; - this.coordAdjust = null; - } - - // call the super-method. do it after origHit has been computed - DragListener.prototype.handleInteractionStart.apply(this, arguments); - }, - - - // Called when the actual drag has started - handleDragStart: function(ev) { - var hit; - - DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method - - // might be different from this.origHit if the min-distance is large - hit = this.queryHit(getEvX(ev), getEvY(ev)); - - // report the initial hit the mouse is over - // especially important if no min-distance and drag starts immediately - if (hit) { - this.handleHitOver(hit); - } - }, - - - // Called when the drag moves - handleDrag: function(dx, dy, ev) { - var hit; - - DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method - - hit = this.queryHit(getEvX(ev), getEvY(ev)); - - if (!isHitsEqual(hit, this.hit)) { // a different hit than before? - if (this.hit) { - this.handleHitOut(); - } - if (hit) { - this.handleHitOver(hit); - } - } - }, - - - // Called when dragging has been stopped - handleDragEnd: function() { - this.handleHitDone(); - DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method - }, - - - // Called when a the mouse has just moved over a new hit - handleHitOver: function(hit) { - var isOrig = isHitsEqual(hit, this.origHit); - - this.hit = hit; - - this.trigger('hitOver', this.hit, isOrig, this.origHit); - }, - - - // Called when the mouse has just moved out of a hit - handleHitOut: function() { - if (this.hit) { - this.trigger('hitOut', this.hit); - this.handleHitDone(); - this.hit = null; - } - }, - - - // Called after a hitOut. Also called before a dragStop - handleHitDone: function() { - if (this.hit) { - this.trigger('hitDone', this.hit); - } - }, - - - // Called when the interaction ends, whether there was a real drag or not - handleInteractionEnd: function() { - DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method - - this.origHit = null; - this.hit = null; - - this.component.hitsNotNeeded(); - }, - - - // Called when scrolling has stopped, whether through auto scroll, or the user scrolling - handleScrollEnd: function() { - DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method - - // hits' absolute positions will be in new places after a user's scroll. - // HACK for recomputing. - if (this.isDragging) { - this.component.releaseHits(); - this.component.prepareHits(); - } - }, - - - // Gets the hit underneath the coordinates for the given mouse event - queryHit: function(left, top) { - - if (this.coordAdjust) { - left += this.coordAdjust.left; - top += this.coordAdjust.top; - } - - return this.component.queryHit(left, top); - } - -}); - - -// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component. -// Two null values will be considered equal, as two "out of the component" states are the same. -function isHitsEqual(hit0, hit1) { - - if (!hit0 && !hit1) { - return true; - } - - if (hit0 && hit1) { - return hit0.component === hit1.component && - isHitPropsWithin(hit0, hit1) && - isHitPropsWithin(hit1, hit0); // ensures all props are identical - } - - return false; -} - - -// Returns true if all of subHit's non-standard properties are within superHit -function isHitPropsWithin(subHit, superHit) { - for (var propName in subHit) { - if (!/^(component|left|right|top|bottom)$/.test(propName)) { - if (subHit[propName] !== superHit[propName]) { - return false; - } - } - } - return true; -} - -;; - -/* -Listens to document and window-level user-interaction events, like touch events and mouse events, -and fires these events as-is to whoever is observing a GlobalEmitter. -Best when used as a singleton via GlobalEmitter.get() - -Normalizes mouse/touch events. For examples: -- ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click -- compensates for various buggy scenarios where a touchend does not fire -*/ - -FC.touchMouseIgnoreWait = 500; - -var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, { - - isTouching: false, - mouseIgnoreDepth: 0, - handleScrollProxy: null, - - - bind: function() { - var _this = this; - - this.listenTo($(document), { - touchstart: this.handleTouchStart, - touchcancel: this.handleTouchCancel, - touchend: this.handleTouchEnd, - mousedown: this.handleMouseDown, - mousemove: this.handleMouseMove, - mouseup: this.handleMouseUp, - click: this.handleClick, - selectstart: this.handleSelectStart, - contextmenu: this.handleContextMenu - }); - - // because we need to call preventDefault - // because https://www.chromestatus.com/features/5093566007214080 - // TODO: investigate performance because this is a global handler - window.addEventListener( - 'touchmove', - this.handleTouchMoveProxy = function(ev) { - _this.handleTouchMove($.Event(ev)); - }, - { passive: false } // allows preventDefault() - ); - - // attach a handler to get called when ANY scroll action happens on the page. - // this was impossible to do with normal on/off because 'scroll' doesn't bubble. - // http://stackoverflow.com/a/32954565/96342 - window.addEventListener( - 'scroll', - this.handleScrollProxy = function(ev) { - _this.handleScroll($.Event(ev)); - }, - true // useCapture - ); - }, - - unbind: function() { - this.stopListeningTo($(document)); - - window.removeEventListener( - 'touchmove', - this.handleTouchMoveProxy - ); - - window.removeEventListener( - 'scroll', - this.handleScrollProxy, - true // useCapture - ); - }, - - - // Touch Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleTouchStart: function(ev) { - - // if a previous touch interaction never ended with a touchend, then implicitly end it, - // but since a new touch interaction is about to begin, don't start the mouse ignore period. - this.stopTouch(ev, true); // skipMouseIgnore=true - - this.isTouching = true; - this.trigger('touchstart', ev); - }, - - handleTouchMove: function(ev) { - if (this.isTouching) { - this.trigger('touchmove', ev); - } - }, - - handleTouchCancel: function(ev) { - if (this.isTouching) { - this.trigger('touchcancel', ev); - - // Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both. - // If touchend fires later, it won't have any effect b/c isTouching will be false. - this.stopTouch(ev); - } - }, - - handleTouchEnd: function(ev) { - this.stopTouch(ev); - }, - - - // Mouse Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleMouseDown: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mousedown', ev); - } - }, - - handleMouseMove: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mousemove', ev); - } - }, - - handleMouseUp: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('mouseup', ev); - } - }, - - handleClick: function(ev) { - if (!this.shouldIgnoreMouse()) { - this.trigger('click', ev); - } - }, - - - // Misc Handlers - // ----------------------------------------------------------------------------------------------------------------- - - handleSelectStart: function(ev) { - this.trigger('selectstart', ev); - }, - - handleContextMenu: function(ev) { - this.trigger('contextmenu', ev); - }, - - handleScroll: function(ev) { - this.trigger('scroll', ev); - }, - - - // Utils - // ----------------------------------------------------------------------------------------------------------------- - - stopTouch: function(ev, skipMouseIgnore) { - if (this.isTouching) { - this.isTouching = false; - this.trigger('touchend', ev); - - if (!skipMouseIgnore) { - this.startTouchMouseIgnore(); - } - } - }, - - startTouchMouseIgnore: function() { - var _this = this; - var wait = FC.touchMouseIgnoreWait; - - if (wait) { - this.mouseIgnoreDepth++; - setTimeout(function() { - _this.mouseIgnoreDepth--; - }, wait); - } - }, - - shouldIgnoreMouse: function() { - return this.isTouching || Boolean(this.mouseIgnoreDepth); - } - -}); - - -// Singleton -// --------------------------------------------------------------------------------------------------------------------- - -(function() { - var globalEmitter = null; - var neededCount = 0; - - - // gets the singleton - GlobalEmitter.get = function() { - - if (!globalEmitter) { - globalEmitter = new GlobalEmitter(); - globalEmitter.bind(); - } - - return globalEmitter; - }; - - - // called when an object knows it will need a GlobalEmitter in the near future. - GlobalEmitter.needed = function() { - GlobalEmitter.get(); // ensures globalEmitter - neededCount++; - }; - - - // called when the object that originally called needed() doesn't need a GlobalEmitter anymore. - GlobalEmitter.unneeded = function() { - neededCount--; - - if (!neededCount) { // nobody else needs it - globalEmitter.unbind(); - globalEmitter = null; - } - }; - -})(); - -;; - -/* Creates a clone of an element and lets it track the mouse as it moves -----------------------------------------------------------------------------------------------------------------------*/ - -var MouseFollower = Class.extend(ListenerMixin, { - - options: null, - - sourceEl: null, // the element that will be cloned and made to look like it is dragging - el: null, // the clone of `sourceEl` that will track the mouse - parentEl: null, // the element that `el` (the clone) will be attached to - - // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl - top0: null, - left0: null, - - // the absolute coordinates of the initiating touch/mouse action - y0: null, - x0: null, - - // the number of pixels the mouse has moved from its initial position - topDelta: null, - leftDelta: null, - - isFollowing: false, - isHidden: false, - isAnimating: false, // doing the revert animation? - - constructor: function(sourceEl, options) { - this.options = options = options || {}; - this.sourceEl = sourceEl; - this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent - }, - - - // Causes the element to start following the mouse - start: function(ev) { - if (!this.isFollowing) { - this.isFollowing = true; - - this.y0 = getEvY(ev); - this.x0 = getEvX(ev); - this.topDelta = 0; - this.leftDelta = 0; - - if (!this.isHidden) { - this.updatePosition(); - } - - if (getEvIsTouch(ev)) { - this.listenTo($(document), 'touchmove', this.handleMove); - } - else { - this.listenTo($(document), 'mousemove', this.handleMove); - } - } - }, - - - // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. - // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. - stop: function(shouldRevert, callback) { - var _this = this; - var revertDuration = this.options.revertDuration; - - function complete() { // might be called by .animate(), which might change `this` context - _this.isAnimating = false; - _this.removeElement(); - - _this.top0 = _this.left0 = null; // reset state for future updatePosition calls - - if (callback) { - callback(); - } - } - - if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time - this.isFollowing = false; - - this.stopListeningTo($(document)); - - if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? - this.isAnimating = true; - this.el.animate({ - top: this.top0, - left: this.left0 - }, { - duration: revertDuration, - complete: complete - }); - } - else { - complete(); - } - } - }, - - - // Gets the tracking element. Create it if necessary - getEl: function() { - var el = this.el; - - if (!el) { - el = this.el = this.sourceEl.clone() - .addClass(this.options.additionalClass || '') - .css({ - position: 'absolute', - visibility: '', // in case original element was hidden (commonly through hideEvents()) - display: this.isHidden ? 'none' : '', // for when initially hidden - margin: 0, - right: 'auto', // erase and set width instead - bottom: 'auto', // erase and set height instead - width: this.sourceEl.width(), // explicit height in case there was a 'right' value - height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value - opacity: this.options.opacity || '', - zIndex: this.options.zIndex - }); - - // we don't want long taps or any mouse interaction causing selection/menus. - // would use preventSelection(), but that prevents selectstart, causing problems. - el.addClass('fc-unselectable'); - - el.appendTo(this.parentEl); - } - - return el; - }, - - - // Removes the tracking element if it has already been created - removeElement: function() { - if (this.el) { - this.el.remove(); - this.el = null; - } - }, - - - // Update the CSS position of the tracking element - updatePosition: function() { - var sourceOffset; - var origin; - - this.getEl(); // ensure this.el - - // make sure origin info was computed - if (this.top0 === null) { - sourceOffset = this.sourceEl.offset(); - origin = this.el.offsetParent().offset(); - this.top0 = sourceOffset.top - origin.top; - this.left0 = sourceOffset.left - origin.left; - } - - this.el.css({ - top: this.top0 + this.topDelta, - left: this.left0 + this.leftDelta - }); - }, - - - // Gets called when the user moves the mouse - handleMove: function(ev) { - this.topDelta = getEvY(ev) - this.y0; - this.leftDelta = getEvX(ev) - this.x0; - - if (!this.isHidden) { - this.updatePosition(); - } - }, - - - // Temporarily makes the tracking element invisible. Can be called before following starts - hide: function() { - if (!this.isHidden) { - this.isHidden = true; - if (this.el) { - this.el.hide(); - } - } - }, - - - // Show the tracking element after it has been temporarily hidden - show: function() { - if (this.isHidden) { - this.isHidden = false; - this.updatePosition(); - this.getEl().show(); - } - } - -}); - -;; - -/* An abstract class comprised of a "grid" of areas that each represent a specific datetime -----------------------------------------------------------------------------------------------------------------------*/ - -var Grid = FC.Grid = Class.extend(ListenerMixin, { - - // self-config, overridable by subclasses - hasDayInteractions: true, // can user click/select ranges of time? - - view: null, // a View object - isRTL: null, // shortcut to the view's isRTL option - - start: null, - end: null, - - el: null, // the containing element - elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. - - // derived from options - eventTimeFormat: null, - displayEventTime: null, - displayEventEnd: null, - - minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration - - // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity - // of the date areas. if not defined, assumes to be day and time granularity. - // TODO: port isTimeScale into same system? - largeUnit: null, - - dayClickListener: null, - daySelectListener: null, - segDragListener: null, - segResizeListener: null, - externalDragListener: null, - - - constructor: function(view) { - this.view = view; - this.isRTL = view.opt('isRTL'); - this.elsByFill = {}; - - this.dayClickListener = this.buildDayClickListener(); - this.daySelectListener = this.buildDaySelectListener(); - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' - computeEventTimeFormat: function() { - return this.view.opt('smallTimeFormat'); - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. - // Only applies to non-all-day events. - computeDisplayEventTime: function() { - return true; - }, - - - // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' - computeDisplayEventEnd: function() { - return true; - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - // Tells the grid about what period of time to display. - // Any date-related internal data should be generated. - setRange: function(range) { - this.start = range.start.clone(); - this.end = range.end.clone(); - - this.rangeUpdated(); - this.processRangeOptions(); - }, - - - // Called when internal variables that rely on the range should be updated - rangeUpdated: function() { - }, - - - // Updates values that rely on options and also relate to range - processRangeOptions: function() { - var view = this.view; - var displayEventTime; - var displayEventEnd; - - this.eventTimeFormat = - view.opt('eventTimeFormat') || - view.opt('timeFormat') || // deprecated - this.computeEventTimeFormat(); - - displayEventTime = view.opt('displayEventTime'); - if (displayEventTime == null) { - displayEventTime = this.computeDisplayEventTime(); // might be based off of range - } - - displayEventEnd = view.opt('displayEventEnd'); - if (displayEventEnd == null) { - displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range - } - - this.displayEventTime = displayEventTime; - this.displayEventEnd = displayEventEnd; - }, - - - // Converts a span (has unzoned start/end and any other grid-specific location information) - // into an array of segments (pieces of events whose format is decided by the grid). - spanToSegs: function(span) { - // subclasses must implement - }, - - - // Diffs the two dates, returning a duration, based on granularity of the grid - // TODO: port isTimeScale into this system? - diffDates: function(a, b) { - if (this.largeUnit) { - return diffByUnit(a, b, this.largeUnit); - } - else { - return diffDayTime(a, b); - } - }, - - - /* Hit Area - ------------------------------------------------------------------------------------------------------------------*/ - - hitsNeededDepth: 0, // necessary because multiple callers might need the same hits - - hitsNeeded: function() { - if (!(this.hitsNeededDepth++)) { - this.prepareHits(); - } - }, - - hitsNotNeeded: function() { - if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) { - this.releaseHits(); - } - }, - - - // Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit - prepareHits: function() { - }, - - - // Called when queryHit calls have subsided. Good place to clear any coordinate caches. - releaseHits: function() { - }, - - - // Given coordinates from the topleft of the document, return data about the date-related area underneath. - // Can return an object with arbitrary properties (although top/right/left/bottom are encouraged). - // Must have a `grid` property, a reference to this current grid. TODO: avoid this - // The returned object will be processed by getHitSpan and getHitEl. - queryHit: function(leftOffset, topOffset) { - }, - - - // like getHitSpan, but returns null if the resulting span's range is invalid - getSafeHitSpan: function(hit) { - var hitSpan = this.getHitSpan(hit); - - if (!isRangeWithinRange(hitSpan, this.view.activeRange)) { - return null; - } - - return hitSpan; - }, - - - // Given position-level information about a date-related area within the grid, - // should return an object with at least a start/end date. Can provide other information as well. - getHitSpan: function(hit) { - }, - - - // Given position-level information about a date-related area within the grid, - // should return a jQuery element that best represents it. passed to dayClick callback. - getHitEl: function(hit) { - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the container element that the grid should render inside of. - // Does other DOM-related initializations. - setElement: function(el) { - this.el = el; - - if (this.hasDayInteractions) { - preventSelection(el); - - this.bindDayHandler('touchstart', this.dayTouchStart); - this.bindDayHandler('mousedown', this.dayMousedown); - } - - // attach event-element-related handlers. in Grid.events - // same garbage collection note as above. - this.bindSegHandlers(); - - this.bindGlobalHandlers(); - }, - - - bindDayHandler: function(name, handler) { - var _this = this; - - // attach a handler to the grid's root element. - // jQuery will take care of unregistering them when removeElement gets called. - this.el.on(name, function(ev) { - if ( - !$(ev.target).is( - _this.segSelector + ',' + // directly on an event element - _this.segSelector + ' *,' + // within an event element - '.fc-more,' + // a "more.." link - 'a[data-goto]' // a clickable nav link - ) - ) { - return handler.call(_this, ev); - } - }); - }, - - - // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. - // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View - removeElement: function() { - this.unbindGlobalHandlers(); - this.clearDragListeners(); - - this.el.remove(); - - // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement - }, - - - // Renders the basic structure of grid view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Renders the grid's date-related content (like areas that represent days/times). - // Assumes setRange has already been called and the skeleton has already been rendered. - renderDates: function() { - // subclasses should implement - }, - - - // Unrenders the grid's date-related content - unrenderDates: function() { - // subclasses should implement - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Binds DOM handlers to elements that reside outside the grid, such as the document - bindGlobalHandlers: function() { - this.listenTo($(document), { - dragstart: this.externalDragStart, // jqui - sortstart: this.externalDragStart // jqui - }); - }, - - - // Unbinds DOM handlers from elements that reside outside the grid - unbindGlobalHandlers: function() { - this.stopListeningTo($(document)); - }, - - - // Process a mousedown on an element that represents a day. For day clicking and selecting. - dayMousedown: function(ev) { - var view = this.view; - - // HACK - // This will still work even though bindDayHandler doesn't use GlobalEmitter. - if (GlobalEmitter.get().shouldIgnoreMouse()) { - return; - } - - this.dayClickListener.startInteraction(ev); - - if (view.opt('selectable')) { - this.daySelectListener.startInteraction(ev, { - distance: view.opt('selectMinDistance') - }); - } - }, - - - dayTouchStart: function(ev) { - var view = this.view; - var selectLongPressDelay; - - // On iOS (and Android?) when a new selection is initiated overtop another selection, - // the touchend never fires because the elements gets removed mid-touch-interaction (my theory). - // HACK: simply don't allow this to happen. - // ALSO: prevent selection when an *event* is already raised. - if (view.isSelected || view.selectedEvent) { - return; - } - - selectLongPressDelay = view.opt('selectLongPressDelay'); - if (selectLongPressDelay == null) { - selectLongPressDelay = view.opt('longPressDelay'); // fallback - } - - this.dayClickListener.startInteraction(ev); - - if (view.opt('selectable')) { - this.daySelectListener.startInteraction(ev, { - delay: selectLongPressDelay - }); - } - }, - - - // Creates a listener that tracks the user's drag across day elements, for day clicking. - buildDayClickListener: function() { - var _this = this; - var view = this.view; - var dayClickHit; // null if invalid dayClick - - var dragListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - interactionStart: function() { - dayClickHit = dragListener.origHit; - }, - hitOver: function(hit, isOrig, origHit) { - // if user dragged to another cell at any point, it can no longer be a dayClick - if (!isOrig) { - dayClickHit = null; - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - dayClickHit = null; - }, - interactionEnd: function(ev, isCancelled) { - var hitSpan; - - if (!isCancelled && dayClickHit) { - hitSpan = _this.getSafeHitSpan(dayClickHit); - - if (hitSpan) { - view.triggerDayClick(hitSpan, _this.getHitEl(dayClickHit), ev); - } - } - } - }); - - // because dayClickListener won't be called with any time delay, "dragging" will begin immediately, - // which will kill any touchmoving/scrolling. Prevent this. - dragListener.shouldCancelTouchScroll = false; - - dragListener.scrollAlwaysKills = true; - - return dragListener; - }, - - - // Creates a listener that tracks the user's drag across day elements, for day selecting. - buildDaySelectListener: function() { - var _this = this; - var view = this.view; - var selectionSpan; // null if invalid selection - - var dragListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - interactionStart: function() { - selectionSpan = null; - }, - dragStart: function() { - view.unselect(); // since we could be rendering a new selection, we want to clear any old one - }, - hitOver: function(hit, isOrig, origHit) { - var origHitSpan; - var hitSpan; - - if (origHit) { // click needs to have started on a hit - - origHitSpan = _this.getSafeHitSpan(origHit); - hitSpan = _this.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - selectionSpan = _this.computeSelection(origHitSpan, hitSpan); - } - else { - selectionSpan = null; - } - - if (selectionSpan) { - _this.renderSelection(selectionSpan); - } - else if (selectionSpan === false) { - disableCursor(); - } - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - selectionSpan = null; - _this.unrenderSelection(); - }, - hitDone: function() { // called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev, isCancelled) { - if (!isCancelled && selectionSpan) { - // the selection will already have been rendered. just report it - view.reportSelection(selectionSpan, ev); - } - } - }); - - return dragListener; - }, - - - // Kills all in-progress dragging. - // Useful for when public API methods that result in re-rendering are invoked during a drag. - // Also useful for when touch devices misbehave and don't fire their touchend. - clearDragListeners: function() { - this.dayClickListener.endInteraction(); - this.daySelectListener.endInteraction(); - - if (this.segDragListener) { - this.segDragListener.endInteraction(); // will clear this.segDragListener - } - if (this.segResizeListener) { - this.segResizeListener.endInteraction(); // will clear this.segResizeListener - } - if (this.externalDragListener) { - this.externalDragListener.endInteraction(); // will clear this.externalDragListener - } - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: should probably move this to Grid.events, like we did event dragging / resizing - - - // Renders a mock event at the given event location, which contains zoned start/end properties. - // Returns all mock event elements. - renderEventLocationHelper: function(eventLocation, sourceSeg) { - var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg); - - return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering - }, - - - // Builds a fake event given zoned event date properties and a segment is should be inspired from. - // The range's end can be null, in which case the mock event that is rendered will have a null end time. - // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. - fabricateHelperEvent: function(eventLocation, sourceSeg) { - var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible - - fakeEvent.start = eventLocation.start.clone(); - fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null; - fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates - this.view.calendar.normalizeEventDates(fakeEvent); - - // this extra className will be useful for differentiating real events from mock events in CSS - fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); - - // if something external is being dragged in, don't render a resizer - if (!sourceSeg) { - fakeEvent.editable = false; - } - - return fakeEvent; - }, - - - // Renders a mock event. Given zoned event date properties. - // Must return all mock event elements. - renderHelper: function(eventLocation, sourceSeg) { - // subclasses must implement - }, - - - // Unrenders a mock event - unrenderHelper: function() { - // subclasses must implement - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. - // Given a span (unzoned start/end and other misc data) - renderSelection: function(span) { - this.renderHighlight(span); - }, - - - // Unrenders any visual indications of a selection. Will unrender a highlight by default. - unrenderSelection: function() { - this.unrenderHighlight(); - }, - - - // Given the first and last date-spans of a selection, returns another date-span object. - // Subclasses can override and provide additional data in the span object. Will be passed to renderSelection(). - // Will return false if the selection is invalid and this should be indicated to the user. - // Will return null/undefined if a selection invalid but no error should be reported. - computeSelection: function(span0, span1) { - var span = this.computeSelectionSpan(span0, span1); - - if (span && !this.view.calendar.isSelectionSpanAllowed(span)) { - return false; - } - - return span; - }, - - - // Given two spans, must return the combination of the two. - // TODO: do this separation of concerns (combining VS validation) for event dnd/resize too. - computeSelectionSpan: function(span0, span1) { - var dates = [ span0.start, span0.end, span1.start, span1.end ]; - - dates.sort(compareNumbers); // sorts chronologically. works with Moments - - return { start: dates[0].clone(), end: dates[3].clone() }; - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data) - renderHighlight: function(span) { - this.renderFill('highlight', this.spanToSegs(span)); - }, - - - // Unrenders the emphasis on a date range - unrenderHighlight: function() { - this.unrenderFill('highlight'); - }, - - - // Generates an array of classNames for rendering the highlight. Used by the fill system. - highlightSegClasses: function() { - return [ 'fc-highlight' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - }, - - - unrenderBusinessHours: function() { - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - }, - - - renderNowIndicator: function(date) { - }, - - - unrenderNowIndicator: function() { - }, - - - /* Fill System (highlight, background events, business hours) - -------------------------------------------------------------------------------------------------------------------- - TODO: remove this system. like we did in TimeGrid - */ - - - // Renders a set of rectangles over the given segments of time. - // MUST RETURN a subset of segs, the segs that were actually rendered. - // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement - renderFill: function(type, segs) { - // subclasses must implement - }, - - - // Unrenders a specific type of fill that is currently rendered on the grid - unrenderFill: function(type) { - var el = this.elsByFill[type]; - - if (el) { - el.remove(); - delete this.elsByFill[type]; - } - }, - - - // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. - // Only returns segments that successfully rendered. - // To be harnessed by renderFill (implemented by subclasses). - // Analagous to renderFgSegEls. - renderFillSegEls: function(type, segs) { - var _this = this; - var segElMethod = this[type + 'SegEl']; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { - - // build a large concatenation of segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fillSegHtml(type, segs[i]); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = $(node); - - // allow custom filter methods per-type - if (segElMethod) { - el = segElMethod.call(_this, seg, el); - } - - if (el) { // custom filters did not cancel the render - el = $(el); // allow custom filter to return raw DOM node - - // correct element type? (would be bad if a non-TD were inserted into a table for example) - if (el.is(_this.fillSegTag)) { - seg.el = el; - renderedSegs.push(seg); - } - } - }); - } - - return renderedSegs; - }, - - - fillSegTag: 'div', // subclasses can override - - - // Builds the HTML needed for one fill segment. Generic enough to work with different types. - fillSegHtml: function(type, seg) { - - // custom hooks per-type - var classesMethod = this[type + 'SegClasses']; - var cssMethod = this[type + 'SegCss']; - - var classes = classesMethod ? classesMethod.call(this, seg) : []; - var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); - - return '<' + this.fillSegTag + - (classes.length ? ' class="' + classes.join(' ') + '"' : '') + - (css ? ' style="' + css + '"' : '') + - ' />'; - }, - - - - /* Generic rendering utilities for subclasses - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes HTML classNames for a single-day element - getDayClasses: function(date, noThemeHighlight) { - var view = this.view; - var classes = []; - var today; - - if (!isDateWithinRange(date, view.activeRange)) { - classes.push('fc-disabled-day'); // TODO: jQuery UI theme? - } - else { - classes.push('fc-' + dayIDs[date.day()]); - - if ( - view.currentRangeAs('months') == 1 && // TODO: somehow get into MonthView - date.month() != view.currentRange.start.month() - ) { - classes.push('fc-other-month'); - } - - today = view.calendar.getNow(); - - if (date.isSame(today, 'day')) { - classes.push('fc-today'); - - if (noThemeHighlight !== true) { - classes.push(view.highlightStateClass); - } - } - else if (date < today) { - classes.push('fc-past'); - } - else { - classes.push('fc-future'); - } - } - - return classes; - } - -}); - -;; - -/* Event-rendering and event-interaction methods for the abstract Grid class ----------------------------------------------------------------------------------------------------------------------- - -Data Types: - event - { title, id, start, (end), whatever } - location - { start, (end), allDay } - rawEventRange - { start, end } - eventRange - { start, end, isStart, isEnd } - eventSpan - { start, end, isStart, isEnd, whatever } - eventSeg - { event, whatever } - seg - { whatever } -*/ - -Grid.mixin({ - - // self-config, overridable by subclasses - segSelector: '.fc-event-container > *', // what constitutes an event element? - - mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing - isDraggingSeg: false, // is a segment being dragged? boolean - isResizingSeg: false, // is a segment being resized? boolean - isDraggingExternal: false, // jqui-dragging an external element? boolean - segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs` - - - // Renders the given events onto the grid - renderEvents: function(events) { - var bgEvents = []; - var fgEvents = []; - var i; - - for (i = 0; i < events.length; i++) { - (isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]); - } - - this.segs = [].concat( // record all segs - this.renderBgEvents(bgEvents), - this.renderFgEvents(fgEvents) - ); - }, - - - renderBgEvents: function(events) { - var segs = this.eventsToSegs(events); - - // renderBgSegs might return a subset of segs, segs that were actually rendered - return this.renderBgSegs(segs) || segs; - }, - - - renderFgEvents: function(events) { - var segs = this.eventsToSegs(events); - - // renderFgSegs might return a subset of segs, segs that were actually rendered - return this.renderFgSegs(segs) || segs; - }, - - - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event - this.clearDragListeners(); - - this.unrenderFgSegs(); - this.unrenderBgSegs(); - - this.segs = null; - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return this.segs || []; - }, - - - /* Foreground Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. - renderFgSegs: function(segs) { - // subclasses must implement - }, - - - // Unrenders all currently rendered foreground segments - unrenderFgSegs: function() { - // subclasses must implement - }, - - - // Renders and assigns an `el` property for each foreground event segment. - // Only returns segments that successfully rendered. - // A utility that subclasses may use. - renderFgSegEls: function(segs, disableResizing) { - var view = this.view; - var html = ''; - var renderedSegs = []; - var i; - - if (segs.length) { // don't build an empty html string - - // build a large concatenation of event segment HTML - for (i = 0; i < segs.length; i++) { - html += this.fgSegHtml(segs[i], disableResizing); - } - - // Grab individual elements from the combined HTML string. Use each as the default rendering. - // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. - $(html).each(function(i, node) { - var seg = segs[i]; - var el = view.resolveEventEl(seg.event, $(node)); - - if (el) { - el.data('fc-seg', seg); // used by handlers - seg.el = el; - renderedSegs.push(seg); - } - }); - } - - return renderedSegs; - }, - - - // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() - fgSegHtml: function(seg, disableResizing) { - // subclasses should implement - }, - - - /* Background Segment Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given background event segments onto the grid. - // Returns a subset of the segs that were actually rendered. - renderBgSegs: function(segs) { - return this.renderFill('bgEvent', segs); - }, - - - // Unrenders all the currently rendered background event segments - unrenderBgSegs: function() { - this.unrenderFill('bgEvent'); - }, - - - // Renders a background event element, given the default rendering. Called by the fill system. - bgEventSegEl: function(seg, el) { - return this.view.resolveEventEl(seg.event, el); // will filter through eventRender - }, - - - // Generates an array of classNames to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegClasses: function(seg) { - var event = seg.event; - var source = event.source || {}; - - return [ 'fc-bgevent' ].concat( - event.className, - source.className || [] - ); - }, - - - // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. - // Called by fillSegHtml. - bgEventSegCss: function(seg) { - return { - 'background-color': this.getSegSkinCss(seg)['background-color'] - }; - }, - - - // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. - // Called by fillSegHtml. - businessHoursSegClasses: function(seg) { - return [ 'fc-nonbusiness', 'fc-bgevent' ]; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute business hour segs for the grid's current date range. - // Caller must ask if whole-day business hours are needed. - // If no `businessHours` configuration value is specified, assumes the calendar default. - buildBusinessHourSegs: function(wholeDay, businessHours) { - return this.eventsToSegs( - this.buildBusinessHourEvents(wholeDay, businessHours) - ); - }, - - - // Compute business hour *events* for the grid's current date range. - // Caller must ask if whole-day business hours are needed. - // If no `businessHours` configuration value is specified, assumes the calendar default. - buildBusinessHourEvents: function(wholeDay, businessHours) { - var calendar = this.view.calendar; - var events; - - if (businessHours == null) { - // fallback - // access from calendawr. don't access from view. doesn't update with dynamic options. - businessHours = calendar.opt('businessHours'); - } - - events = calendar.computeBusinessHourEvents(wholeDay, businessHours); - - // HACK. Eventually refactor business hours "events" system. - // If no events are given, but businessHours is activated, this means the entire visible range should be - // marked as *not* business-hours, via inverse-background rendering. - if (!events.length && businessHours) { - events = [ - $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, { - start: this.view.activeRange.end, // guaranteed out-of-range - end: this.view.activeRange.end, // " - dow: null - }) - ]; - } - - return events; - }, - - - /* Handlers - ------------------------------------------------------------------------------------------------------------------*/ - - - // Attaches event-element-related handlers for *all* rendered event segments of the view. - bindSegHandlers: function() { - this.bindSegHandlersToEl(this.el); - }, - - - // Attaches event-element-related handlers to an arbitrary container element. leverages bubbling. - bindSegHandlersToEl: function(el) { - this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart); - this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover); - this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout); - this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown); - this.bindSegHandlerToEl(el, 'click', this.handleSegClick); - }, - - - // Executes a handler for any a user-interaction on a segment. - // Handler gets called with (seg, ev), and with the `this` context of the Grid - bindSegHandlerToEl: function(el, name, handler) { - var _this = this; - - el.on(name, this.segSelector, function(ev) { - var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents - - // only call the handlers if there is not a drag/resize in progress - if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { - return handler.call(_this, seg, ev); // context will be the Grid - } - }); - }, - - - handleSegClick: function(seg, ev) { - var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel - if (res === false) { - ev.preventDefault(); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused over - handleSegMouseover: function(seg, ev) { - if ( - !GlobalEmitter.get().shouldIgnoreMouse() && - !this.mousedOverSeg - ) { - this.mousedOverSeg = seg; - if (this.view.isEventResizable(seg.event)) { - seg.el.addClass('fc-allow-mouse-resize'); - } - this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev); - } - }, - - - // Updates internal state and triggers handlers for when an event element is moused out. - // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. - handleSegMouseout: function(seg, ev) { - ev = ev || {}; // if given no args, make a mock mouse event - - if (this.mousedOverSeg) { - seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment - this.mousedOverSeg = null; - if (this.view.isEventResizable(seg.event)) { - seg.el.removeClass('fc-allow-mouse-resize'); - } - this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev); - } - }, - - - handleSegMousedown: function(seg, ev) { - var isResizing = this.startSegResize(seg, ev, { distance: 5 }); - - if (!isResizing && this.view.isEventDraggable(seg.event)) { - this.buildSegDragListener(seg) - .startInteraction(ev, { - distance: 5 - }); - } - }, - - - handleSegTouchStart: function(seg, ev) { - var view = this.view; - var event = seg.event; - var isSelected = view.isEventSelected(event); - var isDraggable = view.isEventDraggable(event); - var isResizable = view.isEventResizable(event); - var isResizing = false; - var dragListener; - var eventLongPressDelay; - - if (isSelected && isResizable) { - // only allow resizing of the event is selected - isResizing = this.startSegResize(seg, ev); - } - - if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected? - - eventLongPressDelay = view.opt('eventLongPressDelay'); - if (eventLongPressDelay == null) { - eventLongPressDelay = view.opt('longPressDelay'); // fallback - } - - dragListener = isDraggable ? - this.buildSegDragListener(seg) : - this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected - - dragListener.startInteraction(ev, { // won't start if already started - delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected - }); - } - }, - - - // returns boolean whether resizing actually started or not. - // assumes the seg allows resizing. - // `dragOptions` are optional. - startSegResize: function(seg, ev, dragOptions) { - if ($(ev.target).is('.fc-resizer')) { - this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer')) - .startInteraction(ev, dragOptions); - return true; - } - return false; - }, - - - - /* Event Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Builds a listener that will track user-dragging on an event segment. - // Generic enough to work with any type of Grid. - // Has side effect of setting/unsetting `segDragListener` - buildSegDragListener: function(seg) { - var _this = this; - var view = this.view; - var el = seg.el; - var event = seg.event; - var isDragging; - var mouseFollower; // A clone of the original element that will move with the mouse - var dropLocation; // zoned event date properties - - if (this.segDragListener) { - return this.segDragListener; - } - - // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents - // of the view. - var dragListener = this.segDragListener = new HitDragListener(view, { - scroll: view.opt('dragScroll'), - subjectEl: el, - subjectCenter: true, - interactionStart: function(ev) { - seg.component = _this; // for renderDrag - isDragging = false; - mouseFollower = new MouseFollower(seg.el, { - additionalClass: 'fc-dragging', - parentEl: view.el, - opacity: dragListener.isTouch ? null : view.opt('dragOpacity'), - revertDuration: view.opt('dragRevertDuration'), - zIndex: 2 // one above the .fc-view - }); - mouseFollower.hide(); // don't show until we know this is a real drag - mouseFollower.start(ev); - }, - dragStart: function(ev) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segDragStart(seg, ev); - view.hideEvent(event); // hide all event segments. our mouseFollower will take over - }, - hitOver: function(hit, isOrig, origHit) { - var isAllowed = true; - var origHitSpan; - var hitSpan; - var dragHelperEls; - - // starting hit could be forced (DayGrid.limit) - if (seg.hit) { - origHit = seg.hit; - } - - // hit might not belong to this grid, so query origin grid - origHitSpan = origHit.component.getSafeHitSpan(origHit); - hitSpan = hit.component.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - dropLocation = _this.computeEventDrop(origHitSpan, hitSpan, event); - isAllowed = dropLocation && _this.isEventLocationAllowed(dropLocation, event); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - dropLocation = null; - disableCursor(); - } - - // if a valid drop location, have the subclass render a visual indication - if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) { - - dragHelperEls.addClass('fc-dragging'); - if (!dragListener.isTouch) { - _this.applyDragOpacity(dragHelperEls); - } - - mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own - } - else { - mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) - } - - if (isOrig) { - dropLocation = null; // needs to have moved hits to be a valid drop - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - view.unrenderDrag(); // unrender whatever was done in renderDrag - mouseFollower.show(); // show in case we are moving out of all hits - dropLocation = null; - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - }, - interactionEnd: function(ev) { - delete seg.component; // prevent side effects - - // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) - mouseFollower.stop(!dropLocation, function() { - if (isDragging) { - view.unrenderDrag(); - _this.segDragStop(seg, ev); - } - - if (dropLocation) { - // no need to re-show original, will rerender all anyways. esp important if eventRenderWait - view.reportSegDrop(seg, dropLocation, _this.largeUnit, el, ev); - } - else { - view.showEvent(event); - } - }); - _this.segDragListener = null; - } - }); - - return dragListener; - }, - - - // seg isn't draggable, but let's use a generic DragListener - // simply for the delay, so it can be selected. - // Has side effect of setting/unsetting `segDragListener` - buildSegSelectListener: function(seg) { - var _this = this; - var view = this.view; - var event = seg.event; - - if (this.segDragListener) { - return this.segDragListener; - } - - var dragListener = this.segDragListener = new DragListener({ - dragStart: function(ev) { - if (dragListener.isTouch && !view.isEventSelected(event)) { - // if not previously selected, will fire after a delay. then, select the event - view.selectEvent(event); - } - }, - interactionEnd: function(ev) { - _this.segDragListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment dragging starts - segDragStart: function(seg, ev) { - this.isDraggingSeg = true; - this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment dragging stops - segDragStop: function(seg, ev) { - this.isDraggingSeg = false; - this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay - // values for the event. Subclasses may override and set additional properties to be used by renderDrag. - // A falsy returned value indicates an invalid drop. - // DOES NOT consider overlap/constraint. - computeEventDrop: function(startSpan, endSpan, event) { - var calendar = this.view.calendar; - var dragStart = startSpan.start; - var dragEnd = endSpan.start; - var delta; - var dropLocation; // zoned event date properties - - if (dragStart.hasTime() === dragEnd.hasTime()) { - delta = this.diffDates(dragEnd, dragStart); - - // if an all-day event was in a timed area and it was dragged to a different time, - // guarantee an end and adjust start/end to have times - if (event.allDay && durationHasTime(delta)) { - dropLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), // will be an ambig day - allDay: false // for normalizeEventTimes - }; - calendar.normalizeEventTimes(dropLocation); - } - // othewise, work off existing values - else { - dropLocation = pluckEventDateProps(event); - } - - dropLocation.start.add(delta); - if (dropLocation.end) { - dropLocation.end.add(delta); - } - } - else { - // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared - dropLocation = { - start: dragEnd.clone(), - end: null, // end should be cleared - allDay: !dragEnd.hasTime() - }; - } - - return dropLocation; - }, - - - // Utility for apply dragOpacity to a jQuery set - applyDragOpacity: function(els) { - var opacity = this.view.opt('dragOpacity'); - - if (opacity != null) { - els.css('opacity', opacity); - } - }, - - - /* External Element Dragging - ------------------------------------------------------------------------------------------------------------------*/ - - - // Called when a jQuery UI drag is initiated anywhere in the DOM - externalDragStart: function(ev, ui) { - var view = this.view; - var el; - var accept; - - if (view.opt('droppable')) { // only listen if this setting is on - el = $((ui ? ui.item : null) || ev.target); - - // Test that the dragged element passes the dropAccept selector or filter function. - // FYI, the default is "*" (matches all) - accept = view.opt('dropAccept'); - if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { - if (!this.isDraggingExternal) { // prevent double-listening if fired twice - this.listenToExternalDrag(el, ev, ui); - } - } - } - }, - - - // Called when a jQuery UI drag starts and it needs to be monitored for dropping - listenToExternalDrag: function(el, ev, ui) { - var _this = this; - var view = this.view; - var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create - var dropLocation; // a null value signals an unsuccessful drag - - // listener that tracks mouse movement over date-associated pixel regions - var dragListener = _this.externalDragListener = new HitDragListener(this, { - interactionStart: function() { - _this.isDraggingExternal = true; - }, - hitOver: function(hit) { - var isAllowed = true; - var hitSpan = hit.component.getSafeHitSpan(hit); // hit might not belong to this grid - - if (hitSpan) { - dropLocation = _this.computeExternalDrop(hitSpan, meta); - isAllowed = dropLocation && _this.isExternalLocationAllowed(dropLocation, meta.eventProps); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - dropLocation = null; - disableCursor(); - } - - if (dropLocation) { - _this.renderDrag(dropLocation); // called without a seg parameter - } - }, - hitOut: function() { - dropLocation = null; // signal unsuccessful - }, - hitDone: function() { // Called after a hitOut OR before a dragEnd - enableCursor(); - _this.unrenderDrag(); - }, - interactionEnd: function(ev) { - if (dropLocation) { // element was dropped on a valid hit - view.reportExternalDrop(meta, dropLocation, el, ev, ui); - } - _this.isDraggingExternal = false; - _this.externalDragListener = null; - } - }); - - dragListener.startDrag(ev); // start listening immediately - }, - - - // Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), - // returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null. - // Returning a null value signals an invalid drop hit. - // DOES NOT consider overlap/constraint. - computeExternalDrop: function(span, meta) { - var calendar = this.view.calendar; - var dropLocation = { - start: calendar.applyTimezone(span.start), // simulate a zoned event start date - end: null - }; - - // if dropped on an all-day span, and element's metadata specified a time, set it - if (meta.startTime && !dropLocation.start.hasTime()) { - dropLocation.start.time(meta.startTime); - } - - if (meta.duration) { - dropLocation.end = dropLocation.start.clone().add(meta.duration); - } - - return dropLocation; - }, - - - - /* Drag Rendering (for both events and an external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event or external element being dragged. - // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. - // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. - // A truthy returned value indicates this method has rendered a helper element. - // Must return elements used for any mock events. - renderDrag: function(dropLocation, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event or external element being dragged - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Creates a listener that tracks the user as they resize an event segment. - // Generic enough to work with any type of Grid. - buildSegResizeListener: function(seg, isStart) { - var _this = this; - var view = this.view; - var calendar = view.calendar; - var el = seg.el; - var event = seg.event; - var eventEnd = calendar.getEventEnd(event); - var isDragging; - var resizeLocation; // zoned event date properties. falsy if invalid resize - - // Tracks mouse movement over the *grid's* coordinate map - var dragListener = this.segResizeListener = new HitDragListener(this, { - scroll: view.opt('dragScroll'), - subjectEl: el, - interactionStart: function() { - isDragging = false; - }, - dragStart: function(ev) { - isDragging = true; - _this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported - _this.segResizeStart(seg, ev); - }, - hitOver: function(hit, isOrig, origHit) { - var isAllowed = true; - var origHitSpan = _this.getSafeHitSpan(origHit); - var hitSpan = _this.getSafeHitSpan(hit); - - if (origHitSpan && hitSpan) { - resizeLocation = isStart ? - _this.computeEventStartResize(origHitSpan, hitSpan, event) : - _this.computeEventEndResize(origHitSpan, hitSpan, event); - - isAllowed = resizeLocation && _this.isEventLocationAllowed(resizeLocation, event); - } - else { - isAllowed = false; - } - - if (!isAllowed) { - resizeLocation = null; - disableCursor(); - } - else { - if ( - resizeLocation.start.isSame(event.start.clone().stripZone()) && - resizeLocation.end.isSame(eventEnd.clone().stripZone()) - ) { - // no change. (FYI, event dates might have zones) - resizeLocation = null; - } - } - - if (resizeLocation) { - view.hideEvent(event); - _this.renderEventResize(resizeLocation, seg); - } - }, - hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits - resizeLocation = null; - view.showEvent(event); // for when out-of-bounds. show original - }, - hitDone: function() { // resets the rendering to show the original event - _this.unrenderEventResize(); - enableCursor(); - }, - interactionEnd: function(ev) { - if (isDragging) { - _this.segResizeStop(seg, ev); - } - - if (resizeLocation) { // valid date to resize to? - // no need to re-show original, will rerender all anyways. esp important if eventRenderWait - view.reportSegResize(seg, resizeLocation, _this.largeUnit, el, ev); - } - else { - view.showEvent(event); - } - _this.segResizeListener = null; - } - }); - - return dragListener; - }, - - - // Called before event segment resizing starts - segResizeStart: function(seg, ev) { - this.isResizingSeg = true; - this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Called after event segment resizing stops - segResizeStop: function(seg, ev) { - this.isResizingSeg = false; - this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy - }, - - - // Returns new date-information for an event segment being resized from its start - computeEventStartResize: function(startSpan, endSpan, event) { - return this.computeEventResize('start', startSpan, endSpan, event); - }, - - - // Returns new date-information for an event segment being resized from its end - computeEventEndResize: function(startSpan, endSpan, event) { - return this.computeEventResize('end', startSpan, endSpan, event); - }, - - - // Returns new zoned date information for an event segment being resized from its start OR end - // `type` is either 'start' or 'end'. - // DOES NOT consider overlap/constraint. - computeEventResize: function(type, startSpan, endSpan, event) { - var calendar = this.view.calendar; - var delta = this.diffDates(endSpan[type], startSpan[type]); - var resizeLocation; // zoned event date properties - var defaultDuration; - - // build original values to work from, guaranteeing a start and end - resizeLocation = { - start: event.start.clone(), - end: calendar.getEventEnd(event), - allDay: event.allDay - }; - - // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times - if (resizeLocation.allDay && durationHasTime(delta)) { - resizeLocation.allDay = false; - calendar.normalizeEventTimes(resizeLocation); - } - - resizeLocation[type].add(delta); // apply delta to start or end - - // if the event was compressed too small, find a new reasonable duration for it - if (!resizeLocation.start.isBefore(resizeLocation.end)) { - - defaultDuration = - this.minResizeDuration || // TODO: hack - (event.allDay ? - calendar.defaultAllDayEventDuration : - calendar.defaultTimedEventDuration); - - if (type == 'start') { // resizing the start? - resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration); - } - else { // resizing the end? - resizeLocation.end = resizeLocation.start.clone().add(defaultDuration); - } - } - - return resizeLocation; - }, - - - // Renders a visual indication of an event being resized. - // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. - // Must return elements used for any mock events. - renderEventResize: function(range, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event being resized. - unrenderEventResize: function() { - // subclasses must implement - }, - - - /* Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Compute the text that should be displayed on an event's element. - // `range` can be the Event object itself, or something range-like, with at least a `start`. - // If event times are disabled, or the event has no time, will return a blank string. - // If not specified, formatStr will default to the eventTimeFormat setting, - // and displayEnd will default to the displayEventEnd setting. - getEventTimeText: function(range, formatStr, displayEnd) { - - if (formatStr == null) { - formatStr = this.eventTimeFormat; - } - - if (displayEnd == null) { - displayEnd = this.displayEventEnd; - } - - if (this.displayEventTime && range.start.hasTime()) { - if (displayEnd && range.end) { - return this.view.formatRange(range, formatStr); - } - else { - return range.start.format(formatStr); - } - } - - return ''; - }, - - - // Generic utility for generating the HTML classNames for an event segment's element - getSegClasses: function(seg, isDraggable, isResizable) { - var view = this.view; - var classes = [ - 'fc-event', - seg.isStart ? 'fc-start' : 'fc-not-start', - seg.isEnd ? 'fc-end' : 'fc-not-end' - ].concat(this.getSegCustomClasses(seg)); - - if (isDraggable) { - classes.push('fc-draggable'); - } - if (isResizable) { - classes.push('fc-resizable'); - } - - // event is currently selected? attach a className. - if (view.isEventSelected(seg.event)) { - classes.push('fc-selected'); - } - - return classes; - }, - - - // List of classes that were defined by the caller of the API in some way - getSegCustomClasses: function(seg) { - var event = seg.event; - - return [].concat( - event.className, // guaranteed to be an array - event.source ? event.source.className : [] - ); - }, - - - // Utility for generating event skin-related CSS properties - getSegSkinCss: function(seg) { - return { - 'background-color': this.getSegBackgroundColor(seg), - 'border-color': this.getSegBorderColor(seg), - color: this.getSegTextColor(seg) - }; - }, - - - // Queries for caller-specified color, then falls back to default - getSegBackgroundColor: function(seg) { - return seg.event.backgroundColor || - seg.event.color || - this.getSegDefaultBackgroundColor(seg); - }, - - - getSegDefaultBackgroundColor: function(seg) { - var source = seg.event.source || {}; - - return source.backgroundColor || - source.color || - this.view.opt('eventBackgroundColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegBorderColor: function(seg) { - return seg.event.borderColor || - seg.event.color || - this.getSegDefaultBorderColor(seg); - }, - - - getSegDefaultBorderColor: function(seg) { - var source = seg.event.source || {}; - - return source.borderColor || - source.color || - this.view.opt('eventBorderColor') || - this.view.opt('eventColor'); - }, - - - // Queries for caller-specified color, then falls back to default - getSegTextColor: function(seg) { - return seg.event.textColor || - this.getSegDefaultTextColor(seg); - }, - - - getSegDefaultTextColor: function(seg) { - var source = seg.event.source || {}; - - return source.textColor || - this.view.opt('eventTextColor'); - }, - - - /* Event Location Validation - ------------------------------------------------------------------------------------------------------------------*/ - - - isEventLocationAllowed: function(eventLocation, event) { - if (this.isEventLocationInRange(eventLocation)) { - var calendar = this.view.calendar; - var eventSpans = this.eventToSpans(eventLocation); - var i; - - if (eventSpans.length) { - for (i = 0; i < eventSpans.length; i++) { - if (!calendar.isEventSpanAllowed(eventSpans[i], event)) { - return false; - } - } - - return true; - } - } - - return false; - }, - - - isExternalLocationAllowed: function(eventLocation, metaProps) { // FOR the external element - if (this.isEventLocationInRange(eventLocation)) { - var calendar = this.view.calendar; - var eventSpans = this.eventToSpans(eventLocation); - var i; - - if (eventSpans.length) { - for (i = 0; i < eventSpans.length; i++) { - if (!calendar.isExternalSpanAllowed(eventSpans[i], eventLocation, metaProps)) { - return false; - } - } - - return true; - } - } - - return false; - }, - - - isEventLocationInRange: function(eventLocation) { - return isRangeWithinRange( - this.eventToRawRange(eventLocation), - this.view.validRange - ); - }, - - - /* Converting events -> eventRange -> eventSpan -> eventSegs - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates an array of segments for the given single event - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSegs: function(event) { - return this.eventsToSegs([ event ]); - }, - - - // Generates spans (always unzoned) for the given event. - // Does not do any inverting for inverse-background events. - // Can accept an event "location" as well (which only has start/end and no allDay) - eventToSpans: function(event) { - var eventRange = this.eventToRange(event); // { start, end, isStart, isEnd } - - if (eventRange) { - return this.eventRangeToSpans(eventRange, event); - } - else { // out of view's valid range - return []; - } - }, - - - - // Converts an array of event objects into an array of event segment objects. - // A custom `segSliceFunc` may be given for arbitrarily slicing up events. - // Doesn't guarantee an order for the resulting array. - eventsToSegs: function(allEvents, segSliceFunc) { - var _this = this; - var eventsById = groupEventsById(allEvents); - var segs = []; - - $.each(eventsById, function(id, events) { - var visibleEvents = []; - var eventRanges = []; - var eventRange; // { start, end, isStart, isEnd } - var i; - - for (i = 0; i < events.length; i++) { - eventRange = _this.eventToRange(events[i]); // might be null if completely out of range - - if (eventRange) { - eventRanges.push(eventRange); - visibleEvents.push(events[i]); - } - } - - // inverse-background events (utilize only the first event in calculations) - if (isInverseBgEvent(events[0])) { - eventRanges = _this.invertRanges(eventRanges); // will lose isStart/isEnd - - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(eventRanges[i], events[0], segSliceFunc) - ); - } - } - // normal event ranges - else { - for (i = 0; i < eventRanges.length; i++) { - segs.push.apply(segs, // append to - _this.eventRangeToSegs(eventRanges[i], visibleEvents[i], segSliceFunc) - ); - } - } - }); - - return segs; - }, - - - // Generates the unzoned start/end dates an event appears to occupy - // Can accept an event "location" as well (which only has start/end and no allDay) - // returns { start, end, isStart, isEnd } - // If the event is completely outside of the grid's valid range, will return undefined. - eventToRange: function(event) { - return this.refineRawEventRange( - this.eventToRawRange(event) - ); - }, - - - // Ensures the given range is within the view's activeRange and is correctly localized. - // Always returns a result - refineRawEventRange: function(rawRange) { - var view = this.view; - var calendar = view.calendar; - var range = intersectRanges(rawRange, view.activeRange); - - if (range) { // otherwise, event doesn't have valid range - - // hack: dynamic locale change forgets to upate stored event localed - calendar.localizeMoment(range.start); - calendar.localizeMoment(range.end); - - return range; - } - }, - - - // not constrained to valid dates - // not given localizeMoment hack - eventToRawRange: function(event) { - var calendar = this.view.calendar; - var start = event.start.clone().stripZone(); - var end = ( - event.end ? - event.end.clone() : - // derive the end from the start and allDay. compute allDay if necessary - calendar.getDefaultEventEnd( - event.allDay != null ? - event.allDay : - !event.start.hasTime(), - event.start - ) - ).stripZone(); - - return { start: start, end: end }; - }, - - - // Given an event's range (unzoned start/end), and the event itself, - // slice into segments (using the segSliceFunc function if specified) - // eventRange - { start, end, isStart, isEnd } - eventRangeToSegs: function(eventRange, event, segSliceFunc) { - var eventSpans = this.eventRangeToSpans(eventRange, event); - var segs = []; - var i; - - for (i = 0; i < eventSpans.length; i++) { - segs.push.apply(segs, // append to - this.eventSpanToSegs(eventSpans[i], event, segSliceFunc) - ); - } - - return segs; - }, - - - // Given an event's unzoned date range, return an array of eventSpan objects. - // eventSpan - { start, end, isStart, isEnd, otherthings... } - // Subclasses can override. - // Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans. - eventRangeToSpans: function(eventRange, event) { - return [ $.extend({}, eventRange) ]; // copy into a single-item array - }, - - - // Given an event's span (unzoned start/end and other misc data), and the event itself, - // slices into segments and attaches event-derived properties to them. - // eventSpan - { start, end, isStart, isEnd, otherthings... } - eventSpanToSegs: function(eventSpan, event, segSliceFunc) { - var segs = segSliceFunc ? segSliceFunc(eventSpan) : this.spanToSegs(eventSpan); - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // the eventSpan's isStart/isEnd takes precedence over the seg's - if (!eventSpan.isStart) { - seg.isStart = false; - } - if (!eventSpan.isEnd) { - seg.isEnd = false; - } - - seg.event = event; - seg.eventStartMS = +eventSpan.start; // TODO: not the best name after making spans unzoned - seg.eventDurationMS = eventSpan.end - eventSpan.start; - } - - return segs; - }, - - - // Produces a new array of range objects that will cover all the time NOT covered by the given ranges. - // SIDE EFFECT: will mutate the given array and will use its date references. - invertRanges: function(ranges) { - var view = this.view; - var viewStart = view.activeRange.start.clone(); // need a copy - var viewEnd = view.activeRange.end.clone(); // need a copy - var inverseRanges = []; - var start = viewStart; // the end of the previous range. the start of the new range - var i, range; - - // ranges need to be in order. required for our date-walking algorithm - ranges.sort(compareRanges); - - for (i = 0; i < ranges.length; i++) { - range = ranges[i]; - - // add the span of time before the event (if there is any) - if (range.start > start) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: range.start - }); - } - - if (range.end > start) { - start = range.end; - } - } - - // add the span of time after the last event (if there is any) - if (start < viewEnd) { // compare millisecond time (skip any ambig logic) - inverseRanges.push({ - start: start, - end: viewEnd - }); - } - - return inverseRanges; - }, - - - sortEventSegs: function(segs) { - segs.sort(proxy(this, 'compareEventSegs')); - }, - - - // A cmp function for determining which segments should take visual priority - compareEventSegs: function(seg1, seg2) { - return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first - seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first - seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) - compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs); - } - -}); - - -/* Utilities -----------------------------------------------------------------------------------------------------------------------*/ - - -function pluckEventDateProps(event) { - return { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay // keep it the same - }; -} -FC.pluckEventDateProps = pluckEventDateProps; - - -function isBgEvent(event) { // returns true if background OR inverse-background - var rendering = getEventRendering(event); - return rendering === 'background' || rendering === 'inverse-background'; -} -FC.isBgEvent = isBgEvent; // export - - -function isInverseBgEvent(event) { - return getEventRendering(event) === 'inverse-background'; -} - - -function getEventRendering(event) { - return firstDefined((event.source || {}).rendering, event.rendering); -} - - -function groupEventsById(events) { - var eventsById = {}; - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - (eventsById[event._id] || (eventsById[event._id] = [])).push(event); - } - - return eventsById; -} - - -// A cmp function for determining which non-inverted "ranges" (see above) happen earlier -function compareRanges(range1, range2) { - return range1.start - range2.start; // earlier ranges go first -} - - -/* External-Dragging-Element Data -----------------------------------------------------------------------------------------------------------------------*/ - -// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. -// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. -FC.dataAttrPrefix = ''; - -// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure -// to be used for Event Object creation. -// A defined `.eventProps`, even when empty, indicates that an event should be created. -function getDraggedElMeta(el) { - var prefix = FC.dataAttrPrefix; - var eventProps; // properties for creating the event, not related to date/time - var startTime; // a Duration - var duration; - var stick; - - if (prefix) { prefix += '-'; } - eventProps = el.data(prefix + 'event') || null; - - if (eventProps) { - if (typeof eventProps === 'object') { - eventProps = $.extend({}, eventProps); // make a copy - } - else { // something like 1 or true. still signal event creation - eventProps = {}; - } - - // pluck special-cased date/time properties - startTime = eventProps.start; - if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well - duration = eventProps.duration; - stick = eventProps.stick; - delete eventProps.start; - delete eventProps.time; - delete eventProps.duration; - delete eventProps.stick; - } - - // fallback to standalone attribute values for each of the date/time properties - if (startTime == null) { startTime = el.data(prefix + 'start'); } - if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well - if (duration == null) { duration = el.data(prefix + 'duration'); } - if (stick == null) { stick = el.data(prefix + 'stick'); } - - // massage into correct data types - startTime = startTime != null ? moment.duration(startTime) : null; - duration = duration != null ? moment.duration(duration) : null; - stick = Boolean(stick); - - return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; -} - - -;; - -/* -A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns. -Prerequisite: the object being mixed into needs to be a *Grid* -*/ -var DayTableMixin = FC.DayTableMixin = { - - breakOnWeeks: false, // should create a new row for each week? - dayDates: null, // whole-day dates for each column. left to right - dayIndices: null, // for each day from start, the offset - daysPerRow: null, - rowCnt: null, - colCnt: null, - colHeadFormat: null, - - - // Populates internal variables used for date calculation and rendering - updateDayTable: function() { - var view = this.view; - var date = this.start.clone(); - var dayIndex = -1; - var dayIndices = []; - var dayDates = []; - var daysPerRow; - var firstDay; - var rowCnt; - - while (date.isBefore(this.end)) { // loop each day from start to end - if (view.isHiddenDay(date)) { - dayIndices.push(dayIndex + 0.5); // mark that it's between indices - } - else { - dayIndex++; - dayIndices.push(dayIndex); - dayDates.push(date.clone()); - } - date.add(1, 'days'); - } - - if (this.breakOnWeeks) { - // count columns until the day-of-week repeats - firstDay = dayDates[0].day(); - for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) { - if (dayDates[daysPerRow].day() == firstDay) { - break; - } - } - rowCnt = Math.ceil(dayDates.length / daysPerRow); - } - else { - rowCnt = 1; - daysPerRow = dayDates.length; - } - - this.dayDates = dayDates; - this.dayIndices = dayIndices; - this.daysPerRow = daysPerRow; - this.rowCnt = rowCnt; - - this.updateDayTableCols(); - }, - - - // Computes and assigned the colCnt property and updates any options that may be computed from it - updateDayTableCols: function() { - this.colCnt = this.computeColCnt(); - this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat(); - }, - - - // Determines how many columns there should be in the table - computeColCnt: function() { - return this.daysPerRow; - }, - - - // Computes the ambiguously-timed moment for the given cell - getCellDate: function(row, col) { - return this.dayDates[ - this.getCellDayIndex(row, col) - ].clone(); - }, - - - // Computes the ambiguously-timed date range for the given cell - getCellRange: function(row, col) { - var start = this.getCellDate(row, col); - var end = start.clone().add(1, 'days'); - - return { start: start, end: end }; - }, - - - // Returns the number of day cells, chronologically, from the first of the grid (0-based) - getCellDayIndex: function(row, col) { - return row * this.daysPerRow + this.getColDayIndex(col); - }, - - - // Returns the numner of day cells, chronologically, from the first cell in *any given row* - getColDayIndex: function(col) { - if (this.isRTL) { - return this.colCnt - 1 - col; - } - else { - return col; - } - }, - - - // Given a date, returns its chronolocial cell-index from the first cell of the grid. - // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. - // If before the first offset, returns a negative number. - // If after the last offset, returns an offset past the last cell offset. - // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. - getDateDayIndex: function(date) { - var dayIndices = this.dayIndices; - var dayOffset = date.diff(this.start, 'days'); - - if (dayOffset < 0) { - return dayIndices[0] - 1; - } - else if (dayOffset >= dayIndices.length) { - return dayIndices[dayIndices.length - 1] + 1; - } - else { - return dayIndices[dayOffset]; - } - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes a default column header formatting string if `colFormat` is not explicitly defined - computeColHeadFormat: function() { - // if more than one week row, or if there are a lot of columns with not much space, - // put just the day numbers will be in each cell - if (this.rowCnt > 1 || this.colCnt > 10) { - return 'ddd'; // "Sat" - } - // multiple days, so full single date string WON'T be in title text - else if (this.colCnt > 1) { - return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" - } - // single day, so full single date string will probably be in title text - else { - return 'dddd'; // "Saturday" - } - }, - - - /* Slicing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Slices up a date range into a segment for every week-row it intersects with - sliceRangeByRow: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, rowFirst); - segLast = Math.min(rangeLast, rowLast); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - - return segs; - }, - - - // Slices up a date range into a segment for every day-cell it intersects with. - // TODO: make more DRY with sliceRangeByRow somehow. - sliceRangeByDay: function(range) { - var daysPerRow = this.daysPerRow; - var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold - var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index - var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index - var segs = []; - var row; - var rowFirst, rowLast; // inclusive day-index range for current row - var i; - var segFirst, segLast; // inclusive day-index range for segment - - for (row = 0; row < this.rowCnt; row++) { - rowFirst = row * daysPerRow; - rowLast = rowFirst + daysPerRow - 1; - - for (i = rowFirst; i <= rowLast; i++) { - - // intersect segment's offset range with the row's - segFirst = Math.max(rangeFirst, i); - segLast = Math.min(rangeLast, i); - - // deal with in-between indices - segFirst = Math.ceil(segFirst); // in-between starts round to next cell - segLast = Math.floor(segLast); // in-between ends round to prev cell - - if (segFirst <= segLast) { // was there any intersection with the current row? - segs.push({ - row: row, - - // normalize to start of row - firstRowDayIndex: segFirst - rowFirst, - lastRowDayIndex: segLast - rowFirst, - - // must be matching integers to be the segment's start/end - isStart: segFirst === rangeFirst, - isEnd: segLast === rangeLast - }); - } - } - } - - return segs; - }, - - - /* Header Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHeadHtml: function() { - var view = this.view; - - return '' + - '<div class="fc-row ' + view.widgetHeaderClass + '">' + - '<table>' + - '<thead>' + - this.renderHeadTrHtml() + - '</thead>' + - '</table>' + - '</div>'; - }, - - - renderHeadIntroHtml: function() { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderHeadTrHtml: function() { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderHeadIntroHtml()) + - this.renderHeadDateCellsHtml() + - (this.isRTL ? this.renderHeadIntroHtml() : '') + - '</tr>'; - }, - - - renderHeadDateCellsHtml: function() { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(0, col); - htmls.push(this.renderHeadDateCellHtml(date)); - } - - return htmls.join(''); - }, - - - // TODO: when internalApiVersion, accept an object for HTML attributes - // (colspan should be no different) - renderHeadDateCellHtml: function(date, colspan, otherAttrs) { - var view = this.view; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var classNames = [ - 'fc-day-header', - view.widgetHeaderClass - ]; - var innerHtml = htmlEscape(date.format(this.colHeadFormat)); - - // if only one row of days, the classNames on the header can represent the specific days beneath - if (this.rowCnt === 1) { - classNames = classNames.concat( - // includes the day-of-week class - // noThemeHighlight=true (don't highlight the header) - this.getDayClasses(date, true) - ); - } - else { - classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class - } - - return '' + - '<th class="' + classNames.join(' ') + '"' + - ((isDateValid && this.rowCnt) === 1 ? - ' data-date="' + date.format('YYYY-MM-DD') + '"' : - '') + - (colspan > 1 ? - ' colspan="' + colspan + '"' : - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '>' + - (isDateValid ? - // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff) - view.buildGotoAnchorHtml( - { date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 }, - innerHtml - ) : - // if not valid, display text, but no link - innerHtml - ) + - '</th>'; - }, - - - /* Background Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBgTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderBgIntroHtml(row)) + - this.renderBgCellsHtml(row) + - (this.isRTL ? this.renderBgIntroHtml(row) : '') + - '</tr>'; - }, - - - renderBgIntroHtml: function(row) { - return this.renderIntroHtml(); // fall back to generic - }, - - - renderBgCellsHtml: function(row) { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderBgCellHtml(date)); - } - - return htmls.join(''); - }, - - - renderBgCellHtml: function(date, otherAttrs) { - var view = this.view; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var classes = this.getDayClasses(date); - - classes.unshift('fc-day', view.widgetContentClass); - - return '<td class="' + classes.join(' ') + '"' + - (isDateValid ? - ' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it - '') + - (otherAttrs ? - ' ' + otherAttrs : - '') + - '></td>'; - }, - - - /* Generic - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates the default HTML intro for any row. User classes should override - renderIntroHtml: function() { - }, - - - // TODO: a generic method for dealing with <tr>, RTL, intro - // when increment internalApiVersion - // wrapTr (scheduler) - - - /* Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Applies the generic "intro" and "outro" HTML to the given cells. - // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. - bookendCells: function(trEl) { - var introHtml = this.renderIntroHtml(); - - if (introHtml) { - if (this.isRTL) { - trEl.append(introHtml); - } - else { - trEl.prepend(introHtml); - } - } - } - -}; - -;; - -/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. -----------------------------------------------------------------------------------------------------------------------*/ - -var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, { - - numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal - bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid - - rowEls: null, // set of fake row elements - cellEls: null, // set of whole-day elements comprising the row's background - helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" - - rowCoordCache: null, - colCoordCache: null, - - - // Renders the rows and columns into the component's `this.el`, which should already be assigned. - // isRigid determins whether the individual rows should ignore the contents and be a constant height. - // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. - renderDates: function(isRigid) { - var view = this.view; - var rowCnt = this.rowCnt; - var colCnt = this.colCnt; - var html = ''; - var row; - var col; - - for (row = 0; row < rowCnt; row++) { - html += this.renderDayRowHtml(row, isRigid); - } - this.el.html(html); - - this.rowEls = this.el.find('.fc-row'); - this.cellEls = this.el.find('.fc-day, .fc-disabled-day'); - - this.rowCoordCache = new CoordCache({ - els: this.rowEls, - isVertical: true - }); - this.colCoordCache = new CoordCache({ - els: this.cellEls.slice(0, this.colCnt), // only the first row - isHorizontal: true - }); - - // trigger dayRender with each cell's element - for (row = 0; row < rowCnt; row++) { - for (col = 0; col < colCnt; col++) { - view.publiclyTrigger( - 'dayRender', - null, - this.getCellDate(row, col), - this.getCellEl(row, col) - ); - } - } - }, - - - unrenderDates: function() { - this.removeSegPopover(); - }, - - - renderBusinessHours: function() { - var segs = this.buildBusinessHourSegs(true); // wholeDay=true - this.renderFill('businessHours', segs, 'bgevent'); - }, - - - unrenderBusinessHours: function() { - this.unrenderFill('businessHours'); - }, - - - // Generates the HTML for a single row, which is a div that wraps a table. - // `row` is the row number. - renderDayRowHtml: function(row, isRigid) { - var view = this.view; - var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; - - if (isRigid) { - classes.push('fc-rigid'); - } - - return '' + - '<div class="' + classes.join(' ') + '">' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(row) + - '</table>' + - '</div>' + - '<div class="fc-content-skeleton">' + - '<table>' + - (this.numbersVisible ? - '<thead>' + - this.renderNumberTrHtml(row) + - '</thead>' : - '' - ) + - '</table>' + - '</div>' + - '</div>'; - }, - - - /* Grid Number Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - renderNumberTrHtml: function(row) { - return '' + - '<tr>' + - (this.isRTL ? '' : this.renderNumberIntroHtml(row)) + - this.renderNumberCellsHtml(row) + - (this.isRTL ? this.renderNumberIntroHtml(row) : '') + - '</tr>'; - }, - - - renderNumberIntroHtml: function(row) { - return this.renderIntroHtml(); - }, - - - renderNumberCellsHtml: function(row) { - var htmls = []; - var col, date; - - for (col = 0; col < this.colCnt; col++) { - date = this.getCellDate(row, col); - htmls.push(this.renderNumberCellHtml(date)); - } - - return htmls.join(''); - }, - - - // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton. - // The number row will only exist if either day numbers or week numbers are turned on. - renderNumberCellHtml: function(date) { - var view = this.view; - var html = ''; - var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow. - var isDayNumberVisible = view.dayNumbersVisible && isDateValid; - var classes; - var weekCalcFirstDoW; - - if (!isDayNumberVisible && !view.cellWeekNumbersVisible) { - // no numbers in day cell (week number must be along the side) - return '<td/>'; // will create an empty space above events :( - } - - classes = this.getDayClasses(date); - classes.unshift('fc-day-top'); - - if (view.cellWeekNumbersVisible) { - // To determine the day of week number change under ISO, we cannot - // rely on moment.js methods such as firstDayOfWeek() or weekday(), - // because they rely on the locale's dow (possibly overridden by - // our firstDay option), which may not be Monday. We cannot change - // dow, because that would affect the calendar start day as well. - if (date._locale._fullCalendar_weekCalc === 'ISO') { - weekCalcFirstDoW = 1; // Monday by ISO 8601 definition - } - else { - weekCalcFirstDoW = date._locale.firstDayOfWeek(); - } - } - - html += '<td class="' + classes.join(' ') + '"' + - (isDateValid ? - ' data-date="' + date.format() + '"' : - '' - ) + - '>'; - - if (view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) { - html += view.buildGotoAnchorHtml( - { date: date, type: 'week' }, - { 'class': 'fc-week-number' }, - date.format('w') // inner HTML - ); - } - - if (isDayNumberVisible) { - html += view.buildGotoAnchorHtml( - date, - { 'class': 'fc-day-number' }, - date.date() // inner HTML - ); - } - - html += '</td>'; - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" - }, - - - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return this.colCnt == 1; // we'll likely have space if there's only one day - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByRow(span); - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (this.isRTL) { - seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex; - seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex; - } - else { - seg.leftCol = seg.firstRowDayIndex; - seg.rightCol = seg.lastRowDayIndex; - } - } - - return segs; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.rowCoordCache.build(); - this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - this.rowCoordCache.clear(); - }, - - - queryHit: function(leftOffset, topOffset) { - if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) { - var col = this.colCoordCache.getHorizontalIndex(leftOffset); - var row = this.rowCoordCache.getVerticalIndex(topOffset); - - if (row != null && col != null) { - return this.getCellHit(row, col); - } - } - }, - - - getHitSpan: function(hit) { - return this.getCellRange(hit.row, hit.col); - }, - - - getHitEl: function(hit) { - return this.getCellEl(hit.row, hit.col); - }, - - - /* Cell System - ------------------------------------------------------------------------------------------------------------------*/ - // FYI: the first column is the leftmost column, regardless of date - - - getCellHit: function(row, col) { - return { - row: row, - col: col, - component: this, // needed unfortunately :( - left: this.colCoordCache.getLeftOffset(col), - right: this.colCoordCache.getRightOffset(col), - top: this.rowCoordCache.getTopOffset(row), - bottom: this.rowCoordCache.getBottomOffset(row) - }; - }, - - - getCellEl: function(row, col) { - return this.cellEls.eq(row * this.colCnt + col); - }, - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods - - - // Renders a visual indication of an event or external element being dragged. - // `eventLocation` has zoned start and end (optional) - renderDrag: function(eventLocation, seg) { - var eventSpans = this.eventToSpans(eventLocation); - var i; - - // always render a highlight underneath - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - - // if a segment from the same calendar but another component is being dragged, render a helper event - if (seg && seg.component !== this) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - } - }, - - - // Unrenders any visual indication of a hovering event - unrenderDrag: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - var eventSpans = this.eventToSpans(eventLocation); - var i; - - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, - - - // Unrenders a visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHighlight(); - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. - renderHelper: function(event, sourceSeg) { - var helperNodes = []; - var segs = this.eventToSegs(event); - var rowStructs; - - segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered - rowStructs = this.renderSegRows(segs); - - // inject each new event skeleton into each associated row - this.rowEls.each(function(row, rowNode) { - var rowEl = $(rowNode); // the .fc-row - var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned - var skeletonTop; - - // If there is an original segment, match the top position. Otherwise, put it at the row's top level - if (sourceSeg && sourceSeg.row === row) { - skeletonTop = sourceSeg.el.position().top; - } - else { - skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; - } - - skeletonEl.css('top', skeletonTop) - .find('table') - .append(rowStructs[row].tbodyEl); - - rowEl.append(skeletonEl); - helperNodes.push(skeletonEl[0]); - }); - - return ( // must return the elements rendered - this.helperEls = $(helperNodes) // array -> jQuery set - ); - }, - - - // Unrenders any visual indication of a mock helper event - unrenderHelper: function() { - if (this.helperEls) { - this.helperEls.remove(); - this.helperEls = null; - } - }, - - - /* Fill System (highlight, background events, business hours) - ------------------------------------------------------------------------------------------------------------------*/ - - - fillSegTag: 'td', // override the default tag name - - - // Renders a set of rectangles over the given segments of days. - // Only returns segments that successfully rendered. - renderFill: function(type, segs, className) { - var nodes = []; - var i, seg; - var skeletonEl; - - segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - skeletonEl = this.renderFillRow(type, seg, className); - this.rowEls.eq(seg.row).append(skeletonEl); - nodes.push(skeletonEl[0]); - } - - this.elsByFill[type] = $(nodes); - - return segs; - }, - - - // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. - renderFillRow: function(type, seg, className) { - var colCnt = this.colCnt; - var startCol = seg.leftCol; - var endCol = seg.rightCol + 1; - var skeletonEl; - var trEl; - - className = className || type.toLowerCase(); - - skeletonEl = $( - '<div class="fc-' + className + '-skeleton">' + - '<table><tr/></table>' + - '</div>' - ); - trEl = skeletonEl.find('tr'); - - if (startCol > 0) { - trEl.append('<td colspan="' + startCol + '"/>'); - } - - trEl.append( - seg.el.attr('colspan', endCol - startCol) - ); - - if (endCol < colCnt) { - trEl.append('<td colspan="' + (colCnt - endCol) + '"/>'); - } - - this.bookendCells(trEl); - - return skeletonEl; - } - -}); - -;; - -/* Event-rendering methods for the DayGrid class -----------------------------------------------------------------------------------------------------------------------*/ - -DayGrid.mixin({ - - rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering - - - // Unrenders all events currently rendered on the grid - unrenderEvents: function() { - this.removeSegPopover(); // removes the "more.." events popover - Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method - }, - - - // Retrieves all rendered segment objects currently rendered on the grid - getEventSegs: function() { - return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method - .concat(this.popoverSegs || []); // append the segments from the "more..." popover - }, - - - // Renders the given background event segments onto the grid - renderBgSegs: function(segs) { - - // don't render timed background events - var allDaySegs = $.grep(segs, function(seg) { - return seg.event.allDay; - }); - - return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method - }, - - - // Renders the given foreground event segments onto the grid - renderFgSegs: function(segs) { - var rowStructs; - - // render an `.el` on each seg - // returns a subset of the segs. segs that were actually rendered - segs = this.renderFgSegEls(segs); - - rowStructs = this.rowStructs = this.renderSegRows(segs); - - // append to each row's content skeleton - this.rowEls.each(function(i, rowNode) { - $(rowNode).find('.fc-content-skeleton > table').append( - rowStructs[i].tbodyEl - ); - }); - - return segs; // return only the segs that were actually rendered - }, - - - // Unrenders all currently rendered foreground event segments - unrenderFgSegs: function() { - var rowStructs = this.rowStructs || []; - var rowStruct; - - while ((rowStruct = rowStructs.pop())) { - rowStruct.tbodyEl.remove(); - } - - this.rowStructs = null; - }, - - - // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton. - // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). - // PRECONDITION: each segment shoud already have a rendered and assigned `.el` - renderSegRows: function(segs) { - var rowStructs = []; - var segRows; - var row; - - segRows = this.groupSegRows(segs); // group into nested arrays - - // iterate each row of segment groupings - for (row = 0; row < segRows.length; row++) { - rowStructs.push( - this.renderSegRow(row, segRows[row]) - ); - } - - return rowStructs; - }, - - - // Builds the HTML to be used for the default element for an individual segment - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && event.allDay && - seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && event.allDay && - seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeHtml = ''; - var timeText; - var titleHtml; - - classes.unshift('fc-day-grid-event', 'fc-h-event'); - - // Only display a timed events time if it is the starting segment - if (seg.isStart) { - timeText = this.getEventTimeText(event); - if (timeText) { - timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'; - } - } - - titleHtml = - '<span class="fc-title">' + - (htmlEscape(event.title || '') || ' ') + // we always want one line of height - '</span>'; - - return '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (this.isRTL ? - titleHtml + ' ' + timeHtml : // put a natural space in between - timeHtml + ' ' + titleHtml // - ) + - '</div>' + - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains - // the segments. Returns object with a bunch of internal data about how the render was calculated. - // NOTE: modifies rowSegs - renderSegRow: function(row, rowSegs) { - var colCnt = this.colCnt; - var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels - var levelCnt = Math.max(1, segLevels.length); // ensure at least one level - var tbody = $('<tbody/>'); - var segMatrix = []; // lookup for which segments are rendered into which level+col cells - var cellMatrix = []; // lookup for all <td> elements of the level+col matrix - var loneCellMatrix = []; // lookup for <td> elements that only take up a single column - var i, levelSegs; - var col; - var tr; - var j, seg; - var td; - - // populates empty cells from the current column (`col`) to `endCol` - function emptyCellsUntil(endCol) { - while (col < endCol) { - // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell - td = (loneCellMatrix[i - 1] || [])[col]; - if (td) { - td.attr( - 'rowspan', - parseInt(td.attr('rowspan') || 1, 10) + 1 - ); - } - else { - td = $('<td/>'); - tr.append(td); - } - cellMatrix[i][col] = td; - loneCellMatrix[i][col] = td; - col++; - } - } - - for (i = 0; i < levelCnt; i++) { // iterate through all levels - levelSegs = segLevels[i]; - col = 0; - tr = $('<tr/>'); - - segMatrix.push([]); - cellMatrix.push([]); - loneCellMatrix.push([]); - - // levelCnt might be 1 even though there are no actual levels. protect against this. - // this single empty row is useful for styling. - if (levelSegs) { - for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level - seg = levelSegs[j]; - - emptyCellsUntil(seg.leftCol); - - // create a container that occupies or more columns. append the event element. - td = $('<td class="fc-event-container"/>').append(seg.el); - if (seg.leftCol != seg.rightCol) { - td.attr('colspan', seg.rightCol - seg.leftCol + 1); - } - else { // a single-column segment - loneCellMatrix[i][col] = td; - } - - while (col <= seg.rightCol) { - cellMatrix[i][col] = td; - segMatrix[i][col] = seg; - col++; - } - - tr.append(td); - } - } - - emptyCellsUntil(colCnt); // finish off the row - this.bookendCells(tr); - tbody.append(tr); - } - - return { // a "rowStruct" - row: row, // the row number - tbodyEl: tbody, - cellMatrix: cellMatrix, - segMatrix: segMatrix, - segLevels: segLevels, - segs: rowSegs - }; - }, - - - // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. - // NOTE: modifies segs - buildSegLevels: function(segs) { - var levels = []; - var i, seg; - var j; - - // Give preference to elements with certain criteria, so they have - // a chance to be closer to the top. - this.sortEventSegs(segs); - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - - // loop through levels, starting with the topmost, until the segment doesn't collide with other segments - for (j = 0; j < levels.length; j++) { - if (!isDaySegCollision(seg, levels[j])) { - break; - } - } - // `j` now holds the desired subrow index - seg.level = j; - - // create new level array if needed and append segment - (levels[j] || (levels[j] = [])).push(seg); - } - - // order segments left-to-right. very important if calendar is RTL - for (j = 0; j < levels.length; j++) { - levels[j].sort(compareDaySegCols); - } - - return levels; - }, - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row - groupSegRows: function(segs) { - var segRows = []; - var i; - - for (i = 0; i < this.rowCnt; i++) { - segRows.push([]); - } - - for (i = 0; i < segs.length; i++) { - segRows[segs[i].row].push(segs[i]); - } - - return segRows; - } - -}); - - -// Computes whether two segments' columns collide. They are assumed to be in the same row. -function isDaySegCollision(seg, otherSegs) { - var i, otherSeg; - - for (i = 0; i < otherSegs.length; i++) { - otherSeg = otherSegs[i]; - - if ( - otherSeg.leftCol <= seg.rightCol && - otherSeg.rightCol >= seg.leftCol - ) { - return true; - } - } - - return false; -} - - -// A cmp function for determining the leftmost event -function compareDaySegCols(a, b) { - return a.leftCol - b.leftCol; -} - -;; - -/* Methods relate to limiting the number events for a given day on a DayGrid -----------------------------------------------------------------------------------------------------------------------*/ -// NOTE: all the segs being passed around in here are foreground segs - -DayGrid.mixin({ - - segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible - popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible - - - removeSegPopover: function() { - if (this.segPopover) { - this.segPopover.hide(); // in handler, will call segPopover's removeElement - } - }, - - - // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. - // `levelLimit` can be false (don't limit), a number, or true (should be computed). - limitRows: function(levelLimit) { - var rowStructs = this.rowStructs || []; - var row; // row # - var rowLevelLimit; - - for (row = 0; row < rowStructs.length; row++) { - this.unlimitRow(row); - - if (!levelLimit) { - rowLevelLimit = false; - } - else if (typeof levelLimit === 'number') { - rowLevelLimit = levelLimit; - } - else { - rowLevelLimit = this.computeRowLevelLimit(row); - } - - if (rowLevelLimit !== false) { - this.limitRow(row, rowLevelLimit); - } - } - }, - - - // Computes the number of levels a row will accomodate without going outside its bounds. - // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). - // `row` is the row number. - computeRowLevelLimit: function(row) { - var rowEl = this.rowEls.eq(row); // the containing "fake" row div - var rowHeight = rowEl.height(); // TODO: cache somehow? - var trEls = this.rowStructs[row].tbodyEl.children(); - var i, trEl; - var trHeight; - - function iterInnerHeights(i, childNode) { - trHeight = Math.max(trHeight, $(childNode).outerHeight()); - } - - // Reveal one level <tr> at a time and stop when we find one out of bounds - for (i = 0; i < trEls.length; i++) { - trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) - - // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, - // so instead, find the tallest inner content element. - trHeight = 0; - trEl.find('> td > :first-child').each(iterInnerHeights); - - if (trEl.position().top + trHeight > rowHeight) { - return i; - } - } - - return false; // should not limit at all - }, - - - // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. - // `row` is the row number. - // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. - limitRow: function(row, levelLimit) { - var _this = this; - var rowStruct = this.rowStructs[row]; - var moreNodes = []; // array of "more" <a> links and <td> DOM nodes - var col = 0; // col #, left-to-right (not chronologically) - var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right - var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row - var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes - var i, seg; - var segsBelow; // array of segment objects below `seg` in the current `col` - var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies - var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) - var td, rowspan; - var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell - var j; - var moreTd, moreWrap, moreLink; - - // Iterates through empty level cells and places "more" links inside if need be - function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` - while (col < endCol) { - segsBelow = _this.getCellSegs(row, col, levelLimit); - if (segsBelow.length) { - td = cellMatrix[levelLimit - 1][col]; - moreLink = _this.renderMoreLink(row, col, segsBelow); - moreWrap = $('<div/>').append(moreLink); - td.append(moreWrap); - moreNodes.push(moreWrap[0]); - } - col++; - } - } - - if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? - levelSegs = rowStruct.segLevels[levelLimit - 1]; - cellMatrix = rowStruct.cellMatrix; - - limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit - .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array - - // iterate though segments in the last allowable level - for (i = 0; i < levelSegs.length; i++) { - seg = levelSegs[i]; - emptyCellsUntil(seg.leftCol); // process empty cells before the segment - - // determine *all* segments below `seg` that occupy the same columns - colSegsBelow = []; - totalSegsBelow = 0; - while (col <= seg.rightCol) { - segsBelow = this.getCellSegs(row, col, levelLimit); - colSegsBelow.push(segsBelow); - totalSegsBelow += segsBelow.length; - col++; - } - - if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? - td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell - rowspan = td.attr('rowspan') || 1; - segMoreNodes = []; - - // make a replacement <td> for each column the segment occupies. will be one for each colspan - for (j = 0; j < colSegsBelow.length; j++) { - moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); - segsBelow = colSegsBelow[j]; - moreLink = this.renderMoreLink( - row, - seg.leftCol + j, - [ seg ].concat(segsBelow) // count seg as hidden too - ); - moreWrap = $('<div/>').append(moreLink); - moreTd.append(moreWrap); - segMoreNodes.push(moreTd[0]); - moreNodes.push(moreTd[0]); - } - - td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements - limitedNodes.push(td[0]); - } - } - - emptyCellsUntil(this.colCnt); // finish off the level - rowStruct.moreEls = $(moreNodes); // for easy undoing later - rowStruct.limitedEls = $(limitedNodes); // for easy undoing later - } - }, - - - // Reveals all levels and removes all "more"-related elements for a grid's row. - // `row` is a row number. - unlimitRow: function(row) { - var rowStruct = this.rowStructs[row]; - - if (rowStruct.moreEls) { - rowStruct.moreEls.remove(); - rowStruct.moreEls = null; - } - - if (rowStruct.limitedEls) { - rowStruct.limitedEls.removeClass('fc-limited'); - rowStruct.limitedEls = null; - } - }, - - - // Renders an <a> element that represents hidden event element for a cell. - // Responsible for attaching click handler as well. - renderMoreLink: function(row, col, hiddenSegs) { - var _this = this; - var view = this.view; - - return $('<a class="fc-more"/>') - .text( - this.getMoreLinkText(hiddenSegs.length) - ) - .on('click', function(ev) { - var clickOption = view.opt('eventLimitClick'); - var date = _this.getCellDate(row, col); - var moreEl = $(this); - var dayEl = _this.getCellEl(row, col); - var allSegs = _this.getCellSegs(row, col); - - // rescope the segments to be within the cell's date - var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); - var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); - - if (typeof clickOption === 'function') { - // the returned value can be an atomic option - clickOption = view.publiclyTrigger('eventLimitClick', null, { - date: date, - dayEl: dayEl, - moreEl: moreEl, - segs: reslicedAllSegs, - hiddenSegs: reslicedHiddenSegs - }, ev); - } - - if (clickOption === 'popover') { - _this.showSegPopover(row, col, moreEl, reslicedAllSegs); - } - else if (typeof clickOption === 'string') { // a view name - view.calendar.zoomTo(date, clickOption); - } - }); - }, - - - // Reveals the popover that displays all events within a cell - showSegPopover: function(row, col, moreLink, segs) { - var _this = this; - var view = this.view; - var moreWrap = moreLink.parent(); // the <div> wrapper around the <a> - var topEl; // the element we want to match the top coordinate of - var options; - - if (this.rowCnt == 1) { - topEl = view.el; // will cause the popover to cover any sort of header - } - else { - topEl = this.rowEls.eq(row); // will align with top of row - } - - options = { - className: 'fc-more-popover', - content: this.renderSegPopoverContent(row, col, segs), - parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars. - top: topEl.offset().top, - autoHide: true, // when the user clicks elsewhere, hide the popover - viewportConstrain: view.opt('popoverViewportConstrain'), - hide: function() { - // kill everything when the popover is hidden - // notify events to be removed - if (_this.popoverSegs) { - var seg; - for (var i = 0; i < _this.popoverSegs.length; ++i) { - seg = _this.popoverSegs[i]; - view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el); - } - } - _this.segPopover.removeElement(); - _this.segPopover = null; - _this.popoverSegs = null; - } - }; - - // Determine horizontal coordinate. - // We use the moreWrap instead of the <td> to avoid border confusion. - if (this.isRTL) { - options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border - } - else { - options.left = moreWrap.offset().left - 1; // -1 to be over cell border - } - - this.segPopover = new Popover(options); - this.segPopover.show(); - - // the popover doesn't live within the grid's container element, and thus won't get the event - // delegated-handlers for free. attach event-related handlers to the popover. - this.bindSegHandlersToEl(this.segPopover.el); - }, - - - // Builds the inner DOM contents of the segment popover - renderSegPopoverContent: function(row, col, segs) { - var view = this.view; - var isTheme = view.opt('theme'); - var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat')); - var content = $( - '<div class="fc-header ' + view.widgetHeaderClass + '">' + - '<span class="fc-close ' + - (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + - '"></span>' + - '<span class="fc-title">' + - htmlEscape(title) + - '</span>' + - '<div class="fc-clear"/>' + - '</div>' + - '<div class="fc-body ' + view.widgetContentClass + '">' + - '<div class="fc-event-container"></div>' + - '</div>' - ); - var segContainer = content.find('.fc-event-container'); - var i; - - // render each seg's `el` and only return the visible segs - segs = this.renderFgSegEls(segs, true); // disableResizing=true - this.popoverSegs = segs; - - for (i = 0; i < segs.length; i++) { - - // because segments in the popover are not part of a grid coordinate system, provide a hint to any - // grids that want to do drag-n-drop about which cell it came from - this.hitsNeeded(); - segs[i].hit = this.getCellHit(row, col); - this.hitsNotNeeded(); - - segContainer.append(segs[i].el); - } - - return content; - }, - - - // Given the events within an array of segment objects, reslice them to be in a single day - resliceDaySegs: function(segs, dayDate) { - - // build an array of the original events - var events = $.map(segs, function(seg) { - return seg.event; - }); - - var dayStart = dayDate.clone(); - var dayEnd = dayStart.clone().add(1, 'days'); - var dayRange = { start: dayStart, end: dayEnd }; - - // slice the events with a custom slicing function - segs = this.eventsToSegs( - events, - function(range) { - var seg = intersectRanges(range, dayRange); // undefind if no intersection - return seg ? [ seg ] : []; // must return an array of segments - } - ); - - // force an order because eventsToSegs doesn't guarantee one - this.sortEventSegs(segs); - - return segs; - }, - - - // Generates the text that should be inside a "more" link, given the number of events it represents - getMoreLinkText: function(num) { - var opt = this.view.opt('eventLimitText'); - - if (typeof opt === 'function') { - return opt(num); - } - else { - return '+' + num + ' ' + opt; - } - }, - - - // Returns segments within a given cell. - // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. - getCellSegs: function(row, col, startLevel) { - var segMatrix = this.rowStructs[row].segMatrix; - var level = startLevel || 0; - var segs = []; - var seg; - - while (level < segMatrix.length) { - seg = segMatrix[level][col]; - if (seg) { - segs.push(seg); - } - level++; - } - - return segs; - } - -}); - -;; - -/* A component that renders one or more columns of vertical time slots -----------------------------------------------------------------------------------------------------------------------*/ -// We mixin DayTable, even though there is only a single row of days - -var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, { - - slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines - snapDuration: null, // granularity of time for dragging and selecting - snapsPerSlot: null, - labelFormat: null, // formatting string for times running along vertical axis - labelInterval: null, // duration of how often a label should be displayed for a slot - - colEls: null, // cells elements in the day-row background - slatContainerEl: null, // div that wraps all the slat rows - slatEls: null, // elements running horizontally across all columns - nowIndicatorEls: null, - - colCoordCache: null, - slatCoordCache: null, - - - constructor: function() { - Grid.apply(this, arguments); // call the super-constructor - - this.processOptions(); - }, - - - // Renders the time grid into `this.el`, which should already be assigned. - // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. - renderDates: function() { - this.el.html(this.renderHtml()); - this.colEls = this.el.find('.fc-day, .fc-disabled-day'); - this.slatContainerEl = this.el.find('.fc-slats'); - this.slatEls = this.slatContainerEl.find('tr'); - - this.colCoordCache = new CoordCache({ - els: this.colEls, - isHorizontal: true - }); - this.slatCoordCache = new CoordCache({ - els: this.slatEls, - isVertical: true - }); - - this.renderContentSkeleton(); - }, - - - // Renders the basic HTML skeleton for the grid - renderHtml: function() { - return '' + - '<div class="fc-bg">' + - '<table>' + - this.renderBgTrHtml(0) + // row=0 - '</table>' + - '</div>' + - '<div class="fc-slats">' + - '<table>' + - this.renderSlatRowHtml() + - '</table>' + - '</div>'; - }, - - - // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. - renderSlatRowHtml: function() { - var view = this.view; - var isRTL = this.isRTL; - var html = ''; - var slotTime = moment.duration(+this.view.minTime); // wish there was .clone() for durations - var slotDate; // will be on the view's first day, but we only care about its time - var isLabeled; - var axisHtml; - - // Calculate the time for each slot - while (slotTime < this.view.maxTime) { - slotDate = this.start.clone().time(slotTime); - isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval)); - - axisHtml = - '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - (isLabeled ? - '<span>' + // for matchCellWidths - htmlEscape(slotDate.format(this.labelFormat)) + - '</span>' : - '' - ) + - '</td>'; - - html += - '<tr data-time="' + slotDate.format('HH:mm:ss') + '"' + - (isLabeled ? '' : ' class="fc-minor"') + - '>' + - (!isRTL ? axisHtml : '') + - '<td class="' + view.widgetContentClass + '"/>' + - (isRTL ? axisHtml : '') + - "</tr>"; - - slotTime.add(this.slotDuration); - } - - return html; - }, - - - /* Options - ------------------------------------------------------------------------------------------------------------------*/ - - - // Parses various options into properties of this object - processOptions: function() { - var view = this.view; - var slotDuration = view.opt('slotDuration'); - var snapDuration = view.opt('snapDuration'); - var input; - - slotDuration = moment.duration(slotDuration); - snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; - - this.slotDuration = slotDuration; - this.snapDuration = snapDuration; - this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple? - - this.minResizeDuration = snapDuration; // hack - - // might be an array value (for TimelineView). - // if so, getting the most granular entry (the last one probably). - input = view.opt('slotLabelFormat'); - if ($.isArray(input)) { - input = input[input.length - 1]; - } - - this.labelFormat = - input || - view.opt('smallTimeFormat'); // the computed default - - input = view.opt('slotLabelInterval'); - this.labelInterval = input ? - moment.duration(input) : - this.computeLabelInterval(slotDuration); - }, - - - // Computes an automatic value for slotLabelInterval - computeLabelInterval: function(slotDuration) { - var i; - var labelInterval; - var slotsPerLabel; - - // find the smallest stock label interval that results in more than one slots-per-label - for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) { - labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]); - slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration); - if (isInt(slotsPerLabel) && slotsPerLabel > 1) { - return labelInterval; - } - } - - return moment.duration(slotDuration); // fall back. clone - }, - - - // Computes a default event time formatting string if `timeFormat` is not explicitly defined - computeEventTimeFormat: function() { - return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) - }, - - - // Computes a default `displayEventEnd` value if one is not expliclty defined - computeDisplayEventEnd: function() { - return true; - }, - - - /* Hit System - ------------------------------------------------------------------------------------------------------------------*/ - - - prepareHits: function() { - this.colCoordCache.build(); - this.slatCoordCache.build(); - }, - - - releaseHits: function() { - this.colCoordCache.clear(); - // NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop - }, - - - queryHit: function(leftOffset, topOffset) { - var snapsPerSlot = this.snapsPerSlot; - var colCoordCache = this.colCoordCache; - var slatCoordCache = this.slatCoordCache; - - if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) { - var colIndex = colCoordCache.getHorizontalIndex(leftOffset); - var slatIndex = slatCoordCache.getVerticalIndex(topOffset); - - if (colIndex != null && slatIndex != null) { - var slatTop = slatCoordCache.getTopOffset(slatIndex); - var slatHeight = slatCoordCache.getHeight(slatIndex); - var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1 - var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat - var snapIndex = slatIndex * snapsPerSlot + localSnapIndex; - var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight; - var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight; - - return { - col: colIndex, - snap: snapIndex, - component: this, // needed unfortunately :( - left: colCoordCache.getLeftOffset(colIndex), - right: colCoordCache.getRightOffset(colIndex), - top: snapTop, - bottom: snapBottom - }; - } - } - }, - - - getHitSpan: function(hit) { - var start = this.getCellDate(0, hit.col); // row=0 - var time = this.computeSnapTime(hit.snap); // pass in the snap-index - var end; - - start.time(time); - end = start.clone().add(this.snapDuration); - - return { start: start, end: end }; - }, - - - getHitEl: function(hit) { - return this.colEls.eq(hit.col); - }, - - - /* Dates - ------------------------------------------------------------------------------------------------------------------*/ - - - rangeUpdated: function() { - this.updateDayTable(); - }, - - - // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day - computeSnapTime: function(snapIndex) { - return moment.duration(this.view.minTime + this.snapDuration * snapIndex); - }, - - - // Slices up the given span (unzoned start/end with other misc data) into an array of segments - spanToSegs: function(span) { - var segs = this.sliceRangeByTimes(span); - var i; - - for (i = 0; i < segs.length; i++) { - if (this.isRTL) { - segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex; - } - else { - segs[i].col = segs[i].dayIndex; - } - } - - return segs; - }, - - - sliceRangeByTimes: function(range) { - var segs = []; - var seg; - var dayIndex; - var dayDate; - var dayRange; - - for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) { - dayDate = this.dayDates[dayIndex].clone().time(0); // TODO: better API for this? - dayRange = { - start: dayDate.clone().add(this.view.minTime), // don't use .time() because it sux with negatives - end: dayDate.clone().add(this.view.maxTime) - }; - seg = intersectRanges(range, dayRange); // both will be ambig timezone - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - } - - return segs; - }, - - - /* Coordinates - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { // NOT a standard Grid method - this.slatCoordCache.build(); - - if (isResize) { - this.updateSegVerticals( - [].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || []) - ); - } - }, - - - getTotalSlatHeight: function() { - return this.slatContainerEl.outerHeight(); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given date. - // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. - computeDateTop: function(date, startOfDayDate) { - return this.computeTimeTop( - moment.duration( - date - startOfDayDate.clone().stripTime() - ) - ); - }, - - - // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). - computeTimeTop: function(time) { - var len = this.slatEls.length; - var slatCoverage = (time - this.view.minTime) / this.slotDuration; // floating-point value of # of slots covered - var slatIndex; - var slatRemainder; - - // compute a floating-point number for how many slats should be progressed through. - // from 0 to number of slats (inclusive) - // constrained because minTime/maxTime might be customized. - slatCoverage = Math.max(0, slatCoverage); - slatCoverage = Math.min(len, slatCoverage); - - // an integer index of the furthest whole slat - // from 0 to number slats (*exclusive*, so len-1) - slatIndex = Math.floor(slatCoverage); - slatIndex = Math.min(slatIndex, len - 1); - - // how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition. - // could be 1.0 if slatCoverage is covering *all* the slots - slatRemainder = slatCoverage - slatIndex; - - return this.slatCoordCache.getTopPosition(slatIndex) + - this.slatCoordCache.getHeight(slatIndex) * slatRemainder; - }, - - - - /* Event Drag Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being dragged over the specified date(s). - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(eventLocation, seg) { - var eventSpans; - var i; - - if (seg) { // if there is event information for this drag, render a helper event - - // returns mock event elements - // signal that a helper has been rendered - return this.renderEventLocationHelper(eventLocation, seg); - } - else { // otherwise, just render a highlight - eventSpans = this.eventToSpans(eventLocation); - - for (i = 0; i < eventSpans.length; i++) { - this.renderHighlight(eventSpans[i]); - } - } - }, - - - // Unrenders any visual indication of an event being dragged - unrenderDrag: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Event Resize Visualization - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of an event being resized - renderEventResize: function(eventLocation, seg) { - return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements - }, - - - // Unrenders any visual indication of an event being resized - unrenderEventResize: function() { - this.unrenderHelper(); - }, - - - /* Event Helper - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) - renderHelper: function(event, sourceSeg) { - return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements - }, - - - // Unrenders any mock helper event - unrenderHelper: function() { - this.unrenderHelperSegs(); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.renderBusinessSegs( - this.buildBusinessHourSegs() - ); - }, - - - unrenderBusinessHours: function() { - this.unrenderBusinessSegs(); - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return 'minute'; // will refresh on the minute - }, - - - renderNowIndicator: function(date) { - // seg system might be overkill, but it handles scenario where line needs to be rendered - // more than once because of columns with the same date (resources columns for example) - var segs = this.spanToSegs({ start: date, end: date }); - var top = this.computeDateTop(date, date); - var nodes = []; - var i; - - // render lines within the columns - for (i = 0; i < segs.length; i++) { - nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>') - .css('top', top) - .appendTo(this.colContainerEls.eq(segs[i].col))[0]); - } - - // render an arrow over the axis - if (segs.length > 0) { // is the current time in view? - nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>') - .css('top', top) - .appendTo(this.el.find('.fc-content-skeleton'))[0]); - } - - this.nowIndicatorEls = $(nodes); - }, - - - unrenderNowIndicator: function() { - if (this.nowIndicatorEls) { - this.nowIndicatorEls.remove(); - this.nowIndicatorEls = null; - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. - renderSelection: function(span) { - if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered - - // normally acceps an eventLocation, span has a start/end, which is good enough - this.renderEventLocationHelper(span); - } - else { - this.renderHighlight(span); - } - }, - - - // Unrenders any visual indication of a selection - unrenderSelection: function() { - this.unrenderHelper(); - this.unrenderHighlight(); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHighlight: function(span) { - this.renderHighlightSegs(this.spanToSegs(span)); - }, - - - unrenderHighlight: function() { - this.unrenderHighlightSegs(); - } - -}); - -;; - -/* Methods for rendering SEGMENTS, pieces of content that live on the view - ( this file is no longer just for events ) -----------------------------------------------------------------------------------------------------------------------*/ - -TimeGrid.mixin({ - - colContainerEls: null, // containers for each column - - // inner-containers for each column where different types of segs live - fgContainerEls: null, - bgContainerEls: null, - helperContainerEls: null, - highlightContainerEls: null, - businessContainerEls: null, - - // arrays of different types of displayed segments - fgSegs: null, - bgSegs: null, - helperSegs: null, - highlightSegs: null, - businessSegs: null, - - - // Renders the DOM that the view's content will live in - renderContentSkeleton: function() { - var cellHtml = ''; - var i; - var skeletonEl; - - for (i = 0; i < this.colCnt; i++) { - cellHtml += - '<td>' + - '<div class="fc-content-col">' + - '<div class="fc-event-container fc-helper-container"></div>' + - '<div class="fc-event-container"></div>' + - '<div class="fc-highlight-container"></div>' + - '<div class="fc-bgevent-container"></div>' + - '<div class="fc-business-container"></div>' + - '</div>' + - '</td>'; - } - - skeletonEl = $( - '<div class="fc-content-skeleton">' + - '<table>' + - '<tr>' + cellHtml + '</tr>' + - '</table>' + - '</div>' - ); - - this.colContainerEls = skeletonEl.find('.fc-content-col'); - this.helperContainerEls = skeletonEl.find('.fc-helper-container'); - this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)'); - this.bgContainerEls = skeletonEl.find('.fc-bgevent-container'); - this.highlightContainerEls = skeletonEl.find('.fc-highlight-container'); - this.businessContainerEls = skeletonEl.find('.fc-business-container'); - - this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level - this.el.append(skeletonEl); - }, - - - /* Foreground Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderFgSegs: function(segs) { - segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls); - this.fgSegs = segs; - return segs; // needed for Grid::renderEvents - }, - - - unrenderFgSegs: function() { - this.unrenderNamedSegs('fgSegs'); - }, - - - /* Foreground Helper Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHelperSegs: function(segs, sourceSeg) { - var helperEls = []; - var i, seg; - var sourceEl; - - segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls); - - // Try to make the segment that is in the same row as sourceSeg look the same - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - if (sourceSeg && sourceSeg.col === seg.col) { - sourceEl = sourceSeg.el; - seg.el.css({ - left: sourceEl.css('left'), - right: sourceEl.css('right'), - 'margin-left': sourceEl.css('margin-left'), - 'margin-right': sourceEl.css('margin-right') - }); - } - helperEls.push(seg.el[0]); - } - - this.helperSegs = segs; - - return $(helperEls); // must return rendered helpers - }, - - - unrenderHelperSegs: function() { - this.unrenderNamedSegs('helperSegs'); - }, - - - /* Background Events - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBgSegs: function(segs) { - segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls); - this.bgSegs = segs; - return segs; // needed for Grid::renderEvents - }, - - - unrenderBgSegs: function() { - this.unrenderNamedSegs('bgSegs'); - }, - - - /* Highlight - ------------------------------------------------------------------------------------------------------------------*/ - - - renderHighlightSegs: function(segs) { - segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls); - this.highlightSegs = segs; - }, - - - unrenderHighlightSegs: function() { - this.unrenderNamedSegs('highlightSegs'); - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessSegs: function(segs) { - segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system - this.updateSegVerticals(segs); - this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls); - this.businessSegs = segs; - }, - - - unrenderBusinessSegs: function() { - this.unrenderNamedSegs('businessSegs'); - }, - - - /* Seg Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col - groupSegsByCol: function(segs) { - var segsByCol = []; - var i; - - for (i = 0; i < this.colCnt; i++) { - segsByCol.push([]); - } - - for (i = 0; i < segs.length; i++) { - segsByCol[segs[i].col].push(segs[i]); - } - - return segsByCol; - }, - - - // Given segments grouped by column, insert the segments' elements into a parallel array of container - // elements, each living within a column. - attachSegsByCol: function(segsByCol, containerEls) { - var col; - var segs; - var i; - - for (col = 0; col < this.colCnt; col++) { // iterate each column grouping - segs = segsByCol[col]; - - for (i = 0; i < segs.length; i++) { - containerEls.eq(col).append(segs[i].el); - } - } - }, - - - // Given the name of a property of `this` object, assumed to be an array of segments, - // loops through each segment and removes from DOM. Will null-out the property afterwards. - unrenderNamedSegs: function(propName) { - var segs = this[propName]; - var i; - - if (segs) { - for (i = 0; i < segs.length; i++) { - segs[i].el.remove(); - } - this[propName] = null; - } - }, - - - - /* Foreground Event Rendering Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given an array of foreground segments, render a DOM element for each, computes position, - // and attaches to the column inner-container elements. - renderFgSegsIntoContainers: function(segs, containerEls) { - var segsByCol; - var col; - - segs = this.renderFgSegEls(segs); // will call fgSegHtml - segsByCol = this.groupSegsByCol(segs); - - for (col = 0; col < this.colCnt; col++) { - this.updateFgSegCoords(segsByCol[col]); - } - - this.attachSegsByCol(segsByCol, containerEls); - - return segs; - }, - - - // Renders the HTML for a single event segment's default rendering - fgSegHtml: function(seg, disableResizing) { - var view = this.view; - var event = seg.event; - var isDraggable = view.isEventDraggable(event); - var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); - var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); - var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); - var skinCss = cssToStr(this.getSegSkinCss(seg)); - var timeText; - var fullTimeText; // more verbose time text. for the print stylesheet - var startTimeText; // just the start time text - - classes.unshift('fc-time-grid-event', 'fc-v-event'); - - if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... - // Don't display time text on segments that run entirely through a day. - // That would appear as midnight-midnight and would look dumb. - // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) - if (seg.isStart || seg.isEnd) { - timeText = this.getEventTimeText(seg); - fullTimeText = this.getEventTimeText(seg, 'LT'); - startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false - } - } else { - // Display the normal time text for the *event's* times - timeText = this.getEventTimeText(event); - fullTimeText = this.getEventTimeText(event, 'LT'); - startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false - } - - return '<a class="' + classes.join(' ') + '"' + - (event.url ? - ' href="' + htmlEscape(event.url) + '"' : - '' - ) + - (skinCss ? - ' style="' + skinCss + '"' : - '' - ) + - '>' + - '<div class="fc-content">' + - (timeText ? - '<div class="fc-time"' + - ' data-start="' + htmlEscape(startTimeText) + '"' + - ' data-full="' + htmlEscape(fullTimeText) + '"' + - '>' + - '<span>' + htmlEscape(timeText) + '</span>' + - '</div>' : - '' - ) + - (event.title ? - '<div class="fc-title">' + - htmlEscape(event.title) + - '</div>' : - '' - ) + - '</div>' + - '<div class="fc-bg"/>' + - /* TODO: write CSS for this - (isResizableFromStart ? - '<div class="fc-resizer fc-start-resizer" />' : - '' - ) + - */ - (isResizableFromEnd ? - '<div class="fc-resizer fc-end-resizer" />' : - '' - ) + - '</a>'; - }, - - - /* Seg Position Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the CSS top/bottom coordinates for each segment element. - // Works when called after initial render, after a window resize/zoom for example. - updateSegVerticals: function(segs) { - this.computeSegVerticals(segs); - this.assignSegVerticals(segs); - }, - - - // For each segment in an array, computes and assigns its top and bottom properties - computeSegVerticals: function(segs) { - var i, seg; - var dayDate; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - dayDate = this.dayDates[seg.dayIndex]; - - seg.top = this.computeDateTop(seg.start, dayDate); - seg.bottom = this.computeDateTop(seg.end, dayDate); - } - }, - - - // Given segments that already have their top/bottom properties computed, applies those values to - // the segments' elements. - assignSegVerticals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateSegVerticalCss(seg)); - } - }, - - - // Generates an object with CSS properties for the top/bottom coordinates of a segment element - generateSegVerticalCss: function(seg) { - return { - top: seg.top, - bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container - }; - }, - - - /* Foreground Event Positioning Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Given segments that are assumed to all live in the *same column*, - // compute their verical/horizontal coordinates and assign to their elements. - updateFgSegCoords: function(segs) { - this.computeSegVerticals(segs); // horizontals relies on this - this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array - this.assignSegVerticals(segs); - this.assignFgSegHorizontals(segs); - }, - - - // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. - // NOTE: Also reorders the given array by date! - computeFgSegHorizontals: function(segs) { - var levels; - var level0; - var i; - - this.sortEventSegs(segs); // order by certain criteria - levels = buildSlotSegLevels(segs); - computeForwardSlotSegs(levels); - - if ((level0 = levels[0])) { - - for (i = 0; i < level0.length; i++) { - computeSlotSegPressures(level0[i]); - } - - for (i = 0; i < level0.length; i++) { - this.computeFgSegForwardBack(level0[i], 0, 0); - } - } - }, - - - // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range - // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and - // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. - // - // The segment might be part of a "series", which means consecutive segments with the same pressure - // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of - // segments behind this one in the current series, and `seriesBackwardCoord` is the starting - // coordinate of the first segment in the series. - computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) { - var forwardSegs = seg.forwardSegs; - var i; - - if (seg.forwardCoord === undefined) { // not already computed - - if (!forwardSegs.length) { - - // if there are no forward segments, this segment should butt up against the edge - seg.forwardCoord = 1; - } - else { - - // sort highest pressure first - this.sortForwardSegs(forwardSegs); - - // this segment's forwardCoord will be calculated from the backwardCoord of the - // highest-pressure forward segment. - this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); - seg.forwardCoord = forwardSegs[0].backwardCoord; - } - - // calculate the backwardCoord from the forwardCoord. consider the series - seg.backwardCoord = seg.forwardCoord - - (seg.forwardCoord - seriesBackwardCoord) / // available width for series - (seriesBackwardPressure + 1); // # of segments in the series - - // use this segment's coordinates to computed the coordinates of the less-pressurized - // forward segments - for (i=0; i<forwardSegs.length; i++) { - this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord); - } - } - }, - - - sortForwardSegs: function(forwardSegs) { - forwardSegs.sort(proxy(this, 'compareForwardSegs')); - }, - - - // A cmp function for determining which forward segment to rely on more when computing coordinates. - compareForwardSegs: function(seg1, seg2) { - // put higher-pressure first - return seg2.forwardPressure - seg1.forwardPressure || - // put segments that are closer to initial edge first (and favor ones with no coords yet) - (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || - // do normal sorting... - this.compareEventSegs(seg1, seg2); - }, - - - // Given foreground event segments that have already had their position coordinates computed, - // assigns position-related CSS values to their elements. - assignFgSegHorizontals: function(segs) { - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - seg.el.css(this.generateFgSegHorizontalCss(seg)); - - // if the height is short, add a className for alternate styling - if (seg.bottom - seg.top < 30) { - seg.el.addClass('fc-short'); - } - } - }, - - - // Generates an object with CSS properties/values that should be applied to an event segment element. - // Contains important positioning-related properties that should be applied to any event element, customized or not. - generateFgSegHorizontalCss: function(seg) { - var shouldOverlap = this.view.opt('slotEventOverlap'); - var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point - var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point - var props = this.generateSegVerticalCss(seg); // get top/bottom first - var left; // amount of space from left edge, a fraction of the total width - var right; // amount of space from right edge, a fraction of the total width - - if (shouldOverlap) { - // double the width, but don't go beyond the maximum forward coordinate (1.0) - forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); - } - - if (this.isRTL) { - left = 1 - forwardCoord; - right = backwardCoord; - } - else { - left = backwardCoord; - right = 1 - forwardCoord; - } - - props.zIndex = seg.level + 1; // convert from 0-base to 1-based - props.left = left * 100 + '%'; - props.right = right * 100 + '%'; - - if (shouldOverlap && seg.forwardPressure) { - // add padding to the edge so that forward stacked events don't cover the resizer's icon - props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width - } - - return props; - } - -}); - - -// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is -// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. -function buildSlotSegLevels(segs) { - var levels = []; - var i, seg; - var j; - - for (i=0; i<segs.length; i++) { - seg = segs[i]; - - // go through all the levels and stop on the first level where there are no collisions - for (j=0; j<levels.length; j++) { - if (!computeSlotSegCollisions(seg, levels[j]).length) { - break; - } - } - - seg.level = j; - - (levels[j] || (levels[j] = [])).push(seg); - } - - return levels; -} - - -// For every segment, figure out the other segments that are in subsequent -// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs -function computeForwardSlotSegs(levels) { - var i, level; - var j, seg; - var k; - - for (i=0; i<levels.length; i++) { - level = levels[i]; - - for (j=0; j<level.length; j++) { - seg = level[j]; - - seg.forwardSegs = []; - for (k=i+1; k<levels.length; k++) { - computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); - } - } - } -} - - -// Figure out which path forward (via seg.forwardSegs) results in the longest path until -// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure -function computeSlotSegPressures(seg) { - var forwardSegs = seg.forwardSegs; - var forwardPressure = 0; - var i, forwardSeg; - - if (seg.forwardPressure === undefined) { // not already computed - - for (i=0; i<forwardSegs.length; i++) { - forwardSeg = forwardSegs[i]; - - // figure out the child's maximum forward path - computeSlotSegPressures(forwardSeg); - - // either use the existing maximum, or use the child's forward pressure - // plus one (for the forwardSeg itself) - forwardPressure = Math.max( - forwardPressure, - 1 + forwardSeg.forwardPressure - ); - } - - seg.forwardPressure = forwardPressure; - } -} - - -// Find all the segments in `otherSegs` that vertically collide with `seg`. -// Append into an optionally-supplied `results` array and return. -function computeSlotSegCollisions(seg, otherSegs, results) { - results = results || []; - - for (var i=0; i<otherSegs.length; i++) { - if (isSlotSegCollision(seg, otherSegs[i])) { - results.push(otherSegs[i]); - } - } - - return results; -} - - -// Do these segments occupy the same vertical space? -function isSlotSegCollision(seg1, seg2) { - return seg1.bottom > seg2.top && seg1.top < seg2.bottom; -} - -;; - -/* An abstract class from which other views inherit from -----------------------------------------------------------------------------------------------------------------------*/ - -var View = FC.View = Model.extend({ - - type: null, // subclass' view name (string) - name: null, // deprecated. use `type` instead - title: null, // the text that will be displayed in the header's title - - calendar: null, // owner Calendar object - viewSpec: null, - options: null, // hash containing all options. already merged with view-specific-options - el: null, // the view's containing element. set by Calendar - - renderQueue: null, - batchRenderDepth: 0, - isDatesRendered: false, - isEventsRendered: false, - isBaseRendered: false, // related to viewRender/viewDestroy triggers - - queuedScroll: null, - - isRTL: false, - isSelected: false, // boolean whether a range of time is user-selected or not - selectedEvent: null, - - eventOrderSpecs: null, // criteria for ordering events when they have same date/time - - // classNames styled by jqui themes - widgetHeaderClass: null, - widgetContentClass: null, - highlightStateClass: null, - - // for date utils, computed from options - nextDayThreshold: null, - isHiddenDayHash: null, - - // now indicator - isNowIndicatorRendered: null, - initialNowDate: null, // result first getNow call - initialNowQueriedMs: null, // ms time the getNow was called - nowIndicatorTimeoutID: null, // for refresh timing of now indicator - nowIndicatorIntervalID: null, // " - - - constructor: function(calendar, viewSpec) { - Model.prototype.constructor.call(this); - - this.calendar = calendar; - this.viewSpec = viewSpec; - - // shortcuts - this.type = viewSpec.type; - this.options = viewSpec.options; - - // .name is deprecated - this.name = this.type; - - this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); - this.initThemingProps(); - this.initHiddenDays(); - this.isRTL = this.opt('isRTL'); - - this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder')); - - this.renderQueue = this.buildRenderQueue(); - this.initAutoBatchRender(); - - this.initialize(); - }, - - - buildRenderQueue: function() { - var _this = this; - var renderQueue = new RenderQueue({ - event: this.opt('eventRenderWait') - }); - - renderQueue.on('start', function() { - _this.freezeHeight(); - _this.addScroll(_this.queryScroll()); - }); - - renderQueue.on('stop', function() { - _this.thawHeight(); - _this.popScroll(); - }); - - return renderQueue; - }, - - - initAutoBatchRender: function() { - var _this = this; - - this.on('before:change', function() { - _this.startBatchRender(); - }); - - this.on('change', function() { - _this.stopBatchRender(); - }); - }, - - - startBatchRender: function() { - if (!(this.batchRenderDepth++)) { - this.renderQueue.pause(); - } - }, - - - stopBatchRender: function() { - if (!(--this.batchRenderDepth)) { - this.renderQueue.resume(); - } - }, - - - // A good place for subclasses to initialize member variables - initialize: function() { - // subclasses can implement - }, - - - // Retrieves an option with the given name - opt: function(name) { - return this.options[name]; - }, - - - // Triggers handlers that are view-related. Modifies args before passing to calendar. - publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along - var calendar = this.calendar; - - return calendar.publiclyTrigger.apply( - calendar, - [name, thisObj || this].concat( - Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj - [ this ] // always make the last argument a reference to the view. TODO: deprecate - ) - ); - }, - - - /* Title and Date Formatting - ------------------------------------------------------------------------------------------------------------------*/ - - - // Sets the view's title property to the most updated computed value - updateTitle: function() { - this.title = this.computeTitle(); - this.calendar.setToolbarsTitle(this.title); - }, - - - // Computes what the title at the top of the calendar should be for this view - computeTitle: function() { - var range; - - // for views that span a large unit of time, show the proper interval, ignoring stray days before and after - if (/^(year|month)$/.test(this.currentRangeUnit)) { - range = this.currentRange; - } - else { // for day units or smaller, use the actual day range - range = this.activeRange; - } - - return this.formatRange( - { - // in case currentRange has a time, make sure timezone is correct - start: this.calendar.applyTimezone(range.start), - end: this.calendar.applyTimezone(range.end) - }, - this.opt('titleFormat') || this.computeTitleFormat(), - this.opt('titleRangeSeparator') - ); - }, - - - // Generates the format string that should be used to generate the title for the current date range. - // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. - computeTitleFormat: function() { - if (this.currentRangeUnit == 'year') { - return 'YYYY'; - } - else if (this.currentRangeUnit == 'month') { - return this.opt('monthYearFormat'); // like "September 2014" - } - else if (this.currentRangeAs('days') > 1) { - return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" - } - else { - return 'LL'; // one day. longer, like "September 9 2014" - } - }, - - - // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. - // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. - // The timezones of the dates within `range` will be respected. - formatRange: function(range, formatStr, separator) { - var end = range.end; - - if (!end.hasTime()) { // all-day? - end = end.clone().subtract(1); // convert to inclusive. last ms of previous day - } - - return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); - }, - - - getAllDayHtml: function() { - return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText')); - }, - - - /* Navigation - ------------------------------------------------------------------------------------------------------------------*/ - - - // Generates HTML for an anchor to another view into the calendar. - // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings. - // `gotoOptions` can either be a moment input, or an object with the form: - // { date, type, forceOff } - // `type` is a view-type like "day" or "week". default value is "day". - // `attrs` and `innerHtml` are use to generate the rest of the HTML tag. - buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) { - var date, type, forceOff; - var finalOptions; - - if ($.isPlainObject(gotoOptions)) { - date = gotoOptions.date; - type = gotoOptions.type; - forceOff = gotoOptions.forceOff; - } - else { - date = gotoOptions; // a single moment input - } - date = FC.moment(date); // if a string, parse it - - finalOptions = { // for serialization into the link - date: date.format('YYYY-MM-DD'), - type: type || 'day' - }; - - if (typeof attrs === 'string') { - innerHtml = attrs; - attrs = null; - } - - attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space - innerHtml = innerHtml || ''; - - if (!forceOff && this.opt('navLinks')) { - return '<a' + attrs + - ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' + - innerHtml + - '</a>'; - } - else { - return '<span' + attrs + '>' + - innerHtml + - '</span>'; - } - }, - - - // Rendering Non-date-related Content - // ----------------------------------------------------------------------------------------------------------------- - - - // Sets the container element that the view should render inside of, does global DOM-related initializations, - // and renders all the non-date-related content inside. - setElement: function(el) { - this.el = el; - this.bindGlobalHandlers(); - this.bindBaseRenderHandlers(); - this.renderSkeleton(); - }, - - - // Removes the view's container element from the DOM, clearing any content beforehand. - // Undoes any other DOM-related attachments. - removeElement: function() { - this.unsetDate(); - this.unrenderSkeleton(); - - this.unbindGlobalHandlers(); - this.unbindBaseRenderHandlers(); - - this.el.remove(); - // NOTE: don't null-out this.el in case the View was destroyed within an API callback. - // We don't null-out the View's other jQuery element references upon destroy, - // so we shouldn't kill this.el either. - }, - - - // Renders the basic structure of the view before any content is rendered - renderSkeleton: function() { - // subclasses should implement - }, - - - // Unrenders the basic structure of the view - unrenderSkeleton: function() { - // subclasses should implement - }, - - - // Date Setting/Unsetting - // ----------------------------------------------------------------------------------------------------------------- - - - setDate: function(date) { - var currentDateProfile = this.get('dateProfile'); - var newDateProfile = this.buildDateProfile(date, null, true); // forceToValid=true - - if ( - !currentDateProfile || - !isRangesEqual(currentDateProfile.activeRange, newDateProfile.activeRange) - ) { - this.set('dateProfile', newDateProfile); - } - - return newDateProfile.date; - }, - - - unsetDate: function() { - this.unset('dateProfile'); - }, - - - // Date Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - requestDateRender: function(dateProfile) { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeDateRender(dateProfile); - }, 'date', 'init'); - }, - - - requestDateUnrender: function() { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeDateUnrender(); - }, 'date', 'destroy'); - }, - - - // Event Data - // ----------------------------------------------------------------------------------------------------------------- - - - fetchInitialEvents: function(dateProfile) { - return this.calendar.requestEvents( - dateProfile.activeRange.start, - dateProfile.activeRange.end - ); - }, - - - bindEventChanges: function() { - this.listenTo(this.calendar, 'eventsReset', this.resetEvents); - }, - - - unbindEventChanges: function() { - this.stopListeningTo(this.calendar, 'eventsReset'); - }, - - - setEvents: function(events) { - this.set('currentEvents', events); - this.set('hasEvents', true); - }, - - - unsetEvents: function() { - this.unset('currentEvents'); - this.unset('hasEvents'); - }, - - - resetEvents: function(events) { - this.startBatchRender(); - this.unsetEvents(); - this.setEvents(events); - this.stopBatchRender(); - }, - - - // Event Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - requestEventsRender: function(events) { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeEventsRender(events); - }, 'event', 'init'); - }, - - - requestEventsUnrender: function() { - var _this = this; - - this.renderQueue.queue(function() { - _this.executeEventsUnrender(); - }, 'event', 'destroy'); - }, - - - // Date High-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // if dateProfile not specified, uses current - executeDateRender: function(dateProfile, skipScroll) { - - this.setDateProfileForRendering(dateProfile); - this.updateTitle(); - this.calendar.updateToolbarButtons(); - - if (this.render) { - this.render(); // TODO: deprecate - } - - this.renderDates(); - this.updateSize(); - this.renderBusinessHours(); // might need coordinates, so should go after updateSize() - this.startNowIndicator(); - - if (!skipScroll) { - this.addScroll(this.computeInitialDateScroll()); - } - - this.isDatesRendered = true; - this.trigger('datesRendered'); - }, - - - executeDateUnrender: function() { - - this.unselect(); - this.stopNowIndicator(); - - this.trigger('before:datesUnrendered'); - - this.unrenderBusinessHours(); - this.unrenderDates(); - - if (this.destroy) { - this.destroy(); // TODO: deprecate - } - - this.isDatesRendered = false; - }, - - - // Date Low-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // date-cell content only - renderDates: function() { - // subclasses should implement - }, - - - // date-cell content only - unrenderDates: function() { - // subclasses should override - }, - - - // Determing when the "meat" of the view is rendered (aka the base) - // ----------------------------------------------------------------------------------------------------------------- - - - bindBaseRenderHandlers: function() { - var _this = this; - - this.on('datesRendered.baseHandler', function() { - _this.onBaseRender(); - }); - - this.on('before:datesUnrendered.baseHandler', function() { - _this.onBeforeBaseUnrender(); - }); - }, - - - unbindBaseRenderHandlers: function() { - this.off('.baseHandler'); - }, - - - onBaseRender: function() { - this.applyScreenState(); - this.publiclyTrigger('viewRender', this, this, this.el); - }, - - - onBeforeBaseUnrender: function() { - this.applyScreenState(); - this.publiclyTrigger('viewDestroy', this, this, this.el); - }, - - - // Misc view rendering utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Binds DOM handlers to elements that reside outside the view container, such as the document - bindGlobalHandlers: function() { - this.listenTo(GlobalEmitter.get(), { - touchstart: this.processUnselect, - mousedown: this.handleDocumentMousedown - }); - }, - - - // Unbinds DOM handlers from elements that reside outside the view container - unbindGlobalHandlers: function() { - this.stopListeningTo(GlobalEmitter.get()); - }, - - - // Initializes internal variables related to theming - initThemingProps: function() { - var tm = this.opt('theme') ? 'ui' : 'fc'; - - this.widgetHeaderClass = tm + '-widget-header'; - this.widgetContentClass = tm + '-widget-content'; - this.highlightStateClass = tm + '-state-highlight'; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders business-hours onto the view. Assumes updateSize has already been called. - renderBusinessHours: function() { - // subclasses should implement - }, - - - // Unrenders previously-rendered business-hours - unrenderBusinessHours: function() { - // subclasses should implement - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - // Immediately render the current time indicator and begins re-rendering it at an interval, - // which is defined by this.getNowIndicatorUnit(). - // TODO: somehow do this for the current whole day's background too - startNowIndicator: function() { - var _this = this; - var unit; - var update; - var delay; // ms wait value - - if (this.opt('nowIndicator')) { - unit = this.getNowIndicatorUnit(); - if (unit) { - update = proxy(this, 'updateNowIndicator'); // bind to `this` - - this.initialNowDate = this.calendar.getNow(); - this.initialNowQueriedMs = +new Date(); - this.renderNowIndicator(this.initialNowDate); - this.isNowIndicatorRendered = true; - - // wait until the beginning of the next interval - delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate; - this.nowIndicatorTimeoutID = setTimeout(function() { - _this.nowIndicatorTimeoutID = null; - update(); - delay = +moment.duration(1, unit); - delay = Math.max(100, delay); // prevent too frequent - _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval - }, delay); - } - } - }, - - - // rerenders the now indicator, computing the new current time from the amount of time that has passed - // since the initial getNow call. - updateNowIndicator: function() { - if (this.isNowIndicatorRendered) { - this.unrenderNowIndicator(); - this.renderNowIndicator( - this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms - ); - } - }, - - - // Immediately unrenders the view's current time indicator and stops any re-rendering timers. - // Won't cause side effects if indicator isn't rendered. - stopNowIndicator: function() { - if (this.isNowIndicatorRendered) { - - if (this.nowIndicatorTimeoutID) { - clearTimeout(this.nowIndicatorTimeoutID); - this.nowIndicatorTimeoutID = null; - } - if (this.nowIndicatorIntervalID) { - clearTimeout(this.nowIndicatorIntervalID); - this.nowIndicatorIntervalID = null; - } - - this.unrenderNowIndicator(); - this.isNowIndicatorRendered = false; - } - }, - - - // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator - // should be refreshed. If something falsy is returned, no time indicator is rendered at all. - getNowIndicatorUnit: function() { - // subclasses should implement - }, - - - // Renders a current time indicator at the given datetime - renderNowIndicator: function(date) { - // subclasses should implement - }, - - - // Undoes the rendering actions from renderNowIndicator - unrenderNowIndicator: function() { - // subclasses should implement - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes anything dependant upon sizing of the container element of the grid - updateSize: function(isResize) { - var scroll; - - if (isResize) { - scroll = this.queryScroll(); - } - - this.updateHeight(isResize); - this.updateWidth(isResize); - this.updateNowIndicator(); - - if (isResize) { - this.applyScroll(scroll); - } - }, - - - // Refreshes the horizontal dimensions of the calendar - updateWidth: function(isResize) { - // subclasses should implement - }, - - - // Refreshes the vertical dimensions of the calendar - updateHeight: function(isResize) { - var calendar = this.calendar; // we poll the calendar for height information - - this.setHeight( - calendar.getSuggestedViewHeight(), - calendar.isHeightAuto() - ); - }, - - - // Updates the vertical dimensions of the calendar to the specified height. - // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. - setHeight: function(height, isAuto) { - // subclasses should implement - }, - - - /* Scroller - ------------------------------------------------------------------------------------------------------------------*/ - - - addForcedScroll: function(scroll) { - this.addScroll( - $.extend(scroll, { isForced: true }) - ); - }, - - - addScroll: function(scroll) { - var queuedScroll = this.queuedScroll || (this.queuedScroll = {}); - - if (!queuedScroll.isForced) { - $.extend(queuedScroll, scroll); - } - }, - - - popScroll: function() { - this.applyQueuedScroll(); - this.queuedScroll = null; - }, - - - applyQueuedScroll: function() { - if (this.queuedScroll) { - this.applyScroll(this.queuedScroll); - } - }, - - - queryScroll: function() { - var scroll = {}; - - if (this.isDatesRendered) { - $.extend(scroll, this.queryDateScroll()); - } - - return scroll; - }, - - - applyScroll: function(scroll) { - if (this.isDatesRendered) { - this.applyDateScroll(scroll); - } - }, - - - computeInitialDateScroll: function() { - return {}; // subclasses must implement - }, - - - queryDateScroll: function() { - return {}; // subclasses must implement - }, - - - applyDateScroll: function(scroll) { - ; // subclasses must implement - }, - - - /* Height Freezing - ------------------------------------------------------------------------------------------------------------------*/ - - - freezeHeight: function() { - this.calendar.freezeContentHeight(); - }, - - - thawHeight: function() { - this.calendar.thawContentHeight(); - }, - - - // Event High-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - executeEventsRender: function(events) { - this.renderEvents(events); - this.isEventsRendered = true; - - this.onEventsRender(); - }, - - - executeEventsUnrender: function() { - this.onBeforeEventsUnrender(); - - if (this.destroyEvents) { - this.destroyEvents(); // TODO: deprecate - } - - this.unrenderEvents(); - this.isEventsRendered = false; - }, - - - // Event Rendering Triggers - // ----------------------------------------------------------------------------------------------------------------- - - - // Signals that all events have been rendered - onEventsRender: function() { - this.applyScreenState(); - - this.renderedEventSegEach(function(seg) { - this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el); - }); - this.publiclyTrigger('eventAfterAllRender'); - }, - - - // Signals that all event elements are about to be removed - onBeforeEventsUnrender: function() { - this.applyScreenState(); - - this.renderedEventSegEach(function(seg) { - this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el); - }); - }, - - - applyScreenState: function() { - this.thawHeight(); - this.freezeHeight(); - this.applyQueuedScroll(); - }, - - - // Event Low-level Rendering - // ----------------------------------------------------------------------------------------------------------------- - - - // Renders the events onto the view. - renderEvents: function(events) { - // subclasses should implement - }, - - - // Removes event elements from the view. - unrenderEvents: function() { - // subclasses should implement - }, - - - // Event Rendering Utils - // ----------------------------------------------------------------------------------------------------------------- - - - // Given an event and the default element used for rendering, returns the element that should actually be used. - // Basically runs events and elements through the eventRender hook. - resolveEventEl: function(event, el) { - var custom = this.publiclyTrigger('eventRender', event, event, el); - - if (custom === false) { // means don't render at all - el = null; - } - else if (custom && custom !== true) { - el = $(custom); - } - - return el; - }, - - - // Hides all rendered event segments linked to the given event - showEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', ''); - }, event); - }, - - - // Shows all rendered event segments linked to the given event - hideEvent: function(event) { - this.renderedEventSegEach(function(seg) { - seg.el.css('visibility', 'hidden'); - }, event); - }, - - - // Iterates through event segments that have been rendered (have an el). Goes through all by default. - // If the optional `event` argument is specified, only iterates through segments linked to that event. - // The `this` value of the callback function will be the view. - renderedEventSegEach: function(func, event) { - var segs = this.getEventSegs(); - var i; - - for (i = 0; i < segs.length; i++) { - if (!event || segs[i].event._id === event._id) { - if (segs[i].el) { - func.call(this, segs[i]); - } - } - } - }, - - - // Retrieves all the rendered segment objects for the view - getEventSegs: function() { - // subclasses must implement - return []; - }, - - - /* Event Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be dragged by the user - isEventDraggable: function(event) { - return this.isEventStartEditable(event); - }, - - - isEventStartEditable: function(event) { - return firstDefined( - event.startEditable, - (event.source || {}).startEditable, - this.opt('eventStartEditable'), - this.isEventGenerallyEditable(event) - ); - }, - - - isEventGenerallyEditable: function(event) { - return firstDefined( - event.editable, - (event.source || {}).editable, - this.opt('editable') - ); - }, - - - // Must be called when an event in the view is dropped onto new location. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportSegDrop: function(seg, dropLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateSeg(seg, dropLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; - - this.triggerEventDrop(seg.event, mutateResult.dateDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, - - - // Triggers event-drop handlers that have subscribed via the API - triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { - this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy - }, - - - /* External Element Drag-n-Drop - ------------------------------------------------------------------------------------------------------------------*/ - - - // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. - // `meta` is the parsed data that has been embedded into the dragging event. - // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event. - reportExternalDrop: function(meta, dropLocation, el, ev, ui) { - var eventProps = meta.eventProps; - var eventInput; - var event; - - // Try to build an event object and render it. TODO: decouple the two - if (eventProps) { - eventInput = $.extend({}, eventProps, dropLocation); - event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array - } - - this.triggerExternalDrop(event, dropLocation, el, ev, ui); - }, - - - // Triggers external-drop handlers that have subscribed via the API - triggerExternalDrop: function(event, dropLocation, el, ev, ui) { - - // trigger 'drop' regardless of whether element represents an event - this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui); - - if (event) { - this.publiclyTrigger('eventReceive', null, event); // signal an external event landed - } - }, - - - /* Drag-n-Drop Rendering (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a event or external-element drag over the given drop zone. - // If an external-element, seg will be `null`. - // Must return elements used for any mock events. - renderDrag: function(dropLocation, seg) { - // subclasses must implement - }, - - - // Unrenders a visual indication of an event or external-element being dragged. - unrenderDrag: function() { - // subclasses must implement - }, - - - /* Event Resizing - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes if the given event is allowed to be resized from its starting edge - isEventResizableFromStart: function(event) { - return this.opt('eventResizableFromStart') && this.isEventResizable(event); - }, - - - // Computes if the given event is allowed to be resized from its ending edge - isEventResizableFromEnd: function(event) { - return this.isEventResizable(event); - }, - - - // Computes if the given event is allowed to be resized by the user at all - isEventResizable: function(event) { - var source = event.source || {}; - - return firstDefined( - event.durationEditable, - source.durationEditable, - this.opt('eventDurationEditable'), - event.editable, - source.editable, - this.opt('editable') - ); - }, - - - // Must be called when an event in the view has been resized to a new length - reportSegResize: function(seg, resizeLocation, largeUnit, el, ev) { - var calendar = this.calendar; - var mutateResult = calendar.mutateSeg(seg, resizeLocation, largeUnit); - var undoFunc = function() { - mutateResult.undo(); - calendar.reportEventChange(); - }; - - this.triggerEventResize(seg.event, mutateResult.durationDelta, undoFunc, el, ev); - calendar.reportEventChange(); // will rerender events - }, - - - // Triggers event-resize handlers that have subscribed via the API - triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { - this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy - }, - - - /* Selection (time range) - ------------------------------------------------------------------------------------------------------------------*/ - - - // Selects a date span on the view. `start` and `end` are both Moments. - // `ev` is the native mouse event that begin the interaction. - select: function(span, ev) { - this.unselect(ev); - this.renderSelection(span); - this.reportSelection(span, ev); - }, - - - // Renders a visual indication of the selection - renderSelection: function(span) { - // subclasses should implement - }, - - - // Called when a new selection is made. Updates internal state and triggers handlers. - reportSelection: function(span, ev) { - this.isSelected = true; - this.triggerSelect(span, ev); - }, - - - // Triggers handlers to 'select' - triggerSelect: function(span, ev) { - this.publiclyTrigger( - 'select', - null, - this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API - this.calendar.applyTimezone(span.end), // " - ev - ); - }, - - - // Undoes a selection. updates in the internal state and triggers handlers. - // `ev` is the native mouse event that began the interaction. - unselect: function(ev) { - if (this.isSelected) { - this.isSelected = false; - if (this.destroySelection) { - this.destroySelection(); // TODO: deprecate - } - this.unrenderSelection(); - this.publiclyTrigger('unselect', null, ev); - } - }, - - - // Unrenders a visual indication of selection - unrenderSelection: function() { - // subclasses should implement - }, - - - /* Event Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - selectEvent: function(event) { - if (!this.selectedEvent || this.selectedEvent !== event) { - this.unselectEvent(); - this.renderedEventSegEach(function(seg) { - seg.el.addClass('fc-selected'); - }, event); - this.selectedEvent = event; - } - }, - - - unselectEvent: function() { - if (this.selectedEvent) { - this.renderedEventSegEach(function(seg) { - seg.el.removeClass('fc-selected'); - }, this.selectedEvent); - this.selectedEvent = null; - } - }, - - - isEventSelected: function(event) { - // event references might change on refetchEvents(), while selectedEvent doesn't, - // so compare IDs - return this.selectedEvent && this.selectedEvent._id === event._id; - }, - - - /* Mouse / Touch Unselecting (time range & event unselection) - ------------------------------------------------------------------------------------------------------------------*/ - // TODO: move consistently to down/start or up/end? - // TODO: don't kill previous selection if touch scrolling - - - handleDocumentMousedown: function(ev) { - if (isPrimaryMouseButton(ev)) { - this.processUnselect(ev); - } - }, - - - processUnselect: function(ev) { - this.processRangeUnselect(ev); - this.processEventUnselect(ev); - }, - - - processRangeUnselect: function(ev) { - var ignore; - - // is there a time-range selection? - if (this.isSelected && this.opt('unselectAuto')) { - // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element - ignore = this.opt('unselectCancel'); - if (!ignore || !$(ev.target).closest(ignore).length) { - this.unselect(ev); - } - } - }, - - - processEventUnselect: function(ev) { - if (this.selectedEvent) { - if (!$(ev.target).closest('.fc-selected').length) { - this.unselectEvent(); - } - } - }, - - - /* Day Click - ------------------------------------------------------------------------------------------------------------------*/ - - - // Triggers handlers to 'dayClick' - // Span has start/end of the clicked area. Only the start is useful. - triggerDayClick: function(span, dayEl, ev) { - this.publiclyTrigger( - 'dayClick', - dayEl, - this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API - ev - ); - }, - - - /* Date Utils - ------------------------------------------------------------------------------------------------------------------*/ - - - // Returns the date range of the full days the given range visually appears to occupy. - // Returns a new range object. - computeDayRange: function(range) { - var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts - var end = range.end; - var endDay = null; - var endTimeMS; - - if (end) { - endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends - endTimeMS = +end.time(); // # of milliseconds into `endDay` - - // If the end time is actually inclusively part of the next day and is equal to or - // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. - // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. - if (endTimeMS && endTimeMS >= this.nextDayThreshold) { - endDay.add(1, 'days'); - } - } - - // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, - // assign the default duration of one day. - if (!end || endDay <= startDay) { - endDay = startDay.clone().add(1, 'days'); - } - - return { start: startDay, end: endDay }; - }, - - - // Does the given event visually appear to occupy more than one day? - isMultiDayEvent: function(event) { - var range = this.computeDayRange(event); // event is range-ish - - return range.end.diff(range.start, 'days') > 1; - } - -}); - - -View.watch('displayingDates', [ 'dateProfile' ], function(deps) { - this.requestDateRender(deps.dateProfile); -}, function() { - this.requestDateUnrender(); -}); - - -View.watch('initialEvents', [ 'dateProfile' ], function(deps) { - return this.fetchInitialEvents(deps.dateProfile); -}); - - -View.watch('bindingEvents', [ 'initialEvents' ], function(deps) { - this.setEvents(deps.initialEvents); - this.bindEventChanges(); -}, function() { - this.unbindEventChanges(); - this.unsetEvents(); -}); - - -View.watch('displayingEvents', [ 'displayingDates', 'hasEvents' ], function() { - this.requestEventsRender(this.get('currentEvents')); // if there were event mutations after initialEvents -}, function() { - this.requestEventsUnrender(); -}); - -;; - -View.mixin({ - - // range the view is formally responsible for. - // for example, a month view might have 1st-31st, excluding padded dates - currentRange: null, - currentRangeUnit: null, // name of largest unit being displayed, like "month" or "week" - - // date range with a rendered skeleton - // includes not-active days that need some sort of DOM - renderRange: null, - - // dates that display events and accept drag-n-drop - activeRange: null, - - // constraint for where prev/next operations can go and where events can be dragged/resized to. - // an object with optional start and end properties. - validRange: null, - - // how far the current date will move for a prev/next operation - dateIncrement: null, - - minTime: null, // Duration object that denotes the first visible time of any given day - maxTime: null, // Duration object that denotes the exclusive visible end time of any given day - usesMinMaxTime: false, // whether minTime/maxTime will affect the activeRange. Views must opt-in. - - // DEPRECATED - start: null, // use activeRange.start - end: null, // use activeRange.end - intervalStart: null, // use currentRange.start - intervalEnd: null, // use currentRange.end - - - /* Date Range Computation - ------------------------------------------------------------------------------------------------------------------*/ - - - setDateProfileForRendering: function(dateProfile) { - this.currentRange = dateProfile.currentRange; - this.currentRangeUnit = dateProfile.currentRangeUnit; - this.renderRange = dateProfile.renderRange; - this.activeRange = dateProfile.activeRange; - this.validRange = dateProfile.validRange; - this.dateIncrement = dateProfile.dateIncrement; - this.minTime = dateProfile.minTime; - this.maxTime = dateProfile.maxTime; - - // DEPRECATED, but we need to keep it updated - this.start = dateProfile.activeRange.start; - this.end = dateProfile.activeRange.end; - this.intervalStart = dateProfile.currentRange.start; - this.intervalEnd = dateProfile.currentRange.end; - }, - - - // Builds a structure with info about what the dates/ranges will be for the "prev" view. - buildPrevDateProfile: function(date) { - var prevDate = date.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement); - - return this.buildDateProfile(prevDate, -1); - }, - - - // Builds a structure with info about what the dates/ranges will be for the "next" view. - buildNextDateProfile: function(date) { - var nextDate = date.clone().startOf(this.currentRangeUnit).add(this.dateIncrement); - - return this.buildDateProfile(nextDate, 1); - }, - - - // Builds a structure holding dates/ranges for rendering around the given date. - // Optional direction param indicates whether the date is being incremented/decremented - // from its previous value. decremented = -1, incremented = 1 (default). - buildDateProfile: function(date, direction, forceToValid) { - var validRange = this.buildValidRange(); - var minTime = null; - var maxTime = null; - var currentInfo; - var renderRange; - var activeRange; - var isValid; - - if (forceToValid) { - date = constrainDate(date, validRange); - } - - currentInfo = this.buildCurrentRangeInfo(date, direction); - renderRange = this.buildRenderRange(currentInfo.range, currentInfo.unit); - activeRange = cloneRange(renderRange); - - if (!this.opt('showNonCurrentDates')) { - activeRange = constrainRange(activeRange, currentInfo.range); - } - - minTime = moment.duration(this.opt('minTime')); - maxTime = moment.duration(this.opt('maxTime')); - this.adjustActiveRange(activeRange, minTime, maxTime); - - activeRange = constrainRange(activeRange, validRange); - date = constrainDate(date, activeRange); - - // it's invalid if the originally requested date is not contained, - // or if the range is completely outside of the valid range. - isValid = doRangesIntersect(currentInfo.range, validRange); - - return { - validRange: validRange, - currentRange: currentInfo.range, - currentRangeUnit: currentInfo.unit, - activeRange: activeRange, - renderRange: renderRange, - minTime: minTime, - maxTime: maxTime, - isValid: isValid, - date: date, - dateIncrement: this.buildDateIncrement(currentInfo.duration) - // pass a fallback (might be null) ^ - }; - }, - - - // Builds an object with optional start/end properties. - // Indicates the minimum/maximum dates to display. - buildValidRange: function() { - return this.getRangeOption('validRange', this.calendar.getNow()) || {}; - }, - - - // Builds a structure with info about the "current" range, the range that is - // highlighted as being the current month for example. - // See buildDateProfile for a description of `direction`. - // Guaranteed to have `range` and `unit` properties. `duration` is optional. - buildCurrentRangeInfo: function(date, direction) { - var duration = null; - var unit = null; - var range = null; - var dayCount; - - if (this.viewSpec.duration) { - duration = this.viewSpec.duration; - unit = this.viewSpec.durationUnit; - range = this.buildRangeFromDuration(date, direction, duration, unit); - } - else if ((dayCount = this.opt('dayCount'))) { - unit = 'day'; - range = this.buildRangeFromDayCount(date, direction, dayCount); - } - else if ((range = this.buildCustomVisibleRange(date))) { - unit = computeGreatestUnit(range.start, range.end); - } - else { - duration = this.getFallbackDuration(); - unit = computeGreatestUnit(duration); - range = this.buildRangeFromDuration(date, direction, duration, unit); - } - - this.normalizeCurrentRange(range, unit); // modifies in-place - - return { duration: duration, unit: unit, range: range }; - }, - - - getFallbackDuration: function() { - return moment.duration({ days: 1 }); - }, - - - // If the range has day units or larger, remove times. Otherwise, ensure times. - normalizeCurrentRange: function(range, unit) { - - if (/^(year|month|week|day)$/.test(unit)) { // whole-days? - range.start.stripTime(); - range.end.stripTime(); - } - else { // needs to have a time? - if (!range.start.hasTime()) { - range.start.time(0); // give 00:00 time - } - if (!range.end.hasTime()) { - range.end.time(0); // give 00:00 time - } - } - }, - - - // Mutates the given activeRange to have time values (un-ambiguate) - // if the minTime or maxTime causes the range to expand. - // TODO: eventually activeRange should *always* have times. - adjustActiveRange: function(range, minTime, maxTime) { - var hasSpecialTimes = false; - - if (this.usesMinMaxTime) { - - if (minTime < 0) { - range.start.time(0).add(minTime); - hasSpecialTimes = true; - } - - if (maxTime > 24 * 60 * 60 * 1000) { // beyond 24 hours? - range.end.time(maxTime - (24 * 60 * 60 * 1000)); - hasSpecialTimes = true; - } - - if (hasSpecialTimes) { - if (!range.start.hasTime()) { - range.start.time(0); - } - if (!range.end.hasTime()) { - range.end.time(0); - } - } - } - }, - - - // Builds the "current" range when it is specified as an explicit duration. - // `unit` is the already-computed computeGreatestUnit value of duration. - buildRangeFromDuration: function(date, direction, duration, unit) { - var alignment = this.opt('dateAlignment'); - var start = date.clone(); - var end; - var dateIncrementInput; - var dateIncrementDuration; - - // if the view displays a single day or smaller - if (duration.as('days') <= 1) { - if (this.isHiddenDay(start)) { - start = this.skipHiddenDays(start, direction); - start.startOf('day'); - } - } - - // compute what the alignment should be - if (!alignment) { - dateIncrementInput = this.opt('dateIncrement'); - - if (dateIncrementInput) { - dateIncrementDuration = moment.duration(dateIncrementInput); - - // use the smaller of the two units - if (dateIncrementDuration < duration) { - alignment = computeDurationGreatestUnit(dateIncrementDuration, dateIncrementInput); - } - else { - alignment = unit; - } - } - else { - alignment = unit; - } - } - - start.startOf(alignment); - end = start.clone().add(duration); - - return { start: start, end: end }; - }, - - - // Builds the "current" range when a dayCount is specified. - buildRangeFromDayCount: function(date, direction, dayCount) { - var customAlignment = this.opt('dateAlignment'); - var runningCount = 0; - var start = date.clone(); - var end; - - if (customAlignment) { - start.startOf(customAlignment); - } - - start.startOf('day'); - start = this.skipHiddenDays(start, direction); - - end = start.clone(); - do { - end.add(1, 'day'); - if (!this.isHiddenDay(end)) { - runningCount++; - } - } while (runningCount < dayCount); - - return { start: start, end: end }; - }, - - - // Builds a normalized range object for the "visible" range, - // which is a way to define the currentRange and activeRange at the same time. - buildCustomVisibleRange: function(date) { - var visibleRange = this.getRangeOption( - 'visibleRange', - this.calendar.moment(date) // correct zone. also generates new obj that avoids mutations - ); - - if (visibleRange && (!visibleRange.start || !visibleRange.end)) { - return null; - } - - return visibleRange; - }, - - - // Computes the range that will represent the element/cells for *rendering*, - // but which may have voided days/times. - buildRenderRange: function(currentRange, currentRangeUnit) { - // cut off days in the currentRange that are hidden - return this.trimHiddenDays(currentRange); - }, - - - // Compute the duration value that should be added/substracted to the current date - // when a prev/next operation happens. - buildDateIncrement: function(fallback) { - var dateIncrementInput = this.opt('dateIncrement'); - var customAlignment; - - if (dateIncrementInput) { - return moment.duration(dateIncrementInput); - } - else if ((customAlignment = this.opt('dateAlignment'))) { - return moment.duration(1, customAlignment); - } - else if (fallback) { - return fallback; - } - else { - return moment.duration({ days: 1 }); - } - }, - - - // Remove days from the beginning and end of the range that are computed as hidden. - trimHiddenDays: function(inputRange) { - return { - start: this.skipHiddenDays(inputRange.start), - end: this.skipHiddenDays(inputRange.end, -1, true) // exclusively move backwards - }; - }, - - - // Compute the number of the give units in the "current" range. - // Will return a floating-point number. Won't round. - currentRangeAs: function(unit) { - var currentRange = this.currentRange; - return currentRange.end.diff(currentRange.start, unit, true); - }, - - - // Arguments after name will be forwarded to a hypothetical function value - // WARNING: passed-in arguments will be given to generator functions as-is and can cause side-effects. - // Always clone your objects if you fear mutation. - getRangeOption: function(name) { - var val = this.opt(name); - - if (typeof val === 'function') { - val = val.apply( - null, - Array.prototype.slice.call(arguments, 1) - ); - } - - if (val) { - return this.calendar.parseRange(val); - } - }, - - - /* Hidden Days - ------------------------------------------------------------------------------------------------------------------*/ - - - // Initializes internal variables related to calculating hidden days-of-week - initHiddenDays: function() { - var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden - var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) - var dayCnt = 0; - var i; - - if (this.opt('weekends') === false) { - hiddenDays.push(0, 6); // 0=sunday, 6=saturday - } - - for (i = 0; i < 7; i++) { - if ( - !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) - ) { - dayCnt++; - } - } - - if (!dayCnt) { - throw 'invalid hiddenDays'; // all days were hidden? bad. - } - - this.isHiddenDayHash = isHiddenDayHash; - }, - - - // Is the current day hidden? - // `day` is a day-of-week index (0-6), or a Moment - isHiddenDay: function(day) { - if (moment.isMoment(day)) { - day = day.day(); - } - return this.isHiddenDayHash[day]; - }, - - - // Incrementing the current day until it is no longer a hidden day, returning a copy. - // DOES NOT CONSIDER validRange! - // If the initial value of `date` is not a hidden day, don't do anything. - // Pass `isExclusive` as `true` if you are dealing with an end date. - // `inc` defaults to `1` (increment one day forward each time) - skipHiddenDays: function(date, inc, isExclusive) { - var out = date.clone(); - inc = inc || 1; - while ( - this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] - ) { - out.add(inc, 'days'); - } - return out; - } - -}); - -;; - -/* -Embodies a div that has potential scrollbars -*/ -var Scroller = FC.Scroller = Class.extend({ - - el: null, // the guaranteed outer element - scrollEl: null, // the element with the scrollbars - overflowX: null, - overflowY: null, - - - constructor: function(options) { - options = options || {}; - this.overflowX = options.overflowX || options.overflow || 'auto'; - this.overflowY = options.overflowY || options.overflow || 'auto'; - }, - - - render: function() { - this.el = this.renderEl(); - this.applyOverflow(); - }, - - - renderEl: function() { - return (this.scrollEl = $('<div class="fc-scroller"></div>')); - }, - - - // sets to natural height, unlocks overflow - clear: function() { - this.setHeight('auto'); - this.applyOverflow(); - }, - - - destroy: function() { - this.el.remove(); - }, - - - // Overflow - // ----------------------------------------------------------------------------------------------------------------- - - - applyOverflow: function() { - this.scrollEl.css({ - 'overflow-x': this.overflowX, - 'overflow-y': this.overflowY - }); - }, - - - // Causes any 'auto' overflow values to resolves to 'scroll' or 'hidden'. - // Useful for preserving scrollbar widths regardless of future resizes. - // Can pass in scrollbarWidths for optimization. - lockOverflow: function(scrollbarWidths) { - var overflowX = this.overflowX; - var overflowY = this.overflowY; - - scrollbarWidths = scrollbarWidths || this.getScrollbarWidths(); - - if (overflowX === 'auto') { - overflowX = ( - scrollbarWidths.top || scrollbarWidths.bottom || // horizontal scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollWidth - 1 > this.scrollEl[0].clientWidth - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } - - if (overflowY === 'auto') { - overflowY = ( - scrollbarWidths.left || scrollbarWidths.right || // vertical scrollbars? - // OR scrolling pane with massless scrollbars? - this.scrollEl[0].scrollHeight - 1 > this.scrollEl[0].clientHeight - // subtract 1 because of IE off-by-one issue - ) ? 'scroll' : 'hidden'; - } - - this.scrollEl.css({ 'overflow-x': overflowX, 'overflow-y': overflowY }); - }, - - - // Getters / Setters - // ----------------------------------------------------------------------------------------------------------------- - - - setHeight: function(height) { - this.scrollEl.height(height); - }, - - - getScrollTop: function() { - return this.scrollEl.scrollTop(); - }, - - - setScrollTop: function(top) { - this.scrollEl.scrollTop(top); - }, - - - getClientWidth: function() { - return this.scrollEl[0].clientWidth; - }, - - - getClientHeight: function() { - return this.scrollEl[0].clientHeight; - }, - - - getScrollbarWidths: function() { - return getScrollbarWidths(this.scrollEl); - } - -}); - -;; -function Iterator(items) { - this.items = items || []; -} - - -/* Calls a method on every item passing the arguments through */ -Iterator.prototype.proxyCall = function(methodName) { - var args = Array.prototype.slice.call(arguments, 1); - var results = []; - - this.items.forEach(function(item) { - results.push(item[methodName].apply(item, args)); - }); - - return results; -}; - -;; - -/* Toolbar with buttons and title -----------------------------------------------------------------------------------------------------------------------*/ - -function Toolbar(calendar, toolbarOptions) { - var t = this; - - // exports - t.setToolbarOptions = setToolbarOptions; - t.render = render; - t.removeElement = removeElement; - t.updateTitle = updateTitle; - t.activateButton = activateButton; - t.deactivateButton = deactivateButton; - t.disableButton = disableButton; - t.enableButton = enableButton; - t.getViewsWithButtons = getViewsWithButtons; - t.el = null; // mirrors local `el` - - // locals - var el; - var viewsWithButtons = []; - var tm; - - // method to update toolbar-specific options, not calendar-wide options - function setToolbarOptions(newToolbarOptions) { - toolbarOptions = newToolbarOptions; - } - - // can be called repeatedly and will rerender - function render() { - var sections = toolbarOptions.layout; - - tm = calendar.opt('theme') ? 'ui' : 'fc'; - - if (sections) { - if (!el) { - el = this.el = $("<div class='fc-toolbar "+ toolbarOptions.extraClasses + "'/>"); - } - else { - el.empty(); - } - el.append(renderSection('left')) - .append(renderSection('right')) - .append(renderSection('center')) - .append('<div class="fc-clear"/>'); - } - else { - removeElement(); - } - } - - - function removeElement() { - if (el) { - el.remove(); - el = t.el = null; - } - } - - - function renderSection(position) { - var sectionEl = $('<div class="fc-' + position + '"/>'); - var buttonStr = toolbarOptions.layout[position]; - var calendarCustomButtons = calendar.opt('customButtons') || {}; - var calendarButtonText = calendar.opt('buttonText') || {}; - - if (buttonStr) { - $.each(buttonStr.split(' '), function(i) { - var groupChildren = $(); - var isOnlyButtons = true; - var groupEl; - - $.each(this.split(','), function(j, buttonName) { - var customButtonProps; - var viewSpec; - var buttonClick; - var overrideText; // text explicitly set by calendar's constructor options. overcomes icons - var defaultText; - var themeIcon; - var normalIcon; - var innerHtml; - var classes; - var button; // the element - - if (buttonName == 'title') { - groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height - isOnlyButtons = false; - } - else { - if ((customButtonProps = calendarCustomButtons[buttonName])) { - buttonClick = function(ev) { - if (customButtonProps.click) { - customButtonProps.click.call(button[0], ev); - } - }; - overrideText = ''; // icons will override text - defaultText = customButtonProps.text; - } - else if ((viewSpec = calendar.getViewSpec(buttonName))) { - buttonClick = function() { - calendar.changeView(buttonName); - }; - viewsWithButtons.push(buttonName); - overrideText = viewSpec.buttonTextOverride; - defaultText = viewSpec.buttonTextDefault; - } - else if (calendar[buttonName]) { // a calendar method - buttonClick = function() { - calendar[buttonName](); - }; - overrideText = (calendar.overrides.buttonText || {})[buttonName]; - defaultText = calendarButtonText[buttonName]; // everything else is considered default - } - - if (buttonClick) { - - themeIcon = - customButtonProps ? - customButtonProps.themeIcon : - calendar.opt('themeButtonIcons')[buttonName]; - - normalIcon = - customButtonProps ? - customButtonProps.icon : - calendar.opt('buttonIcons')[buttonName]; - - if (overrideText) { - innerHtml = htmlEscape(overrideText); - } - else if (themeIcon && calendar.opt('theme')) { - innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; - } - else if (normalIcon && !calendar.opt('theme')) { - innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; - } - else { - innerHtml = htmlEscape(defaultText); - } - - classes = [ - 'fc-' + buttonName + '-button', - tm + '-button', - tm + '-state-default' - ]; - - button = $( // type="button" so that it doesn't submit a form - '<button type="button" class="' + classes.join(' ') + '">' + - innerHtml + - '</button>' - ) - .click(function(ev) { - // don't process clicks for disabled buttons - if (!button.hasClass(tm + '-state-disabled')) { - - buttonClick(ev); - - // after the click action, if the button becomes the "active" tab, or disabled, - // it should never have a hover class, so remove it now. - if ( - button.hasClass(tm + '-state-active') || - button.hasClass(tm + '-state-disabled') - ) { - button.removeClass(tm + '-state-hover'); - } - } - }) - .mousedown(function() { - // the *down* effect (mouse pressed in). - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - // undo the *down* effect - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - // the *hover* effect. - // only on buttons that are not the "active" tab, or disabled - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - // undo the *hover* effect - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup - } - ); - - groupChildren = groupChildren.add(button); - } - } - }); - - if (isOnlyButtons) { - groupChildren - .first().addClass(tm + '-corner-left').end() - .last().addClass(tm + '-corner-right').end(); - } - - if (groupChildren.length > 1) { - groupEl = $('<div/>'); - if (isOnlyButtons) { - groupEl.addClass('fc-button-group'); - } - groupEl.append(groupChildren); - sectionEl.append(groupEl); - } - else { - sectionEl.append(groupChildren); // 1 or 0 children - } - }); - } - - return sectionEl; - } - - - function updateTitle(text) { - if (el) { - el.find('h2').text(text); - } - } - - - function activateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .addClass(tm + '-state-active'); - } - } - - - function deactivateButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .removeClass(tm + '-state-active'); - } - } - - - function disableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', true) - .addClass(tm + '-state-disabled'); - } - } - - - function enableButton(buttonName) { - if (el) { - el.find('.fc-' + buttonName + '-button') - .prop('disabled', false) - .removeClass(tm + '-state-disabled'); - } - } - - - function getViewsWithButtons() { - return viewsWithButtons; - } - -} - -;; - -var Calendar = FC.Calendar = Class.extend(EmitterMixin, { - - view: null, // current View object - viewsByType: null, // holds all instantiated view instances, current or not - currentDate: null, // unzoned moment. private (public API should use getDate instead) - loadingLevel: 0, // number of simultaneous loading tasks - - - constructor: function(el, overrides) { - - // declare the current calendar instance relies on GlobalEmitter. needed for garbage collection. - // unneeded() is called in destroy. - GlobalEmitter.needed(); - - this.el = el; - this.viewsByType = {}; - this.viewSpecCache = {}; - - this.initOptionsInternals(overrides); - this.initMomentInternals(); // needs to happen after options hash initialized - this.initCurrentDate(); - - EventManager.call(this); // needs options immediately - this.initialize(); - }, - - - // Subclasses can override this for initialization logic after the constructor has been called - initialize: function() { - }, - - - // Public API - // ----------------------------------------------------------------------------------------------------------------- - - - getCalendar: function() { - return this; - }, - - - getView: function() { - return this.view; - }, - - - publiclyTrigger: function(name, thisObj) { - var args = Array.prototype.slice.call(arguments, 2); - var optHandler = this.opt(name); - - thisObj = thisObj || this.el[0]; - this.triggerWith(name, thisObj, args); // Emitter's method - - if (optHandler) { - return optHandler.apply(thisObj, args); - } - }, - - - // View - // ----------------------------------------------------------------------------------------------------------------- - - - // Given a view name for a custom view or a standard view, creates a ready-to-go View object - instantiateView: function(viewType) { - var spec = this.getViewSpec(viewType); - - return new spec['class'](this, spec); - }, - - - // Returns a boolean about whether the view is okay to instantiate at some point - isValidViewType: function(viewType) { - return Boolean(this.getViewSpec(viewType)); - }, - - - changeView: function(viewName, dateOrRange) { - - if (dateOrRange) { - - if (dateOrRange.start && dateOrRange.end) { // a range - this.recordOptionOverrides({ // will not rerender - visibleRange: dateOrRange - }); - } - else { // a date - this.currentDate = this.moment(dateOrRange).stripZone(); // just like gotoDate - } - } - - this.renderView(viewName); - }, - - - // Forces navigation to a view for the given date. - // `viewType` can be a specific view name or a generic one like "week" or "day". - zoomTo: function(newDate, viewType) { - var spec; - - viewType = viewType || 'day'; // day is default zoom - spec = this.getViewSpec(viewType) || this.getUnitViewSpec(viewType); - - this.currentDate = newDate.clone(); - this.renderView(spec ? spec.type : null); - }, - - - // Current Date - // ----------------------------------------------------------------------------------------------------------------- - - - initCurrentDate: function() { - var defaultDateInput = this.opt('defaultDate'); - - // compute the initial ambig-timezone date - if (defaultDateInput != null) { - this.currentDate = this.moment(defaultDateInput).stripZone(); - } - else { - this.currentDate = this.getNow(); // getNow already returns unzoned - } - }, - - - prev: function() { - var prevInfo = this.view.buildPrevDateProfile(this.currentDate); - - if (prevInfo.isValid) { - this.currentDate = prevInfo.date; - this.renderView(); - } - }, - - - next: function() { - var nextInfo = this.view.buildNextDateProfile(this.currentDate); - - if (nextInfo.isValid) { - this.currentDate = nextInfo.date; - this.renderView(); - } - }, - - - prevYear: function() { - this.currentDate.add(-1, 'years'); - this.renderView(); - }, - - - nextYear: function() { - this.currentDate.add(1, 'years'); - this.renderView(); - }, - - - today: function() { - this.currentDate = this.getNow(); // should deny like prev/next? - this.renderView(); - }, - - - gotoDate: function(zonedDateInput) { - this.currentDate = this.moment(zonedDateInput).stripZone(); - this.renderView(); - }, - - - incrementDate: function(delta) { - this.currentDate.add(moment.duration(delta)); - this.renderView(); - }, - - - // for external API - getDate: function() { - return this.applyTimezone(this.currentDate); // infuse the calendar's timezone - }, - - - // Loading Triggering - // ----------------------------------------------------------------------------------------------------------------- - - - // Should be called when any type of async data fetching begins - pushLoading: function() { - if (!(this.loadingLevel++)) { - this.publiclyTrigger('loading', null, true, this.view); - } - }, - - - // Should be called when any type of async data fetching completes - popLoading: function() { - if (!(--this.loadingLevel)) { - this.publiclyTrigger('loading', null, false, this.view); - } - }, - - - // Selection - // ----------------------------------------------------------------------------------------------------------------- - - - // this public method receives start/end dates in any format, with any timezone - select: function(zonedStartInput, zonedEndInput) { - this.view.select( - this.buildSelectSpan.apply(this, arguments) - ); - }, - - - unselect: function() { // safe to be called before renderView - if (this.view) { - this.view.unselect(); - } - }, - - - // Given arguments to the select method in the API, returns a span (unzoned start/end and other info) - buildSelectSpan: function(zonedStartInput, zonedEndInput) { - var start = this.moment(zonedStartInput).stripZone(); - var end; - - if (zonedEndInput) { - end = this.moment(zonedEndInput).stripZone(); - } - else if (start.hasTime()) { - end = start.clone().add(this.defaultTimedEventDuration); - } - else { - end = start.clone().add(this.defaultAllDayEventDuration); - } - - return { start: start, end: end }; - }, - - - // Misc - // ----------------------------------------------------------------------------------------------------------------- - - - // will return `null` if invalid range - parseRange: function(rangeInput) { - var start = null; - var end = null; - - if (rangeInput.start) { - start = this.moment(rangeInput.start).stripZone(); - } - - if (rangeInput.end) { - end = this.moment(rangeInput.end).stripZone(); - } - - if (!start && !end) { - return null; - } - - if (start && end && end.isBefore(start)) { - return null; - } - - return { start: start, end: end }; - }, - - - rerenderEvents: function() { // API method. destroys old events if previously rendered. - if (this.elementVisible()) { - this.reportEventChange(); // will re-trasmit events to the view, causing a rerender - } - } - -}); - -;; -/* -Options binding/triggering system. -*/ -Calendar.mixin({ - - dirDefaults: null, // option defaults related to LTR or RTL - localeDefaults: null, // option defaults related to current locale - overrides: null, // option overrides given to the fullCalendar constructor - dynamicOverrides: null, // options set with dynamic setter method. higher precedence than view overrides. - optionsModel: null, // all defaults combined with overrides - - - initOptionsInternals: function(overrides) { - this.overrides = $.extend({}, overrides); // make a copy - this.dynamicOverrides = {}; - this.optionsModel = new Model(); - - this.populateOptionsHash(); - }, - - - // public getter/setter - option: function(name, value) { - var newOptionHash; - - if (typeof name === 'string') { - if (value === undefined) { // getter - return this.optionsModel.get(name); - } - else { // setter for individual option - newOptionHash = {}; - newOptionHash[name] = value; - this.setOptions(newOptionHash); - } - } - else if (typeof name === 'object') { // compound setter with object input - this.setOptions(name); - } - }, - - - // private getter - opt: function(name) { - return this.optionsModel.get(name); - }, - - - setOptions: function(newOptionHash) { - var optionCnt = 0; - var optionName; - - this.recordOptionOverrides(newOptionHash); - - for (optionName in newOptionHash) { - optionCnt++; - } - - // special-case handling of single option change. - // if only one option change, `optionName` will be its name. - if (optionCnt === 1) { - if (optionName === 'height' || optionName === 'contentHeight' || optionName === 'aspectRatio') { - this.updateSize(true); // true = allow recalculation of height - return; - } - else if (optionName === 'defaultDate') { - return; // can't change date this way. use gotoDate instead - } - else if (optionName === 'businessHours') { - if (this.view) { - this.view.unrenderBusinessHours(); - this.view.renderBusinessHours(); - } - return; - } - else if (optionName === 'timezone') { - this.rezoneArrayEventSources(); - this.refetchEvents(); - return; - } - } - - // catch-all. rerender the header and footer and rebuild/rerender the current view - this.renderHeader(); - this.renderFooter(); - - // even non-current views will be affected by this option change. do before rerender - // TODO: detangle - this.viewsByType = {}; - - this.reinitView(); - }, - - - // Computes the flattened options hash for the calendar and assigns to `this.options`. - // Assumes this.overrides and this.dynamicOverrides have already been initialized. - populateOptionsHash: function() { - var locale, localeDefaults; - var isRTL, dirDefaults; - var rawOptions; - - locale = firstDefined( // explicit locale option given? - this.dynamicOverrides.locale, - this.overrides.locale - ); - localeDefaults = localeOptionHash[locale]; - if (!localeDefaults) { // explicit locale option not given or invalid? - locale = Calendar.defaults.locale; - localeDefaults = localeOptionHash[locale] || {}; - } - - isRTL = firstDefined( // based on options computed so far, is direction RTL? - this.dynamicOverrides.isRTL, - this.overrides.isRTL, - localeDefaults.isRTL, - Calendar.defaults.isRTL - ); - dirDefaults = isRTL ? Calendar.rtlDefaults : {}; - - this.dirDefaults = dirDefaults; - this.localeDefaults = localeDefaults; - - rawOptions = mergeOptions([ // merge defaults and overrides. lowest to highest precedence - Calendar.defaults, // global defaults - dirDefaults, - localeDefaults, - this.overrides, - this.dynamicOverrides - ]); - populateInstanceComputableOptions(rawOptions); // fill in gaps with computed options - - this.optionsModel.reset(rawOptions); - }, - - - // stores the new options internally, but does not rerender anything. - recordOptionOverrides: function(newOptionHash) { - var optionName; - - for (optionName in newOptionHash) { - this.dynamicOverrides[optionName] = newOptionHash[optionName]; - } - - this.viewSpecCache = {}; // the dynamic override invalidates the options in this cache, so just clear it - this.populateOptionsHash(); // this.options needs to be recomputed after the dynamic override - } - -}); - -;; - -Calendar.mixin({ - - defaultAllDayEventDuration: null, - defaultTimedEventDuration: null, - localeData: null, - - - initMomentInternals: function() { - var _this = this; - - this.defaultAllDayEventDuration = moment.duration(this.opt('defaultAllDayEventDuration')); - this.defaultTimedEventDuration = moment.duration(this.opt('defaultTimedEventDuration')); - - // Called immediately, and when any of the options change. - // Happens before any internal objects rebuild or rerender, because this is very core. - this.optionsModel.watch('buildingMomentLocale', [ - '?locale', '?monthNames', '?monthNamesShort', '?dayNames', '?dayNamesShort', - '?firstDay', '?weekNumberCalculation' - ], function(opts) { - var weekNumberCalculation = opts.weekNumberCalculation; - var firstDay = opts.firstDay; - var _week; - - // normalize - if (weekNumberCalculation === 'iso') { - weekNumberCalculation = 'ISO'; // normalize - } - - var localeData = createObject( // make a cheap copy - getMomentLocaleData(opts.locale) // will fall back to en - ); - - if (opts.monthNames) { - localeData._months = opts.monthNames; - } - if (opts.monthNamesShort) { - localeData._monthsShort = opts.monthNamesShort; - } - if (opts.dayNames) { - localeData._weekdays = opts.dayNames; - } - if (opts.dayNamesShort) { - localeData._weekdaysShort = opts.dayNamesShort; - } - - if (firstDay == null && weekNumberCalculation === 'ISO') { - firstDay = 1; - } - if (firstDay != null) { - _week = createObject(localeData._week); // _week: { dow: # } - _week.dow = firstDay; - localeData._week = _week; - } - - if ( // whitelist certain kinds of input - weekNumberCalculation === 'ISO' || - weekNumberCalculation === 'local' || - typeof weekNumberCalculation === 'function' - ) { - localeData._fullCalendar_weekCalc = weekNumberCalculation; // moment-ext will know what to do with it - } - - _this.localeData = localeData; - - // If the internal current date object already exists, move to new locale. - // We do NOT need to do this technique for event dates, because this happens when converting to "segments". - if (_this.currentDate) { - _this.localizeMoment(_this.currentDate); // sets to localeData - } - }); - }, - - - // Builds a moment using the settings of the current calendar: timezone and locale. - // Accepts anything the vanilla moment() constructor accepts. - moment: function() { - var mom; - - if (this.opt('timezone') === 'local') { - mom = FC.moment.apply(null, arguments); - - // Force the moment to be local, because FC.moment doesn't guarantee it. - if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone - mom.local(); - } - } - else if (this.opt('timezone') === 'UTC') { - mom = FC.moment.utc.apply(null, arguments); // process as UTC - } - else { - mom = FC.moment.parseZone.apply(null, arguments); // let the input decide the zone - } - - this.localizeMoment(mom); // TODO - - return mom; - }, - - - // Updates the given moment's locale settings to the current calendar locale settings. - localizeMoment: function(mom) { - mom._locale = this.localeData; - }, - - - // Returns a boolean about whether or not the calendar knows how to calculate - // the timezone offset of arbitrary dates in the current timezone. - getIsAmbigTimezone: function() { - return this.opt('timezone') !== 'local' && this.opt('timezone') !== 'UTC'; - }, - - - // Returns a copy of the given date in the current timezone. Has no effect on dates without times. - applyTimezone: function(date) { - if (!date.hasTime()) { - return date.clone(); - } - - var zonedDate = this.moment(date.toArray()); - var timeAdjust = date.time() - zonedDate.time(); - var adjustedZonedDate; - - // Safari sometimes has problems with this coersion when near DST. Adjust if necessary. (bug #2396) - if (timeAdjust) { // is the time result different than expected? - adjustedZonedDate = zonedDate.clone().add(timeAdjust); // add milliseconds - if (date.time() - adjustedZonedDate.time() === 0) { // does it match perfectly now? - zonedDate = adjustedZonedDate; - } - } - - return zonedDate; - }, - - - // Returns a moment for the current date, as defined by the client's computer or from the `now` option. - // Will return an moment with an ambiguous timezone. - getNow: function() { - var now = this.opt('now'); - if (typeof now === 'function') { - now = now(); - } - return this.moment(now).stripZone(); - }, - - - // Produces a human-readable string for the given duration. - // Side-effect: changes the locale of the given duration. - humanizeDuration: function(duration) { - return duration.locale(this.opt('locale')).humanize(); - }, - - - - // Event-Specific Date Utilities. TODO: move - // ----------------------------------------------------------------------------------------------------------------- - - - // Get an event's normalized end date. If not present, calculate it from the defaults. - getEventEnd: function(event) { - if (event.end) { - return event.end.clone(); - } - else { - return this.getDefaultEventEnd(event.allDay, event.start); - } - }, - - - // Given an event's allDay status and start date, return what its fallback end date should be. - // TODO: rename to computeDefaultEventEnd - getDefaultEventEnd: function(allDay, zonedStart) { - var end = zonedStart.clone(); - - if (allDay) { - end.stripTime().add(this.defaultAllDayEventDuration); - } - else { - end.add(this.defaultTimedEventDuration); - } - - if (this.getIsAmbigTimezone()) { - end.stripZone(); // we don't know what the tzo should be - } - - return end; - } - -}); - -;; - -Calendar.mixin({ - - viewSpecCache: null, // cache of view definitions (initialized in Calendar.js) - - - // Gets information about how to create a view. Will use a cache. - getViewSpec: function(viewType) { - var cache = this.viewSpecCache; - - return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); - }, - - - // Given a duration singular unit, like "week" or "day", finds a matching view spec. - // Preference is given to views that have corresponding buttons. - getUnitViewSpec: function(unit) { - var viewTypes; - var i; - var spec; - - if ($.inArray(unit, unitsDesc) != -1) { - - // put views that have buttons first. there will be duplicates, but oh well - viewTypes = this.header.getViewsWithButtons(); // TODO: include footer as well? - $.each(FC.views, function(viewType) { // all views - viewTypes.push(viewType); - }); - - for (i = 0; i < viewTypes.length; i++) { - spec = this.getViewSpec(viewTypes[i]); - if (spec) { - if (spec.singleUnit == unit) { - return spec; - } - } - } - } - }, - - - // Builds an object with information on how to create a given view - buildViewSpec: function(requestedViewType) { - var viewOverrides = this.overrides.views || {}; - var specChain = []; // for the view. lowest to highest priority - var defaultsChain = []; // for the view. lowest to highest priority - var overridesChain = []; // for the view. lowest to highest priority - var viewType = requestedViewType; - var spec; // for the view - var overrides; // for the view - var durationInput; - var duration; - var unit; - - // iterate from the specific view definition to a more general one until we hit an actual View class - while (viewType) { - spec = fcViews[viewType]; - overrides = viewOverrides[viewType]; - viewType = null; // clear. might repopulate for another iteration - - if (typeof spec === 'function') { // TODO: deprecate - spec = { 'class': spec }; - } - - if (spec) { - specChain.unshift(spec); - defaultsChain.unshift(spec.defaults || {}); - durationInput = durationInput || spec.duration; - viewType = viewType || spec.type; - } - - if (overrides) { - overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level - durationInput = durationInput || overrides.duration; - viewType = viewType || overrides.type; - } - } - - spec = mergeProps(specChain); - spec.type = requestedViewType; - if (!spec['class']) { - return false; - } - - // fall back to top-level `duration` option - durationInput = durationInput || - this.dynamicOverrides.duration || - this.overrides.duration; - - if (durationInput) { - duration = moment.duration(durationInput); - - if (duration.valueOf()) { // valid? - - unit = computeDurationGreatestUnit(duration, durationInput); - - spec.duration = duration; - spec.durationUnit = unit; - - // view is a single-unit duration, like "week" or "day" - // incorporate options for this. lowest priority - if (duration.as(unit) === 1) { - spec.singleUnit = unit; - overridesChain.unshift(viewOverrides[unit] || {}); - } - } - } - - spec.defaults = mergeOptions(defaultsChain); - spec.overrides = mergeOptions(overridesChain); - - this.buildViewSpecOptions(spec); - this.buildViewSpecButtonText(spec, requestedViewType); - - return spec; - }, - - - // Builds and assigns a view spec's options object from its already-assigned defaults and overrides - buildViewSpecOptions: function(spec) { - spec.options = mergeOptions([ // lowest to highest priority - Calendar.defaults, // global defaults - spec.defaults, // view's defaults (from ViewSubclass.defaults) - this.dirDefaults, - this.localeDefaults, // locale and dir take precedence over view's defaults! - this.overrides, // calendar's overrides (options given to constructor) - spec.overrides, // view's overrides (view-specific options) - this.dynamicOverrides // dynamically set via setter. highest precedence - ]); - populateInstanceComputableOptions(spec.options); - }, - - - // Computes and assigns a view spec's buttonText-related options - buildViewSpecButtonText: function(spec, requestedViewType) { - - // given an options object with a possible `buttonText` hash, lookup the buttonText for the - // requested view, falling back to a generic unit entry like "week" or "day" - function queryButtonText(options) { - var buttonText = options.buttonText || {}; - return buttonText[requestedViewType] || - // view can decide to look up a certain key - (spec.buttonTextKey ? buttonText[spec.buttonTextKey] : null) || - // a key like "month" - (spec.singleUnit ? buttonText[spec.singleUnit] : null); - } - - // highest to lowest priority - spec.buttonTextOverride = - queryButtonText(this.dynamicOverrides) || - queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence - spec.overrides.buttonText; // `buttonText` for view-specific options is a string - - // highest to lowest priority. mirrors buildViewSpecOptions - spec.buttonTextDefault = - queryButtonText(this.localeDefaults) || - queryButtonText(this.dirDefaults) || - spec.defaults.buttonText || // a single string. from ViewSubclass.defaults - queryButtonText(Calendar.defaults) || - (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" - requestedViewType; // fall back to given view name - } - -}); - -;; - -Calendar.mixin({ - - el: null, - contentEl: null, - suggestedViewHeight: null, - windowResizeProxy: null, - ignoreWindowResize: 0, - - - render: function() { - if (!this.contentEl) { - this.initialRender(); - } - else if (this.elementVisible()) { - // mainly for the public API - this.calcSize(); - this.renderView(); - } - }, - - - initialRender: function() { - var _this = this; - var el = this.el; - - el.addClass('fc'); - - // event delegation for nav links - el.on('click.fc', 'a[data-goto]', function(ev) { - var anchorEl = $(this); - var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON - var date = _this.moment(gotoOptions.date); - var viewType = gotoOptions.type; - - // property like "navLinkDayClick". might be a string or a function - var customAction = _this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); - - if (typeof customAction === 'function') { - customAction(date, ev); - } - else { - if (typeof customAction === 'string') { - viewType = customAction; - } - _this.zoomTo(date, viewType); - } - }); - - // called immediately, and upon option change - this.optionsModel.watch('applyingThemeClasses', [ '?theme' ], function(opts) { - el.toggleClass('ui-widget', opts.theme); - el.toggleClass('fc-unthemed', !opts.theme); - }); - - // called immediately, and upon option change. - // HACK: locale often affects isRTL, so we explicitly listen to that too. - this.optionsModel.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts) { - el.toggleClass('fc-ltr', !opts.isRTL); - el.toggleClass('fc-rtl', opts.isRTL); - }); - - this.contentEl = $("<div class='fc-view-container'/>").prependTo(el); - - this.initToolbars(); - this.renderHeader(); - this.renderFooter(); - this.renderView(this.opt('defaultView')); - - if (this.opt('handleWindowResize')) { - $(window).resize( - this.windowResizeProxy = debounce( // prevents rapid calls - this.windowResize.bind(this), - this.opt('windowResizeDelay') - ) - ); - } - }, - - - destroy: function() { - - if (this.view) { - this.view.removeElement(); - - // NOTE: don't null-out this.view in case API methods are called after destroy. - // It is still the "current" view, just not rendered. - } - - this.toolbarsManager.proxyCall('removeElement'); - this.contentEl.remove(); - this.el.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); - - this.el.off('.fc'); // unbind nav link handlers - - if (this.windowResizeProxy) { - $(window).unbind('resize', this.windowResizeProxy); - this.windowResizeProxy = null; - } - - GlobalEmitter.unneeded(); - }, - - - elementVisible: function() { - return this.el.is(':visible'); - }, - - - - // View Rendering - // ----------------------------------------------------------------------------------- - - - // Renders a view because of a date change, view-type change, or for the first time. - // If not given a viewType, keep the current view but render different dates. - // Accepts an optional scroll state to restore to. - renderView: function(viewType, forcedScroll) { - - this.ignoreWindowResize++; - - var needsClearView = this.view && viewType && this.view.type !== viewType; - - // if viewType is changing, remove the old view's rendering - if (needsClearView) { - this.freezeContentHeight(); // prevent a scroll jump when view element is removed - this.clearView(); - } - - // if viewType changed, or the view was never created, create a fresh view - if (!this.view && viewType) { - this.view = - this.viewsByType[viewType] || - (this.viewsByType[viewType] = this.instantiateView(viewType)); - - this.view.setElement( - $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl) - ); - this.toolbarsManager.proxyCall('activateButton', viewType); - } - - if (this.view) { - - if (forcedScroll) { - this.view.addForcedScroll(forcedScroll); - } - - if (this.elementVisible()) { - this.currentDate = this.view.setDate(this.currentDate); - } - } - - if (needsClearView) { - this.thawContentHeight(); - } - - this.ignoreWindowResize--; - }, - - - // Unrenders the current view and reflects this change in the Header. - // Unregsiters the `view`, but does not remove from viewByType hash. - clearView: function() { - this.toolbarsManager.proxyCall('deactivateButton', this.view.type); - this.view.removeElement(); - this.view = null; - }, - - - // Destroys the view, including the view object. Then, re-instantiates it and renders it. - // Maintains the same scroll state. - // TODO: maintain any other user-manipulated state. - reinitView: function() { - this.ignoreWindowResize++; - this.freezeContentHeight(); - - var viewType = this.view.type; - var scrollState = this.view.queryScroll(); - this.clearView(); - this.calcSize(); - this.renderView(viewType, scrollState); - - this.thawContentHeight(); - this.ignoreWindowResize--; - }, - - - // Resizing - // ----------------------------------------------------------------------------------- - - - getSuggestedViewHeight: function() { - if (this.suggestedViewHeight === null) { - this.calcSize(); - } - return this.suggestedViewHeight; - }, - - - isHeightAuto: function() { - return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto'; - }, - - - updateSize: function(shouldRecalc) { - if (this.elementVisible()) { - - if (shouldRecalc) { - this._calcSize(); - } - - this.ignoreWindowResize++; - this.view.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() - this.ignoreWindowResize--; - - return true; // signal success - } - }, - - - calcSize: function() { - if (this.elementVisible()) { - this._calcSize(); - } - }, - - - _calcSize: function() { // assumes elementVisible - var contentHeightInput = this.opt('contentHeight'); - var heightInput = this.opt('height'); - - if (typeof contentHeightInput === 'number') { // exists and not 'auto' - this.suggestedViewHeight = contentHeightInput; - } - else if (typeof contentHeightInput === 'function') { // exists and is a function - this.suggestedViewHeight = contentHeightInput(); - } - else if (typeof heightInput === 'number') { // exists and not 'auto' - this.suggestedViewHeight = heightInput - this.queryToolbarsHeight(); - } - else if (typeof heightInput === 'function') { // exists and is a function - this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight(); - } - else if (heightInput === 'parent') { // set to height of parent element - this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight(); - } - else { - this.suggestedViewHeight = Math.round( - this.contentEl.width() / - Math.max(this.opt('aspectRatio'), .5) - ); - } - }, - - - windowResize: function(ev) { - if ( - !this.ignoreWindowResize && - ev.target === window && // so we don't process jqui "resize" events that have bubbled up - this.view.renderRange // view has already been rendered - ) { - if (this.updateSize(true)) { - this.view.publiclyTrigger('windowResize', this.el[0]); - } - } - }, - - - /* Height "Freezing" - -----------------------------------------------------------------------------*/ - - - freezeContentHeight: function() { - this.contentEl.css({ - width: '100%', - height: this.contentEl.height(), - overflow: 'hidden' - }); - }, - - - thawContentHeight: function() { - this.contentEl.css({ - width: '', - height: '', - overflow: '' - }); - } - -}); - -;; - -Calendar.mixin({ - - header: null, - footer: null, - toolbarsManager: null, - - - initToolbars: function() { - this.header = new Toolbar(this, this.computeHeaderOptions()); - this.footer = new Toolbar(this, this.computeFooterOptions()); - this.toolbarsManager = new Iterator([ this.header, this.footer ]); - }, - - - computeHeaderOptions: function() { - return { - extraClasses: 'fc-header-toolbar', - layout: this.opt('header') - }; - }, - - - computeFooterOptions: function() { - return { - extraClasses: 'fc-footer-toolbar', - layout: this.opt('footer') - }; - }, - - - // can be called repeatedly and Header will rerender - renderHeader: function() { - var header = this.header; - - header.setToolbarOptions(this.computeHeaderOptions()); - header.render(); - - if (header.el) { - this.el.prepend(header.el); - } - }, - - - // can be called repeatedly and Footer will rerender - renderFooter: function() { - var footer = this.footer; - - footer.setToolbarOptions(this.computeFooterOptions()); - footer.render(); - - if (footer.el) { - this.el.append(footer.el); - } - }, - - - setToolbarsTitle: function(title) { - this.toolbarsManager.proxyCall('updateTitle', title); - }, - - - updateToolbarButtons: function() { - var now = this.getNow(); - var view = this.view; - var todayInfo = view.buildDateProfile(now); - var prevInfo = view.buildPrevDateProfile(this.currentDate); - var nextInfo = view.buildNextDateProfile(this.currentDate); - - this.toolbarsManager.proxyCall( - (todayInfo.isValid && !isDateWithinRange(now, view.currentRange)) ? - 'enableButton' : - 'disableButton', - 'today' - ); - - this.toolbarsManager.proxyCall( - prevInfo.isValid ? - 'enableButton' : - 'disableButton', - 'prev' - ); - - this.toolbarsManager.proxyCall( - nextInfo.isValid ? - 'enableButton' : - 'disableButton', - 'next' - ); - }, - - - queryToolbarsHeight: function() { - return this.toolbarsManager.items.reduce(function(accumulator, toolbar) { - var toolbarHeight = toolbar.el ? toolbar.el.outerHeight(true) : 0; // includes margin - return accumulator + toolbarHeight; - }, 0); - } - -}); - -;; - -Calendar.defaults = { - - titleRangeSeparator: ' \u2013 ', // en dash - monthYearFormat: 'MMMM YYYY', // required for en. other locales rely on datepicker computable option - - defaultTimedEventDuration: '02:00:00', - defaultAllDayEventDuration: { days: 1 }, - forceEventDuration: false, - nextDayThreshold: '09:00:00', // 9am - - // display - defaultView: 'month', - aspectRatio: 1.35, - header: { - left: 'title', - center: '', - right: 'today prev,next' - }, - weekends: true, - weekNumbers: false, - - weekNumberTitle: 'W', - weekNumberCalculation: 'local', - - //editable: false, - - //nowIndicator: false, - - scrollTime: '06:00:00', - minTime: '00:00:00', - maxTime: '24:00:00', - showNonCurrentDates: true, - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - timezoneParam: 'timezone', - - timezone: false, - - //allDayDefault: undefined, - - // locale - isRTL: false, - buttonText: { - prev: "prev", - next: "next", - prevYear: "prev year", - nextYear: "next year", - year: 'year', // TODO: locale files need to specify this - today: 'today', - month: 'month', - week: 'week', - day: 'day' - }, - - buttonIcons: { - prev: 'left-single-arrow', - next: 'right-single-arrow', - prevYear: 'left-double-arrow', - nextYear: 'right-double-arrow' - }, - - allDayText: 'all-day', - - // jquery-ui theming - theme: false, - themeButtonIcons: { - prev: 'circle-triangle-w', - next: 'circle-triangle-e', - prevYear: 'seek-prev', - nextYear: 'seek-next' - }, - - //eventResizableFromStart: false, - dragOpacity: .75, - dragRevertDuration: 500, - dragScroll: true, - - //selectable: false, - unselectAuto: true, - //selectMinDistance: 0, - - dropAccept: '*', - - eventOrder: 'title', - //eventRenderWait: null, - - eventLimit: false, - eventLimitText: 'more', - eventLimitClick: 'popover', - dayPopoverFormat: 'LL', - - handleWindowResize: true, - windowResizeDelay: 100, // milliseconds before an updateSize happens - - longPressDelay: 1000 - -}; - - -Calendar.englishDefaults = { // used by locale.js - dayPopoverFormat: 'dddd, MMMM D' -}; - - -Calendar.rtlDefaults = { // right-to-left defaults - header: { // TODO: smarter solution (first/center/last ?) - left: 'next,prev today', - center: '', - right: 'title' - }, - buttonIcons: { - prev: 'right-single-arrow', - next: 'left-single-arrow', - prevYear: 'right-double-arrow', - nextYear: 'left-double-arrow' - }, - themeButtonIcons: { - prev: 'circle-triangle-e', - next: 'circle-triangle-w', - nextYear: 'seek-prev', - prevYear: 'seek-next' - } -}; - -;; - -var localeOptionHash = FC.locales = {}; // initialize and expose - - -// TODO: document the structure and ordering of a FullCalendar locale file - - -// Initialize jQuery UI datepicker translations while using some of the translations -// Will set this as the default locales for datepicker. -FC.datepickerLocale = function(localeCode, dpLocaleCode, dpOptions) { - - // get the FullCalendar internal option hash for this locale. create if necessary - var fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // transfer some simple options from datepicker to fc - fcOptions.isRTL = dpOptions.isRTL; - fcOptions.weekNumberTitle = dpOptions.weekHeader; - - // compute some more complex options from datepicker - $.each(dpComputableOptions, function(name, func) { - fcOptions[name] = func(dpOptions); - }); - - // is jQuery UI Datepicker is on the page? - if ($.datepicker) { - - // Register the locale data. - // FullCalendar and MomentJS use locale codes like "pt-br" but Datepicker - // does it like "pt-BR" or if it doesn't have the locale, maybe just "pt". - // Make an alias so the locale can be referenced either way. - $.datepicker.regional[dpLocaleCode] = - $.datepicker.regional[localeCode] = // alias - dpOptions; - - // Alias 'en' to the default locale data. Do this every time. - $.datepicker.regional.en = $.datepicker.regional['']; - - // Set as Datepicker's global defaults. - $.datepicker.setDefaults(dpOptions); - } -}; - - -// Sets FullCalendar-specific translations. Will set the locales as the global default. -FC.locale = function(localeCode, newFcOptions) { - var fcOptions; - var momOptions; - - // get the FullCalendar internal option hash for this locale. create if necessary - fcOptions = localeOptionHash[localeCode] || (localeOptionHash[localeCode] = {}); - - // provided new options for this locales? merge them in - if (newFcOptions) { - fcOptions = localeOptionHash[localeCode] = mergeOptions([ fcOptions, newFcOptions ]); - } - - // compute locale options that weren't defined. - // always do this. newFcOptions can be undefined when initializing from i18n file, - // so no way to tell if this is an initialization or a default-setting. - momOptions = getMomentLocaleData(localeCode); // will fall back to en - $.each(momComputableOptions, function(name, func) { - if (fcOptions[name] == null) { - fcOptions[name] = func(momOptions, fcOptions); - } - }); - - // set it as the default locale for FullCalendar - Calendar.defaults.locale = localeCode; -}; - - -// NOTE: can't guarantee any of these computations will run because not every locale has datepicker -// configs, so make sure there are English fallbacks for these in the defaults file. -var dpComputableOptions = { - - buttonText: function(dpOptions) { - return { - // the translations sometimes wrongly contain HTML entities - prev: stripHtmlEntities(dpOptions.prevText), - next: stripHtmlEntities(dpOptions.nextText), - today: stripHtmlEntities(dpOptions.currentText) - }; - }, - - // Produces format strings like "MMMM YYYY" -> "September 2014" - monthYearFormat: function(dpOptions) { - return dpOptions.showMonthAfterYear ? - 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : - 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; - } - -}; - -var momComputableOptions = { - - // Produces format strings like "ddd M/D" -> "Fri 9/15" - dayOfMonthFormat: function(momOptions, fcOptions) { - var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" - - // strip the year off the edge, as well as other misc non-whitespace chars - format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); - - if (fcOptions.isRTL) { - format += ' ddd'; // for RTL, add day-of-week to end - } - else { - format = 'ddd ' + format; // for LTR, add day-of-week to beginning - } - return format; - }, - - // Produces format strings like "h:mma" -> "6:00pm" - mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" - smallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" - extraSmallTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '(:mm)') - .replace(/(\Wmm)$/, '($1)') // like above, but for foreign locales - .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand - }, - - // Produces format strings like "ha" / "H" -> "6pm" / "18" - hourFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(':mm', '') - .replace(/(\Wmm)$/, '') // like above, but for foreign locales - .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand - }, - - // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) - noMeridiemTimeFormat: function(momOptions) { - return momOptions.longDateFormat('LT') - .replace(/\s*a$/i, ''); // remove trailing AM/PM - } - -}; - - -// options that should be computed off live calendar options (considers override options) -// TODO: best place for this? related to locale? -// TODO: flipping text based on isRTL is a bad idea because the CSS `direction` might want to handle it -var instanceComputableOptions = { - - // Produces format strings for results like "Mo 16" - smallDayDateFormat: function(options) { - return options.isRTL ? - 'D dd' : - 'dd D'; - }, - - // Produces format strings for results like "Wk 5" - weekFormat: function(options) { - return options.isRTL ? - 'w[ ' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ' ]w'; - }, - - // Produces format strings for results like "Wk5" - smallWeekFormat: function(options) { - return options.isRTL ? - 'w[' + options.weekNumberTitle + ']' : - '[' + options.weekNumberTitle + ']w'; - } - -}; - -// TODO: make these computable properties in optionsModel -function populateInstanceComputableOptions(options) { - $.each(instanceComputableOptions, function(name, func) { - if (options[name] == null) { - options[name] = func(options); - } - }); -} - - -// Returns moment's internal locale data. If doesn't exist, returns English. -function getMomentLocaleData(localeCode) { - return moment.localeData(localeCode) || moment.localeData('en'); -} - - -// Initialize English by forcing computation of moment-derived options. -// Also, sets it as the default. -FC.locale('en', Calendar.englishDefaults); - -;; - -FC.sourceNormalizers = []; -FC.sourceFetchers = []; - -var ajaxDefaults = { - dataType: 'json', - cache: false -}; - -var eventGUID = 1; - - -function EventManager() { // assumed to be a calendar - var t = this; - - - // exports - t.requestEvents = requestEvents; - t.reportEventChange = reportEventChange; - t.isFetchNeeded = isFetchNeeded; - t.fetchEvents = fetchEvents; - t.fetchEventSources = fetchEventSources; - t.refetchEvents = refetchEvents; - t.refetchEventSources = refetchEventSources; - t.getEventSources = getEventSources; - t.getEventSourceById = getEventSourceById; - t.addEventSource = addEventSource; - t.removeEventSource = removeEventSource; - t.removeEventSources = removeEventSources; - t.updateEvent = updateEvent; - t.updateEvents = updateEvents; - t.renderEvent = renderEvent; - t.renderEvents = renderEvents; - t.removeEvents = removeEvents; - t.clientEvents = clientEvents; - t.mutateEvent = mutateEvent; - t.normalizeEventDates = normalizeEventDates; - t.normalizeEventTimes = normalizeEventTimes; - - - // locals - var stickySource = { events: [] }; - var sources = [ stickySource ]; - var rangeStart, rangeEnd; - var pendingSourceCnt = 0; // outstanding fetch requests, max one per source - var cache = []; // holds events that have already been expanded - var prunedCache; // like cache, but only events that intersect with rangeStart/rangeEnd - - - $.each( - (t.opt('events') ? [ t.opt('events') ] : []).concat(t.opt('eventSources') || []), - function(i, sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - } - } - ); - - - - function requestEvents(start, end) { - if (!t.opt('lazyFetching') || isFetchNeeded(start, end)) { - return fetchEvents(start, end); - } - else { - return Promise.resolve(prunedCache); - } - } - - - function reportEventChange() { - prunedCache = filterEventsWithinRange(cache); - t.trigger('eventsReset', prunedCache); - } - - - function filterEventsWithinRange(events) { - var filteredEvents = []; - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - - if ( - event.start.clone().stripZone() < rangeEnd && - t.getEventEnd(event).stripZone() > rangeStart - ) { - filteredEvents.push(event); - } - } - - return filteredEvents; - } - - - t.getEventCache = function() { - return cache; - }; - - - - /* Fetching - -----------------------------------------------------------------------------*/ - - - // start and end are assumed to be unzoned - function isFetchNeeded(start, end) { - return !rangeStart || // nothing has been fetched yet? - start < rangeStart || end > rangeEnd; // is part of the new range outside of the old range? - } - - - function fetchEvents(start, end) { - rangeStart = start; - rangeEnd = end; - return refetchEvents(); - } - - - // poorly named. fetches all sources with current `rangeStart` and `rangeEnd`. - function refetchEvents() { - return fetchEventSources(sources, 'reset'); - } - - - // poorly named. fetches a subset of event sources. - function refetchEventSources(matchInputs) { - return fetchEventSources(getEventSourcesByMatchArray(matchInputs)); - } - - - // expects an array of event source objects (the originals, not copies) - // `specialFetchType` is an optimization parameter that affects purging of the event cache. - function fetchEventSources(specificSources, specialFetchType) { - var i, source; - - if (specialFetchType === 'reset') { - cache = []; - } - else if (specialFetchType !== 'add') { - cache = excludeEventsBySources(cache, specificSources); - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - - // already-pending sources have already been accounted for in pendingSourceCnt - if (source._status !== 'pending') { - pendingSourceCnt++; - } - - source._fetchId = (source._fetchId || 0) + 1; - source._status = 'pending'; - } - - for (i = 0; i < specificSources.length; i++) { - source = specificSources[i]; - tryFetchEventSource(source, source._fetchId); - } - - if (pendingSourceCnt) { - return Promise.construct(function(resolve) { - t.one('eventsReceived', resolve); // will send prunedCache - }); - } - else { // executed all synchronously, or no sources at all - return Promise.resolve(prunedCache); - } - } - - - // fetches an event source and processes its result ONLY if it is still the current fetch. - // caller is responsible for incrementing pendingSourceCnt first. - function tryFetchEventSource(source, fetchId) { - _fetchEventSource(source, function(eventInputs) { - var isArraySource = $.isArray(source.events); - var i, eventInput; - var abstractEvent; - - if ( - // is this the source's most recent fetch? - // if not, rely on an upcoming fetch of this source to decrement pendingSourceCnt - fetchId === source._fetchId && - // event source no longer valid? - source._status !== 'rejected' - ) { - source._status = 'resolved'; - - if (eventInputs) { - for (i = 0; i < eventInputs.length; i++) { - eventInput = eventInputs[i]; - - if (isArraySource) { // array sources have already been convert to Event Objects - abstractEvent = eventInput; - } - else { - abstractEvent = buildEventFromInput(eventInput, source); - } - - if (abstractEvent) { // not false (an invalid event) - cache.push.apply( // append - cache, - expandEvent(abstractEvent) // add individual expanded events to the cache - ); - } - } - } - - decrementPendingSourceCnt(); - } - }); - } - - - function rejectEventSource(source) { - var wasPending = source._status === 'pending'; - - source._status = 'rejected'; - - if (wasPending) { - decrementPendingSourceCnt(); - } - } - - - function decrementPendingSourceCnt() { - pendingSourceCnt--; - if (!pendingSourceCnt) { - reportEventChange(cache); // updates prunedCache - t.trigger('eventsReceived', prunedCache); - } - } - - - function _fetchEventSource(source, callback) { - var i; - var fetchers = FC.sourceFetchers; - var res; - - for (i=0; i<fetchers.length; i++) { - res = fetchers[i].call( - t, // this, the Calendar object - source, - rangeStart.clone(), - rangeEnd.clone(), - t.opt('timezone'), - callback - ); - - if (res === true) { - // the fetcher is in charge. made its own async request - return; - } - else if (typeof res == 'object') { - // the fetcher returned a new source. process it - _fetchEventSource(res, callback); - return; - } - } - - var events = source.events; - if (events) { - if ($.isFunction(events)) { - t.pushLoading(); - events.call( - t, // this, the Calendar object - rangeStart.clone(), - rangeEnd.clone(), - t.opt('timezone'), - function(events) { - callback(events); - t.popLoading(); - } - ); - } - else if ($.isArray(events)) { - callback(events); - } - else { - callback(); - } - }else{ - var url = source.url; - if (url) { - var success = source.success; - var error = source.error; - var complete = source.complete; - - // retrieve any outbound GET/POST $.ajax data from the options - var customData; - if ($.isFunction(source.data)) { - // supplied as a function that returns a key/value object - customData = source.data(); - } - else { - // supplied as a straight key/value object - customData = source.data; - } - - // use a copy of the custom data so we can modify the parameters - // and not affect the passed-in object. - var data = $.extend({}, customData || {}); - - var startParam = firstDefined(source.startParam, t.opt('startParam')); - var endParam = firstDefined(source.endParam, t.opt('endParam')); - var timezoneParam = firstDefined(source.timezoneParam, t.opt('timezoneParam')); - - if (startParam) { - data[startParam] = rangeStart.format(); - } - if (endParam) { - data[endParam] = rangeEnd.format(); - } - if (t.opt('timezone') && t.opt('timezone') != 'local') { - data[timezoneParam] = t.opt('timezone'); - } - - t.pushLoading(); - $.ajax($.extend({}, ajaxDefaults, source, { - data: data, - success: function(events) { - events = events || []; - var res = applyAll(success, this, arguments); - if ($.isArray(res)) { - events = res; - } - callback(events); - }, - error: function() { - applyAll(error, this, arguments); - callback(); - }, - complete: function() { - applyAll(complete, this, arguments); - t.popLoading(); - } - })); - }else{ - callback(); - } - } - } - - - - /* Sources - -----------------------------------------------------------------------------*/ - - - function addEventSource(sourceInput) { - var source = buildEventSource(sourceInput); - if (source) { - sources.push(source); - fetchEventSources([ source ], 'add'); // will eventually call reportEventChange - } - } - - - function buildEventSource(sourceInput) { // will return undefined if invalid source - var normalizers = FC.sourceNormalizers; - var source; - var i; - - if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { - source = { events: sourceInput }; - } - else if (typeof sourceInput === 'string') { - source = { url: sourceInput }; - } - else if (typeof sourceInput === 'object') { - source = $.extend({}, sourceInput); // shallow copy - } - - if (source) { - - // TODO: repeat code, same code for event classNames - if (source.className) { - if (typeof source.className === 'string') { - source.className = source.className.split(/\s+/); - } - // otherwise, assumed to be an array - } - else { - source.className = []; - } - - // for array sources, we convert to standard Event Objects up front - if ($.isArray(source.events)) { - source.origArray = source.events; // for removeEventSource - source.events = $.map(source.events, function(eventInput) { - return buildEventFromInput(eventInput, source); - }); - } - - for (i=0; i<normalizers.length; i++) { - normalizers[i].call(t, source); - } - - return source; - } - } - - - function removeEventSource(matchInput) { - removeSpecificEventSources( - getEventSourcesByMatch(matchInput) - ); - } - - - // if called with no arguments, removes all. - function removeEventSources(matchInputs) { - if (matchInputs == null) { - removeSpecificEventSources(sources, true); // isAll=true - } - else { - removeSpecificEventSources( - getEventSourcesByMatchArray(matchInputs) - ); - } - } - - - function removeSpecificEventSources(targetSources, isAll) { - var i; - - // cancel pending requests - for (i = 0; i < targetSources.length; i++) { - rejectEventSource(targetSources[i]); - } - - if (isAll) { // an optimization - sources = []; - cache = []; - } - else { - // remove from persisted source list - sources = $.grep(sources, function(source) { - for (i = 0; i < targetSources.length; i++) { - if (source === targetSources[i]) { - return false; // exclude - } - } - return true; // include - }); - - cache = excludeEventsBySources(cache, targetSources); - } - - reportEventChange(); - } - - - function getEventSources() { - return sources.slice(1); // returns a shallow copy of sources with stickySource removed - } - - - function getEventSourceById(id) { - return $.grep(sources, function(source) { - return source.id && source.id === id; - })[0]; - } - - - // like getEventSourcesByMatch, but accepts multple match criteria (like multiple IDs) - function getEventSourcesByMatchArray(matchInputs) { - - // coerce into an array - if (!matchInputs) { - matchInputs = []; - } - else if (!$.isArray(matchInputs)) { - matchInputs = [ matchInputs ]; - } - - var matchingSources = []; - var i; - - // resolve raw inputs to real event source objects - for (i = 0; i < matchInputs.length; i++) { - matchingSources.push.apply( // append - matchingSources, - getEventSourcesByMatch(matchInputs[i]) - ); - } - - return matchingSources; - } - - - // matchInput can either by a real event source object, an ID, or the function/URL for the source. - // returns an array of matching source objects. - function getEventSourcesByMatch(matchInput) { - var i, source; - - // given an proper event source object - for (i = 0; i < sources.length; i++) { - source = sources[i]; - if (source === matchInput) { - return [ source ]; - } - } - - // an ID match - source = getEventSourceById(matchInput); - if (source) { - return [ source ]; - } - - return $.grep(sources, function(source) { - return isSourcesEquivalent(matchInput, source); - }); - } - - - function isSourcesEquivalent(source1, source2) { - return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); - } - - - function getSourcePrimitive(source) { - return ( - (typeof source === 'object') ? // a normalized event source? - (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive - null - ) || - source; // the given argument *is* the primitive - } - - - // util - // returns a filtered array without events that are part of any of the given sources - function excludeEventsBySources(specificEvents, specificSources) { - return $.grep(specificEvents, function(event) { - for (var i = 0; i < specificSources.length; i++) { - if (event.source === specificSources[i]) { - return false; // exclude - } - } - return true; // keep - }); - } - - - - /* Manipulation - -----------------------------------------------------------------------------*/ - - - // Only ever called from the externally-facing API - function updateEvent(event) { - updateEvents([ event ]); - } - - - // Only ever called from the externally-facing API - function updateEvents(events) { - var i, event; - - for (i = 0; i < events.length; i++) { - event = events[i]; - - // massage start/end values, even if date string values - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - else { - event.end = null; - } - - mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization - } - - reportEventChange(); // reports event modifications (so we can redraw) - } - - - // Returns a hash of misc event properties that should be copied over to related events. - function getMiscEventProps(event) { - var props = {}; - - $.each(event, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined && isAtomic(val)) { // a defined non-object - props[name] = val; - } - } - }); - - return props; - } - - // non-date-related, non-id-related, non-secret - function isMiscEventPropName(name) { - return !/^_|^(id|allDay|start|end)$/.test(name); - } - - - // returns the expanded events that were created - function renderEvent(eventInput, stick) { - return renderEvents([ eventInput ], stick); - } - - - // returns the expanded events that were created - function renderEvents(eventInputs, stick) { - var renderedEvents = []; - var renderableEvents; - var abstractEvent; - var i, j, event; - - for (i = 0; i < eventInputs.length; i++) { - abstractEvent = buildEventFromInput(eventInputs[i]); - - if (abstractEvent) { // not false (a valid input) - renderableEvents = expandEvent(abstractEvent); - - for (j = 0; j < renderableEvents.length; j++) { - event = renderableEvents[j]; - - if (!event.source) { - if (stick) { - stickySource.events.push(event); - event.source = stickySource; - } - cache.push(event); - } - } - - renderedEvents = renderedEvents.concat(renderableEvents); - } - } - - if (renderedEvents.length) { // any new events rendered? - reportEventChange(); - } - - return renderedEvents; - } - - - function removeEvents(filter) { - var eventID; - var i; - - if (filter == null) { // null or undefined. remove all events - filter = function() { return true; }; // will always match - } - else if (!$.isFunction(filter)) { // an event ID - eventID = filter + ''; - filter = function(event) { - return event._id == eventID; - }; - } - - // Purge event(s) from our local cache - cache = $.grep(cache, filter, true); // inverse=true - - // Remove events from array sources. - // This works because they have been converted to official Event Objects up front. - // (and as a result, event._id has been calculated). - for (i=0; i<sources.length; i++) { - if ($.isArray(sources[i].events)) { - sources[i].events = $.grep(sources[i].events, filter, true); - } - } - - reportEventChange(); - } - - - function clientEvents(filter) { - if ($.isFunction(filter)) { - return $.grep(cache, filter); - } - else if (filter != null) { // not null, not undefined. an event ID - filter += ''; - return $.grep(cache, function(e) { - return e._id == filter; - }); - } - return cache; // else, return all - } - - - // Makes sure all array event sources have their internal event objects - // converted over to the Calendar's current timezone. - t.rezoneArrayEventSources = function() { - var i; - var events; - var j; - - for (i = 0; i < sources.length; i++) { - events = sources[i].events; - if ($.isArray(events)) { - - for (j = 0; j < events.length; j++) { - rezoneEventDates(events[j]); - } - } - } - }; - - function rezoneEventDates(event) { - event.start = t.moment(event.start); - if (event.end) { - event.end = t.moment(event.end); - } - backupEventDates(event); - } - - - /* Event Normalization - -----------------------------------------------------------------------------*/ - - - // Given a raw object with key/value properties, returns an "abstract" Event object. - // An "abstract" event is an event that, if recurring, will not have been expanded yet. - // Will return `false` when input is invalid. - // `source` is optional - function buildEventFromInput(input, source) { - var calendarEventDataTransform = t.opt('eventDataTransform'); - var out = {}; - var start, end; - var allDay; - - if (calendarEventDataTransform) { - input = calendarEventDataTransform(input); - } - if (source && source.eventDataTransform) { - input = source.eventDataTransform(input); - } - - // Copy all properties over to the resulting object. - // The special-case properties will be copied over afterwards. - $.extend(out, input); - - if (source) { - out.source = source; - } - - out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + ''); - - if (input.className) { - if (typeof input.className == 'string') { - out.className = input.className.split(/\s+/); - } - else { // assumed to be an array - out.className = input.className; - } - } - else { - out.className = []; - } - - start = input.start || input.date; // "date" is an alias for "start" - end = input.end; - - // parse as a time (Duration) if applicable - if (isTimeString(start)) { - start = moment.duration(start); - } - if (isTimeString(end)) { - end = moment.duration(end); - } - - if (input.dow || moment.isDuration(start) || moment.isDuration(end)) { - - // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet - out.start = start ? moment.duration(start) : null; // will be a Duration or null - out.end = end ? moment.duration(end) : null; // will be a Duration or null - out._recurring = true; // our internal marker - } - else { - - if (start) { - start = t.moment(start); - if (!start.isValid()) { - return false; - } - } - - if (end) { - end = t.moment(end); - if (!end.isValid()) { - end = null; // let defaults take over - } - } - - allDay = input.allDay; - if (allDay === undefined) { // still undefined? fallback to default - allDay = firstDefined( - source ? source.allDayDefault : undefined, - t.opt('allDayDefault') - ); - // still undefined? normalizeEventDates will calculate it - } - - assignDatesToEvent(start, end, allDay, out); - } - - t.normalizeEvent(out); // hook for external use. a prototype method - - return out; - } - t.buildEventFromInput = buildEventFromInput; - - - // Normalizes and assigns the given dates to the given partially-formed event object. - // NOTE: mutates the given start/end moments. does not make a copy. - function assignDatesToEvent(start, end, allDay, event) { - event.start = start; - event.end = end; - event.allDay = allDay; - normalizeEventDates(event); - backupEventDates(event); - } - - - // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. - // NOTE: Will modify the given object. - function normalizeEventDates(eventProps) { - - normalizeEventTimes(eventProps); - - if (eventProps.end && !eventProps.end.isAfter(eventProps.start)) { - eventProps.end = null; - } - - if (!eventProps.end) { - if (t.opt('forceEventDuration')) { - eventProps.end = t.getDefaultEventEnd(eventProps.allDay, eventProps.start); - } - else { - eventProps.end = null; - } - } - } - - - // Ensures the allDay property exists and the timeliness of the start/end dates are consistent - function normalizeEventTimes(eventProps) { - if (eventProps.allDay == null) { - eventProps.allDay = !(eventProps.start.hasTime() || (eventProps.end && eventProps.end.hasTime())); - } - - if (eventProps.allDay) { - eventProps.start.stripTime(); - if (eventProps.end) { - // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment - eventProps.end.stripTime(); - } - } - else { - if (!eventProps.start.hasTime()) { - eventProps.start = t.applyTimezone(eventProps.start.time(0)); // will assign a 00:00 time - } - if (eventProps.end && !eventProps.end.hasTime()) { - eventProps.end = t.applyTimezone(eventProps.end.time(0)); // will assign a 00:00 time - } - } - } - - - // If the given event is a recurring event, break it down into an array of individual instances. - // If not a recurring event, return an array with the single original event. - // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. - // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours). - function expandEvent(abstractEvent, _rangeStart, _rangeEnd) { - var events = []; - var dowHash; - var dow; - var i; - var date; - var startTime, endTime; - var start, end; - var event; - - _rangeStart = _rangeStart || rangeStart; - _rangeEnd = _rangeEnd || rangeEnd; - - if (abstractEvent) { - if (abstractEvent._recurring) { - - // make a boolean hash as to whether the event occurs on each day-of-week - if ((dow = abstractEvent.dow)) { - dowHash = {}; - for (i = 0; i < dow.length; i++) { - dowHash[dow[i]] = true; - } - } - - // iterate through every day in the current range - date = _rangeStart.clone().stripTime(); // holds the date of the current day - while (date.isBefore(_rangeEnd)) { - - if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week - - startTime = abstractEvent.start; // the stored start and end properties are times (Durations) - endTime = abstractEvent.end; // " - start = date.clone(); - end = null; - - if (startTime) { - start = start.time(startTime); - } - if (endTime) { - end = date.clone().time(endTime); - } - - event = $.extend({}, abstractEvent); // make a copy of the original - assignDatesToEvent( - start, end, - !startTime && !endTime, // allDay? - event - ); - events.push(event); - } - - date.add(1, 'days'); - } - } - else { - events.push(abstractEvent); // return the original event. will be a one-item array - } - } - - return events; - } - t.expandEvent = expandEvent; - - - - /* Event Modification Math - -----------------------------------------------------------------------------------------*/ - - - // Modifies an event and all related events by applying the given properties. - // Special date-diffing logic is used for manipulation of dates. - // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. - // All date comparisons are done against the event's pristine _start and _end dates. - // Returns an object with delta information and a function to undo all operations. - // For making computations in a granularity greater than day/time, specify largeUnit. - // NOTE: The given `newProps` might be mutated for normalization purposes. - function mutateEvent(event, newProps, largeUnit) { - var miscProps = {}; - var oldProps; - var clearEnd; - var startDelta; - var endDelta; - var durationDelta; - var undoFunc; - - // diffs the dates in the appropriate way, returning a duration - function diffDates(date1, date0) { // date1 - date0 - if (largeUnit) { - return diffByUnit(date1, date0, largeUnit); - } - else if (newProps.allDay) { - return diffDay(date1, date0); - } - else { - return diffDayTime(date1, date0); - } - } - - newProps = newProps || {}; - - // normalize new date-related properties - if (!newProps.start) { - newProps.start = event.start.clone(); - } - if (newProps.end === undefined) { - newProps.end = event.end ? event.end.clone() : null; - } - if (newProps.allDay == null) { // is null or undefined? - newProps.allDay = event.allDay; - } - normalizeEventDates(newProps); - - // create normalized versions of the original props to compare against - // need a real end value, for diffing - oldProps = { - start: event._start.clone(), - end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), - allDay: newProps.allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(oldProps); - - // need to clear the end date if explicitly changed to null - clearEnd = event._end !== null && newProps.end === null; - - // compute the delta for moving the start date - startDelta = diffDates(newProps.start, oldProps.start); - - // compute the delta for moving the end date - if (newProps.end) { - endDelta = diffDates(newProps.end, oldProps.end); - durationDelta = endDelta.subtract(startDelta); - } - else { - durationDelta = null; - } - - // gather all non-date-related properties - $.each(newProps, function(name, val) { - if (isMiscEventPropName(name)) { - if (val !== undefined) { - miscProps[name] = val; - } - } - }); - - // apply the operations to the event and all related events - undoFunc = mutateEvents( - clientEvents(event._id), // get events with this ID - clearEnd, - newProps.allDay, - startDelta, - durationDelta, - miscProps - ); - - return { - dateDelta: startDelta, - durationDelta: durationDelta, - undo: undoFunc - }; - } - - - // Modifies an array of events in the following ways (operations are in order): - // - clear the event's `end` - // - convert the event to allDay - // - add `dateDelta` to the start and end - // - add `durationDelta` to the event's duration - // - assign `miscProps` to the event - // - // Returns a function that can be called to undo all the operations. - // - // TODO: don't use so many closures. possible memory issues when lots of events with same ID. - // - function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) { - var isAmbigTimezone = t.getIsAmbigTimezone(); - var undoFunctions = []; - - // normalize zero-length deltas to be null - if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; } - if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; } - - $.each(events, function(i, event) { - var oldProps; - var newProps; - - // build an object holding all the old values, both date-related and misc. - // for the undo function. - oldProps = { - start: event.start.clone(), - end: event.end ? event.end.clone() : null, - allDay: event.allDay - }; - $.each(miscProps, function(name) { - oldProps[name] = event[name]; - }); - - // new date-related properties. work off the original date snapshot. - // ok to use references because they will be thrown away when backupEventDates is called. - newProps = { - start: event._start, - end: event._end, - allDay: allDay // normalize the dates in the same regard as the new properties - }; - normalizeEventDates(newProps); // massages start/end/allDay - - // strip or ensure the end date - if (clearEnd) { - newProps.end = null; - } - else if (durationDelta && !newProps.end) { // the duration translation requires an end date - newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); - } - - if (dateDelta) { - newProps.start.add(dateDelta); - if (newProps.end) { - newProps.end.add(dateDelta); - } - } - - if (durationDelta) { - newProps.end.add(durationDelta); // end already ensured above - } - - // if the dates have changed, and we know it is impossible to recompute the - // timezone offsets, strip the zone. - if ( - isAmbigTimezone && - !newProps.allDay && - (dateDelta || durationDelta) - ) { - newProps.start.stripZone(); - if (newProps.end) { - newProps.end.stripZone(); - } - } - - $.extend(event, miscProps, newProps); // copy over misc props, then date-related props - backupEventDates(event); // regenerate internal _start/_end/_allDay - - undoFunctions.push(function() { - $.extend(event, oldProps); - backupEventDates(event); // regenerate internal _start/_end/_allDay - }); - }); - - return function() { - for (var i = 0; i < undoFunctions.length; i++) { - undoFunctions[i](); - } - }; - } - -} - - -// returns an undo function -Calendar.prototype.mutateSeg = function(seg, newProps) { - return this.mutateEvent(seg.event, newProps); -}; - - -// hook for external libs to manipulate event properties upon creation. -// should manipulate the event in-place. -Calendar.prototype.normalizeEvent = function(event) { -}; - - -// Does the given span (start, end, and other location information) -// fully contain the other? -Calendar.prototype.spanContainsSpan = function(outerSpan, innerSpan) { - var eventStart = outerSpan.start.clone().stripZone(); - var eventEnd = this.getEventEnd(outerSpan).stripZone(); - - return innerSpan.start >= eventStart && innerSpan.end <= eventEnd; -}; - - -// Returns a list of events that the given event should be compared against when being considered for a move to -// the specified span. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. -Calendar.prototype.getPeerEvents = function(span, event) { - var cache = this.getEventCache(); - var peerEvents = []; - var i, otherEvent; - - for (i = 0; i < cache.length; i++) { - otherEvent = cache[i]; - if ( - !event || - event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events - ) { - peerEvents.push(otherEvent); - } - } - - return peerEvents; -}; - - -// updates the "backup" properties, which are preserved in order to compute diffs later on. -function backupEventDates(event) { - event._allDay = event.allDay; - event._start = event.start.clone(); - event._end = event.end ? event.end.clone() : null; -} - - -/* Overlapping / Constraining ------------------------------------------------------------------------------------------*/ - - -// Determines if the given event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isEventSpanAllowed = function(span, event) { - var source = event.source || {}; - var eventAllowFunc = this.opt('eventAllow'); - - var constraint = firstDefined( - event.constraint, - source.constraint, - this.opt('eventConstraint') - ); - - var overlap = firstDefined( - event.overlap, - source.overlap, - this.opt('eventOverlap') - ); - - return this.isSpanAllowed(span, constraint, overlap, event) && - (!eventAllowFunc || eventAllowFunc(span, event) !== false); -}; - - -// Determines if an external event can be relocated to the given span (unzoned start/end with other misc data) -Calendar.prototype.isExternalSpanAllowed = function(eventSpan, eventLocation, eventProps) { - var eventInput; - var event; - - // note: very similar logic is in View's reportExternalDrop - if (eventProps) { - eventInput = $.extend({}, eventProps, eventLocation); - event = this.expandEvent( - this.buildEventFromInput(eventInput) - )[0]; - } - - if (event) { - return this.isEventSpanAllowed(eventSpan, event); - } - else { // treat it as a selection - - return this.isSelectionSpanAllowed(eventSpan); - } -}; - - -// Determines the given span (unzoned start/end with other misc data) can be selected. -Calendar.prototype.isSelectionSpanAllowed = function(span) { - var selectAllowFunc = this.opt('selectAllow'); - - return this.isSpanAllowed(span, this.opt('selectConstraint'), this.opt('selectOverlap')) && - (!selectAllowFunc || selectAllowFunc(span) !== false); -}; - - -// Returns true if the given span (caused by an event drop/resize or a selection) is allowed to exist -// according to the constraint/overlap settings. -// `event` is not required if checking a selection. -Calendar.prototype.isSpanAllowed = function(span, constraint, overlap, event) { - var constraintEvents; - var anyContainment; - var peerEvents; - var i, peerEvent; - var peerOverlap; - - // the range must be fully contained by at least one of produced constraint events - if (constraint != null) { - - // not treated as an event! intermediate data structure - // TODO: use ranges in the future - constraintEvents = this.constraintToEvents(constraint); - if (constraintEvents) { // not invalid - - anyContainment = false; - for (i = 0; i < constraintEvents.length; i++) { - if (this.spanContainsSpan(constraintEvents[i], span)) { - anyContainment = true; - break; - } - } - - if (!anyContainment) { - return false; - } - } - } - - peerEvents = this.getPeerEvents(span, event); - - for (i = 0; i < peerEvents.length; i++) { - peerEvent = peerEvents[i]; - - // there needs to be an actual intersection before disallowing anything - if (this.eventIntersectsRange(peerEvent, span)) { - - // evaluate overlap for the given range and short-circuit if necessary - if (overlap === false) { - return false; - } - // if the event's overlap is a test function, pass the peer event in question as the first param - else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { - return false; - } - - // if we are computing if the given range is allowable for an event, consider the other event's - // EventObject-specific or Source-specific `overlap` property - if (event) { - peerOverlap = firstDefined( - peerEvent.overlap, - (peerEvent.source || {}).overlap - // we already considered the global `eventOverlap` - ); - if (peerOverlap === false) { - return false; - } - // if the peer event's overlap is a test function, pass the subject event as the first param - if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { - return false; - } - } - } - } - - return true; -}; - - -// Given an event input from the API, produces an array of event objects. Possible event inputs: -// 'businessHours' -// An event ID (number or string) -// An object with specific start/end dates or a recurring event (like what businessHours accepts) -Calendar.prototype.constraintToEvents = function(constraintInput) { - - if (constraintInput === 'businessHours') { - return this.getCurrentBusinessHourEvents(); - } - - if (typeof constraintInput === 'object') { - if (constraintInput.start != null) { // needs to be event-like input - return this.expandEvent(this.buildEventFromInput(constraintInput)); - } - else { - return null; // invalid - } - } - - return this.clientEvents(constraintInput); // probably an ID -}; - - -// Does the event's date range intersect with the given range? -// start/end already assumed to have stripped zones :( -Calendar.prototype.eventIntersectsRange = function(event, range) { - var eventStart = event.start.clone().stripZone(); - var eventEnd = this.getEventEnd(event).stripZone(); - - return range.start < eventEnd && range.end > eventStart; -}; - - -/* Business Hours ------------------------------------------------------------------------------------------*/ - -var BUSINESS_HOUR_EVENT_DEFAULTS = { - id: '_fcBusinessHours', // will relate events from different calls to expandEvent - start: '09:00', - end: '17:00', - dow: [ 1, 2, 3, 4, 5 ], // monday - friday - rendering: 'inverse-background' - // classNames are defined in businessHoursSegClasses -}; - -// Return events objects for business hours within the current view. -// Abuse of our event system :( -Calendar.prototype.getCurrentBusinessHourEvents = function(wholeDay) { - return this.computeBusinessHourEvents(wholeDay, this.opt('businessHours')); -}; - -// Given a raw input value from options, return events objects for business hours within the current view. -Calendar.prototype.computeBusinessHourEvents = function(wholeDay, input) { - if (input === true) { - return this.expandBusinessHourEvents(wholeDay, [ {} ]); - } - else if ($.isPlainObject(input)) { - return this.expandBusinessHourEvents(wholeDay, [ input ]); - } - else if ($.isArray(input)) { - return this.expandBusinessHourEvents(wholeDay, input, true); - } - else { - return []; - } -}; - -// inputs expected to be an array of objects. -// if ignoreNoDow is true, will ignore entries that don't specify a day-of-week (dow) key. -Calendar.prototype.expandBusinessHourEvents = function(wholeDay, inputs, ignoreNoDow) { - var view = this.getView(); - var events = []; - var i, input; - - for (i = 0; i < inputs.length; i++) { - input = inputs[i]; - - if (ignoreNoDow && !input.dow) { - continue; - } - - // give defaults. will make a copy - input = $.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, input); - - // if a whole-day series is requested, clear the start/end times - if (wholeDay) { - input.start = null; - input.end = null; - } - - events.push.apply(events, // append - this.expandEvent( - this.buildEventFromInput(input), - view.activeRange.start, - view.activeRange.end - ) - ); - } - - return events; -}; - -;; - -/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. -----------------------------------------------------------------------------------------------------------------------*/ -// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. -// It is responsible for managing width/height. - -var BasicView = FC.BasicView = View.extend({ - - scroller: null, - - dayGridClass: DayGrid, // class the dayGrid will be instantiated from (overridable by subclasses) - dayGrid: null, // the main subcomponent that does most of the heavy lifting - - dayNumbersVisible: false, // display day numbers on each day cell? - colWeekNumbersVisible: false, // display week numbers along the side? - cellWeekNumbersVisible: false, // display week numbers in day cell? - - weekNumberWidth: null, // width of all the week-number cells running down the side - - headContainerEl: null, // div that hold's the dayGrid's rendered date header - headRowEl: null, // the fake row element of the day-of-week header - - - initialize: function() { - this.dayGrid = this.instantiateDayGrid(); - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // Generates the DayGrid object this view needs. Draws from this.dayGridClass - instantiateDayGrid: function() { - // generate a subclass on the fly with BasicView-specific behavior - // TODO: cache this subclass - var subclass = this.dayGridClass.extend(basicDayGridMethods); - - return new subclass(this); - }, - - - // Computes the date range that will be rendered. - buildRenderRange: function(currentRange, currentRangeUnit) { - var renderRange = View.prototype.buildRenderRange.apply(this, arguments); - - // year and month views should be aligned with weeks. this is already done for week - if (/^(year|month)$/.test(currentRangeUnit)) { - renderRange.start.startOf('week'); - - // make end-of-week if not already - if (renderRange.end.weekday()) { - renderRange.end.add(1, 'week').startOf('week'); // exclusively move backwards - } - } - - return this.trimHiddenDays(renderRange); - }, - - - // Renders the view into `this.el`, which should already be assigned - renderDates: function() { - - this.dayGrid.breakOnWeeks = /year|month|week/.test(this.currentRangeUnit); // do before Grid::setRange - this.dayGrid.setRange(this.renderRange); - - this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible - if (this.opt('weekNumbers')) { - if (this.opt('weekNumbersWithinDays')) { - this.cellWeekNumbersVisible = true; - this.colWeekNumbersVisible = false; - } - else { - this.cellWeekNumbersVisible = false; - this.colWeekNumbersVisible = true; - }; - } - this.dayGrid.numbersVisible = this.dayNumbersVisible || - this.cellWeekNumbersVisible || this.colWeekNumbersVisible; - - this.el.addClass('fc-basic-view').html(this.renderSkeletonHtml()); - this.renderHead(); - - this.scroller.render(); - var dayGridContainerEl = this.scroller.el.addClass('fc-day-grid-container'); - var dayGridEl = $('<div class="fc-day-grid" />').appendTo(dayGridContainerEl); - this.el.find('.fc-body > tr > td').append(dayGridContainerEl); - - this.dayGrid.setElement(dayGridEl); - this.dayGrid.renderDates(this.hasRigidRows()); - }, - - - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.dayGrid.renderHeadHtml()); - this.headRowEl = this.headContainerEl.find('.fc-row'); - }, - - - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill the dayGrid's rendering. - unrenderDates: function() { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - this.scroller.destroy(); - }, - - - renderBusinessHours: function() { - this.dayGrid.renderBusinessHours(); - }, - - - unrenderBusinessHours: function() { - this.dayGrid.unrenderBusinessHours(); - }, - - - // Builds the HTML skeleton for the view. - // The day-grid component will render inside of a container defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '"></td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // Generates an HTML attribute string for setting the width of the week number column, if it is known - weekNumberStyleAttr: function() { - if (this.weekNumberWidth !== null) { - return 'style="width:' + this.weekNumberWidth + 'px"'; - } - return ''; - }, - - - // Determines whether each row should have a constant height - hasRigidRows: function() { - var eventLimit = this.opt('eventLimit'); - return eventLimit && typeof eventLimit !== 'number'; - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - if (this.colWeekNumbersVisible) { - // Make sure all week number cells running down the side have the same width. - // Record the width for cells created later. - this.weekNumberWidth = matchCellWidths( - this.el.find('.fc-week-number') - ); - } - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit = this.opt('eventLimit'); - var scrollerHeight; - var scrollbarWidths; - - // reset all heights to be natural - this.scroller.clear(); - uncompensateScroll(this.headRowEl); - - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - - // is the event limit a constant level number? - if (eventLimit && typeof eventLimit === 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after - } - - // distribute the height to the rows - // (totalHeight is a "recommended" value if isAuto) - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.setGridHeight(scrollerHeight, isAuto); - - // is the event limit dynamically calculated? - if (eventLimit && typeof eventLimit !== 'number') { - this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set - } - - if (!isAuto) { // should we force dimensions of the scroll container? - - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - compensateScroll(this.headRowEl, scrollbarWidths); - - // doing the scrollbar compensation might have created text overflow which created more height. redo - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } - - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); - } - }, - - - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - - // Sets the height of just the DayGrid component in this view - setGridHeight: function(height, isAuto) { - if (isAuto) { - undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding - } - else { - distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows - } - }, - - - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ - - - computeInitialDateScroll: function() { - return { top: 0 }; - }, - - - queryDateScroll: function() { - return { top: this.scroller.getScrollTop() }; - }, - - - applyDateScroll: function(scroll) { - if (scroll.top !== undefined) { - this.scroller.setScrollTop(scroll.top); - } - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to dayGrid - - - hitsNeeded: function() { - this.dayGrid.hitsNeeded(); - }, - - - hitsNotNeeded: function() { - this.dayGrid.hitsNotNeeded(); - }, - - - prepareHits: function() { - this.dayGrid.prepareHits(); - }, - - - releaseHits: function() { - this.dayGrid.releaseHits(); - }, - - - queryHit: function(left, top) { - return this.dayGrid.queryHit(left, top); - }, - - - getHitSpan: function(hit) { - return this.dayGrid.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - return this.dayGrid.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the given events onto the view and populates the segments array - renderEvents: function(events) { - this.dayGrid.renderEvents(events); - - this.updateHeight(); // must compensate for events that overflow the row - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.dayGrid.getEventSegs(); - }, - - - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - this.dayGrid.unrenderEvents(); - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for both events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - return this.dayGrid.renderDrag(dropLocation, seg); - }, - - - unrenderDrag: function() { - this.dayGrid.unrenderDrag(); - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(span) { - this.dayGrid.renderSelection(span); - }, - - - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.dayGrid.unrenderSelection(); - } - -}); - - -// Methods that will customize the rendering behavior of the BasicView's dayGrid -var basicDayGridMethods = { - - - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '' + - '<th class="fc-week-number ' + view.widgetHeaderClass + '" ' + view.weekNumberStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - htmlEscape(view.opt('weekNumberTitle')) + - '</span>' + - '</th>'; - } - - return ''; - }, - - - // Generates the HTML that will go before content-skeleton cells that display the day/week numbers - renderNumberIntroHtml: function(row) { - var view = this.view; - var weekStart = this.getCellDate(row, 0); - - if (view.colWeekNumbersVisible) { - return '' + - '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: weekStart, type: 'week', forceOff: this.colCnt === 1 }, - weekStart.format('w') // inner HTML - ) + - '</td>'; - } - - return ''; - }, - - - // Generates the HTML that goes before the day bg cells for each day-row - renderBgIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '<td class="fc-week-number ' + view.widgetContentClass + '" ' + - view.weekNumberStyleAttr() + '></td>'; - } - - return ''; - }, - - - // Generates the HTML that goes before every other type of row generated by DayGrid. - // Affects helper-skeleton and highlight-skeleton rows. - renderIntroHtml: function() { - var view = this.view; - - if (view.colWeekNumbersVisible) { - return '<td class="fc-week-number" ' + view.weekNumberStyleAttr() + '></td>'; - } - - return ''; - } - -}; - -;; - -/* A month view with day cells running in rows (one-per-week) and columns -----------------------------------------------------------------------------------------------------------------------*/ - -var MonthView = FC.MonthView = BasicView.extend({ - - - // Computes the date range that will be rendered. - buildRenderRange: function() { - var renderRange = BasicView.prototype.buildRenderRange.apply(this, arguments); - var rowCnt; - - // ensure 6 weeks - if (this.isFixedWeeks()) { - rowCnt = Math.ceil( // could be partial weeks due to hiddenDays - renderRange.end.diff(renderRange.start, 'weeks', true) // dontRound=true - ); - renderRange.end.add(6 - rowCnt, 'weeks'); - } - - return renderRange; - }, - - - // Overrides the default BasicView behavior to have special multi-week auto-height logic - setGridHeight: function(height, isAuto) { - - // if auto, make the height of each row the height that it would be if there were 6 weeks - if (isAuto) { - height *= this.rowCnt / 6; - } - - distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows - }, - - - isFixedWeeks: function() { - return this.opt('fixedWeekCount'); - } - -}); - -;; - -fcViews.basic = { - 'class': BasicView -}; - -fcViews.basicDay = { - type: 'basic', - duration: { days: 1 } -}; - -fcViews.basicWeek = { - type: 'basic', - duration: { weeks: 1 } -}; - -fcViews.month = { - 'class': MonthView, - duration: { months: 1 }, // important for prev/next - defaults: { - fixedWeekCount: true - } -}; -;; - -/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. -----------------------------------------------------------------------------------------------------------------------*/ -// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). -// Responsible for managing width/height. - -var AgendaView = FC.AgendaView = View.extend({ - - scroller: null, - - timeGridClass: TimeGrid, // class used to instantiate the timeGrid. subclasses can override - timeGrid: null, // the main time-grid subcomponent of this view - - dayGridClass: DayGrid, // class used to instantiate the dayGrid. subclasses can override - dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null - - axisWidth: null, // the width of the time axis running down the side - - headContainerEl: null, // div that hold's the timeGrid's rendered date header - noScrollRowEls: null, // set of fake row elements that must compensate when scroller has scrollbars - - // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath - bottomRuleEl: null, - - // indicates that minTime/maxTime affects rendering - usesMinMaxTime: true, - - - initialize: function() { - this.timeGrid = this.instantiateTimeGrid(); - - if (this.opt('allDaySlot')) { // should we display the "all-day" area? - this.dayGrid = this.instantiateDayGrid(); // the all-day subcomponent of this view - } - - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - - // Instantiates the TimeGrid object this view needs. Draws from this.timeGridClass - instantiateTimeGrid: function() { - var subclass = this.timeGridClass.extend(agendaTimeGridMethods); - - return new subclass(this); - }, - - - // Instantiates the DayGrid object this view might need. Draws from this.dayGridClass - instantiateDayGrid: function() { - var subclass = this.dayGridClass.extend(agendaDayGridMethods); - - return new subclass(this); - }, - - - /* Rendering - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders the view into `this.el`, which has already been assigned - renderDates: function() { - - this.timeGrid.setRange(this.renderRange); - - if (this.dayGrid) { - this.dayGrid.setRange(this.renderRange); - } - - this.el.addClass('fc-agenda-view').html(this.renderSkeletonHtml()); - this.renderHead(); - - this.scroller.render(); - var timeGridWrapEl = this.scroller.el.addClass('fc-time-grid-container'); - var timeGridEl = $('<div class="fc-time-grid" />').appendTo(timeGridWrapEl); - this.el.find('.fc-body > tr > td').append(timeGridWrapEl); - - this.timeGrid.setElement(timeGridEl); - this.timeGrid.renderDates(); - - // the <hr> that sometimes displays under the time-grid - this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>') - .appendTo(this.timeGrid.el); // inject it into the time-grid - - if (this.dayGrid) { - this.dayGrid.setElement(this.el.find('.fc-day-grid')); - this.dayGrid.renderDates(); - - // have the day-grid extend it's coordinate area over the <hr> dividing the two grids - this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); - } - - this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller - }, - - - // render the day-of-week headers - renderHead: function() { - this.headContainerEl = - this.el.find('.fc-head-container') - .html(this.timeGrid.renderHeadHtml()); - }, - - - // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, - // always completely kill each grid's rendering. - unrenderDates: function() { - this.timeGrid.unrenderDates(); - this.timeGrid.removeElement(); - - if (this.dayGrid) { - this.dayGrid.unrenderDates(); - this.dayGrid.removeElement(); - } - - this.scroller.destroy(); - }, - - - // Builds the HTML skeleton for the view. - // The day-grid and time-grid components will render inside containers defined by this HTML. - renderSkeletonHtml: function() { - return '' + - '<table>' + - '<thead class="fc-head">' + - '<tr>' + - '<td class="fc-head-container ' + this.widgetHeaderClass + '"></td>' + - '</tr>' + - '</thead>' + - '<tbody class="fc-body">' + - '<tr>' + - '<td class="' + this.widgetContentClass + '">' + - (this.dayGrid ? - '<div class="fc-day-grid"/>' + - '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' : - '' - ) + - '</td>' + - '</tr>' + - '</tbody>' + - '</table>'; - }, - - - // Generates an HTML attribute string for setting the width of the axis, if it is known - axisStyleAttr: function() { - if (this.axisWidth !== null) { - return 'style="width:' + this.axisWidth + 'px"'; - } - return ''; - }, - - - /* Business Hours - ------------------------------------------------------------------------------------------------------------------*/ - - - renderBusinessHours: function() { - this.timeGrid.renderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.renderBusinessHours(); - } - }, - - - unrenderBusinessHours: function() { - this.timeGrid.unrenderBusinessHours(); - - if (this.dayGrid) { - this.dayGrid.unrenderBusinessHours(); - } - }, - - - /* Now Indicator - ------------------------------------------------------------------------------------------------------------------*/ - - - getNowIndicatorUnit: function() { - return this.timeGrid.getNowIndicatorUnit(); - }, - - - renderNowIndicator: function(date) { - this.timeGrid.renderNowIndicator(date); - }, - - - unrenderNowIndicator: function() { - this.timeGrid.unrenderNowIndicator(); - }, - - - /* Dimensions - ------------------------------------------------------------------------------------------------------------------*/ - - - updateSize: function(isResize) { - this.timeGrid.updateSize(isResize); - - View.prototype.updateSize.call(this, isResize); // call the super-method - }, - - - // Refreshes the horizontal dimensions of the view - updateWidth: function() { - // make all axis cells line up, and record the width so newly created axis cells will have it - this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); - }, - - - // Adjusts the vertical dimensions of the view to the specified values - setHeight: function(totalHeight, isAuto) { - var eventLimit; - var scrollerHeight; - var scrollbarWidths; - - // reset all dimensions back to the original state - this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary - this.scroller.clear(); // sets height to 'auto' and clears overflow - uncompensateScroll(this.noScrollRowEls); - - // limit number of events in the all-day area - if (this.dayGrid) { - this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed - - eventLimit = this.opt('eventLimit'); - if (eventLimit && typeof eventLimit !== 'number') { - eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number - } - if (eventLimit) { - this.dayGrid.limitRows(eventLimit); - } - } - - if (!isAuto) { // should we force dimensions of the scroll container? - - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - scrollbarWidths = this.scroller.getScrollbarWidths(); - - if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? - - // make the all-day and header rows lines up - compensateScroll(this.noScrollRowEls, scrollbarWidths); - - // the scrollbar compensation might have changed text flow, which might affect height, so recalculate - // and reapply the desired height to the scroller. - scrollerHeight = this.computeScrollerHeight(totalHeight); - this.scroller.setHeight(scrollerHeight); - } - - // guarantees the same scrollbar widths - this.scroller.lockOverflow(scrollbarWidths); - - // if there's any space below the slats, show the horizontal rule. - // this won't cause any new overflow, because lockOverflow already called. - if (this.timeGrid.getTotalSlatHeight() < scrollerHeight) { - this.bottomRuleEl.show(); - } - } - }, - - - // given a desired total height of the view, returns what the height of the scroller should be - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - - /* Scroll - ------------------------------------------------------------------------------------------------------------------*/ - - - // Computes the initial pre-configured scroll state prior to allowing the user to change it - computeInitialDateScroll: function() { - var scrollTime = moment.duration(this.opt('scrollTime')); - var top = this.timeGrid.computeTimeTop(scrollTime); - - // zoom can give weird floating-point values. rather scroll a little bit further - top = Math.ceil(top); - - if (top) { - top++; // to overcome top border that slots beyond the first have. looks better - } - - return { top: top }; - }, - - - queryDateScroll: function() { - return { top: this.scroller.getScrollTop() }; - }, - - - applyDateScroll: function(scroll) { - if (scroll.top !== undefined) { - this.scroller.setScrollTop(scroll.top); - } - }, - - - /* Hit Areas - ------------------------------------------------------------------------------------------------------------------*/ - // forward all hit-related method calls to the grids (dayGrid might not be defined) - - - hitsNeeded: function() { - this.timeGrid.hitsNeeded(); - if (this.dayGrid) { - this.dayGrid.hitsNeeded(); - } - }, - - - hitsNotNeeded: function() { - this.timeGrid.hitsNotNeeded(); - if (this.dayGrid) { - this.dayGrid.hitsNotNeeded(); - } - }, - - - prepareHits: function() { - this.timeGrid.prepareHits(); - if (this.dayGrid) { - this.dayGrid.prepareHits(); - } - }, - - - releaseHits: function() { - this.timeGrid.releaseHits(); - if (this.dayGrid) { - this.dayGrid.releaseHits(); - } - }, - - - queryHit: function(left, top) { - var hit = this.timeGrid.queryHit(left, top); - - if (!hit && this.dayGrid) { - hit = this.dayGrid.queryHit(left, top); - } - - return hit; - }, - - - getHitSpan: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitSpan(hit); - }, - - - getHitEl: function(hit) { - // TODO: hit.component is set as a hack to identify where the hit came from - return hit.component.getHitEl(hit); - }, - - - /* Events - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders events onto the view and populates the View's segment array - renderEvents: function(events) { - var dayEvents = []; - var timedEvents = []; - var daySegs = []; - var timedSegs; - var i; - - // separate the events into all-day and timed - for (i = 0; i < events.length; i++) { - if (events[i].allDay) { - dayEvents.push(events[i]); - } - else { - timedEvents.push(events[i]); - } - } - - // render the events in the subcomponents - timedSegs = this.timeGrid.renderEvents(timedEvents); - if (this.dayGrid) { - daySegs = this.dayGrid.renderEvents(dayEvents); - } - - // the all-day area is flexible and might have a lot of events, so shift the height - this.updateHeight(); - }, - - - // Retrieves all segment objects that are rendered in the view - getEventSegs: function() { - return this.timeGrid.getEventSegs().concat( - this.dayGrid ? this.dayGrid.getEventSegs() : [] - ); - }, - - - // Unrenders all event elements and clears internal segment data - unrenderEvents: function() { - - // unrender the events in the subcomponents - this.timeGrid.unrenderEvents(); - if (this.dayGrid) { - this.dayGrid.unrenderEvents(); - } - - // we DON'T need to call updateHeight() because - // a renderEvents() call always happens after this, which will eventually call updateHeight() - }, - - - /* Dragging (for events and external elements) - ------------------------------------------------------------------------------------------------------------------*/ - - - // A returned value of `true` signals that a mock "helper" event has been rendered. - renderDrag: function(dropLocation, seg) { - if (dropLocation.start.hasTime()) { - return this.timeGrid.renderDrag(dropLocation, seg); - } - else if (this.dayGrid) { - return this.dayGrid.renderDrag(dropLocation, seg); - } - }, - - - unrenderDrag: function() { - this.timeGrid.unrenderDrag(); - if (this.dayGrid) { - this.dayGrid.unrenderDrag(); - } - }, - - - /* Selection - ------------------------------------------------------------------------------------------------------------------*/ - - - // Renders a visual indication of a selection - renderSelection: function(span) { - if (span.start.hasTime() || span.end.hasTime()) { - this.timeGrid.renderSelection(span); - } - else if (this.dayGrid) { - this.dayGrid.renderSelection(span); - } - }, - - - // Unrenders a visual indications of a selection - unrenderSelection: function() { - this.timeGrid.unrenderSelection(); - if (this.dayGrid) { - this.dayGrid.unrenderSelection(); - } - } - -}); - - -// Methods that will customize the rendering behavior of the AgendaView's timeGrid -// TODO: move into TimeGrid -var agendaTimeGridMethods = { - - - // Generates the HTML that will go before the day-of week header cells - renderHeadIntroHtml: function() { - var view = this.view; - var weekText; - - if (view.opt('weekNumbers')) { - weekText = this.start.format(view.opt('smallWeekFormat')); - - return '' + - '<th class="fc-axis fc-week-number ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '>' + - view.buildGotoAnchorHtml( // aside from link, important for matchCellWidths - { date: this.start, type: 'week', forceOff: this.colCnt > 1 }, - htmlEscape(weekText) // inner HTML - ) + - '</th>'; - } - else { - return '<th class="fc-axis ' + view.widgetHeaderClass + '" ' + view.axisStyleAttr() + '></th>'; - } - }, - - - // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. - renderBgIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '></td>'; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - - -// Methods that will customize the rendering behavior of the AgendaView's dayGrid -var agendaDayGridMethods = { - - - // Generates the HTML that goes before the all-day cells - renderBgIntroHtml: function() { - var view = this.view; - - return '' + - '<td class="fc-axis ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + - '<span>' + // needed for matchCellWidths - view.getAllDayHtml() + - '</span>' + - '</td>'; - }, - - - // Generates the HTML that goes before all other types of cells. - // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. - renderIntroHtml: function() { - var view = this.view; - - return '<td class="fc-axis" ' + view.axisStyleAttr() + '></td>'; - } - -}; - -;; - -var AGENDA_ALL_DAY_EVENT_LIMIT = 5; - -// potential nice values for the slot-duration and interval-duration -// from largest to smallest -var AGENDA_STOCK_SUB_DURATIONS = [ - { hours: 1 }, - { minutes: 30 }, - { minutes: 15 }, - { seconds: 30 }, - { seconds: 15 } -]; - -fcViews.agenda = { - 'class': AgendaView, - defaults: { - allDaySlot: true, - slotDuration: '00:30:00', - slotEventOverlap: true // a bad name. confused with overlap/constraint system - } -}; - -fcViews.agendaDay = { - type: 'agenda', - duration: { days: 1 } -}; - -fcViews.agendaWeek = { - type: 'agenda', - duration: { weeks: 1 } -}; -;; - -/* -Responsible for the scroller, and forwarding event-related actions into the "grid" -*/ -var ListView = View.extend({ - - grid: null, - scroller: null, - - initialize: function() { - this.grid = new ListViewGrid(this); - this.scroller = new Scroller({ - overflowX: 'hidden', - overflowY: 'auto' - }); - }, - - renderSkeleton: function() { - this.el.addClass( - 'fc-list-view ' + - this.widgetContentClass - ); - - this.scroller.render(); - this.scroller.el.appendTo(this.el); - - this.grid.setElement(this.scroller.scrollEl); - }, - - unrenderSkeleton: function() { - this.scroller.destroy(); // will remove the Grid too - }, - - setHeight: function(totalHeight, isAuto) { - this.scroller.setHeight(this.computeScrollerHeight(totalHeight)); - }, - - computeScrollerHeight: function(totalHeight) { - return totalHeight - - subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller - }, - - renderDates: function() { - this.grid.setRange(this.renderRange); // needs to process range-related options - }, - - renderEvents: function(events) { - this.grid.renderEvents(events); - }, - - unrenderEvents: function() { - this.grid.unrenderEvents(); - }, - - isEventResizable: function(event) { - return false; - }, - - isEventDraggable: function(event) { - return false; - } - -}); - -/* -Responsible for event rendering and user-interaction. -Its "el" is the inner-content of the above view's scroller. -*/ -var ListViewGrid = Grid.extend({ - - segSelector: '.fc-list-item', // which elements accept event actions - hasDayInteractions: false, // no day selection or day clicking - - // slices by day - spanToSegs: function(span) { - var view = this.view; - var dayStart = view.renderRange.start.clone().time(0); // timed, so segs get times! - var dayIndex = 0; - var seg; - var segs = []; - - while (dayStart < view.renderRange.end) { - - seg = intersectRanges(span, { - start: dayStart, - end: dayStart.clone().add(1, 'day') - }); - - if (seg) { - seg.dayIndex = dayIndex; - segs.push(seg); - } - - dayStart.add(1, 'day'); - dayIndex++; - - // detect when span won't go fully into the next day, - // and mutate the latest seg to the be the end. - if ( - seg && !seg.isEnd && span.end.hasTime() && - span.end < dayStart.clone().add(this.view.nextDayThreshold) - ) { - seg.end = span.end.clone(); - seg.isEnd = true; - break; - } - } - - return segs; - }, - - // like "4:00am" - computeEventTimeFormat: function() { - return this.view.opt('mediumTimeFormat'); - }, - - // for events with a url, the whole <tr> should be clickable, - // but it's impossible to wrap with an <a> tag. simulate this. - handleSegClick: function(seg, ev) { - var url; - - Grid.prototype.handleSegClick.apply(this, arguments); // super. might prevent the default action - - // not clicking on or within an <a> with an href - if (!$(ev.target).closest('a[href]').length) { - url = seg.event.url; - if (url && !ev.isDefaultPrevented()) { // jsEvent not cancelled in handler - window.location.href = url; // simulate link click - } - } - }, - - // returns list of foreground segs that were actually rendered - renderFgSegs: function(segs) { - segs = this.renderFgSegEls(segs); // might filter away hidden events - - if (!segs.length) { - this.renderEmptyMessage(); - } - else { - this.renderSegList(segs); - } - - return segs; - }, - - renderEmptyMessage: function() { - this.el.html( - '<div class="fc-list-empty-wrap2">' + // TODO: try less wraps - '<div class="fc-list-empty-wrap1">' + - '<div class="fc-list-empty">' + - htmlEscape(this.view.opt('noEventsMessage')) + - '</div>' + - '</div>' + - '</div>' - ); - }, - - // render the event segments in the view - renderSegList: function(allSegs) { - var segsByDay = this.groupSegsByDay(allSegs); // sparse array - var dayIndex; - var daySegs; - var i; - var tableEl = $('<table class="fc-list-table"><tbody/></table>'); - var tbodyEl = tableEl.find('tbody'); - - for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { - daySegs = segsByDay[dayIndex]; - if (daySegs) { // sparse array, so might be undefined - - // append a day header - tbodyEl.append(this.dayHeaderHtml( - this.view.renderRange.start.clone().add(dayIndex, 'days') - )); - - this.sortEventSegs(daySegs); - - for (i = 0; i < daySegs.length; i++) { - tbodyEl.append(daySegs[i].el); // append event row - } - } - } - - this.el.empty().append(tableEl); - }, - - // Returns a sparse array of arrays, segs grouped by their dayIndex - groupSegsByDay: function(segs) { - var segsByDay = []; // sparse array - var i, seg; - - for (i = 0; i < segs.length; i++) { - seg = segs[i]; - (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) - .push(seg); - } - - return segsByDay; - }, - - // generates the HTML for the day headers that live amongst the event rows - dayHeaderHtml: function(dayDate) { - var view = this.view; - var mainFormat = view.opt('listDayFormat'); - var altFormat = view.opt('listDayAltFormat'); - - return '<tr class="fc-list-heading" data-date="' + dayDate.format('YYYY-MM-DD') + '">' + - '<td class="' + view.widgetHeaderClass + '" colspan="3">' + - (mainFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-main' }, - htmlEscape(dayDate.format(mainFormat)) // inner HTML - ) : - '') + - (altFormat ? - view.buildGotoAnchorHtml( - dayDate, - { 'class': 'fc-list-heading-alt' }, - htmlEscape(dayDate.format(altFormat)) // inner HTML - ) : - '') + - '</td>' + - '</tr>'; - }, - - // generates the HTML for a single event row - fgSegHtml: function(seg) { - var view = this.view; - var classes = [ 'fc-list-item' ].concat(this.getSegCustomClasses(seg)); - var bgColor = this.getSegBackgroundColor(seg); - var event = seg.event; - var url = event.url; - var timeHtml; - - if (event.allDay) { - timeHtml = view.getAllDayHtml(); - } - else if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day - if (seg.isStart || seg.isEnd) { // outer segment that probably lasts part of the day - timeHtml = htmlEscape(this.getEventTimeText(seg)); - } - else { // inner segment that lasts the whole day - timeHtml = view.getAllDayHtml(); - } - } - else { - // Display the normal time text for the *event's* times - timeHtml = htmlEscape(this.getEventTimeText(event)); - } - - if (url) { - classes.push('fc-has-url'); - } - - return '<tr class="' + classes.join(' ') + '">' + - (this.displayEventTime ? - '<td class="fc-list-item-time ' + view.widgetContentClass + '">' + - (timeHtml || '') + - '</td>' : - '') + - '<td class="fc-list-item-marker ' + view.widgetContentClass + '">' + - '<span class="fc-event-dot"' + - (bgColor ? - ' style="background-color:' + bgColor + '"' : - '') + - '></span>' + - '</td>' + - '<td class="fc-list-item-title ' + view.widgetContentClass + '">' + - '<a' + (url ? ' href="' + htmlEscape(url) + '"' : '') + '>' + - htmlEscape(seg.event.title || '') + - '</a>' + - '</td>' + - '</tr>'; - } - -}); - -;; - -fcViews.list = { - 'class': ListView, - buttonTextKey: 'list', // what to lookup in locale files - defaults: { - buttonText: 'list', // text to display for English - listDayFormat: 'LL', // like "January 1, 2016" - noEventsMessage: 'No events to display' - } -}; - -fcViews.listDay = { - type: 'list', - duration: { days: 1 }, - defaults: { - listDayFormat: 'dddd' // day-of-week is all we need. full date is probably in header - } -}; - -fcViews.listWeek = { - type: 'list', - duration: { weeks: 1 }, - defaults: { - listDayFormat: 'dddd', // day-of-week is more important - listDayAltFormat: 'LL' - } -}; - -fcViews.listMonth = { - type: 'list', - duration: { month: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -fcViews.listYear = { - type: 'list', - duration: { year: 1 }, - defaults: { - listDayAltFormat: 'dddd' // day-of-week is nice-to-have - } -}; - -;; - -return FC; // export for Node/CommonJS -}); \ No newline at end of file diff --git a/src/UI/JsLibraries/handlebars.helpers.js b/src/UI/JsLibraries/handlebars.helpers.js deleted file mode 100644 index 56df9b642..000000000 --- a/src/UI/JsLibraries/handlebars.helpers.js +++ /dev/null @@ -1,145 +0,0 @@ -/* Handlebars Helpers - Dan Harper (http://github.com/danharper) */ - -/* This program is free software. It comes without any warranty, to - * the extent permitted by applicable law. You can redistribute it - * and/or modify it under the terms of the Do What The Fuck You Want - * To Public License, Version 2, as published by Sam Hocevar. See - * http://sam.zoy.org/wtfpl/COPYING for more details. */ - -/** - * Following lines make Handlebars helper function to work with all - * three such as Direct web, RequireJS AMD and Node JS. - * This concepts derived from UMD. - * @courtesy - https://github.com/umdjs/umd/blob/master/returnExports.js - */ - -(function (root, factory) { - if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like enviroments that support module.exports, - // like Node. - module.exports = factory(require('handlebars')); - } else if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['handlebars'], factory); - } else { - // Browser globals (root is window) - root.returnExports = factory(root.Handlebars); - } -}(this, function (Handlebars) { - - /** - * If Equals - * if_eq this compare=that - */ - Handlebars.registerHelper('if_eq', function(context, options) { - if (context == options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Equals - * unless_eq this compare=that - */ - Handlebars.registerHelper('unless_eq', function(context, options) { - if (context == options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Greater Than - * if_gt this compare=that - */ - Handlebars.registerHelper('if_gt', function(context, options) { - if (context > options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Greater Than - * unless_gt this compare=that - */ - Handlebars.registerHelper('unless_gt', function(context, options) { - if (context > options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Less Than - * if_lt this compare=that - */ - Handlebars.registerHelper('if_lt', function(context, options) { - if (context < options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Less Than - * unless_lt this compare=that - */ - Handlebars.registerHelper('unless_lt', function(context, options) { - if (context < options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Greater Than or Equal To - * if_gteq this compare=that - */ - Handlebars.registerHelper('if_gteq', function(context, options) { - if (context >= options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Greater Than or Equal To - * unless_gteq this compare=that - */ - Handlebars.registerHelper('unless_gteq', function(context, options) { - if (context >= options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - - /** - * If Less Than or Equal To - * if_lteq this compare=that - */ - Handlebars.registerHelper('if_lteq', function(context, options) { - if (context <= options.hash.compare) - return options.fn(this); - return options.inverse(this); - }); - - /** - * Unless Less Than or Equal To - * unless_lteq this compare=that - */ - Handlebars.registerHelper('unless_lteq', function(context, options) { - if (context <= options.hash.compare) - return options.inverse(this); - return options.fn(this); - }); - - /** - * Convert new line (\n\r) to <br> - * from http://phpjs.org/functions/nl2br:480 - */ - Handlebars.registerHelper('nl2br', function(text) { - text = Handlebars.Utils.escapeExpression(text); - var nl2br = (text + '').replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1' + '<br>' + '$2'); - return new Handlebars.SafeString(nl2br); - }); - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/handlebars.runtime.js b/src/UI/JsLibraries/handlebars.runtime.js deleted file mode 100644 index 94af5a379..000000000 --- a/src/UI/JsLibraries/handlebars.runtime.js +++ /dev/null @@ -1,660 +0,0 @@ -/*! - - handlebars v2.0.0 - - Copyright (C) 2011-2014 by Yehuda Katz - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - - @license - */ -/* exported Handlebars */ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - define([], factory); - } else if (typeof exports === 'object') { - module.exports = factory(); - } else { - root.Handlebars = root.Handlebars || factory(); - } -}(this, function () { - // handlebars/safe-string.js - var __module3__ = (function() { - "use strict"; - var __exports__; - // Build out our basic SafeString type - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = function() { - return "" + this.string; - }; - - __exports__ = SafeString; - return __exports__; - })(); - - // handlebars/utils.js - var __module2__ = (function(__dependency1__) { - "use strict"; - var __exports__ = {}; - /*jshint -W004 */ - var SafeString = __dependency1__; - - var escape = { - "&": "&", - "<": "<", - ">": ">", - '"': """, - "'": "'", - "`": "`" - }; - - var badChars = /[&<>"'`]/g; - var possible = /[&<>"'`]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - __exports__.extend = extend;var toString = Object.prototype.toString; - __exports__.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - var isFunction = function(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - var isFunction; - __exports__.isFunction = isFunction; - /* istanbul ignore next */ - var isArray = Array.isArray || function(value) { - return (value && typeof value === 'object') ? toString.call(value) === '[object Array]' : false; - }; - __exports__.isArray = isArray; - - function escapeExpression(string) { - // don't escape SafeStrings, since they're already safe - if (string instanceof SafeString) { - return string.toString(); - } else if (string == null) { - return ""; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = "" + string; - - if(!possible.test(string)) { return string; } - return string.replace(badChars, escapeChar); - } - - __exports__.escapeExpression = escapeExpression;function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - __exports__.isEmpty = isEmpty;function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - - __exports__.appendContextPath = appendContextPath; - return __exports__; - })(__module3__); - - // handlebars/exception.js - var __module4__ = (function() { - "use strict"; - var __exports__; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var line; - if (node && node.firstLine) { - line = node.firstLine; - - message += ' - ' + line + ':' + node.firstColumn; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - if (line) { - this.lineNumber = line; - this.column = node.firstColumn; - } - } - - Exception.prototype = new Error(); - - __exports__ = Exception; - return __exports__; - })(); - - // handlebars/base.js - var __module1__ = (function(__dependency1__, __dependency2__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - - var VERSION = "2.0.0"; - __exports__.VERSION = VERSION;var COMPILER_REVISION = 6; - __exports__.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1' - }; - __exports__.REVISION_CHANGES = REVISION_CHANGES; - var isArray = Utils.isArray, - isFunction = Utils.isFunction, - toString = Utils.toString, - objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials) { - this.helpers = helpers || {}; - this.partials = partials || {}; - - registerDefaultHelpers(this); - } - - __exports__.HandlebarsEnvironment = HandlebarsEnvironment;HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: logger, - log: log, - - registerHelper: function(name, fn) { - if (toString.call(name) === objectType) { - if (fn) { throw new Exception('Arg not supported with multiple helpers'); } - Utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function(name) { - delete this.helpers[name]; - }, - - registerPartial: function(name, partial) { - if (toString.call(name) === objectType) { - Utils.extend(this.partials, name); - } else { - this.partials[name] = partial; - } - }, - unregisterPartial: function(name) { - delete this.partials[name]; - } - }; - - function registerDefaultHelpers(instance) { - instance.registerHelper('helperMissing', function(/* [args, ]options */) { - if(arguments.length === 1) { - // A missing field in a {{foo}} constuct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new Exception("Missing helper: '" + arguments[arguments.length-1].name + "'"); - } - }); - - instance.registerHelper('blockHelperMissing', function(context, options) { - var inverse = options.inverse, - fn = options.fn; - - if(context === true) { - return fn(this); - } else if(context === false || context == null) { - return inverse(this); - } else if (isArray(context)) { - if(context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.name); - options = {data: data}; - } - - return fn(context, options); - } - }); - - instance.registerHelper('each', function(context, options) { - if (!options) { - throw new Exception('Must pass iterator to #each'); - } - - var fn = options.fn, inverse = options.inverse; - var i = 0, ret = "", data; - - var contextPath; - if (options.data && options.ids) { - contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (isFunction(context)) { context = context.call(this); } - - if (options.data) { - data = createFrame(options.data); - } - - if(context && typeof context === 'object') { - if (isArray(context)) { - for(var j = context.length; i<j; i++) { - if (data) { - data.index = i; - data.first = (i === 0); - data.last = (i === (context.length-1)); - - if (contextPath) { - data.contextPath = contextPath + i; - } - } - ret = ret + fn(context[i], { data: data }); - } - } else { - for(var key in context) { - if(context.hasOwnProperty(key)) { - if(data) { - data.key = key; - data.index = i; - data.first = (i === 0); - - if (contextPath) { - data.contextPath = contextPath + key; - } - } - ret = ret + fn(context[key], {data: data}); - i++; - } - } - } - } - - if(i === 0){ - ret = inverse(this); - } - - return ret; - }); - - instance.registerHelper('if', function(conditional, options) { - if (isFunction(conditional)) { conditional = conditional.call(this); } - - // Default behavior is to render the positive path if the value is truthy and not empty. - // The `includeZero` option may be set to treat the condtional as purely not empty based on the - // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. - if ((!options.hash.includeZero && !conditional) || Utils.isEmpty(conditional)) { - return options.inverse(this); - } else { - return options.fn(this); - } - }); - - instance.registerHelper('unless', function(conditional, options) { - return instance.helpers['if'].call(this, conditional, {fn: options.inverse, inverse: options.fn, hash: options.hash}); - }); - - instance.registerHelper('with', function(context, options) { - if (isFunction(context)) { context = context.call(this); } - - var fn = options.fn; - - if (!Utils.isEmpty(context)) { - if (options.data && options.ids) { - var data = createFrame(options.data); - data.contextPath = Utils.appendContextPath(options.data.contextPath, options.ids[0]); - options = {data:data}; - } - - return fn(context, options); - } else { - return options.inverse(this); - } - }); - - instance.registerHelper('log', function(message, options) { - var level = options.data && options.data.level != null ? parseInt(options.data.level, 10) : 1; - instance.log(level, message); - }); - - instance.registerHelper('lookup', function(obj, field) { - return obj && obj[field]; - }); - } - - var logger = { - methodMap: { 0: 'debug', 1: 'info', 2: 'warn', 3: 'error' }, - - // State enum - DEBUG: 0, - INFO: 1, - WARN: 2, - ERROR: 3, - level: 3, - - // can be overridden in the host environment - log: function(level, message) { - if (logger.level <= level) { - var method = logger.methodMap[level]; - if (typeof console !== 'undefined' && console[method]) { - console[method].call(console, message); - } - } - } - }; - __exports__.logger = logger; - var log = logger.log; - __exports__.log = log; - var createFrame = function(object) { - var frame = Utils.extend({}, object); - frame._parent = object; - return frame; - }; - __exports__.createFrame = createFrame; - return __exports__; - })(__module2__, __module4__); - - // handlebars/runtime.js - var __module5__ = (function(__dependency1__, __dependency2__, __dependency3__) { - "use strict"; - var __exports__ = {}; - var Utils = __dependency1__; - var Exception = __dependency2__; - var COMPILER_REVISION = __dependency3__.COMPILER_REVISION; - var REVISION_CHANGES = __dependency3__.REVISION_CHANGES; - var createFrame = __dependency3__.createFrame; - - function checkRevision(compilerInfo) { - var compilerRevision = compilerInfo && compilerInfo[0] || 1, - currentRevision = COMPILER_REVISION; - - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = REVISION_CHANGES[currentRevision], - compilerVersions = REVISION_CHANGES[compilerRevision]; - throw new Exception("Template was precompiled with an older version of Handlebars than the current runtime. "+ - "Please update your precompiler to a newer version ("+runtimeVersions+") or downgrade your runtime to an older version ("+compilerVersions+")."); - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw new Exception("Template was precompiled with a newer version of Handlebars than the current runtime. "+ - "Please update your runtime to a newer version ("+compilerInfo[1]+")."); - } - } - } - - __exports__.checkRevision = checkRevision;// TODO: Remove this line and break up compilePartial - - function template(templateSpec, env) { - /* istanbul ignore next */ - if (!env) { - throw new Exception("No environment passed to template"); - } - if (!templateSpec || !templateSpec.main) { - throw new Exception('Unknown template object: ' + typeof templateSpec); - } - - // Note: Using env.VM references rather than local var references throughout this section to allow - // for external users to override these as psuedo-supported APIs. - env.VM.checkRevision(templateSpec.compiler); - - var invokePartialWrapper = function(partial, indent, name, context, hash, helpers, partials, data, depths) { - if (hash) { - context = Utils.extend({}, context, hash); - } - - var result = env.VM.invokePartial.call(this, partial, name, context, helpers, partials, data, depths); - - if (result == null && env.compile) { - var options = { helpers: helpers, partials: partials, data: data, depths: depths }; - partials[name] = env.compile(partial, { data: data !== undefined, compat: templateSpec.compat }, env); - result = partials[name](context, options); - } - if (result != null) { - if (indent) { - var lines = result.split('\n'); - for (var i = 0, l = lines.length; i < l; i++) { - if (!lines[i] && i + 1 === l) { - break; - } - - lines[i] = indent + lines[i]; - } - result = lines.join('\n'); - } - return result; - } else { - throw new Exception("The partial " + name + " could not be compiled when running in runtime-only mode"); - } - }; - - // Just add water - var container = { - lookup: function(depths, name) { - var len = depths.length; - for (var i = 0; i < len; i++) { - if (depths[i] && depths[i][name] != null) { - return depths[i][name]; - } - } - }, - lambda: function(current, context) { - return typeof current === 'function' ? current.call(context) : current; - }, - - escapeExpression: Utils.escapeExpression, - invokePartial: invokePartialWrapper, - - fn: function(i) { - return templateSpec[i]; - }, - - programs: [], - program: function(i, data, depths) { - var programWrapper = this.programs[i], - fn = this.fn(i); - if (data || depths) { - programWrapper = program(this, i, fn, data, depths); - } else if (!programWrapper) { - programWrapper = this.programs[i] = program(this, i, fn); - } - return programWrapper; - }, - - data: function(data, depth) { - while (data && depth--) { - data = data._parent; - } - return data; - }, - merge: function(param, common) { - var ret = param || common; - - if (param && common && (param !== common)) { - ret = Utils.extend({}, common, param); - } - - return ret; - }, - - noop: env.VM.noop, - compilerInfo: templateSpec.compiler - }; - - var ret = function(context, options) { - options = options || {}; - var data = options.data; - - ret._setup(options); - if (!options.partial && templateSpec.useData) { - data = initData(context, data); - } - var depths; - if (templateSpec.useDepths) { - depths = options.depths ? [context].concat(options.depths) : [context]; - } - - return templateSpec.main.call(container, context, container.helpers, container.partials, data, depths); - }; - ret.isTop = true; - - ret._setup = function(options) { - if (!options.partial) { - container.helpers = container.merge(options.helpers, env.helpers); - - if (templateSpec.usePartial) { - container.partials = container.merge(options.partials, env.partials); - } - } else { - container.helpers = options.helpers; - container.partials = options.partials; - } - }; - - ret._child = function(i, data, depths) { - if (templateSpec.useDepths && !depths) { - throw new Exception('must pass parent depths'); - } - - return program(container, i, templateSpec[i], data, depths); - }; - return ret; - } - - __exports__.template = template;function program(container, i, fn, data, depths) { - var prog = function(context, options) { - options = options || {}; - - return fn.call(container, context, container.helpers, container.partials, options.data || data, depths && [context].concat(depths)); - }; - prog.program = i; - prog.depth = depths ? depths.length : 0; - return prog; - } - - __exports__.program = program;function invokePartial(partial, name, context, helpers, partials, data, depths) { - var options = { partial: true, helpers: helpers, partials: partials, data: data, depths: depths }; - - if(partial === undefined) { - throw new Exception("The partial " + name + " could not be found"); - } else if(partial instanceof Function) { - return partial(context, options); - } - } - - __exports__.invokePartial = invokePartial;function noop() { return ""; } - - __exports__.noop = noop;function initData(context, data) { - if (!data || !('root' in data)) { - data = data ? createFrame(data) : {}; - data.root = context; - } - return data; - } - return __exports__; - })(__module2__, __module4__, __module1__); - - // handlebars.runtime.js - var __module0__ = (function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__) { - "use strict"; - var __exports__; - /*globals Handlebars: true */ - var base = __dependency1__; - - // Each of these augment the Handlebars object. No need to setup here. - // (This is done to easily share code between commonjs and browse envs) - var SafeString = __dependency2__; - var Exception = __dependency3__; - var Utils = __dependency4__; - var runtime = __dependency5__; - - // For compatibility and usage outside of module systems, make the Handlebars object a namespace - var create = function() { - var hb = new base.HandlebarsEnvironment(); - - Utils.extend(hb, base); - hb.SafeString = SafeString; - hb.Exception = Exception; - hb.Utils = Utils; - hb.escapeExpression = Utils.escapeExpression; - - hb.VM = runtime; - hb.template = function(spec) { - return runtime.template(spec, hb); - }; - - return hb; - }; - - var Handlebars = create(); - Handlebars.create = create; - - Handlebars['default'] = Handlebars; - - __exports__ = Handlebars; - return __exports__; - })(__module1__, __module3__, __module4__, __module2__, __module5__); - - return __module0__; -})); diff --git a/src/UI/JsLibraries/jquery-ui.js b/src/UI/JsLibraries/jquery-ui.js deleted file mode 100644 index fe44a9c84..000000000 --- a/src/UI/JsLibraries/jquery-ui.js +++ /dev/null @@ -1,4233 +0,0 @@ -/*! jQuery UI - v1.10.4 - 2014-01-22 -* http://jqueryui.com -* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.sortable.js, jquery.ui.slider.js -* Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ - -(function( $, undefined ) { - -var uuid = 0, - runiqueId = /^ui-id-\d+$/; - -// $.ui might exist from components with no dependencies, e.g., $.ui.position -$.ui = $.ui || {}; - -$.extend( $.ui, { - version: "1.10.4", - - keyCode: { - BACKSPACE: 8, - COMMA: 188, - DELETE: 46, - DOWN: 40, - END: 35, - ENTER: 13, - ESCAPE: 27, - HOME: 36, - LEFT: 37, - NUMPAD_ADD: 107, - NUMPAD_DECIMAL: 110, - NUMPAD_DIVIDE: 111, - NUMPAD_ENTER: 108, - NUMPAD_MULTIPLY: 106, - NUMPAD_SUBTRACT: 109, - PAGE_DOWN: 34, - PAGE_UP: 33, - PERIOD: 190, - RIGHT: 39, - SPACE: 32, - TAB: 9, - UP: 38 - } -}); - -// plugins -$.fn.extend({ - focus: (function( orig ) { - return function( delay, fn ) { - return typeof delay === "number" ? - this.each(function() { - var elem = this; - setTimeout(function() { - $( elem ).focus(); - if ( fn ) { - fn.call( elem ); - } - }, delay ); - }) : - orig.apply( this, arguments ); - }; - })( $.fn.focus ), - - scrollParent: function() { - var scrollParent; - if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) { - scrollParent = this.parents().filter(function() { - return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } else { - scrollParent = this.parents().filter(function() { - return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x")); - }).eq(0); - } - - return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent; - }, - - zIndex: function( zIndex ) { - if ( zIndex !== undefined ) { - return this.css( "zIndex", zIndex ); - } - - if ( this.length ) { - var elem = $( this[ 0 ] ), position, value; - while ( elem.length && elem[ 0 ] !== document ) { - // Ignore z-index if position is set to a value where z-index is ignored by the browser - // This makes behavior of this function consistent across browsers - // WebKit always returns auto if the element is positioned - position = elem.css( "position" ); - if ( position === "absolute" || position === "relative" || position === "fixed" ) { - // IE returns 0 when zIndex is not specified - // other browsers return a string - // we ignore the case of nested elements with an explicit value of 0 - // <div style="z-index: -10;"><div style="z-index: 0;"></div></div> - value = parseInt( elem.css( "zIndex" ), 10 ); - if ( !isNaN( value ) && value !== 0 ) { - return value; - } - } - elem = elem.parent(); - } - } - - return 0; - }, - - uniqueId: function() { - return this.each(function() { - if ( !this.id ) { - this.id = "ui-id-" + (++uuid); - } - }); - }, - - removeUniqueId: function() { - return this.each(function() { - if ( runiqueId.test( this.id ) ) { - $( this ).removeAttr( "id" ); - } - }); - } -}); - -// selectors -function focusable( element, isTabIndexNotNaN ) { - var map, mapName, img, - nodeName = element.nodeName.toLowerCase(); - if ( "area" === nodeName ) { - map = element.parentNode; - mapName = map.name; - if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) { - return false; - } - img = $( "img[usemap=#" + mapName + "]" )[0]; - return !!img && visible( img ); - } - return ( /input|select|textarea|button|object/.test( nodeName ) ? - !element.disabled : - "a" === nodeName ? - element.href || isTabIndexNotNaN : - isTabIndexNotNaN) && - // the element and all of its ancestors must be visible - visible( element ); -} - -function visible( element ) { - return $.expr.filters.visible( element ) && - !$( element ).parents().addBack().filter(function() { - return $.css( this, "visibility" ) === "hidden"; - }).length; -} - -$.extend( $.expr[ ":" ], { - data: $.expr.createPseudo ? - $.expr.createPseudo(function( dataName ) { - return function( elem ) { - return !!$.data( elem, dataName ); - }; - }) : - // support: jQuery <1.8 - function( elem, i, match ) { - return !!$.data( elem, match[ 3 ] ); - }, - - focusable: function( element ) { - return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); - }, - - tabbable: function( element ) { - var tabIndex = $.attr( element, "tabindex" ), - isTabIndexNaN = isNaN( tabIndex ); - return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); - } -}); - -// support: jQuery <1.8 -if ( !$( "<a>" ).outerWidth( 1 ).jquery ) { - $.each( [ "Width", "Height" ], function( i, name ) { - var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ], - type = name.toLowerCase(), - orig = { - innerWidth: $.fn.innerWidth, - innerHeight: $.fn.innerHeight, - outerWidth: $.fn.outerWidth, - outerHeight: $.fn.outerHeight - }; - - function reduce( elem, size, border, margin ) { - $.each( side, function() { - size -= parseFloat( $.css( elem, "padding" + this ) ) || 0; - if ( border ) { - size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0; - } - if ( margin ) { - size -= parseFloat( $.css( elem, "margin" + this ) ) || 0; - } - }); - return size; - } - - $.fn[ "inner" + name ] = function( size ) { - if ( size === undefined ) { - return orig[ "inner" + name ].call( this ); - } - - return this.each(function() { - $( this ).css( type, reduce( this, size ) + "px" ); - }); - }; - - $.fn[ "outer" + name] = function( size, margin ) { - if ( typeof size !== "number" ) { - return orig[ "outer" + name ].call( this, size ); - } - - return this.each(function() { - $( this).css( type, reduce( this, size, true, margin ) + "px" ); - }); - }; - }); -} - -// support: jQuery <1.8 -if ( !$.fn.addBack ) { - $.fn.addBack = function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter( selector ) - ); - }; -} - -// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413) -if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) { - $.fn.removeData = (function( removeData ) { - return function( key ) { - if ( arguments.length ) { - return removeData.call( this, $.camelCase( key ) ); - } else { - return removeData.call( this ); - } - }; - })( $.fn.removeData ); -} - - - - - -// deprecated -$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() ); - -$.support.selectstart = "onselectstart" in document.createElement( "div" ); -$.fn.extend({ - disableSelection: function() { - return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) + - ".ui-disableSelection", function( event ) { - event.preventDefault(); - }); - }, - - enableSelection: function() { - return this.unbind( ".ui-disableSelection" ); - } -}); - -$.extend( $.ui, { - // $.ui.plugin is deprecated. Use $.widget() extensions instead. - plugin: { - add: function( module, option, set ) { - var i, - proto = $.ui[ module ].prototype; - for ( i in set ) { - proto.plugins[ i ] = proto.plugins[ i ] || []; - proto.plugins[ i ].push( [ option, set[ i ] ] ); - } - }, - call: function( instance, name, args ) { - var i, - set = instance.plugins[ name ]; - if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) { - return; - } - - for ( i = 0; i < set.length; i++ ) { - if ( instance.options[ set[ i ][ 0 ] ] ) { - set[ i ][ 1 ].apply( instance.element, args ); - } - } - } - }, - - // only used by resizable - hasScroll: function( el, a ) { - - //If overflow is hidden, the element might have extra content, but the user wants to hide it - if ( $( el ).css( "overflow" ) === "hidden") { - return false; - } - - var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop", - has = false; - - if ( el[ scroll ] > 0 ) { - return true; - } - - // TODO: determine which cases actually cause this to happen - // if the element doesn't have the scroll set, see if it's possible to - // set the scroll - el[ scroll ] = 1; - has = ( el[ scroll ] > 0 ); - el[ scroll ] = 0; - return has; - } -}); - -})( jQuery ); -(function( $, undefined ) { - -var uuid = 0, - slice = Array.prototype.slice, - _cleanData = $.cleanData; -$.cleanData = function( elems ) { - for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) { - try { - $( elem ).triggerHandler( "remove" ); - // http://bugs.jquery.com/ticket/8235 - } catch( e ) {} - } - _cleanData( elems ); -}; - -$.widget = function( name, base, prototype ) { - var fullName, existingConstructor, constructor, basePrototype, - // proxiedPrototype allows the provided prototype to remain unmodified - // so that it can be used as a mixin for multiple widgets (#8876) - proxiedPrototype = {}, - namespace = name.split( "." )[ 0 ]; - - name = name.split( "." )[ 1 ]; - fullName = namespace + "-" + name; - - if ( !prototype ) { - prototype = base; - base = $.Widget; - } - - // create selector for plugin - $.expr[ ":" ][ fullName.toLowerCase() ] = function( elem ) { - return !!$.data( elem, fullName ); - }; - - $[ namespace ] = $[ namespace ] || {}; - existingConstructor = $[ namespace ][ name ]; - constructor = $[ namespace ][ name ] = function( options, element ) { - // allow instantiation without "new" keyword - if ( !this._createWidget ) { - return new constructor( options, element ); - } - - // allow instantiation without initializing for simple inheritance - // must use "new" keyword (the code above always passes args) - if ( arguments.length ) { - this._createWidget( options, element ); - } - }; - // extend with the existing constructor to carry over any static properties - $.extend( constructor, existingConstructor, { - version: prototype.version, - // copy the object used to create the prototype in case we need to - // redefine the widget later - _proto: $.extend( {}, prototype ), - // track widgets that inherit from this widget in case this widget is - // redefined after a widget inherits from it - _childConstructors: [] - }); - - basePrototype = new base(); - // we need to make the options hash a property directly on the new instance - // otherwise we'll modify the options hash on the prototype that we're - // inheriting from - basePrototype.options = $.widget.extend( {}, basePrototype.options ); - $.each( prototype, function( prop, value ) { - if ( !$.isFunction( value ) ) { - proxiedPrototype[ prop ] = value; - return; - } - proxiedPrototype[ prop ] = (function() { - var _super = function() { - return base.prototype[ prop ].apply( this, arguments ); - }, - _superApply = function( args ) { - return base.prototype[ prop ].apply( this, args ); - }; - return function() { - var __super = this._super, - __superApply = this._superApply, - returnValue; - - this._super = _super; - this._superApply = _superApply; - - returnValue = value.apply( this, arguments ); - - this._super = __super; - this._superApply = __superApply; - - return returnValue; - }; - })(); - }); - constructor.prototype = $.widget.extend( basePrototype, { - // TODO: remove support for widgetEventPrefix - // always use the name + a colon as the prefix, e.g., draggable:start - // don't prefix for widgets that aren't DOM-based - widgetEventPrefix: existingConstructor ? (basePrototype.widgetEventPrefix || name) : name - }, proxiedPrototype, { - constructor: constructor, - namespace: namespace, - widgetName: name, - widgetFullName: fullName - }); - - // If this widget is being redefined then we need to find all widgets that - // are inheriting from it and redefine all of them so that they inherit from - // the new version of this widget. We're essentially trying to replace one - // level in the prototype chain. - if ( existingConstructor ) { - $.each( existingConstructor._childConstructors, function( i, child ) { - var childPrototype = child.prototype; - - // redefine the child widget using the same prototype that was - // originally used, but inherit from the new version of the base - $.widget( childPrototype.namespace + "." + childPrototype.widgetName, constructor, child._proto ); - }); - // remove the list of existing child constructors from the old constructor - // so the old child constructors can be garbage collected - delete existingConstructor._childConstructors; - } else { - base._childConstructors.push( constructor ); - } - - $.widget.bridge( name, constructor ); -}; - -$.widget.extend = function( target ) { - var input = slice.call( arguments, 1 ), - inputIndex = 0, - inputLength = input.length, - key, - value; - for ( ; inputIndex < inputLength; inputIndex++ ) { - for ( key in input[ inputIndex ] ) { - value = input[ inputIndex ][ key ]; - if ( input[ inputIndex ].hasOwnProperty( key ) && value !== undefined ) { - // Clone objects - if ( $.isPlainObject( value ) ) { - target[ key ] = $.isPlainObject( target[ key ] ) ? - $.widget.extend( {}, target[ key ], value ) : - // Don't extend strings, arrays, etc. with objects - $.widget.extend( {}, value ); - // Copy everything else by reference - } else { - target[ key ] = value; - } - } - } - } - return target; -}; - -$.widget.bridge = function( name, object ) { - var fullName = object.prototype.widgetFullName || name; - $.fn[ name ] = function( options ) { - var isMethodCall = typeof options === "string", - args = slice.call( arguments, 1 ), - returnValue = this; - - // allow multiple hashes to be passed on init - options = !isMethodCall && args.length ? - $.widget.extend.apply( null, [ options ].concat(args) ) : - options; - - if ( isMethodCall ) { - this.each(function() { - var methodValue, - instance = $.data( this, fullName ); - if ( !instance ) { - return $.error( "cannot call methods on " + name + " prior to initialization; " + - "attempted to call method '" + options + "'" ); - } - if ( !$.isFunction( instance[options] ) || options.charAt( 0 ) === "_" ) { - return $.error( "no such method '" + options + "' for " + name + " widget instance" ); - } - methodValue = instance[ options ].apply( instance, args ); - if ( methodValue !== instance && methodValue !== undefined ) { - returnValue = methodValue && methodValue.jquery ? - returnValue.pushStack( methodValue.get() ) : - methodValue; - return false; - } - }); - } else { - this.each(function() { - var instance = $.data( this, fullName ); - if ( instance ) { - instance.option( options || {} )._init(); - } else { - $.data( this, fullName, new object( options, this ) ); - } - }); - } - - return returnValue; - }; -}; - -$.Widget = function( /* options, element */ ) {}; -$.Widget._childConstructors = []; - -$.Widget.prototype = { - widgetName: "widget", - widgetEventPrefix: "", - defaultElement: "<div>", - options: { - disabled: false, - - // callbacks - create: null - }, - _createWidget: function( options, element ) { - element = $( element || this.defaultElement || this )[ 0 ]; - this.element = $( element ); - this.uuid = uuid++; - this.eventNamespace = "." + this.widgetName + this.uuid; - this.options = $.widget.extend( {}, - this.options, - this._getCreateOptions(), - options ); - - this.bindings = $(); - this.hoverable = $(); - this.focusable = $(); - - if ( element !== this ) { - $.data( element, this.widgetFullName, this ); - this._on( true, this.element, { - remove: function( event ) { - if ( event.target === element ) { - this.destroy(); - } - } - }); - this.document = $( element.style ? - // element within the document - element.ownerDocument : - // element is window or document - element.document || element ); - this.window = $( this.document[0].defaultView || this.document[0].parentWindow ); - } - - this._create(); - this._trigger( "create", null, this._getCreateEventData() ); - this._init(); - }, - _getCreateOptions: $.noop, - _getCreateEventData: $.noop, - _create: $.noop, - _init: $.noop, - - destroy: function() { - this._destroy(); - // we can probably remove the unbind calls in 2.0 - // all event bindings should go through this._on() - this.element - .unbind( this.eventNamespace ) - // 1.9 BC for #7810 - // TODO remove dual storage - .removeData( this.widgetName ) - .removeData( this.widgetFullName ) - // support: jquery <1.6.3 - // http://bugs.jquery.com/ticket/9413 - .removeData( $.camelCase( this.widgetFullName ) ); - this.widget() - .unbind( this.eventNamespace ) - .removeAttr( "aria-disabled" ) - .removeClass( - this.widgetFullName + "-disabled " + - "ui-state-disabled" ); - - // clean up events and states - this.bindings.unbind( this.eventNamespace ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - }, - _destroy: $.noop, - - widget: function() { - return this.element; - }, - - option: function( key, value ) { - var options = key, - parts, - curOption, - i; - - if ( arguments.length === 0 ) { - // don't return a reference to the internal hash - return $.widget.extend( {}, this.options ); - } - - if ( typeof key === "string" ) { - // handle nested keys, e.g., "foo.bar" => { foo: { bar: ___ } } - options = {}; - parts = key.split( "." ); - key = parts.shift(); - if ( parts.length ) { - curOption = options[ key ] = $.widget.extend( {}, this.options[ key ] ); - for ( i = 0; i < parts.length - 1; i++ ) { - curOption[ parts[ i ] ] = curOption[ parts[ i ] ] || {}; - curOption = curOption[ parts[ i ] ]; - } - key = parts.pop(); - if ( arguments.length === 1 ) { - return curOption[ key ] === undefined ? null : curOption[ key ]; - } - curOption[ key ] = value; - } else { - if ( arguments.length === 1 ) { - return this.options[ key ] === undefined ? null : this.options[ key ]; - } - options[ key ] = value; - } - } - - this._setOptions( options ); - - return this; - }, - _setOptions: function( options ) { - var key; - - for ( key in options ) { - this._setOption( key, options[ key ] ); - } - - return this; - }, - _setOption: function( key, value ) { - this.options[ key ] = value; - - if ( key === "disabled" ) { - this.widget() - .toggleClass( this.widgetFullName + "-disabled ui-state-disabled", !!value ) - .attr( "aria-disabled", value ); - this.hoverable.removeClass( "ui-state-hover" ); - this.focusable.removeClass( "ui-state-focus" ); - } - - return this; - }, - - enable: function() { - return this._setOption( "disabled", false ); - }, - disable: function() { - return this._setOption( "disabled", true ); - }, - - _on: function( suppressDisabledCheck, element, handlers ) { - var delegateElement, - instance = this; - - // no suppressDisabledCheck flag, shuffle arguments - if ( typeof suppressDisabledCheck !== "boolean" ) { - handlers = element; - element = suppressDisabledCheck; - suppressDisabledCheck = false; - } - - // no element argument, shuffle and use this.element - if ( !handlers ) { - handlers = element; - element = this.element; - delegateElement = this.widget(); - } else { - // accept selectors, DOM elements - element = delegateElement = $( element ); - this.bindings = this.bindings.add( element ); - } - - $.each( handlers, function( event, handler ) { - function handlerProxy() { - // allow widgets to customize the disabled handling - // - disabled as an array instead of boolean - // - disabled class as method for disabling individual parts - if ( !suppressDisabledCheck && - ( instance.options.disabled === true || - $( this ).hasClass( "ui-state-disabled" ) ) ) { - return; - } - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - - // copy the guid so direct unbinding works - if ( typeof handler !== "string" ) { - handlerProxy.guid = handler.guid = - handler.guid || handlerProxy.guid || $.guid++; - } - - var match = event.match( /^(\w+)\s*(.*)$/ ), - eventName = match[1] + instance.eventNamespace, - selector = match[2]; - if ( selector ) { - delegateElement.delegate( selector, eventName, handlerProxy ); - } else { - element.bind( eventName, handlerProxy ); - } - }); - }, - - _off: function( element, eventName ) { - eventName = (eventName || "").split( " " ).join( this.eventNamespace + " " ) + this.eventNamespace; - element.unbind( eventName ).undelegate( eventName ); - }, - - _delay: function( handler, delay ) { - function handlerProxy() { - return ( typeof handler === "string" ? instance[ handler ] : handler ) - .apply( instance, arguments ); - } - var instance = this; - return setTimeout( handlerProxy, delay || 0 ); - }, - - _hoverable: function( element ) { - this.hoverable = this.hoverable.add( element ); - this._on( element, { - mouseenter: function( event ) { - $( event.currentTarget ).addClass( "ui-state-hover" ); - }, - mouseleave: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-hover" ); - } - }); - }, - - _focusable: function( element ) { - this.focusable = this.focusable.add( element ); - this._on( element, { - focusin: function( event ) { - $( event.currentTarget ).addClass( "ui-state-focus" ); - }, - focusout: function( event ) { - $( event.currentTarget ).removeClass( "ui-state-focus" ); - } - }); - }, - - _trigger: function( type, event, data ) { - var prop, orig, - callback = this.options[ type ]; - - data = data || {}; - event = $.Event( event ); - event.type = ( type === this.widgetEventPrefix ? - type : - this.widgetEventPrefix + type ).toLowerCase(); - // the original event may come from any element - // so we need to reset the target on the new event - event.target = this.element[ 0 ]; - - // copy original event properties over to the new event - orig = event.originalEvent; - if ( orig ) { - for ( prop in orig ) { - if ( !( prop in event ) ) { - event[ prop ] = orig[ prop ]; - } - } - } - - this.element.trigger( event, data ); - return !( $.isFunction( callback ) && - callback.apply( this.element[0], [ event ].concat( data ) ) === false || - event.isDefaultPrevented() ); - } -}; - -$.each( { show: "fadeIn", hide: "fadeOut" }, function( method, defaultEffect ) { - $.Widget.prototype[ "_" + method ] = function( element, options, callback ) { - if ( typeof options === "string" ) { - options = { effect: options }; - } - var hasOptions, - effectName = !options ? - method : - options === true || typeof options === "number" ? - defaultEffect : - options.effect || defaultEffect; - options = options || {}; - if ( typeof options === "number" ) { - options = { duration: options }; - } - hasOptions = !$.isEmptyObject( options ); - options.complete = callback; - if ( options.delay ) { - element.delay( options.delay ); - } - if ( hasOptions && $.effects && $.effects.effect[ effectName ] ) { - element[ method ]( options ); - } else if ( effectName !== method && element[ effectName ] ) { - element[ effectName ]( options.duration, options.easing, callback ); - } else { - element.queue(function( next ) { - $( this )[ method ](); - if ( callback ) { - callback.call( element[ 0 ] ); - } - next(); - }); - } - }; -}); - -})( jQuery ); -(function( $, undefined ) { - -var mouseHandled = false; -$( document ).mouseup( function() { - mouseHandled = false; -}); - -$.widget("ui.mouse", { - version: "1.10.4", - options: { - cancel: "input,textarea,button,select,option", - distance: 1, - delay: 0 - }, - _mouseInit: function() { - var that = this; - - this.element - .bind("mousedown."+this.widgetName, function(event) { - return that._mouseDown(event); - }) - .bind("click."+this.widgetName, function(event) { - if (true === $.data(event.target, that.widgetName + ".preventClickEvent")) { - $.removeData(event.target, that.widgetName + ".preventClickEvent"); - event.stopImmediatePropagation(); - return false; - } - }); - - this.started = false; - }, - - // TODO: make sure destroying one instance of mouse doesn't mess with - // other instances of mouse - _mouseDestroy: function() { - this.element.unbind("."+this.widgetName); - if ( this._mouseMoveDelegate ) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - } - }, - - _mouseDown: function(event) { - // don't let more than one widget handle mouseStart - if( mouseHandled ) { return; } - - // we may have missed mouseup (out of window) - (this._mouseStarted && this._mouseUp(event)); - - this._mouseDownEvent = event; - - var that = this, - btnIsLeft = (event.which === 1), - // event.target.nodeName works around a bug in IE 8 with - // disabled inputs (#7620) - elIsCancel = (typeof this.options.cancel === "string" && event.target.nodeName ? $(event.target).closest(this.options.cancel).length : false); - if (!btnIsLeft || elIsCancel || !this._mouseCapture(event)) { - return true; - } - - this.mouseDelayMet = !this.options.delay; - if (!this.mouseDelayMet) { - this._mouseDelayTimer = setTimeout(function() { - that.mouseDelayMet = true; - }, this.options.delay); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = (this._mouseStart(event) !== false); - if (!this._mouseStarted) { - event.preventDefault(); - return true; - } - } - - // Click event may never have fired (Gecko & Opera) - if (true === $.data(event.target, this.widgetName + ".preventClickEvent")) { - $.removeData(event.target, this.widgetName + ".preventClickEvent"); - } - - // these delegates are required to keep context - this._mouseMoveDelegate = function(event) { - return that._mouseMove(event); - }; - this._mouseUpDelegate = function(event) { - return that._mouseUp(event); - }; - $(document) - .bind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .bind("mouseup."+this.widgetName, this._mouseUpDelegate); - - event.preventDefault(); - - mouseHandled = true; - return true; - }, - - _mouseMove: function(event) { - // IE mouseup check - mouseup happened when mouse was out of window - if ($.ui.ie && ( !document.documentMode || document.documentMode < 9 ) && !event.button) { - return this._mouseUp(event); - } - - if (this._mouseStarted) { - this._mouseDrag(event); - return event.preventDefault(); - } - - if (this._mouseDistanceMet(event) && this._mouseDelayMet(event)) { - this._mouseStarted = - (this._mouseStart(this._mouseDownEvent, event) !== false); - (this._mouseStarted ? this._mouseDrag(event) : this._mouseUp(event)); - } - - return !this._mouseStarted; - }, - - _mouseUp: function(event) { - $(document) - .unbind("mousemove."+this.widgetName, this._mouseMoveDelegate) - .unbind("mouseup."+this.widgetName, this._mouseUpDelegate); - - if (this._mouseStarted) { - this._mouseStarted = false; - - if (event.target === this._mouseDownEvent.target) { - $.data(event.target, this.widgetName + ".preventClickEvent", true); - } - - this._mouseStop(event); - } - - return false; - }, - - _mouseDistanceMet: function(event) { - return (Math.max( - Math.abs(this._mouseDownEvent.pageX - event.pageX), - Math.abs(this._mouseDownEvent.pageY - event.pageY) - ) >= this.options.distance - ); - }, - - _mouseDelayMet: function(/* event */) { - return this.mouseDelayMet; - }, - - // These are placeholder methods, to be overriden by extending plugin - _mouseStart: function(/* event */) {}, - _mouseDrag: function(/* event */) {}, - _mouseStop: function(/* event */) {}, - _mouseCapture: function(/* event */) { return true; } -}); - -})(jQuery); -(function( $, undefined ) { - -$.widget("ui.draggable", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if (this.options.helper === "original" && !(/^(?:r|a|f)/).test(this.element.css("position"))) { - this.element[0].style.position = "relative"; - } - if (this.options.addClasses){ - this.element.addClass("ui-draggable"); - } - if (this.options.disabled){ - this.element.addClass("ui-draggable-disabled"); - } - - this._mouseInit(); - - }, - - _destroy: function() { - this.element.removeClass( "ui-draggable ui-draggable-dragging ui-draggable-disabled" ); - this._mouseDestroy(); - }, - - _mouseCapture: function(event) { - - var o = this.options; - - // among others, prevent a drag on a resizable-handle - if (this.helper || o.disabled || $(event.target).closest(".ui-resizable-handle").length > 0) { - return false; - } - - //Quit if we're not on a valid handle - this.handle = this._getHandle(event); - if (!this.handle) { - return false; - } - - $(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() { - $("<div class='ui-draggable-iframeFix' style='background: #fff;'></div>") - .css({ - width: this.offsetWidth+"px", height: this.offsetHeight+"px", - position: "absolute", opacity: "0.001", zIndex: 1000 - }) - .css($(this).offset()) - .appendTo("body"); - }); - - return true; - - }, - - _mouseStart: function(event) { - - var o = this.options; - - //Create and append the visible helper - this.helper = this._createHelper(event); - - this.helper.addClass("ui-draggable-dragging"); - - //Cache the helper size - this._cacheHelperProportions(); - - //If ddmanager is used for droppables, set the global draggable - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Store the helper's css position - this.cssPosition = this.helper.css( "position" ); - this.scrollParent = this.helper.scrollParent(); - this.offsetParent = this.helper.offsetParent(); - this.offsetParentCssPosition = this.offsetParent.css( "position" ); - - //The element's absolute position on the page minus margins - this.offset = this.positionAbs = this.element.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - //Reset scroll cache - this.offset.scroll = false; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - //Generate the original position - this.originalPosition = this.position = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Set a containment if given in the options - this._setContainment(); - - //Trigger event + callbacks - if(this._trigger("start", event) === false) { - this._clear(); - return false; - } - - //Recache the helper size - this._cacheHelperProportions(); - - //Prepare the droppable offsets - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - - this._mouseDrag(event, true); //Execute the drag once - this causes the helper not to be visible before getting its correct position - - //If the ddmanager is used for droppables, inform the manager that dragging has started (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStart(this, event); - } - - return true; - }, - - _mouseDrag: function(event, noPropagation) { - // reset any necessary cached properties (see #5009) - if ( this.offsetParentCssPosition === "fixed" ) { - this.offset.parent = this._getParentOffset(); - } - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - //Call plugins and callbacks and use the resulting position if something is returned - if (!noPropagation) { - var ui = this._uiHash(); - if(this._trigger("drag", event, ui) === false) { - this._mouseUp({}); - return false; - } - this.position = ui.position; - } - - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - return false; - }, - - _mouseStop: function(event) { - - //If we are using droppables, inform the manager about the drop - var that = this, - dropped = false; - if ($.ui.ddmanager && !this.options.dropBehaviour) { - dropped = $.ui.ddmanager.drop(this, event); - } - - //if a drop comes from outside (a sortable) - if(this.dropped) { - dropped = this.dropped; - this.dropped = false; - } - - //if the original element is no longer in the DOM don't bother to continue (see #8269) - if ( this.options.helper === "original" && !$.contains( this.element[ 0 ].ownerDocument, this.element[ 0 ] ) ) { - return false; - } - - if((this.options.revert === "invalid" && !dropped) || (this.options.revert === "valid" && dropped) || this.options.revert === true || ($.isFunction(this.options.revert) && this.options.revert.call(this.element, dropped))) { - $(this.helper).animate(this.originalPosition, parseInt(this.options.revertDuration, 10), function() { - if(that._trigger("stop", event) !== false) { - that._clear(); - } - }); - } else { - if(this._trigger("stop", event) !== false) { - this._clear(); - } - } - - return false; - }, - - _mouseUp: function(event) { - //Remove frame helpers - $("div.ui-draggable-iframeFix").each(function() { - this.parentNode.removeChild(this); - }); - - //If the ddmanager is used for droppables, inform the manager that dragging has stopped (see #5003) - if( $.ui.ddmanager ) { - $.ui.ddmanager.dragStop(this, event); - } - - return $.ui.mouse.prototype._mouseUp.call(this, event); - }, - - cancel: function() { - - if(this.helper.is(".ui-draggable-dragging")) { - this._mouseUp({}); - } else { - this._clear(); - } - - return this; - - }, - - _getHandle: function(event) { - return this.options.handle ? - !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : - true; - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event])) : (o.helper === "clone" ? this.element.clone().removeAttr("id") : this.element); - - if(!helper.parents("body").length) { - helper.appendTo((o.appendTo === "parent" ? this.element[0].parentNode : o.appendTo)); - } - - if(helper[0] !== this.element[0] && !(/(fixed|absolute)/).test(helper.css("position"))) { - helper.css("position", "absolute"); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - //Get the offsetParent and cache its position - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - //This needs to be actually done for all browsers, since pageX/pageY includes this information - //Ugly IE fix - if((this.offsetParent[0] === document.body) || - (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.element.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.element.css("marginLeft"),10) || 0), - top: (parseInt(this.element.css("marginTop"),10) || 0), - right: (parseInt(this.element.css("marginRight"),10) || 0), - bottom: (parseInt(this.element.css("marginBottom"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var over, c, ce, - o = this.options; - - if ( !o.containment ) { - this.containment = null; - return; - } - - if ( o.containment === "window" ) { - this.containment = [ - $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, - $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, - $( window ).scrollLeft() + $( window ).width() - this.helperProportions.width - this.margins.left, - $( window ).scrollTop() + ( $( window ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment === "document") { - this.containment = [ - 0, - 0, - $( document ).width() - this.helperProportions.width - this.margins.left, - ( $( document ).height() || document.body.parentNode.scrollHeight ) - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment.constructor === Array ) { - this.containment = o.containment; - return; - } - - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } - - c = $( o.containment ); - ce = c[ 0 ]; - - if( !ce ) { - return; - } - - over = c.css( "overflow" ) !== "hidden"; - - this.containment = [ - ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), - ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ) , - ( over ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - this.helperProportions.width - this.margins.left - this.margins.right, - ( over ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - this.helperProportions.height - this.margins.top - this.margins.bottom - ]; - this.relative_container = c; - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) * mod ) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) * mod ) - ) - }; - - }, - - _generatePosition: function(event) { - - var containment, co, top, left, - o = this.options, - scroll = this.cssPosition === "absolute" && !( this.scrollParent[ 0 ] !== document && $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) ? this.offsetParent : this.scrollParent, - pageX = event.pageX, - pageY = event.pageY; - - //Cache the scroll - if (!this.offset.scroll) { - this.offset.scroll = {top : scroll.scrollTop(), left : scroll.scrollLeft()}; - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - // If we are not dragging yet, we won't check for options - if ( this.originalPosition ) { - if ( this.containment ) { - if ( this.relative_container ){ - co = this.relative_container.offset(); - containment = [ - this.containment[ 0 ] + co.left, - this.containment[ 1 ] + co.top, - this.containment[ 2 ] + co.left, - this.containment[ 3 ] + co.top - ]; - } - else { - containment = this.containment; - } - - if(event.pageX - this.offset.click.left < containment[0]) { - pageX = containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < containment[1]) { - pageY = containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > containment[2]) { - pageX = containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > containment[3]) { - pageY = containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - //Check for grid elements set to 0 to prevent divide by 0 error causing invalid argument errors in IE (see ticket #6950) - top = o.grid[1] ? this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1] : this.originalPageY; - pageY = containment ? ((top - this.offset.click.top >= containment[1] || top - this.offset.click.top > containment[3]) ? top : ((top - this.offset.click.top >= containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = o.grid[0] ? this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0] : this.originalPageX; - pageX = containment ? ((left - this.offset.click.left >= containment[0] || left - this.offset.click.left > containment[2]) ? left : ((left - this.offset.click.left >= containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : this.offset.scroll.top ) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : this.offset.scroll.left ) - ) - }; - - }, - - _clear: function() { - this.helper.removeClass("ui-draggable-dragging"); - if(this.helper[0] !== this.element[0] && !this.cancelHelperRemoval) { - this.helper.remove(); - } - this.helper = null; - this.cancelHelperRemoval = false; - }, - - // From now on bulk stuff - mainly helpers - - _trigger: function(type, event, ui) { - ui = ui || this._uiHash(); - $.ui.plugin.call(this, type, [event, ui]); - //The absolute position has to be recalculated after plugins - if(type === "drag") { - this.positionAbs = this._convertPositionTo("absolute"); - } - return $.Widget.prototype._trigger.call(this, type, event, ui); - }, - - plugins: {}, - - _uiHash: function() { - return { - helper: this.helper, - position: this.position, - originalPosition: this.originalPosition, - offset: this.positionAbs - }; - } - -}); - -$.ui.plugin.add("draggable", "connectToSortable", { - start: function(event, ui) { - - var inst = $(this).data("ui-draggable"), o = inst.options, - uiSortable = $.extend({}, ui, { item: inst.element }); - inst.sortables = []; - $(o.connectToSortable).each(function() { - var sortable = $.data(this, "ui-sortable"); - if (sortable && !sortable.options.disabled) { - inst.sortables.push({ - instance: sortable, - shouldRevert: sortable.options.revert - }); - sortable.refreshPositions(); // Call the sortable's refreshPositions at drag start to refresh the containerCache since the sortable container cache is used in drag and needs to be up to date (this will ensure it's initialised as well as being kept in step with any changes that might have happened on the page). - sortable._trigger("activate", event, uiSortable); - } - }); - - }, - stop: function(event, ui) { - - //If we are still over the sortable, we fake the stop event of the sortable, but also remove helper - var inst = $(this).data("ui-draggable"), - uiSortable = $.extend({}, ui, { item: inst.element }); - - $.each(inst.sortables, function() { - if(this.instance.isOver) { - - this.instance.isOver = 0; - - inst.cancelHelperRemoval = true; //Don't remove the helper in the draggable instance - this.instance.cancelHelperRemoval = false; //Remove it in the sortable instance (so sortable plugins like revert still work) - - //The sortable revert is supported, and we have to set a temporary dropped variable on the draggable to support revert: "valid/invalid" - if(this.shouldRevert) { - this.instance.options.revert = this.shouldRevert; - } - - //Trigger the stop of the sortable - this.instance._mouseStop(event); - - this.instance.options.helper = this.instance.options._helper; - - //If the helper has been the original item, restore properties in the sortable - if(inst.options.helper === "original") { - this.instance.currentItem.css({ top: "auto", left: "auto" }); - } - - } else { - this.instance.cancelHelperRemoval = false; //Remove the helper in the sortable instance - this.instance._trigger("deactivate", event, uiSortable); - } - - }); - - }, - drag: function(event, ui) { - - var inst = $(this).data("ui-draggable"), that = this; - - $.each(inst.sortables, function() { - - var innermostIntersecting = false, - thisSortable = this; - - //Copy over some variables to allow calling the sortable's native _intersectsWith - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - - if(this.instance._intersectsWith(this.instance.containerCache)) { - innermostIntersecting = true; - $.each(inst.sortables, function () { - this.instance.positionAbs = inst.positionAbs; - this.instance.helperProportions = inst.helperProportions; - this.instance.offset.click = inst.offset.click; - if (this !== thisSortable && - this.instance._intersectsWith(this.instance.containerCache) && - $.contains(thisSortable.instance.element[0], this.instance.element[0]) - ) { - innermostIntersecting = false; - } - return innermostIntersecting; - }); - } - - - if(innermostIntersecting) { - //If it intersects, we use a little isOver variable and set it once, so our move-in stuff gets fired only once - if(!this.instance.isOver) { - - this.instance.isOver = 1; - //Now we fake the start of dragging for the sortable instance, - //by cloning the list group item, appending it to the sortable and using it as inst.currentItem - //We can then fire the start event of the sortable with our passed browser event, and our own helper (so it doesn't create a new one) - this.instance.currentItem = $(that).clone().removeAttr("id").appendTo(this.instance.element).data("ui-sortable-item", true); - this.instance.options._helper = this.instance.options.helper; //Store helper option to later restore it - this.instance.options.helper = function() { return ui.helper[0]; }; - - event.target = this.instance.currentItem[0]; - this.instance._mouseCapture(event, true); - this.instance._mouseStart(event, true, true); - - //Because the browser event is way off the new appended portlet, we modify a couple of variables to reflect the changes - this.instance.offset.click.top = inst.offset.click.top; - this.instance.offset.click.left = inst.offset.click.left; - this.instance.offset.parent.left -= inst.offset.parent.left - this.instance.offset.parent.left; - this.instance.offset.parent.top -= inst.offset.parent.top - this.instance.offset.parent.top; - - inst._trigger("toSortable", event); - inst.dropped = this.instance.element; //draggable revert needs that - //hack so receive/update callbacks work (mostly) - inst.currentItem = inst.element; - this.instance.fromOutside = inst; - - } - - //Provided we did all the previous steps, we can fire the drag event of the sortable on every draggable drag, when it intersects with the sortable - if(this.instance.currentItem) { - this.instance._mouseDrag(event); - } - - } else { - - //If it doesn't intersect with the sortable, and it intersected before, - //we fake the drag stop of the sortable, but make sure it doesn't remove the helper by using cancelHelperRemoval - if(this.instance.isOver) { - - this.instance.isOver = 0; - this.instance.cancelHelperRemoval = true; - - //Prevent reverting on this forced stop - this.instance.options.revert = false; - - // The out event needs to be triggered independently - this.instance._trigger("out", event, this.instance._uiHash(this.instance)); - - this.instance._mouseStop(event, true); - this.instance.options.helper = this.instance.options._helper; - - //Now we remove our currentItem, the list group clone again, and the placeholder, and animate the helper back to it's original size - this.instance.currentItem.remove(); - if(this.instance.placeholder) { - this.instance.placeholder.remove(); - } - - inst._trigger("fromSortable", event); - inst.dropped = false; //draggable revert needs that - } - - } - - }); - - } -}); - -$.ui.plugin.add("draggable", "cursor", { - start: function() { - var t = $("body"), o = $(this).data("ui-draggable").options; - if (t.css("cursor")) { - o._cursor = t.css("cursor"); - } - t.css("cursor", o.cursor); - }, - stop: function() { - var o = $(this).data("ui-draggable").options; - if (o._cursor) { - $("body").css("cursor", o._cursor); - } - } -}); - -$.ui.plugin.add("draggable", "opacity", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("opacity")) { - o._opacity = t.css("opacity"); - } - t.css("opacity", o.opacity); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._opacity) { - $(ui.helper).css("opacity", o._opacity); - } - } -}); - -$.ui.plugin.add("draggable", "scroll", { - start: function() { - var i = $(this).data("ui-draggable"); - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - i.overflowOffset = i.scrollParent.offset(); - } - }, - drag: function( event ) { - - var i = $(this).data("ui-draggable"), o = i.options, scrolled = false; - - if(i.scrollParent[0] !== document && i.scrollParent[0].tagName !== "HTML") { - - if(!o.axis || o.axis !== "x") { - if((i.overflowOffset.top + i.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - i.overflowOffset.top < o.scrollSensitivity) { - i.scrollParent[0].scrollTop = scrolled = i.scrollParent[0].scrollTop - o.scrollSpeed; - } - } - - if(!o.axis || o.axis !== "y") { - if((i.overflowOffset.left + i.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - i.overflowOffset.left < o.scrollSensitivity) { - i.scrollParent[0].scrollLeft = scrolled = i.scrollParent[0].scrollLeft - o.scrollSpeed; - } - } - - } else { - - if(!o.axis || o.axis !== "x") { - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - } - - if(!o.axis || o.axis !== "y") { - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(i, event); - } - - } -}); - -$.ui.plugin.add("draggable", "snap", { - start: function() { - - var i = $(this).data("ui-draggable"), - o = i.options; - - i.snapElements = []; - - $(o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap).each(function() { - var $t = $(this), - $o = $t.offset(); - if(this !== i.element[0]) { - i.snapElements.push({ - item: this, - width: $t.outerWidth(), height: $t.outerHeight(), - top: $o.top, left: $o.left - }); - } - }); - - }, - drag: function(event, ui) { - - var ts, bs, ls, rs, l, r, t, b, i, first, - inst = $(this).data("ui-draggable"), - o = inst.options, - d = o.snapTolerance, - x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, - y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; - - for (i = inst.snapElements.length - 1; i >= 0; i--){ - - l = inst.snapElements[i].left; - r = l + inst.snapElements[i].width; - t = inst.snapElements[i].top; - b = t + inst.snapElements[i].height; - - if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || !$.contains( inst.snapElements[ i ].item.ownerDocument, inst.snapElements[ i ].item ) ) { - if(inst.snapElements[i].snapping) { - (inst.options.snap.release && inst.options.snap.release.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = false; - continue; - } - - if(o.snapMode !== "inner") { - ts = Math.abs(t - y2) <= d; - bs = Math.abs(b - y1) <= d; - ls = Math.abs(l - x2) <= d; - rs = Math.abs(r - x1) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l - inst.helperProportions.width }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r }).left - inst.margins.left; - } - } - - first = (ts || bs || ls || rs); - - if(o.snapMode !== "outer") { - ts = Math.abs(t - y1) <= d; - bs = Math.abs(b - y2) <= d; - ls = Math.abs(l - x1) <= d; - rs = Math.abs(r - x2) <= d; - if(ts) { - ui.position.top = inst._convertPositionTo("relative", { top: t, left: 0 }).top - inst.margins.top; - } - if(bs) { - ui.position.top = inst._convertPositionTo("relative", { top: b - inst.helperProportions.height, left: 0 }).top - inst.margins.top; - } - if(ls) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: l }).left - inst.margins.left; - } - if(rs) { - ui.position.left = inst._convertPositionTo("relative", { top: 0, left: r - inst.helperProportions.width }).left - inst.margins.left; - } - } - - if(!inst.snapElements[i].snapping && (ts || bs || ls || rs || first)) { - (inst.options.snap.snap && inst.options.snap.snap.call(inst.element, event, $.extend(inst._uiHash(), { snapItem: inst.snapElements[i].item }))); - } - inst.snapElements[i].snapping = (ts || bs || ls || rs || first); - - } - - } -}); - -$.ui.plugin.add("draggable", "stack", { - start: function() { - var min, - o = this.data("ui-draggable").options, - group = $.makeArray($(o.stack)).sort(function(a,b) { - return (parseInt($(a).css("zIndex"),10) || 0) - (parseInt($(b).css("zIndex"),10) || 0); - }); - - if (!group.length) { return; } - - min = parseInt($(group[0]).css("zIndex"), 10) || 0; - $(group).each(function(i) { - $(this).css("zIndex", min + i); - }); - this.css("zIndex", (min + group.length)); - } -}); - -$.ui.plugin.add("draggable", "zIndex", { - start: function(event, ui) { - var t = $(ui.helper), o = $(this).data("ui-draggable").options; - if(t.css("zIndex")) { - o._zIndex = t.css("zIndex"); - } - t.css("zIndex", o.zIndex); - }, - stop: function(event, ui) { - var o = $(this).data("ui-draggable").options; - if(o._zIndex) { - $(ui.helper).css("zIndex", o._zIndex); - } - } -}); - -})(jQuery); -(function( $, undefined ) { - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -$.widget("ui.droppable", { - version: "1.10.4", - widgetEventPrefix: "drop", - options: { - accept: "*", - activeClass: false, - addClasses: true, - greedy: false, - hoverClass: false, - scope: "default", - tolerance: "intersect", - - // callbacks - activate: null, - deactivate: null, - drop: null, - out: null, - over: null - }, - _create: function() { - - var proportions, - o = this.options, - accept = o.accept; - - this.isover = false; - this.isout = true; - - this.accept = $.isFunction(accept) ? accept : function(d) { - return d.is(accept); - }; - - this.proportions = function( /* valueToWrite */ ) { - if ( arguments.length ) { - // Store the droppable's proportions - proportions = arguments[ 0 ]; - } else { - // Retrieve or derive the droppable's proportions - return proportions ? - proportions : - proportions = { - width: this.element[ 0 ].offsetWidth, - height: this.element[ 0 ].offsetHeight - }; - } - }; - - // Add the reference and positions to the manager - $.ui.ddmanager.droppables[o.scope] = $.ui.ddmanager.droppables[o.scope] || []; - $.ui.ddmanager.droppables[o.scope].push(this); - - (o.addClasses && this.element.addClass("ui-droppable")); - - }, - - _destroy: function() { - var i = 0, - drop = $.ui.ddmanager.droppables[this.options.scope]; - - for ( ; i < drop.length; i++ ) { - if ( drop[i] === this ) { - drop.splice(i, 1); - } - } - - this.element.removeClass("ui-droppable ui-droppable-disabled"); - }, - - _setOption: function(key, value) { - - if(key === "accept") { - this.accept = $.isFunction(value) ? value : function(d) { - return d.is(value); - }; - } - $.Widget.prototype._setOption.apply(this, arguments); - }, - - _activate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.addClass(this.options.activeClass); - } - if(draggable){ - this._trigger("activate", event, this.ui(draggable)); - } - }, - - _deactivate: function(event) { - var draggable = $.ui.ddmanager.current; - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(draggable){ - this._trigger("deactivate", event, this.ui(draggable)); - } - }, - - _over: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.addClass(this.options.hoverClass); - } - this._trigger("over", event, this.ui(draggable)); - } - - }, - - _out: function(event) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return; - } - - if (this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("out", event, this.ui(draggable)); - } - - }, - - _drop: function(event,custom) { - - var draggable = custom || $.ui.ddmanager.current, - childrenIntersection = false; - - // Bail if draggable and droppable are same element - if (!draggable || (draggable.currentItem || draggable.element)[0] === this.element[0]) { - return false; - } - - this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function() { - var inst = $.data(this, "ui-droppable"); - if( - inst.options.greedy && - !inst.options.disabled && - inst.options.scope === draggable.options.scope && - inst.accept.call(inst.element[0], (draggable.currentItem || draggable.element)) && - $.ui.intersect(draggable, $.extend(inst, { offset: inst.element.offset() }), inst.options.tolerance) - ) { childrenIntersection = true; return false; } - }); - if(childrenIntersection) { - return false; - } - - if(this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - if(this.options.activeClass) { - this.element.removeClass(this.options.activeClass); - } - if(this.options.hoverClass) { - this.element.removeClass(this.options.hoverClass); - } - this._trigger("drop", event, this.ui(draggable)); - return this.element; - } - - return false; - - }, - - ui: function(c) { - return { - draggable: (c.currentItem || c.element), - helper: c.helper, - position: c.position, - offset: c.positionAbs - }; - } - -}); - -$.ui.intersect = function(draggable, droppable, toleranceMode) { - - if (!droppable.offset) { - return false; - } - - var draggableLeft, draggableTop, - x1 = (draggable.positionAbs || draggable.position.absolute).left, - y1 = (draggable.positionAbs || draggable.position.absolute).top, - x2 = x1 + draggable.helperProportions.width, - y2 = y1 + draggable.helperProportions.height, - l = droppable.offset.left, - t = droppable.offset.top, - r = l + droppable.proportions().width, - b = t + droppable.proportions().height; - - switch (toleranceMode) { - case "fit": - return (l <= x1 && x2 <= r && t <= y1 && y2 <= b); - case "intersect": - return (l < x1 + (draggable.helperProportions.width / 2) && // Right Half - x2 - (draggable.helperProportions.width / 2) < r && // Left Half - t < y1 + (draggable.helperProportions.height / 2) && // Bottom Half - y2 - (draggable.helperProportions.height / 2) < b ); // Top Half - case "pointer": - draggableLeft = ((draggable.positionAbs || draggable.position.absolute).left + (draggable.clickOffset || draggable.offset.click).left); - draggableTop = ((draggable.positionAbs || draggable.position.absolute).top + (draggable.clickOffset || draggable.offset.click).top); - return isOverAxis( draggableTop, t, droppable.proportions().height ) && isOverAxis( draggableLeft, l, droppable.proportions().width ); - case "touch": - return ( - (y1 >= t && y1 <= b) || // Top edge touching - (y2 >= t && y2 <= b) || // Bottom edge touching - (y1 < t && y2 > b) // Surrounded vertically - ) && ( - (x1 >= l && x1 <= r) || // Left edge touching - (x2 >= l && x2 <= r) || // Right edge touching - (x1 < l && x2 > r) // Surrounded horizontally - ); - default: - return false; - } - -}; - -/* - This manager tracks offsets of draggables and droppables -*/ -$.ui.ddmanager = { - current: null, - droppables: { "default": [] }, - prepareOffsets: function(t, event) { - - var i, j, - m = $.ui.ddmanager.droppables[t.options.scope] || [], - type = event ? event.type : null, // workaround for #2317 - list = (t.currentItem || t.element).find(":data(ui-droppable)").addBack(); - - droppablesLoop: for (i = 0; i < m.length; i++) { - - //No disabled and non-accepted - if(m[i].options.disabled || (t && !m[i].accept.call(m[i].element[0],(t.currentItem || t.element)))) { - continue; - } - - // Filter out elements in the current dragged item - for (j=0; j < list.length; j++) { - if(list[j] === m[i].element[0]) { - m[i].proportions().height = 0; - continue droppablesLoop; - } - } - - m[i].visible = m[i].element.css("display") !== "none"; - if(!m[i].visible) { - continue; - } - - //Activate the droppable if used directly from draggables - if(type === "mousedown") { - m[i]._activate.call(m[i], event); - } - - m[ i ].offset = m[ i ].element.offset(); - m[ i ].proportions({ width: m[ i ].element[ 0 ].offsetWidth, height: m[ i ].element[ 0 ].offsetHeight }); - - } - - }, - drop: function(draggable, event) { - - var dropped = false; - // Create a copy of the droppables in case the list changes during the drop (#9116) - $.each(($.ui.ddmanager.droppables[draggable.options.scope] || []).slice(), function() { - - if(!this.options) { - return; - } - if (!this.options.disabled && this.visible && $.ui.intersect(draggable, this, this.options.tolerance)) { - dropped = this._drop.call(this, event) || dropped; - } - - if (!this.options.disabled && this.visible && this.accept.call(this.element[0],(draggable.currentItem || draggable.element))) { - this.isout = true; - this.isover = false; - this._deactivate.call(this, event); - } - - }); - return dropped; - - }, - dragStart: function( draggable, event ) { - //Listen for scrolling so that if the dragging causes scrolling the position of the droppables can be recalculated (see #5003) - draggable.element.parentsUntil( "body" ).bind( "scroll.droppable", function() { - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - }); - }, - drag: function(draggable, event) { - - //If you have a highly dynamic page, you might try this option. It renders positions every time you move the mouse. - if(draggable.options.refreshPositions) { - $.ui.ddmanager.prepareOffsets(draggable, event); - } - - //Run through all droppables and check their positions based on specific tolerance options - $.each($.ui.ddmanager.droppables[draggable.options.scope] || [], function() { - - if(this.options.disabled || this.greedyChild || !this.visible) { - return; - } - - var parentInstance, scope, parent, - intersects = $.ui.intersect(draggable, this, this.options.tolerance), - c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null); - if(!c) { - return; - } - - if (this.options.greedy) { - // find droppable parents with same scope - scope = this.options.scope; - parent = this.element.parents(":data(ui-droppable)").filter(function () { - return $.data(this, "ui-droppable").options.scope === scope; - }); - - if (parent.length) { - parentInstance = $.data(parent[0], "ui-droppable"); - parentInstance.greedyChild = (c === "isover"); - } - } - - // we just moved into a greedy child - if (parentInstance && c === "isover") { - parentInstance.isover = false; - parentInstance.isout = true; - parentInstance._out.call(parentInstance, event); - } - - this[c] = true; - this[c === "isout" ? "isover" : "isout"] = false; - this[c === "isover" ? "_over" : "_out"].call(this, event); - - // we just moved out of a greedy child - if (parentInstance && c === "isout") { - parentInstance.isout = false; - parentInstance.isover = true; - parentInstance._over.call(parentInstance, event); - } - }); - - }, - dragStop: function( draggable, event ) { - draggable.element.parentsUntil( "body" ).unbind( "scroll.droppable" ); - //Call prepareOffsets one final time since IE does not fire return scroll events when overflow was caused by drag (see #5003) - if( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } -}; - -})(jQuery); -(function( $, undefined ) { - -function isOverAxis( x, reference, size ) { - return ( x > reference ) && ( x < ( reference + size ) ); -} - -function isFloating(item) { - return (/left|right/).test(item.css("float")) || (/inline|table-cell/).test(item.css("display")); -} - -$.widget("ui.sortable", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "sort", - ready: false, - options: { - appendTo: "parent", - axis: false, - connectWith: false, - containment: false, - cursor: "auto", - cursorAt: false, - dropOnEmpty: true, - forcePlaceholderSize: false, - forceHelperSize: false, - grid: false, - handle: false, - helper: "original", - items: "> *", - opacity: false, - placeholder: false, - revert: false, - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - scope: "default", - tolerance: "intersect", - zIndex: 1000, - - // callbacks - activate: null, - beforeStop: null, - change: null, - deactivate: null, - out: null, - over: null, - receive: null, - remove: null, - sort: null, - start: null, - stop: null, - update: null - }, - _create: function() { - - var o = this.options; - this.containerCache = {}; - this.element.addClass("ui-sortable"); - - //Get the items - this.refresh(); - - //Let's determine if the items are being displayed horizontally - this.floating = this.items.length ? o.axis === "x" || isFloating(this.items[0].item) : false; - - //Let's determine the parent's offset - this.offset = this.element.offset(); - - //Initialize mouse events for interaction - this._mouseInit(); - - //We're ready to go - this.ready = true; - - }, - - _destroy: function() { - this.element - .removeClass("ui-sortable ui-sortable-disabled"); - this._mouseDestroy(); - - for ( var i = this.items.length - 1; i >= 0; i-- ) { - this.items[i].item.removeData(this.widgetName + "-item"); - } - - return this; - }, - - _setOption: function(key, value){ - if ( key === "disabled" ) { - this.options[ key ] = value; - - this.widget().toggleClass( "ui-sortable-disabled", !!value ); - } else { - // Don't call widget base _setOption for disable as it adds ui-state-disabled class - $.Widget.prototype._setOption.apply(this, arguments); - } - }, - - _mouseCapture: function(event, overrideHandle) { - var currentItem = null, - validHandle = false, - that = this; - - if (this.reverting) { - return false; - } - - if(this.options.disabled || this.options.type === "static") { - return false; - } - - //We have to refresh the items data once first - this._refreshItems(event); - - //Find out if the clicked node (or one of its parents) is a actual item in this.items - $(event.target).parents().each(function() { - if($.data(this, that.widgetName + "-item") === that) { - currentItem = $(this); - return false; - } - }); - if($.data(event.target, that.widgetName + "-item") === that) { - currentItem = $(event.target); - } - - if(!currentItem) { - return false; - } - if(this.options.handle && !overrideHandle) { - $(this.options.handle, currentItem).find("*").addBack().each(function() { - if(this === event.target) { - validHandle = true; - } - }); - if(!validHandle) { - return false; - } - } - - this.currentItem = currentItem; - this._removeCurrentsFromItems(); - return true; - - }, - - _mouseStart: function(event, overrideHandle, noActivation) { - - var i, body, - o = this.options; - - this.currentContainer = this; - - //We only need to call refreshPositions, because the refreshItems call has been moved to mouseCapture - this.refreshPositions(); - - //Create and append the visible helper - this.helper = this._createHelper(event); - - //Cache the helper size - this._cacheHelperProportions(); - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Get the next scrolling parent - this.scrollParent = this.helper.scrollParent(); - - //The element's absolute position on the page minus margins - this.offset = this.currentItem.offset(); - this.offset = { - top: this.offset.top - this.margins.top, - left: this.offset.left - this.margins.left - }; - - $.extend(this.offset, { - click: { //Where the click happened, relative to the element - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() //This is a relative to absolute position minus the actual position calculation - only used for relative positioned helper - }); - - // Only after we got the offset, we can change the helper's position to absolute - // TODO: Still need to figure out a way to make relative sorting possible - this.helper.css("position", "absolute"); - this.cssPosition = this.helper.css("position"); - - //Generate the original position - this.originalPosition = this._generatePosition(event); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - (o.cursorAt && this._adjustOffsetFromHelper(o.cursorAt)); - - //Cache the former DOM position - this.domPosition = { prev: this.currentItem.prev()[0], parent: this.currentItem.parent()[0] }; - - //If the helper is not the original, hide the original so it's not playing any role during the drag, won't cause anything bad this way - if(this.helper[0] !== this.currentItem[0]) { - this.currentItem.hide(); - } - - //Create the placeholder - this._createPlaceholder(); - - //Set a containment if given in the options - if(o.containment) { - this._setContainment(); - } - - if( o.cursor && o.cursor !== "auto" ) { // cursor option - body = this.document.find( "body" ); - - // support: IE - this.storedCursor = body.css( "cursor" ); - body.css( "cursor", o.cursor ); - - this.storedStylesheet = $( "<style>*{ cursor: "+o.cursor+" !important; }</style>" ).appendTo( body ); - } - - if(o.opacity) { // opacity option - if (this.helper.css("opacity")) { - this._storedOpacity = this.helper.css("opacity"); - } - this.helper.css("opacity", o.opacity); - } - - if(o.zIndex) { // zIndex option - if (this.helper.css("zIndex")) { - this._storedZIndex = this.helper.css("zIndex"); - } - this.helper.css("zIndex", o.zIndex); - } - - //Prepare scrolling - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - this.overflowOffset = this.scrollParent.offset(); - } - - //Call callbacks - this._trigger("start", event, this._uiHash()); - - //Recache the helper size - if(!this._preserveHelperProportions) { - this._cacheHelperProportions(); - } - - - //Post "activate" events to possible containers - if( !noActivation ) { - for ( i = this.containers.length - 1; i >= 0; i-- ) { - this.containers[ i ]._trigger( "activate", event, this._uiHash( this ) ); - } - } - - //Prepare possible droppables - if($.ui.ddmanager) { - $.ui.ddmanager.current = this; - } - - if ($.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - - this.dragging = true; - - this.helper.addClass("ui-sortable-helper"); - this._mouseDrag(event); //Execute the drag once - this causes the helper not to be visible before getting its correct position - return true; - - }, - - _mouseDrag: function(event) { - var i, item, itemElement, intersection, - o = this.options, - scrolled = false; - - //Compute the helpers position - this.position = this._generatePosition(event); - this.positionAbs = this._convertPositionTo("absolute"); - - if (!this.lastPositionAbs) { - this.lastPositionAbs = this.positionAbs; - } - - //Do scrolling - if(this.options.scroll) { - if(this.scrollParent[0] !== document && this.scrollParent[0].tagName !== "HTML") { - - if((this.overflowOffset.top + this.scrollParent[0].offsetHeight) - event.pageY < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop + o.scrollSpeed; - } else if(event.pageY - this.overflowOffset.top < o.scrollSensitivity) { - this.scrollParent[0].scrollTop = scrolled = this.scrollParent[0].scrollTop - o.scrollSpeed; - } - - if((this.overflowOffset.left + this.scrollParent[0].offsetWidth) - event.pageX < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft + o.scrollSpeed; - } else if(event.pageX - this.overflowOffset.left < o.scrollSensitivity) { - this.scrollParent[0].scrollLeft = scrolled = this.scrollParent[0].scrollLeft - o.scrollSpeed; - } - - } else { - - if(event.pageY - $(document).scrollTop() < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() - o.scrollSpeed); - } else if($(window).height() - (event.pageY - $(document).scrollTop()) < o.scrollSensitivity) { - scrolled = $(document).scrollTop($(document).scrollTop() + o.scrollSpeed); - } - - if(event.pageX - $(document).scrollLeft() < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() - o.scrollSpeed); - } else if($(window).width() - (event.pageX - $(document).scrollLeft()) < o.scrollSensitivity) { - scrolled = $(document).scrollLeft($(document).scrollLeft() + o.scrollSpeed); - } - - } - - if(scrolled !== false && $.ui.ddmanager && !o.dropBehaviour) { - $.ui.ddmanager.prepareOffsets(this, event); - } - } - - //Regenerate the absolute position used for position checks - this.positionAbs = this._convertPositionTo("absolute"); - - //Set the helper position - if(!this.options.axis || this.options.axis !== "y") { - this.helper[0].style.left = this.position.left+"px"; - } - if(!this.options.axis || this.options.axis !== "x") { - this.helper[0].style.top = this.position.top+"px"; - } - - //Rearrange - for (i = this.items.length - 1; i >= 0; i--) { - - //Cache variables and intersection, continue if no intersection - item = this.items[i]; - itemElement = item.item[0]; - intersection = this._intersectsWithPointer(item); - if (!intersection) { - continue; - } - - // Only put the placeholder inside the current Container, skip all - // items from other containers. This works because when moving - // an item from one container to another the - // currentContainer is switched before the placeholder is moved. - // - // Without this, moving items in "sub-sortables" can cause - // the placeholder to jitter beetween the outer and inner container. - if (item.instance !== this.currentContainer) { - continue; - } - - // cannot intersect with itself - // no useless actions that have been done before - // no action if the item moved is the parent of the item checked - if (itemElement !== this.currentItem[0] && - this.placeholder[intersection === 1 ? "next" : "prev"]()[0] !== itemElement && - !$.contains(this.placeholder[0], itemElement) && - (this.options.type === "semi-dynamic" ? !$.contains(this.element[0], itemElement) : true) - ) { - - this.direction = intersection === 1 ? "down" : "up"; - - if (this.options.tolerance === "pointer" || this._intersectsWithSides(item)) { - this._rearrange(event, item); - } else { - break; - } - - this._trigger("change", event, this._uiHash()); - break; - } - } - - //Post events to containers - this._contactContainers(event); - - //Interconnect with droppables - if($.ui.ddmanager) { - $.ui.ddmanager.drag(this, event); - } - - //Call callbacks - this._trigger("sort", event, this._uiHash()); - - this.lastPositionAbs = this.positionAbs; - return false; - - }, - - _mouseStop: function(event, noPropagation) { - - if(!event) { - return; - } - - //If we are using droppables, inform the manager about the drop - if ($.ui.ddmanager && !this.options.dropBehaviour) { - $.ui.ddmanager.drop(this, event); - } - - if(this.options.revert) { - var that = this, - cur = this.placeholder.offset(), - axis = this.options.axis, - animation = {}; - - if ( !axis || axis === "x" ) { - animation.left = cur.left - this.offset.parent.left - this.margins.left + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollLeft); - } - if ( !axis || axis === "y" ) { - animation.top = cur.top - this.offset.parent.top - this.margins.top + (this.offsetParent[0] === document.body ? 0 : this.offsetParent[0].scrollTop); - } - this.reverting = true; - $(this.helper).animate( animation, parseInt(this.options.revert, 10) || 500, function() { - that._clear(event); - }); - } else { - this._clear(event, noPropagation); - } - - return false; - - }, - - cancel: function() { - - if(this.dragging) { - - this._mouseUp({ target: null }); - - if(this.options.helper === "original") { - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - //Post deactivating events to containers - for (var i = this.containers.length - 1; i >= 0; i--){ - this.containers[i]._trigger("deactivate", null, this._uiHash(this)); - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", null, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - if (this.placeholder) { - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - if(this.placeholder[0].parentNode) { - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - } - if(this.options.helper !== "original" && this.helper && this.helper[0].parentNode) { - this.helper.remove(); - } - - $.extend(this, { - helper: null, - dragging: false, - reverting: false, - _noFinalSort: null - }); - - if(this.domPosition.prev) { - $(this.domPosition.prev).after(this.currentItem); - } else { - $(this.domPosition.parent).prepend(this.currentItem); - } - } - - return this; - - }, - - serialize: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - str = []; - o = o || {}; - - $(items).each(function() { - var res = ($(o.item || this).attr(o.attribute || "id") || "").match(o.expression || (/(.+)[\-=_](.+)/)); - if (res) { - str.push((o.key || res[1]+"[]")+"="+(o.key && o.expression ? res[1] : res[2])); - } - }); - - if(!str.length && o.key) { - str.push(o.key + "="); - } - - return str.join("&"); - - }, - - toArray: function(o) { - - var items = this._getItemsAsjQuery(o && o.connected), - ret = []; - - o = o || {}; - - items.each(function() { ret.push($(o.item || this).attr(o.attribute || "id") || ""); }); - return ret; - - }, - - /* Be careful with the following core functions */ - _intersectsWith: function(item) { - - var x1 = this.positionAbs.left, - x2 = x1 + this.helperProportions.width, - y1 = this.positionAbs.top, - y2 = y1 + this.helperProportions.height, - l = item.left, - r = l + item.width, - t = item.top, - b = t + item.height, - dyClick = this.offset.click.top, - dxClick = this.offset.click.left, - isOverElementHeight = ( this.options.axis === "x" ) || ( ( y1 + dyClick ) > t && ( y1 + dyClick ) < b ), - isOverElementWidth = ( this.options.axis === "y" ) || ( ( x1 + dxClick ) > l && ( x1 + dxClick ) < r ), - isOverElement = isOverElementHeight && isOverElementWidth; - - if ( this.options.tolerance === "pointer" || - this.options.forcePointerForContainers || - (this.options.tolerance !== "pointer" && this.helperProportions[this.floating ? "width" : "height"] > item[this.floating ? "width" : "height"]) - ) { - return isOverElement; - } else { - - return (l < x1 + (this.helperProportions.width / 2) && // Right Half - x2 - (this.helperProportions.width / 2) < r && // Left Half - t < y1 + (this.helperProportions.height / 2) && // Bottom Half - y2 - (this.helperProportions.height / 2) < b ); // Top Half - - } - }, - - _intersectsWithPointer: function(item) { - - var isOverElementHeight = (this.options.axis === "x") || isOverAxis(this.positionAbs.top + this.offset.click.top, item.top, item.height), - isOverElementWidth = (this.options.axis === "y") || isOverAxis(this.positionAbs.left + this.offset.click.left, item.left, item.width), - isOverElement = isOverElementHeight && isOverElementWidth, - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (!isOverElement) { - return false; - } - - return this.floating ? - ( ((horizontalDirection && horizontalDirection === "right") || verticalDirection === "down") ? 2 : 1 ) - : ( verticalDirection && (verticalDirection === "down" ? 2 : 1) ); - - }, - - _intersectsWithSides: function(item) { - - var isOverBottomHalf = isOverAxis(this.positionAbs.top + this.offset.click.top, item.top + (item.height/2), item.height), - isOverRightHalf = isOverAxis(this.positionAbs.left + this.offset.click.left, item.left + (item.width/2), item.width), - verticalDirection = this._getDragVerticalDirection(), - horizontalDirection = this._getDragHorizontalDirection(); - - if (this.floating && horizontalDirection) { - return ((horizontalDirection === "right" && isOverRightHalf) || (horizontalDirection === "left" && !isOverRightHalf)); - } else { - return verticalDirection && ((verticalDirection === "down" && isOverBottomHalf) || (verticalDirection === "up" && !isOverBottomHalf)); - } - - }, - - _getDragVerticalDirection: function() { - var delta = this.positionAbs.top - this.lastPositionAbs.top; - return delta !== 0 && (delta > 0 ? "down" : "up"); - }, - - _getDragHorizontalDirection: function() { - var delta = this.positionAbs.left - this.lastPositionAbs.left; - return delta !== 0 && (delta > 0 ? "right" : "left"); - }, - - refresh: function(event) { - this._refreshItems(event); - this.refreshPositions(); - return this; - }, - - _connectWith: function() { - var options = this.options; - return options.connectWith.constructor === String ? [options.connectWith] : options.connectWith; - }, - - _getItemsAsjQuery: function(connected) { - - var i, j, cur, inst, - items = [], - queries = [], - connectWith = this._connectWith(); - - if(connectWith && connected) { - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for ( j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element) : $(inst.options.items, inst.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), inst]); - } - } - } - } - - queries.push([$.isFunction(this.options.items) ? this.options.items.call(this.element, null, { options: this.options, item: this.currentItem }) : $(this.options.items, this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"), this]); - - function addItems() { - items.push( this ); - } - for (i = queries.length - 1; i >= 0; i--){ - queries[i][0].each( addItems ); - } - - return $(items); - - }, - - _removeCurrentsFromItems: function() { - - var list = this.currentItem.find(":data(" + this.widgetName + "-item)"); - - this.items = $.grep(this.items, function (item) { - for (var j=0; j < list.length; j++) { - if(list[j] === item.item[0]) { - return false; - } - } - return true; - }); - - }, - - _refreshItems: function(event) { - - this.items = []; - this.containers = [this]; - - var i, j, cur, inst, targetData, _queries, item, queriesLength, - items = this.items, - queries = [[$.isFunction(this.options.items) ? this.options.items.call(this.element[0], event, { item: this.currentItem }) : $(this.options.items, this.element), this]], - connectWith = this._connectWith(); - - if(connectWith && this.ready) { //Shouldn't be run the first time through due to massive slow-down - for (i = connectWith.length - 1; i >= 0; i--){ - cur = $(connectWith[i]); - for (j = cur.length - 1; j >= 0; j--){ - inst = $.data(cur[j], this.widgetFullName); - if(inst && inst !== this && !inst.options.disabled) { - queries.push([$.isFunction(inst.options.items) ? inst.options.items.call(inst.element[0], event, { item: this.currentItem }) : $(inst.options.items, inst.element), inst]); - this.containers.push(inst); - } - } - } - } - - for (i = queries.length - 1; i >= 0; i--) { - targetData = queries[i][1]; - _queries = queries[i][0]; - - for (j=0, queriesLength = _queries.length; j < queriesLength; j++) { - item = $(_queries[j]); - - item.data(this.widgetName + "-item", targetData); // Data for target checking (mouse manager) - - items.push({ - item: item, - instance: targetData, - width: 0, height: 0, - left: 0, top: 0 - }); - } - } - - }, - - refreshPositions: function(fast) { - - //This has to be redone because due to the item being moved out/into the offsetParent, the offsetParent's position will change - if(this.offsetParent && this.helper) { - this.offset.parent = this._getParentOffset(); - } - - var i, item, t, p; - - for (i = this.items.length - 1; i >= 0; i--){ - item = this.items[i]; - - //We ignore calculating positions of all connected containers when we're not over them - if(item.instance !== this.currentContainer && this.currentContainer && item.item[0] !== this.currentItem[0]) { - continue; - } - - t = this.options.toleranceElement ? $(this.options.toleranceElement, item.item) : item.item; - - if (!fast) { - item.width = t.outerWidth(); - item.height = t.outerHeight(); - } - - p = t.offset(); - item.left = p.left; - item.top = p.top; - } - - if(this.options.custom && this.options.custom.refreshContainers) { - this.options.custom.refreshContainers.call(this); - } else { - for (i = this.containers.length - 1; i >= 0; i--){ - p = this.containers[i].element.offset(); - this.containers[i].containerCache.left = p.left; - this.containers[i].containerCache.top = p.top; - this.containers[i].containerCache.width = this.containers[i].element.outerWidth(); - this.containers[i].containerCache.height = this.containers[i].element.outerHeight(); - } - } - - return this; - }, - - _createPlaceholder: function(that) { - that = that || this; - var className, - o = that.options; - - if(!o.placeholder || o.placeholder.constructor === String) { - className = o.placeholder; - o.placeholder = { - element: function() { - - var nodeName = that.currentItem[0].nodeName.toLowerCase(), - element = $( "<" + nodeName + ">", that.document[0] ) - .addClass(className || that.currentItem[0].className+" ui-sortable-placeholder") - .removeClass("ui-sortable-helper"); - - if ( nodeName === "tr" ) { - that.currentItem.children().each(function() { - $( "<td> </td>", that.document[0] ) - .attr( "colspan", $( this ).attr( "colspan" ) || 1 ) - .appendTo( element ); - }); - } else if ( nodeName === "img" ) { - element.attr( "src", that.currentItem.attr( "src" ) ); - } - - if ( !className ) { - element.css( "visibility", "hidden" ); - } - - return element; - }, - update: function(container, p) { - - // 1. If a className is set as 'placeholder option, we don't force sizes - the class is responsible for that - // 2. The option 'forcePlaceholderSize can be enabled to force it even if a class name is specified - if(className && !o.forcePlaceholderSize) { - return; - } - - //If the element doesn't have a actual height by itself (without styles coming from a stylesheet), it receives the inline height from the dragged item - if(!p.height()) { p.height(that.currentItem.innerHeight() - parseInt(that.currentItem.css("paddingTop")||0, 10) - parseInt(that.currentItem.css("paddingBottom")||0, 10)); } - if(!p.width()) { p.width(that.currentItem.innerWidth() - parseInt(that.currentItem.css("paddingLeft")||0, 10) - parseInt(that.currentItem.css("paddingRight")||0, 10)); } - } - }; - } - - //Create the placeholder - that.placeholder = $(o.placeholder.element.call(that.element, that.currentItem)); - - //Append it after the actual current item - that.currentItem.after(that.placeholder); - - //Update the size of the placeholder (TODO: Logic to fuzzy, see line 316/317) - o.placeholder.update(that, that.placeholder); - - }, - - _contactContainers: function(event) { - var i, j, dist, itemWithLeastDistance, posProperty, sizeProperty, base, cur, nearBottom, floating, - innermostContainer = null, - innermostIndex = null; - - // get innermost container that intersects with item - for (i = this.containers.length - 1; i >= 0; i--) { - - // never consider a container that's located within the item itself - if($.contains(this.currentItem[0], this.containers[i].element[0])) { - continue; - } - - if(this._intersectsWith(this.containers[i].containerCache)) { - - // if we've already found a container and it's more "inner" than this, then continue - if(innermostContainer && $.contains(this.containers[i].element[0], innermostContainer.element[0])) { - continue; - } - - innermostContainer = this.containers[i]; - innermostIndex = i; - - } else { - // container doesn't intersect. trigger "out" event if necessary - if(this.containers[i].containerCache.over) { - this.containers[i]._trigger("out", event, this._uiHash(this)); - this.containers[i].containerCache.over = 0; - } - } - - } - - // if no intersecting containers found, return - if(!innermostContainer) { - return; - } - - // move the item into the container if it's not there already - if(this.containers.length === 1) { - if (!this.containers[innermostIndex].containerCache.over) { - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - } else { - - //When entering a new container, we will find the item with the least distance and append our item near it - dist = 10000; - itemWithLeastDistance = null; - floating = innermostContainer.floating || isFloating(this.currentItem); - posProperty = floating ? "left" : "top"; - sizeProperty = floating ? "width" : "height"; - base = this.positionAbs[posProperty] + this.offset.click[posProperty]; - for (j = this.items.length - 1; j >= 0; j--) { - if(!$.contains(this.containers[innermostIndex].element[0], this.items[j].item[0])) { - continue; - } - if(this.items[j].item[0] === this.currentItem[0]) { - continue; - } - if (floating && !isOverAxis(this.positionAbs.top + this.offset.click.top, this.items[j].top, this.items[j].height)) { - continue; - } - cur = this.items[j].item.offset()[posProperty]; - nearBottom = false; - if(Math.abs(cur - base) > Math.abs(cur + this.items[j][sizeProperty] - base)){ - nearBottom = true; - cur += this.items[j][sizeProperty]; - } - - if(Math.abs(cur - base) < dist) { - dist = Math.abs(cur - base); itemWithLeastDistance = this.items[j]; - this.direction = nearBottom ? "up": "down"; - } - } - - //Check if dropOnEmpty is enabled - if(!itemWithLeastDistance && !this.options.dropOnEmpty) { - return; - } - - if(this.currentContainer === this.containers[innermostIndex]) { - return; - } - - itemWithLeastDistance ? this._rearrange(event, itemWithLeastDistance, null, true) : this._rearrange(event, null, this.containers[innermostIndex].element, true); - this._trigger("change", event, this._uiHash()); - this.containers[innermostIndex]._trigger("change", event, this._uiHash(this)); - this.currentContainer = this.containers[innermostIndex]; - - //Update the placeholder - this.options.placeholder.update(this.currentContainer, this.placeholder); - - this.containers[innermostIndex]._trigger("over", event, this._uiHash(this)); - this.containers[innermostIndex].containerCache.over = 1; - } - - - }, - - _createHelper: function(event) { - - var o = this.options, - helper = $.isFunction(o.helper) ? $(o.helper.apply(this.element[0], [event, this.currentItem])) : (o.helper === "clone" ? this.currentItem.clone() : this.currentItem); - - //Add the helper to the DOM if that didn't happen already - if(!helper.parents("body").length) { - $(o.appendTo !== "parent" ? o.appendTo : this.currentItem[0].parentNode)[0].appendChild(helper[0]); - } - - if(helper[0] === this.currentItem[0]) { - this._storedCSS = { width: this.currentItem[0].style.width, height: this.currentItem[0].style.height, position: this.currentItem.css("position"), top: this.currentItem.css("top"), left: this.currentItem.css("left") }; - } - - if(!helper[0].style.width || o.forceHelperSize) { - helper.width(this.currentItem.width()); - } - if(!helper[0].style.height || o.forceHelperSize) { - helper.height(this.currentItem.height()); - } - - return helper; - - }, - - _adjustOffsetFromHelper: function(obj) { - if (typeof obj === "string") { - obj = obj.split(" "); - } - if ($.isArray(obj)) { - obj = {left: +obj[0], top: +obj[1] || 0}; - } - if ("left" in obj) { - this.offset.click.left = obj.left + this.margins.left; - } - if ("right" in obj) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ("top" in obj) { - this.offset.click.top = obj.top + this.margins.top; - } - if ("bottom" in obj) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _getParentOffset: function() { - - - //Get the offsetParent and cache its position - this.offsetParent = this.helper.offsetParent(); - var po = this.offsetParent.offset(); - - // This is a special case where we need to modify a offset calculated on start, since the following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't the document, which means that - // the scroll is included in the initial calculation of the offset of the parent, and never recalculated upon drag - if(this.cssPosition === "absolute" && this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - // This needs to be actually done for all browsers, since pageX/pageY includes this information - // with an ugly IE fix - if( this.offsetParent[0] === document.body || (this.offsetParent[0].tagName && this.offsetParent[0].tagName.toLowerCase() === "html" && $.ui.ie)) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + (parseInt(this.offsetParent.css("borderTopWidth"),10) || 0), - left: po.left + (parseInt(this.offsetParent.css("borderLeftWidth"),10) || 0) - }; - - }, - - _getRelativeOffset: function() { - - if(this.cssPosition === "relative") { - var p = this.currentItem.position(); - return { - top: p.top - (parseInt(this.helper.css("top"),10) || 0) + this.scrollParent.scrollTop(), - left: p.left - (parseInt(this.helper.css("left"),10) || 0) + this.scrollParent.scrollLeft() - }; - } else { - return { top: 0, left: 0 }; - } - - }, - - _cacheMargins: function() { - this.margins = { - left: (parseInt(this.currentItem.css("marginLeft"),10) || 0), - top: (parseInt(this.currentItem.css("marginTop"),10) || 0) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var ce, co, over, - o = this.options; - if(o.containment === "parent") { - o.containment = this.helper[0].parentNode; - } - if(o.containment === "document" || o.containment === "window") { - this.containment = [ - 0 - this.offset.relative.left - this.offset.parent.left, - 0 - this.offset.relative.top - this.offset.parent.top, - $(o.containment === "document" ? document : window).width() - this.helperProportions.width - this.margins.left, - ($(o.containment === "document" ? document : window).height() || document.body.parentNode.scrollHeight) - this.helperProportions.height - this.margins.top - ]; - } - - if(!(/^(document|window|parent)$/).test(o.containment)) { - ce = $(o.containment)[0]; - co = $(o.containment).offset(); - over = ($(ce).css("overflow") !== "hidden"); - - this.containment = [ - co.left + (parseInt($(ce).css("borderLeftWidth"),10) || 0) + (parseInt($(ce).css("paddingLeft"),10) || 0) - this.margins.left, - co.top + (parseInt($(ce).css("borderTopWidth"),10) || 0) + (parseInt($(ce).css("paddingTop"),10) || 0) - this.margins.top, - co.left+(over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).css("borderLeftWidth"),10) || 0) - (parseInt($(ce).css("paddingRight"),10) || 0) - this.helperProportions.width - this.margins.left, - co.top+(over ? Math.max(ce.scrollHeight,ce.offsetHeight) : ce.offsetHeight) - (parseInt($(ce).css("borderTopWidth"),10) || 0) - (parseInt($(ce).css("paddingBottom"),10) || 0) - this.helperProportions.height - this.margins.top - ]; - } - - }, - - _convertPositionTo: function(d, pos) { - - if(!pos) { - pos = this.position; - } - var mod = d === "absolute" ? 1 : -1, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, - scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - return { - top: ( - pos.top + // The absolute mouse position - this.offset.relative.top * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) ) * mod) - ), - left: ( - pos.left + // The absolute mouse position - this.offset.relative.left * mod + // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left * mod - // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() ) * mod) - ) - }; - - }, - - _generatePosition: function(event) { - - var top, left, - o = this.options, - pageX = event.pageX, - pageY = event.pageY, - scroll = this.cssPosition === "absolute" && !(this.scrollParent[0] !== document && $.contains(this.scrollParent[0], this.offsetParent[0])) ? this.offsetParent : this.scrollParent, scrollIsRootNode = (/(html|body)/i).test(scroll[0].tagName); - - // This is another very weird special case that only happens for relative elements: - // 1. If the css position is relative - // 2. and the scroll parent is the document or similar to the offset parent - // we have to refresh the relative offset during the scroll so there are no jumps - if(this.cssPosition === "relative" && !(this.scrollParent[0] !== document && this.scrollParent[0] !== this.offsetParent[0])) { - this.offset.relative = this._getRelativeOffset(); - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - if(this.originalPosition) { //If we are not dragging yet, we won't check for options - - if(this.containment) { - if(event.pageX - this.offset.click.left < this.containment[0]) { - pageX = this.containment[0] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top < this.containment[1]) { - pageY = this.containment[1] + this.offset.click.top; - } - if(event.pageX - this.offset.click.left > this.containment[2]) { - pageX = this.containment[2] + this.offset.click.left; - } - if(event.pageY - this.offset.click.top > this.containment[3]) { - pageY = this.containment[3] + this.offset.click.top; - } - } - - if(o.grid) { - top = this.originalPageY + Math.round((pageY - this.originalPageY) / o.grid[1]) * o.grid[1]; - pageY = this.containment ? ( (top - this.offset.click.top >= this.containment[1] && top - this.offset.click.top <= this.containment[3]) ? top : ((top - this.offset.click.top >= this.containment[1]) ? top - o.grid[1] : top + o.grid[1])) : top; - - left = this.originalPageX + Math.round((pageX - this.originalPageX) / o.grid[0]) * o.grid[0]; - pageX = this.containment ? ( (left - this.offset.click.left >= this.containment[0] && left - this.offset.click.left <= this.containment[2]) ? left : ((left - this.offset.click.left >= this.containment[0]) ? left - o.grid[0] : left + o.grid[0])) : left; - } - - } - - return { - top: ( - pageY - // The absolute mouse position - this.offset.click.top - // Click offset (relative to the element) - this.offset.relative.top - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.top + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollTop() : ( scrollIsRootNode ? 0 : scroll.scrollTop() ) )) - ), - left: ( - pageX - // The absolute mouse position - this.offset.click.left - // Click offset (relative to the element) - this.offset.relative.left - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.parent.left + // The offsetParent's offset without borders (offset + border) - ( ( this.cssPosition === "fixed" ? -this.scrollParent.scrollLeft() : scrollIsRootNode ? 0 : scroll.scrollLeft() )) - ) - }; - - }, - - _rearrange: function(event, i, a, hardRefresh) { - - a ? a[0].appendChild(this.placeholder[0]) : i.item[0].parentNode.insertBefore(this.placeholder[0], (this.direction === "down" ? i.item[0] : i.item[0].nextSibling)); - - //Various things done here to improve the performance: - // 1. we create a setTimeout, that calls refreshPositions - // 2. on the instance, we have a counter variable, that get's higher after every append - // 3. on the local scope, we copy the counter variable, and check in the timeout, if it's still the same - // 4. this lets only the last addition to the timeout stack through - this.counter = this.counter ? ++this.counter : 1; - var counter = this.counter; - - this._delay(function() { - if(counter === this.counter) { - this.refreshPositions(!hardRefresh); //Precompute after each DOM insertion, NOT on mousemove - } - }); - - }, - - _clear: function(event, noPropagation) { - - this.reverting = false; - // We delay all events that have to be triggered to after the point where the placeholder has been removed and - // everything else normalized again - var i, - delayedTriggers = []; - - // We first have to update the dom position of the actual currentItem - // Note: don't do it if the current item is already removed (by a user), or it gets reappended (see #4088) - if(!this._noFinalSort && this.currentItem.parent().length) { - this.placeholder.before(this.currentItem); - } - this._noFinalSort = null; - - if(this.helper[0] === this.currentItem[0]) { - for(i in this._storedCSS) { - if(this._storedCSS[i] === "auto" || this._storedCSS[i] === "static") { - this._storedCSS[i] = ""; - } - } - this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"); - } else { - this.currentItem.show(); - } - - if(this.fromOutside && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("receive", event, this._uiHash(this.fromOutside)); }); - } - if((this.fromOutside || this.domPosition.prev !== this.currentItem.prev().not(".ui-sortable-helper")[0] || this.domPosition.parent !== this.currentItem.parent()[0]) && !noPropagation) { - delayedTriggers.push(function(event) { this._trigger("update", event, this._uiHash()); }); //Trigger update callback if the DOM position has changed - } - - // Check if the items Container has Changed and trigger appropriate - // events. - if (this !== this.currentContainer) { - if(!noPropagation) { - delayedTriggers.push(function(event) { this._trigger("remove", event, this._uiHash()); }); - delayedTriggers.push((function(c) { return function(event) { c._trigger("receive", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - delayedTriggers.push((function(c) { return function(event) { c._trigger("update", event, this._uiHash(this)); }; }).call(this, this.currentContainer)); - } - } - - - //Post events to containers - function delayEvent( type, instance, container ) { - return function( event ) { - container._trigger( type, event, instance._uiHash( instance ) ); - }; - } - for (i = this.containers.length - 1; i >= 0; i--){ - if (!noPropagation) { - delayedTriggers.push( delayEvent( "deactivate", this, this.containers[ i ] ) ); - } - if(this.containers[i].containerCache.over) { - delayedTriggers.push( delayEvent( "out", this, this.containers[ i ] ) ); - this.containers[i].containerCache.over = 0; - } - } - - //Do what was originally in plugins - if ( this.storedCursor ) { - this.document.find( "body" ).css( "cursor", this.storedCursor ); - this.storedStylesheet.remove(); - } - if(this._storedOpacity) { - this.helper.css("opacity", this._storedOpacity); - } - if(this._storedZIndex) { - this.helper.css("zIndex", this._storedZIndex === "auto" ? "" : this._storedZIndex); - } - - this.dragging = false; - if(this.cancelHelperRemoval) { - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return false; - } - - if(!noPropagation) { - this._trigger("beforeStop", event, this._uiHash()); - } - - //$(this.placeholder[0]).remove(); would have been the jQuery way - unfortunately, it unbinds ALL events from the original node! - this.placeholder[0].parentNode.removeChild(this.placeholder[0]); - - if(this.helper[0] !== this.currentItem[0]) { - this.helper.remove(); - } - this.helper = null; - - if(!noPropagation) { - for (i=0; i < delayedTriggers.length; i++) { - delayedTriggers[i].call(this, event); - } //Trigger all delayed events - this._trigger("stop", event, this._uiHash()); - } - - this.fromOutside = false; - return true; - - }, - - _trigger: function() { - if ($.Widget.prototype._trigger.apply(this, arguments) === false) { - this.cancel(); - } - }, - - _uiHash: function(_inst) { - var inst = _inst || this; - return { - helper: inst.helper, - placeholder: inst.placeholder || $([]), - position: inst.position, - originalPosition: inst.originalPosition, - offset: inst.positionAbs, - item: inst.currentItem, - sender: _inst ? _inst.element : null - }; - } - -}); - -})(jQuery); -(function( $, undefined ) { - -// number of pages in a slider -// (how many times can you page up/down to go through the whole range) -var numPages = 5; - -$.widget( "ui.slider", $.ui.mouse, { - version: "1.10.4", - widgetEventPrefix: "slide", - - options: { - animate: false, - distance: 0, - max: 100, - min: 0, - orientation: "horizontal", - range: false, - step: 1, - value: 0, - values: null, - - // callbacks - change: null, - slide: null, - start: null, - stop: null - }, - - _create: function() { - this._keySliding = false; - this._mouseSliding = false; - this._animateOff = true; - this._handleIndex = null; - this._detectOrientation(); - this._mouseInit(); - - this.element - .addClass( "ui-slider" + - " ui-slider-" + this.orientation + - " ui-widget" + - " ui-widget-content" + - " ui-corner-all"); - - this._refresh(); - this._setOption( "disabled", this.options.disabled ); - - this._animateOff = false; - }, - - _refresh: function() { - this._createRange(); - this._createHandles(); - this._setupEvents(); - this._refreshValue(); - }, - - _createHandles: function() { - var i, handleCount, - options = this.options, - existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), - handle = "<a class='ui-slider-handle ui-state-default ui-corner-all' href='#'></a>", - handles = []; - - handleCount = ( options.values && options.values.length ) || 1; - - if ( existingHandles.length > handleCount ) { - existingHandles.slice( handleCount ).remove(); - existingHandles = existingHandles.slice( 0, handleCount ); - } - - for ( i = existingHandles.length; i < handleCount; i++ ) { - handles.push( handle ); - } - - this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); - - this.handle = this.handles.eq( 0 ); - - this.handles.each(function( i ) { - $( this ).data( "ui-slider-handle-index", i ); - }); - }, - - _createRange: function() { - var options = this.options, - classes = ""; - - if ( options.range ) { - if ( options.range === true ) { - if ( !options.values ) { - options.values = [ this._valueMin(), this._valueMin() ]; - } else if ( options.values.length && options.values.length !== 2 ) { - options.values = [ options.values[0], options.values[0] ]; - } else if ( $.isArray( options.values ) ) { - options.values = options.values.slice(0); - } - } - - if ( !this.range || !this.range.length ) { - this.range = $( "<div></div>" ) - .appendTo( this.element ); - - classes = "ui-slider-range" + - // note: this isn't the most fittingly semantic framework class for this element, - // but worked best visually with a variety of themes - " ui-widget-header ui-corner-all"; - } else { - this.range.removeClass( "ui-slider-range-min ui-slider-range-max" ) - // Handle range switching from true to min/max - .css({ - "left": "", - "bottom": "" - }); - } - - this.range.addClass( classes + - ( ( options.range === "min" || options.range === "max" ) ? " ui-slider-range-" + options.range : "" ) ); - } else { - if ( this.range ) { - this.range.remove(); - } - this.range = null; - } - }, - - _setupEvents: function() { - var elements = this.handles.add( this.range ).filter( "a" ); - this._off( elements ); - this._on( elements, this._handleEvents ); - this._hoverable( elements ); - this._focusable( elements ); - }, - - _destroy: function() { - this.handles.remove(); - if ( this.range ) { - this.range.remove(); - } - - this.element - .removeClass( "ui-slider" + - " ui-slider-horizontal" + - " ui-slider-vertical" + - " ui-widget" + - " ui-widget-content" + - " ui-corner-all" ); - - this._mouseDestroy(); - }, - - _mouseCapture: function( event ) { - var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, - that = this, - o = this.options; - - if ( o.disabled ) { - return false; - } - - this.elementSize = { - width: this.element.outerWidth(), - height: this.element.outerHeight() - }; - this.elementOffset = this.element.offset(); - - position = { x: event.pageX, y: event.pageY }; - normValue = this._normValueFromMouse( position ); - distance = this._valueMax() - this._valueMin() + 1; - this.handles.each(function( i ) { - var thisDistance = Math.abs( normValue - that.values(i) ); - if (( distance > thisDistance ) || - ( distance === thisDistance && - (i === that._lastChangedValue || that.values(i) === o.min ))) { - distance = thisDistance; - closestHandle = $( this ); - index = i; - } - }); - - allowed = this._start( event, index ); - if ( allowed === false ) { - return false; - } - this._mouseSliding = true; - - this._handleIndex = index; - - closestHandle - .addClass( "ui-state-active" ) - .focus(); - - offset = closestHandle.offset(); - mouseOverHandle = !$( event.target ).parents().addBack().is( ".ui-slider-handle" ); - this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { - left: event.pageX - offset.left - ( closestHandle.width() / 2 ), - top: event.pageY - offset.top - - ( closestHandle.height() / 2 ) - - ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - - ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + - ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) - }; - - if ( !this.handles.hasClass( "ui-state-hover" ) ) { - this._slide( event, index, normValue ); - } - this._animateOff = true; - return true; - }, - - _mouseStart: function() { - return true; - }, - - _mouseDrag: function( event ) { - var position = { x: event.pageX, y: event.pageY }, - normValue = this._normValueFromMouse( position ); - - this._slide( event, this._handleIndex, normValue ); - - return false; - }, - - _mouseStop: function( event ) { - this.handles.removeClass( "ui-state-active" ); - this._mouseSliding = false; - - this._stop( event, this._handleIndex ); - this._change( event, this._handleIndex ); - - this._handleIndex = null; - this._clickOffset = null; - this._animateOff = false; - - return false; - }, - - _detectOrientation: function() { - this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; - }, - - _normValueFromMouse: function( position ) { - var pixelTotal, - pixelMouse, - percentMouse, - valueTotal, - valueMouse; - - if ( this.orientation === "horizontal" ) { - pixelTotal = this.elementSize.width; - pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); - } else { - pixelTotal = this.elementSize.height; - pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); - } - - percentMouse = ( pixelMouse / pixelTotal ); - if ( percentMouse > 1 ) { - percentMouse = 1; - } - if ( percentMouse < 0 ) { - percentMouse = 0; - } - if ( this.orientation === "vertical" ) { - percentMouse = 1 - percentMouse; - } - - valueTotal = this._valueMax() - this._valueMin(); - valueMouse = this._valueMin() + percentMouse * valueTotal; - - return this._trimAlignValue( valueMouse ); - }, - - _start: function( event, index ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - return this._trigger( "start", event, uiHash ); - }, - - _slide: function( event, index, newVal ) { - var otherVal, - newValues, - allowed; - - if ( this.options.values && this.options.values.length ) { - otherVal = this.values( index ? 0 : 1 ); - - if ( ( this.options.values.length === 2 && this.options.range === true ) && - ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) - ) { - newVal = otherVal; - } - - if ( newVal !== this.values( index ) ) { - newValues = this.values(); - newValues[ index ] = newVal; - // A slide can be canceled by returning false from the slide callback - allowed = this._trigger( "slide", event, { - handle: this.handles[ index ], - value: newVal, - values: newValues - } ); - otherVal = this.values( index ? 0 : 1 ); - if ( allowed !== false ) { - this.values( index, newVal ); - } - } - } else { - if ( newVal !== this.value() ) { - // A slide can be canceled by returning false from the slide callback - allowed = this._trigger( "slide", event, { - handle: this.handles[ index ], - value: newVal - } ); - if ( allowed !== false ) { - this.value( newVal ); - } - } - } - }, - - _stop: function( event, index ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - - this._trigger( "stop", event, uiHash ); - }, - - _change: function( event, index ) { - if ( !this._keySliding && !this._mouseSliding ) { - var uiHash = { - handle: this.handles[ index ], - value: this.value() - }; - if ( this.options.values && this.options.values.length ) { - uiHash.value = this.values( index ); - uiHash.values = this.values(); - } - - //store the last changed value index for reference when handles overlap - this._lastChangedValue = index; - - this._trigger( "change", event, uiHash ); - } - }, - - value: function( newValue ) { - if ( arguments.length ) { - this.options.value = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, 0 ); - return; - } - - return this._value(); - }, - - values: function( index, newValue ) { - var vals, - newValues, - i; - - if ( arguments.length > 1 ) { - this.options.values[ index ] = this._trimAlignValue( newValue ); - this._refreshValue(); - this._change( null, index ); - return; - } - - if ( arguments.length ) { - if ( $.isArray( arguments[ 0 ] ) ) { - vals = this.options.values; - newValues = arguments[ 0 ]; - for ( i = 0; i < vals.length; i += 1 ) { - vals[ i ] = this._trimAlignValue( newValues[ i ] ); - this._change( null, i ); - } - this._refreshValue(); - } else { - if ( this.options.values && this.options.values.length ) { - return this._values( index ); - } else { - return this.value(); - } - } - } else { - return this._values(); - } - }, - - _setOption: function( key, value ) { - var i, - valsLength = 0; - - if ( key === "range" && this.options.range === true ) { - if ( value === "min" ) { - this.options.value = this._values( 0 ); - this.options.values = null; - } else if ( value === "max" ) { - this.options.value = this._values( this.options.values.length-1 ); - this.options.values = null; - } - } - - if ( $.isArray( this.options.values ) ) { - valsLength = this.options.values.length; - } - - $.Widget.prototype._setOption.apply( this, arguments ); - - switch ( key ) { - case "orientation": - this._detectOrientation(); - this.element - .removeClass( "ui-slider-horizontal ui-slider-vertical" ) - .addClass( "ui-slider-" + this.orientation ); - this._refreshValue(); - break; - case "value": - this._animateOff = true; - this._refreshValue(); - this._change( null, 0 ); - this._animateOff = false; - break; - case "values": - this._animateOff = true; - this._refreshValue(); - for ( i = 0; i < valsLength; i += 1 ) { - this._change( null, i ); - } - this._animateOff = false; - break; - case "min": - case "max": - this._animateOff = true; - this._refreshValue(); - this._animateOff = false; - break; - case "range": - this._animateOff = true; - this._refresh(); - this._animateOff = false; - break; - } - }, - - //internal value getter - // _value() returns value trimmed by min and max, aligned by step - _value: function() { - var val = this.options.value; - val = this._trimAlignValue( val ); - - return val; - }, - - //internal values getter - // _values() returns array of values trimmed by min and max, aligned by step - // _values( index ) returns single value trimmed by min and max, aligned by step - _values: function( index ) { - var val, - vals, - i; - - if ( arguments.length ) { - val = this.options.values[ index ]; - val = this._trimAlignValue( val ); - - return val; - } else if ( this.options.values && this.options.values.length ) { - // .slice() creates a copy of the array - // this copy gets trimmed by min and max and then returned - vals = this.options.values.slice(); - for ( i = 0; i < vals.length; i+= 1) { - vals[ i ] = this._trimAlignValue( vals[ i ] ); - } - - return vals; - } else { - return []; - } - }, - - // returns the step-aligned value that val is closest to, between (inclusive) min and max - _trimAlignValue: function( val ) { - if ( val <= this._valueMin() ) { - return this._valueMin(); - } - if ( val >= this._valueMax() ) { - return this._valueMax(); - } - var step = ( this.options.step > 0 ) ? this.options.step : 1, - valModStep = (val - this._valueMin()) % step, - alignValue = val - valModStep; - - if ( Math.abs(valModStep) * 2 >= step ) { - alignValue += ( valModStep > 0 ) ? step : ( -step ); - } - - // Since JavaScript has problems with large floats, round - // the final value to 5 digits after the decimal point (see #4124) - return parseFloat( alignValue.toFixed(5) ); - }, - - _valueMin: function() { - return this.options.min; - }, - - _valueMax: function() { - return this.options.max; - }, - - _refreshValue: function() { - var lastValPercent, valPercent, value, valueMin, valueMax, - oRange = this.options.range, - o = this.options, - that = this, - animate = ( !this._animateOff ) ? o.animate : false, - _set = {}; - - if ( this.options.values && this.options.values.length ) { - this.handles.each(function( i ) { - valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; - _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - if ( that.options.range === true ) { - if ( that.orientation === "horizontal" ) { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } else { - if ( i === 0 ) { - that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); - } - if ( i === 1 ) { - that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } - } - lastValPercent = valPercent; - }); - } else { - value = this.value(); - valueMin = this._valueMin(); - valueMax = this._valueMax(); - valPercent = ( valueMax !== valueMin ) ? - ( value - valueMin ) / ( valueMax - valueMin ) * 100 : - 0; - _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; - this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); - - if ( oRange === "min" && this.orientation === "horizontal" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); - } - if ( oRange === "max" && this.orientation === "horizontal" ) { - this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - if ( oRange === "min" && this.orientation === "vertical" ) { - this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); - } - if ( oRange === "max" && this.orientation === "vertical" ) { - this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); - } - } - }, - - _handleEvents: { - keydown: function( event ) { - var allowed, curVal, newVal, step, - index = $( event.target ).data( "ui-slider-handle-index" ); - - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - case $.ui.keyCode.END: - case $.ui.keyCode.PAGE_UP: - case $.ui.keyCode.PAGE_DOWN: - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - event.preventDefault(); - if ( !this._keySliding ) { - this._keySliding = true; - $( event.target ).addClass( "ui-state-active" ); - allowed = this._start( event, index ); - if ( allowed === false ) { - return; - } - } - break; - } - - step = this.options.step; - if ( this.options.values && this.options.values.length ) { - curVal = newVal = this.values( index ); - } else { - curVal = newVal = this.value(); - } - - switch ( event.keyCode ) { - case $.ui.keyCode.HOME: - newVal = this._valueMin(); - break; - case $.ui.keyCode.END: - newVal = this._valueMax(); - break; - case $.ui.keyCode.PAGE_UP: - newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); - break; - case $.ui.keyCode.PAGE_DOWN: - newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); - break; - case $.ui.keyCode.UP: - case $.ui.keyCode.RIGHT: - if ( curVal === this._valueMax() ) { - return; - } - newVal = this._trimAlignValue( curVal + step ); - break; - case $.ui.keyCode.DOWN: - case $.ui.keyCode.LEFT: - if ( curVal === this._valueMin() ) { - return; - } - newVal = this._trimAlignValue( curVal - step ); - break; - } - - this._slide( event, index, newVal ); - }, - click: function( event ) { - event.preventDefault(); - }, - keyup: function( event ) { - var index = $( event.target ).data( "ui-slider-handle-index" ); - - if ( this._keySliding ) { - this._keySliding = false; - this._stop( event, index ); - this._change( event, index ); - $( event.target ).removeClass( "ui-state-active" ); - } - } - } - -}); - -}(jQuery)); diff --git a/src/UI/JsLibraries/jquery.backstretch.js b/src/UI/JsLibraries/jquery.backstretch.js deleted file mode 100644 index 7ac0875f8..000000000 --- a/src/UI/JsLibraries/jquery.backstretch.js +++ /dev/null @@ -1,377 +0,0 @@ -/*! Backstretch - v2.0.4 - 2013-06-19 -* http://srobbin.com/jquery-plugins/backstretch/ -* Copyright (c) 2013 Scott Robbin; Licensed MIT */ - -;(function ($, window, undefined) { - 'use strict'; - - /* PLUGIN DEFINITION - * ========================= */ - - $.fn.backstretch = function (images, options) { - // We need at least one image or method name - if (images === undefined || images.length === 0) { - $.error("No images were supplied for Backstretch"); - } - - /* - * Scroll the page one pixel to get the right window height on iOS - * Pretty harmless for everyone else - */ - if ($(window).scrollTop() === 0 ) { - window.scrollTo(0, 0); - } - - return this.each(function () { - var $this = $(this) - , obj = $this.data('backstretch'); - - // Do we already have an instance attached to this element? - if (obj) { - - // Is this a method they're trying to execute? - if (typeof images == 'string' && typeof obj[images] == 'function') { - // Call the method - obj[images](options); - - // No need to do anything further - return; - } - - // Merge the old options with the new - options = $.extend(obj.options, options); - - // Remove the old instance - obj.destroy(true); - } - - obj = new Backstretch(this, images, options); - $this.data('backstretch', obj); - }); - }; - - // If no element is supplied, we'll attach to body - $.backstretch = function (images, options) { - // Return the instance - return $('body') - .backstretch(images, options) - .data('backstretch'); - }; - - // Custom selector - $.expr[':'].backstretch = function(elem) { - return $(elem).data('backstretch') !== undefined; - }; - - /* DEFAULTS - * ========================= */ - - $.fn.backstretch.defaults = { - centeredX: true // Should we center the image on the X axis? - , centeredY: true // Should we center the image on the Y axis? - , duration: 5000 // Amount of time in between slides (if slideshow) - , fade: 0 // Speed of fade transition between slides - }; - - /* STYLES - * - * Baked-in styles that we'll apply to our elements. - * In an effort to keep the plugin simple, these are not exposed as options. - * That said, anyone can override these in their own stylesheet. - * ========================= */ - var styles = { - wrap: { - left: 0 - , top: 0 - , overflow: 'hidden' - , margin: 0 - , padding: 0 - , height: '100%' - , width: '100%' - , zIndex: -999999 - } - , img: { - position: 'absolute' - , display: 'none' - , margin: 0 - , padding: 0 - , border: 'none' - , width: 'auto' - , height: 'auto' - , maxHeight: 'none' - , maxWidth: 'none' - , zIndex: -999999 - } - }; - - /* CLASS DEFINITION - * ========================= */ - var Backstretch = function (container, images, options) { - this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); - - /* In its simplest form, we allow Backstretch to be called on an image path. - * e.g. $.backstretch('/path/to/image.jpg') - * So, we need to turn this back into an array. - */ - this.images = $.isArray(images) ? images : [images]; - - // Preload images - $.each(this.images, function () { - $('<img />')[0].src = this; - }); - - // Convenience reference to know if the container is body. - this.isBody = container === document.body; - - /* We're keeping track of a few different elements - * - * Container: the element that Backstretch was called on. - * Wrap: a DIV that we place the image into, so we can hide the overflow. - * Root: Convenience reference to help calculate the correct height. - */ - this.$container = $(container); - this.$root = this.isBody ? supportsFixedPosition ? $(window) : $(document) : this.$container; - - // Don't create a new wrap if one already exists (from a previous instance of Backstretch) - var $existing = this.$container.children(".backstretch").first(); - this.$wrap = $existing.length ? $existing : $('<div class="backstretch"></div>').css(styles.wrap).appendTo(this.$container); - - // Non-body elements need some style adjustments - if (!this.isBody) { - // If the container is statically positioned, we need to make it relative, - // and if no zIndex is defined, we should set it to zero. - var position = this.$container.css('position') - , zIndex = this.$container.css('zIndex'); - - this.$container.css({ - position: position === 'static' ? 'relative' : position - , zIndex: zIndex === 'auto' ? 0 : zIndex - , background: 'none' - }); - - // Needs a higher z-index - this.$wrap.css({zIndex: -999998}); - } - - // Fixed or absolute positioning? - this.$wrap.css({ - position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' - }); - - // Set the first image - this.index = 0; - this.show(this.index); - - // Listen for resize - $(window).on('resize.backstretch', $.proxy(this.resize, this)) - .on('orientationchange.backstretch', $.proxy(function () { - // Need to do this in order to get the right window height - if (this.isBody && window.pageYOffset === 0) { - window.scrollTo(0, 1); - this.resize(); - } - }, this)); - }; - - /* PUBLIC METHODS - * ========================= */ - Backstretch.prototype = { - resize: function () { - try { - var bgCSS = {left: 0, top: 0} - , rootWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() - , bgWidth = rootWidth - , rootHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() - , bgHeight = bgWidth / this.$img.data('ratio') - , bgOffset; - - // Make adjustments based on image ratio - if (bgHeight >= rootHeight) { - bgOffset = (bgHeight - rootHeight) / 2; - if(this.options.centeredY) { - bgCSS.top = '-' + bgOffset + 'px'; - } - } else { - bgHeight = rootHeight; - bgWidth = bgHeight * this.$img.data('ratio'); - bgOffset = (bgWidth - rootWidth) / 2; - if(this.options.centeredX) { - bgCSS.left = '-' + bgOffset + 'px'; - } - } - - this.$wrap.css({width: rootWidth, height: rootHeight}) - .find('img:not(.deleteable)').css({width: bgWidth, height: bgHeight}).css(bgCSS); - } catch(err) { - // IE7 seems to trigger resize before the image is loaded. - // This try/catch block is a hack to let it fail gracefully. - } - - return this; - } - - // Show the slide at a certain position - , show: function (newIndex) { - - // Validate index - if (Math.abs(newIndex) > this.images.length - 1) { - return; - } - - // Vars - var self = this - , oldImage = self.$wrap.find('img').addClass('deleteable') - , evtOptions = { relatedTarget: self.$container[0] }; - - // Trigger the "before" event - self.$container.trigger($.Event('backstretch.before', evtOptions), [self, newIndex]); - - // Set the new index - this.index = newIndex; - - // Pause the slideshow - clearInterval(self.interval); - - // New image - self.$img = $('<img />') - .css(styles.img) - .bind('load', function (e) { - var imgWidth = this.width || $(e.target).width() - , imgHeight = this.height || $(e.target).height(); - - // Save the ratio - $(this).data('ratio', imgWidth / imgHeight); - - // Show the image, then delete the old one - // "speed" option has been deprecated, but we want backwards compatibilty - $(this).fadeIn(self.options.speed || self.options.fade, function () { - oldImage.remove(); - - // Resume the slideshow - if (!self.paused) { - self.cycle(); - } - - // Trigger the "after" and "show" events - // "show" is being deprecated - $(['after', 'show']).each(function () { - self.$container.trigger($.Event('backstretch.' + this, evtOptions), [self, newIndex]); - }); - }); - - // Resize - self.resize(); - }) - .appendTo(self.$wrap); - - // Hack for IE img onload event - self.$img.attr('src', self.images[newIndex]); - return self; - } - - , next: function () { - // Next slide - return this.show(this.index < this.images.length - 1 ? this.index + 1 : 0); - } - - , prev: function () { - // Previous slide - return this.show(this.index === 0 ? this.images.length - 1 : this.index - 1); - } - - , pause: function () { - // Pause the slideshow - this.paused = true; - return this; - } - - , resume: function () { - // Resume the slideshow - this.paused = false; - this.next(); - return this; - } - - , cycle: function () { - // Start/resume the slideshow - if(this.images.length > 1) { - // Clear the interval, just in case - clearInterval(this.interval); - - this.interval = setInterval($.proxy(function () { - // Check for paused slideshow - if (!this.paused) { - this.next(); - } - }, this), this.options.duration); - } - return this; - } - - , destroy: function (preserveBackground) { - // Stop the resize events - $(window).off('resize.backstretch orientationchange.backstretch'); - - // Clear the interval - clearInterval(this.interval); - - // Remove Backstretch - if(!preserveBackground) { - this.$wrap.remove(); - } - this.$container.removeData('backstretch'); - } - }; - - /* SUPPORTS FIXED POSITION? - * - * Based on code from jQuery Mobile 1.1.0 - * http://jquerymobile.com/ - * - * In a nutshell, we need to figure out if fixed positioning is supported. - * Unfortunately, this is very difficult to do on iOS, and usually involves - * injecting content, scrolling the page, etc.. It's ugly. - * jQuery Mobile uses this workaround. It's not ideal, but works. - * - * Modified to detect IE6 - * ========================= */ - - var supportsFixedPosition = (function () { - var ua = navigator.userAgent - , platform = navigator.platform - // Rendering engine is Webkit, and capture major version - , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) - , wkversion = !!wkmatch && wkmatch[ 1 ] - , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) - , ffversion = !!ffmatch && ffmatch[ 1 ] - , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) - , omversion = !!operammobilematch && operammobilematch[ 1 ] - , iematch = ua.match( /MSIE ([0-9]+)/ ) - , ieversion = !!iematch && iematch[ 1 ]; - - return !( - // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) - ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || - - // Opera Mini - (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || - (operammobilematch && omversion < 7458) || - - //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) - (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || - - // Firefox Mobile before 6.0 - - (ffversion && ffversion < 6) || - - // WebOS less than 3 - ("palmGetResource" in window && wkversion && wkversion < 534) || - - // MeeGo - (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || - - // IE6 - (ieversion && ieversion <= 6) - ); - }()); - -}(jQuery, window)); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery.dotdotdot.js b/src/UI/JsLibraries/jquery.dotdotdot.js deleted file mode 100644 index c7f58d039..000000000 --- a/src/UI/JsLibraries/jquery.dotdotdot.js +++ /dev/null @@ -1,632 +0,0 @@ -/* - * jQuery dotdotdot 1.6.1 - * - * Copyright (c) 2013 Fred Heusschen - * www.frebsite.nl - * - * Plugin website: - * dotdotdot.frebsite.nl - * - * Dual licensed under the MIT and GPL licenses. - * http://en.wikipedia.org/wiki/MIT_License - * http://en.wikipedia.org/wiki/GNU_General_Public_License - */ - -(function( $ ) -{ - if ( $.fn.dotdotdot ) - { - return; - } - - $.fn.dotdotdot = function( o ) - { - if ( this.length == 0 ) - { - if ( !o || o.debug !== false ) - { - debug( true, 'No element found for "' + this.selector + '".' ); - } - return this; - } - if ( this.length > 1 ) - { - return this.each( - function() - { - $(this).dotdotdot( o ); - } - ); - } - - - var $dot = this; - - if ( $dot.data( 'dotdotdot' ) ) - { - $dot.trigger( 'destroy.dot' ); - } - - $dot.data( 'dotdotdot-style', $dot.attr( 'style' ) ); - $dot.css( 'word-wrap', 'break-word' ); - if ($dot.css( 'white-space' ) === 'nowrap') - { - $dot.css( 'white-space', 'normal' ); - } - - $dot.bind_events = function() - { - $dot.bind( - 'update.dot', - function( e, c ) - { - e.preventDefault(); - e.stopPropagation(); - - opts.maxHeight = ( typeof opts.height == 'number' ) - ? opts.height - : getTrueInnerHeight( $dot ); - - opts.maxHeight += opts.tolerance; - - if ( typeof c != 'undefined' ) - { - if ( typeof c == 'string' || c instanceof HTMLElement ) - { - c = $('<div />').append( c ).contents(); - } - if ( c instanceof $ ) - { - orgContent = c; - } - } - - $inr = $dot.wrapInner( '<div class="dotdotdot" />' ).children(); - $inr.empty() - .append( orgContent.clone( true ) ) - .css({ - 'height' : 'auto', - 'width' : 'auto', - 'border' : 'none', - 'padding' : 0, - 'margin' : 0 - }); - - var after = false, - trunc = false; - - if ( conf.afterElement ) - { - after = conf.afterElement.clone( true ); - conf.afterElement.remove(); - } - if ( test( $inr, opts ) ) - { - if ( opts.wrap == 'children' ) - { - trunc = children( $inr, opts, after ); - } - else - { - trunc = ellipsis( $inr, $dot, $inr, opts, after ); - } - } - $inr.replaceWith( $inr.contents() ); - $inr = null; - - if ( $.isFunction( opts.callback ) ) - { - opts.callback.call( $dot[ 0 ], trunc, orgContent ); - } - - conf.isTruncated = trunc; - return trunc; - } - - ).bind( - 'isTruncated.dot', - function( e, fn ) - { - e.preventDefault(); - e.stopPropagation(); - - if ( typeof fn == 'function' ) - { - fn.call( $dot[ 0 ], conf.isTruncated ); - } - return conf.isTruncated; - } - - ).bind( - 'originalContent.dot', - function( e, fn ) - { - e.preventDefault(); - e.stopPropagation(); - - if ( typeof fn == 'function' ) - { - fn.call( $dot[ 0 ], orgContent ); - } - return orgContent; - } - - ).bind( - 'destroy.dot', - function( e ) - { - e.preventDefault(); - e.stopPropagation(); - - $dot.unwatch() - .unbind_events() - .empty() - .append( orgContent ) - .attr( 'style', $dot.data( 'dotdotdot-style' ) ) - .data( 'dotdotdot', false ); - } - ); - return $dot; - }; // /bind_events - - $dot.unbind_events = function() - { - $dot.unbind('.dot'); - return $dot; - }; // /unbind_events - - $dot.watch = function() - { - $dot.unwatch(); - if ( opts.watch == 'window' ) - { - var $window = $(window), - _wWidth = $window.width(), - _wHeight = $window.height(); - - $window.bind( - 'resize.dot' + conf.dotId, - function() - { - if ( _wWidth != $window.width() || _wHeight != $window.height() || !opts.windowResizeFix ) - { - _wWidth = $window.width(); - _wHeight = $window.height(); - - if ( watchInt ) - { - clearInterval( watchInt ); - } - watchInt = setTimeout( - function() - { - $dot.trigger( 'update.dot' ); - }, 10 - ); - } - } - ); - } - else - { - watchOrg = getSizes( $dot ); - watchInt = setInterval( - function() - { - var watchNew = getSizes( $dot ); - if ( watchOrg.width != watchNew.width || - watchOrg.height != watchNew.height ) - { - $dot.trigger( 'update.dot' ); - watchOrg = getSizes( $dot ); - } - }, 100 - ); - } - return $dot; - }; - $dot.unwatch = function() - { - $(window).unbind( 'resize.dot' + conf.dotId ); - if ( watchInt ) - { - clearInterval( watchInt ); - } - return $dot; - }; - - var orgContent = $dot.contents(), - opts = $.extend( true, {}, $.fn.dotdotdot.defaults, o ), - conf = {}, - watchOrg = {}, - watchInt = null, - $inr = null; - - - if ( !( opts.lastCharacter.remove instanceof Array ) ) - { - opts.lastCharacter.remove = $.fn.dotdotdot.defaultArrays.lastCharacter.remove; - } - if ( !( opts.lastCharacter.noEllipsis instanceof Array ) ) - { - opts.lastCharacter.noEllipsis = $.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis; - } - - - conf.afterElement = getElement( opts.after, $dot ); - conf.isTruncated = false; - conf.dotId = dotId++; - - - $dot.data( 'dotdotdot', true ) - .bind_events() - .trigger( 'update.dot' ); - - if ( opts.watch ) - { - $dot.watch(); - } - - return $dot; - }; - - - // public - $.fn.dotdotdot.defaults = { - 'ellipsis' : '... ', - 'wrap' : 'word', - 'fallbackToLetter' : true, - 'lastCharacter' : {}, - 'tolerance' : 0, - 'callback' : null, - 'after' : null, - 'height' : null, - 'watch' : false, - 'windowResizeFix' : true, - 'debug' : false - }; - $.fn.dotdotdot.defaultArrays = { - 'lastCharacter' : { - 'remove' : [ ' ', '\u3000', ',', ';', '.', '!', '?' ], - 'noEllipsis' : [] - } - }; - - - // private - var dotId = 1; - - function children( $elem, o, after ) - { - var $elements = $elem.children(), - isTruncated = false; - - $elem.empty(); - - for ( var a = 0, l = $elements.length; a < l; a++ ) - { - var $e = $elements.eq( a ); - $elem.append( $e ); - if ( after ) - { - $elem.append( after ); - } - if ( test( $elem, o ) ) - { - $e.remove(); - isTruncated = true; - break; - } - else - { - if ( after ) - { - after.detach(); - } - } - } - return isTruncated; - } - function ellipsis( $elem, $d, $i, o, after ) - { - var $elements = $elem.contents(), - isTruncated = false; - - $elem.empty(); - - var notx = 'table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style'; - for ( var a = 0, l = $elements.length; a < l; a++ ) - { - - if ( isTruncated ) - { - break; - } - - var e = $elements[ a ], - $e = $(e); - - if ( typeof e == 'undefined' ) - { - continue; - } - - $elem.append( $e ); - if ( after ) - { - $elem[ ( $elem.is( notx ) ) ? 'after' : 'append' ]( after ); - } - if ( e.nodeType == 3 ) - { - if ( test( $i, o ) ) - { - isTruncated = ellipsisElement( $e, $d, $i, o, after ); - } - } - else - { - isTruncated = ellipsis( $e, $d, $i, o, after ); - } - - if ( !isTruncated ) - { - if ( after ) - { - after.detach(); - } - } - } - return isTruncated; - } - function ellipsisElement( $e, $d, $i, o, after ) - { - var isTruncated = false, - e = $e[ 0 ]; - - if ( typeof e == 'undefined' ) - { - return false; - } - - var txt = getTextContent( e ), - space = ( txt.indexOf(' ') !== -1 ) ? ' ' : '\u3000', - separator = ( o.wrap == 'letter' ) ? '' : space, - textArr = txt.split( separator ), - position = -1, - midPos = -1, - startPos = 0, - endPos = textArr.length - 1; - - while ( startPos <= endPos && !( startPos == 0 && endPos == 0 ) ) - { - var m = Math.floor( ( startPos + endPos ) / 2 ); - if ( m == midPos ) - { - break; - } - midPos = m; - - setTextContent( e, textArr.slice( 0, midPos + 1 ).join( separator ) + o.ellipsis ); - - if ( !test( $i, o ) ) - { - position = midPos; - startPos = midPos; - } - else - { - endPos = midPos; - } - if( endPos == startPos && endPos == 0 && o.fallbackToLetter ) - { - separator = ''; - textArr = textArr[0].split(separator); - position = -1; - midPos = -1; - startPos = 0; - endPos = textArr.length - 1; - } - } - - if ( position != -1 && !( textArr.length == 1 && textArr[ 0 ].length == 0 ) ) - { - txt = addEllipsis( textArr.slice( 0, position + 1 ).join( separator ), o ); - isTruncated = true; - setTextContent( e, txt ); - } - else - { - var $w = $e.parent(); - $e.remove(); - - var afterLength = ( after ) ? after.length : 0 ; - - if ( $w.contents().size() > afterLength ) - { - var $n = $w.contents().eq( -1 - afterLength ); - isTruncated = ellipsisElement( $n, $d, $i, o, after ); - } - else - { - var $p = $w.prev() - var e = $p.contents().eq( -1 )[ 0 ]; - - if ( typeof e != 'undefined' ) - { - var txt = addEllipsis( getTextContent( e ), o ); - setTextContent( e, txt ); - if ( after ) - { - $p.append( after ); - } - $w.remove(); - isTruncated = true; - } - - } - } - - return isTruncated; - } - function test( $i, o ) - { - return $i.innerHeight() > o.maxHeight; - } - function addEllipsis( txt, o ) - { - while( $.inArray( txt.slice( -1 ), o.lastCharacter.remove ) > -1 ) - { - txt = txt.slice( 0, -1 ); - } - if ( $.inArray( txt.slice( -1 ), o.lastCharacter.noEllipsis ) < 0 ) - { - txt += o.ellipsis; - } - return txt; - } - function getSizes( $d ) - { - return { - 'width' : $d.innerWidth(), - 'height': $d.innerHeight() - }; - } - function setTextContent( e, content ) - { - if ( e.innerText ) - { - e.innerText = content; - } - else if ( e.nodeValue ) - { - e.nodeValue = content; - } - else if (e.textContent) - { - e.textContent = content; - } - - } - function getTextContent( e ) - { - if ( e.innerText ) - { - return e.innerText; - } - else if ( e.nodeValue ) - { - return e.nodeValue; - } - else if ( e.textContent ) - { - return e.textContent; - } - else - { - return ""; - } - } - function getElement( e, $i ) - { - if ( typeof e == 'undefined' ) - { - return false; - } - if ( !e ) - { - return false; - } - if ( typeof e == 'string' ) - { - e = $(e, $i); - return ( e.length ) - ? e - : false; - } - if ( typeof e == 'object' ) - { - return ( typeof e.jquery == 'undefined' ) - ? false - : e; - } - return false; - } - function getTrueInnerHeight( $el ) - { - var h = $el.innerHeight(), - a = [ 'paddingTop', 'paddingBottom' ]; - - for ( var z = 0, l = a.length; z < l; z++ ) { - var m = parseInt( $el.css( a[ z ] ), 10 ); - if ( isNaN( m ) ) - { - m = 0; - } - h -= m; - } - return h; - } - function debug( d, m ) - { - if ( !d ) - { - return false; - } - if ( typeof m == 'string' ) - { - m = 'dotdotdot: ' + m; - } - else - { - m = [ 'dotdotdot:', m ]; - } - - if ( typeof window.console != 'undefined' ) - { - if ( typeof window.console.log != 'undefined' ) - { - window.console.log( m ); - } - } - return false; - } - - - // override jQuery.html - var _orgHtml = $.fn.html; - $.fn.html = function( str ) { - if ( typeof str != 'undefined' ) - { - if ( this.data( 'dotdotdot' ) ) - { - if ( typeof str != 'function' ) - { - return this.trigger( 'update', [ str ] ); - } - } - return _orgHtml.call( this, str ); - } - return _orgHtml.call( this ); - }; - - - // override jQuery.text - var _orgText = $.fn.text; - $.fn.text = function( str ) { - if ( typeof str != 'undefined' ) - { - if ( this.data( 'dotdotdot' ) ) - { - var temp = $( '<div />' ); - temp.text( str ); - str = temp.html(); - temp.remove(); - return this.trigger( 'update', [ str ] ); - } - return _orgText.call( this, str ); - } - return _orgText.call( this ); - }; - - -})( jQuery ); diff --git a/src/UI/JsLibraries/jquery.easypiechart.js b/src/UI/JsLibraries/jquery.easypiechart.js deleted file mode 100644 index c600fb85f..000000000 --- a/src/UI/JsLibraries/jquery.easypiechart.js +++ /dev/null @@ -1,357 +0,0 @@ -/**! - * easyPieChart - * Lightweight plugin to render simple, animated and retina optimized pie charts - * - * @license - * @author Robert Fleischmann <rendro87@gmail.com> (http://robert-fleischmann.de) - * @version 2.1.3 - **/ - -(function(root, factory) { - if(typeof exports === 'object') { - module.exports = factory(require('jquery')); - } - else if(typeof define === 'function' && define.amd) { - define(['jquery'], factory); - } - else { - factory(root.jQuery); - } -}(this, function($) { -/** - * Renderer to render the chart on a canvas object - * @param {DOMElement} el DOM element to host the canvas (root of the plugin) - * @param {object} options options object of the plugin - */ -var CanvasRenderer = function(el, options) { - var cachedBackground; - var canvas = document.createElement('canvas'); - - el.appendChild(canvas); - - if (typeof(G_vmlCanvasManager) !== 'undefined') { - G_vmlCanvasManager.initElement(canvas); - } - - var ctx = canvas.getContext('2d'); - - canvas.width = canvas.height = options.size; - - // canvas on retina devices - var scaleBy = 1; - if (window.devicePixelRatio > 1) { - scaleBy = window.devicePixelRatio; - canvas.style.width = canvas.style.height = [options.size, 'px'].join(''); - canvas.width = canvas.height = options.size * scaleBy; - ctx.scale(scaleBy, scaleBy); - } - - // move 0,0 coordinates to the center - ctx.translate(options.size / 2, options.size / 2); - - // rotate canvas -90deg - ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI); - - var radius = (options.size - options.lineWidth) / 2; - if (options.scaleColor && options.scaleLength) { - radius -= options.scaleLength + 2; // 2 is the distance between scale and bar - } - - // IE polyfill for Date - Date.now = Date.now || function() { - return +(new Date()); - }; - - /** - * Draw a circle around the center of the canvas - * @param {strong} color Valid CSS color string - * @param {number} lineWidth Width of the line in px - * @param {number} percent Percentage to draw (float between -1 and 1) - */ - var drawCircle = function(color, lineWidth, percent) { - percent = Math.min(Math.max(-1, percent || 0), 1); - var isNegative = percent <= 0 ? true : false; - - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, isNegative); - - ctx.strokeStyle = color; - ctx.lineWidth = lineWidth; - - ctx.stroke(); - }; - - /** - * Draw the scale of the chart - */ - var drawScale = function() { - var offset; - var length; - - ctx.lineWidth = 1; - ctx.fillStyle = options.scaleColor; - - ctx.save(); - for (var i = 24; i > 0; --i) { - if (i % 6 === 0) { - length = options.scaleLength; - offset = 0; - } else { - length = options.scaleLength * 0.6; - offset = options.scaleLength - length; - } - ctx.fillRect(-options.size/2 + offset, 0, length, 1); - ctx.rotate(Math.PI / 12); - } - ctx.restore(); - }; - - /** - * Request animation frame wrapper with polyfill - * @return {function} Request animation frame method or timeout fallback - */ - var reqAnimationFrame = (function() { - return window.requestAnimationFrame || - window.webkitRequestAnimationFrame || - window.mozRequestAnimationFrame || - function(callback) { - window.setTimeout(callback, 1000 / 60); - }; - }()); - - /** - * Draw the background of the plugin including the scale and the track - */ - var drawBackground = function() { - if(options.scaleColor) drawScale(); - if(options.trackColor) drawCircle(options.trackColor, options.lineWidth, 1); - }; - - /** - * Canvas accessor - */ - this.getCanvas = function() { - return canvas; - }; - - /** - * Canvas 2D context 'ctx' accessor - */ - this.getCtx = function() { - return ctx; - }; - - /** - * Clear the complete canvas - */ - this.clear = function() { - ctx.clearRect(options.size / -2, options.size / -2, options.size, options.size); - }; - - /** - * Draw the complete chart - * @param {number} percent Percent shown by the chart between -100 and 100 - */ - this.draw = function(percent) { - // do we need to render a background - if (!!options.scaleColor || !!options.trackColor) { - // getImageData and putImageData are supported - if (ctx.getImageData && ctx.putImageData) { - if (!cachedBackground) { - drawBackground(); - cachedBackground = ctx.getImageData(0, 0, options.size * scaleBy, options.size * scaleBy); - } else { - ctx.putImageData(cachedBackground, 0, 0); - } - } else { - this.clear(); - drawBackground(); - } - } else { - this.clear(); - } - - ctx.lineCap = options.lineCap; - - // if barcolor is a function execute it and pass the percent as a value - var color; - if (typeof(options.barColor) === 'function') { - color = options.barColor(percent); - } else { - color = options.barColor; - } - - // draw bar - drawCircle(color, options.lineWidth, percent / 100); - }.bind(this); - - /** - * Animate from some percent to some other percentage - * @param {number} from Starting percentage - * @param {number} to Final percentage - */ - this.animate = function(from, to) { - var startTime = Date.now(); - options.onStart(from, to); - var animation = function() { - var process = Math.min(Date.now() - startTime, options.animate.duration); - var currentValue = options.easing(this, process, from, to - from, options.animate.duration); - this.draw(currentValue); - options.onStep(from, to, currentValue); - if (process >= options.animate.duration) { - options.onStop(from, to); - } else { - reqAnimationFrame(animation); - } - }.bind(this); - - reqAnimationFrame(animation); - }.bind(this); -}; - -var EasyPieChart = function(el, opts) { - var defaultOptions = { - barColor: '#ef1e25', - trackColor: '#f9f9f9', - scaleColor: '#dfe0e0', - scaleLength: 5, - lineCap: 'round', - lineWidth: 3, - size: 110, - rotate: 0, - animate: { - duration: 1000, - enabled: true - }, - easing: function (x, t, b, c, d) { // more can be found here: http://gsgd.co.uk/sandbox/jquery/easing/ - t = t / (d/2); - if (t < 1) { - return c / 2 * t * t + b; - } - return -c/2 * ((--t)*(t-2) - 1) + b; - }, - onStart: function(from, to) { - return; - }, - onStep: function(from, to, currentValue) { - return; - }, - onStop: function(from, to) { - return; - } - }; - - // detect present renderer - if (typeof(CanvasRenderer) !== 'undefined') { - defaultOptions.renderer = CanvasRenderer; - } else if (typeof(SVGRenderer) !== 'undefined') { - defaultOptions.renderer = SVGRenderer; - } else { - throw new Error('Please load either the SVG- or the CanvasRenderer'); - } - - var options = {}; - var currentValue = 0; - - /** - * Initialize the plugin by creating the options object and initialize rendering - */ - var init = function() { - this.el = el; - this.options = options; - - // merge user options into default options - for (var i in defaultOptions) { - if (defaultOptions.hasOwnProperty(i)) { - options[i] = opts && typeof(opts[i]) !== 'undefined' ? opts[i] : defaultOptions[i]; - if (typeof(options[i]) === 'function') { - options[i] = options[i].bind(this); - } - } - } - - // check for jQuery easing - if (typeof(options.easing) === 'string' && typeof(jQuery) !== 'undefined' && jQuery.isFunction(jQuery.easing[options.easing])) { - options.easing = jQuery.easing[options.easing]; - } else { - options.easing = defaultOptions.easing; - } - - // process earlier animate option to avoid bc breaks - if (typeof(options.animate) === 'number') { - options.animate = { - duration: options.animate, - enabled: true - }; - } - - if (typeof(options.animate) === 'boolean' && !options.animate) { - options.animate = { - duration: 1000, - enabled: options.animate - }; - } - - // create renderer - this.renderer = new options.renderer(el, options); - - // initial draw - this.renderer.draw(currentValue); - - // initial update - if (el.dataset && el.dataset.percent) { - this.update(parseFloat(el.dataset.percent)); - } else if (el.getAttribute && el.getAttribute('data-percent')) { - this.update(parseFloat(el.getAttribute('data-percent'))); - } - }.bind(this); - - /** - * Update the value of the chart - * @param {number} newValue Number between 0 and 100 - * @return {object} Instance of the plugin for method chaining - */ - this.update = function(newValue) { - newValue = parseFloat(newValue); - if (options.animate.enabled) { - this.renderer.animate(currentValue, newValue); - } else { - this.renderer.draw(newValue); - } - currentValue = newValue; - return this; - }.bind(this); - - /** - * Disable animation - * @return {object} Instance of the plugin for method chaining - */ - this.disableAnimation = function() { - options.animate.enabled = false; - return this; - }; - - /** - * Enable animation - * @return {object} Instance of the plugin for method chaining - */ - this.enableAnimation = function() { - options.animate.enabled = true; - return this; - }; - - init(); -}; - -$.fn.easyPieChart = function(options) { - return this.each(function() { - var instanceOptions; - - if (!$.data(this, 'easyPieChart')) { - instanceOptions = $.extend({}, options, $(this).data()); - $.data(this, 'easyPieChart', new EasyPieChart(this, instanceOptions)); - } - }); -}; - -})); diff --git a/src/UI/JsLibraries/jquery.js b/src/UI/JsLibraries/jquery.js deleted file mode 100644 index 87dd04093..000000000 --- a/src/UI/JsLibraries/jquery.js +++ /dev/null @@ -1,10351 +0,0 @@ -/*! - * jQuery JavaScript Library v1.11.3 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2015-04-28T16:19Z - */ - -(function( global, factory ) { - - if ( typeof module === "object" && typeof module.exports === "object" ) { - // For CommonJS and CommonJS-like environments where a proper window is present, - // execute the factory and get jQuery - // For environments that do not inherently posses a window with a document - // (such as Node.js), expose a jQuery-making factory as module.exports - // This accentuates the need for the creation of a real window - // e.g. var jQuery = require("jquery")(window); - // See ticket #14549 for more info - module.exports = global.document ? - factory( global, true ) : - function( w ) { - if ( !w.document ) { - throw new Error( "jQuery requires a window with a document" ); - } - return factory( w ); - }; - } else { - factory( global ); - } - -// Pass this if window is not defined yet -}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -// - -var deletedIds = []; - -var slice = deletedIds.slice; - -var concat = deletedIds.concat; - -var push = deletedIds.push; - -var indexOf = deletedIds.indexOf; - -var class2type = {}; - -var toString = class2type.toString; - -var hasOwn = class2type.hasOwnProperty; - -var support = {}; - - - -var - version = "1.11.3", - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - // Need init if jQuery is called (just allow error to be thrown if not included) - return new jQuery.fn.init( selector, context ); - }, - - // Support: Android<4.1, IE<9 - // Make sure we trim BOM and NBSP - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: version, - - constructor: jQuery, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function() { - return slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num != null ? - - // Return just the one element from the set - ( num < 0 ? this[ num + this.length ] : this[ num ] ) : - - // Return all the elements in a clean array - slice.call( this ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: deletedIds.sort, - splice: deletedIds.splice -}; - -jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - - // skip the boolean and the target - target = arguments[ i ] || {}; - i++; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( i === length ) { - target = this; - i--; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - // Unique for each copy of jQuery on the page - expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), - - // Assume jQuery is ready without the ready module - isReady: true, - - error: function( msg ) { - throw new Error( msg ); - }, - - noop: function() {}, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - // parseFloat NaNs numeric-cast false positives (null|true|false|"") - // ...but misinterprets leading-number strings, particularly hex literals ("0x...") - // subtraction forces infinities to NaN - // adding 1 corrects loss of precision from parseFloat (#15100) - return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0; - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - isPlainObject: function( obj ) { - var key; - - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if ( support.ownLast ) { - for ( key in obj ) { - return hasOwn.call( obj, key ); - } - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - type: function( obj ) { - if ( obj == null ) { - return obj + ""; - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ toString.call(obj) ] || "object" : - typeof obj; - }, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Support: Android<4.1, IE<9 - trim: function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( indexOf ) { - return indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var len = +second.length, - j = 0, - i = first.length; - - while ( j < len ) { - first[ i++ ] = second[ j++ ]; - } - - // Support: IE<9 - // Workaround casting of .length to NaN on otherwise arraylike objects (e.g., NodeLists) - if ( len !== len ) { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, invert ) { - var callbackInverse, - matches = [], - i = 0, - length = elems.length, - callbackExpect = !invert; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - callbackInverse = !callback( elems[ i ], i ); - if ( callbackInverse !== callbackExpect ) { - matches.push( elems[ i ] ); - } - } - - return matches; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their new values - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret.push( value ); - } - } - } - - // Flatten any nested arrays - return concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var args, proxy, tmp; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - now: function() { - return +( new Date() ); - }, - - // jQuery.support is not used in Core but other projects attach their - // properties to it so it needs to exist. - support: support -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - - // Support: iOS 8.2 (not reproducible in simulator) - // `in` check used to prevent JIT error (gh-2145) - // hasOwn isn't used here due to false negatives - // regarding Nodelist length in IE - var length = "length" in obj && obj.length, - type = jQuery.type( obj ); - - if ( type === "function" || jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj; -} -var Sizzle = -/*! - * Sizzle CSS Selector Engine v2.2.0-pre - * http://sizzlejs.com/ - * - * Copyright 2008, 2014 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2014-12-16 - */ -(function( window ) { - -var i, - support, - Expr, - getText, - isXML, - tokenize, - compile, - select, - outermostContext, - sortInput, - hasDuplicate, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + 1 * new Date(), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - } - return 0; - }, - - // General-purpose constants - MAX_NEGATIVE = 1 << 31, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf as it's faster than native - // http://jsperf.com/thor-indexof-vs-for/5 - indexOf = function( list, elem ) { - var i = 0, - len = list.length; - for ( ; i < len; i++ ) { - if ( list[i] === elem ) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace + - // Operator (capture 2) - "*([*^$|!~]?=)" + whitespace + - // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" - "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + - "*\\]", - - pseudos = ":(" + characterEncoding + ")(?:\\((" + - // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: - // 1. quoted (capture 3; capture 4 or capture 5) - "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + - // 2. simple (capture 6) - "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + - // 3. anything else (capture 2) - ".*" + - ")\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rwhitespace = new RegExp( whitespace + "+", "g" ), - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), - - rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), - - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rsibling = /[+~]/, - rescape = /'|\\/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), - funescape = function( _, escaped, escapedWhitespace ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox<24 - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - high < 0 ? - // BMP codepoint - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }, - - // Used for iframes - // See setDocument() - // Removing the function wrapper causes a "Permission Denied" - // error in IE - unloadHandler = function() { - setDocument(); - }; - -// Optimize for push.apply( _, NodeList ) -try { - push.apply( - (arr = slice.call( preferredDoc.childNodes )), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[ preferredDoc.childNodes.length ].nodeType; -} catch ( e ) { - push = { apply: arr.length ? - - // Leverage slice if possible - function( target, els ) { - push_native.apply( target, slice.call(els) ); - } : - - // Support: IE<9 - // Otherwise append directly - function( target, els ) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ( (target[j++] = els[i++]) ) {} - target.length = j - 1; - } - }; -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - nodeType = context.nodeType; - - if ( typeof selector !== "string" || !selector || - nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { - - return results; - } - - if ( !seed && documentIsHTML ) { - - // Try to shortcut find operations when possible (e.g., not under DocumentFragment) - if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document (jQuery #6963) - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, context.getElementsByTagName( selector ) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getElementsByClassName ) { - push.apply( results, context.getElementsByClassName( m ) ); - return results; - } - } - - // QSA path - if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { - nid = old = expando; - newContext = context; - newSelector = nodeType !== 1 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, - newContext.querySelectorAll( newSelector ) - ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var keys = []; - - function cache( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key + " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key + " " ] = value); - } - return cache; -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return !!fn( div ); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if ( div.parentNode ) { - div.parentNode.removeChild( div ); - } - // release memory in IE - div = null; - } -} - -/** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ -function addHandle( attrs, handler ) { - var arr = attrs.split("|"), - i = attrs.length; - - while ( i-- ) { - Expr.attrHandle[ arr[i] ] = handler; - } -} - -/** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - ( ~b.sourceIndex || MAX_NEGATIVE ) - - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -/** - * Returns a function to use in pseudos for input types - * @param {String} type - */ -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -/** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Checks a node for validity as a Sizzle context - * @param {Element|Object=} context - * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value - */ -function testContext( context ) { - return context && typeof context.getElementsByTagName !== "undefined" && context; -} - -// Expose support vars for convenience -support = Sizzle.support = {}; - -/** - * Detects XML nodes - * @param {Element|Object} elem An element or a document - * @returns {Boolean} True iff elem is a non-HTML XML node - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var hasCompare, parent, - doc = node ? node.ownerDocument || node : preferredDoc; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - parent = doc.defaultView; - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if ( parent && parent !== parent.top ) { - // IE11 does not have attachEvent, so all must suffer - if ( parent.addEventListener ) { - parent.addEventListener( "unload", unloadHandler, false ); - } else if ( parent.attachEvent ) { - parent.attachEvent( "onunload", unloadHandler ); - } - } - - /* Support tests - ---------------------------------------------------------------------- */ - documentIsHTML = !isXML( doc ); - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties - // (excepting IE8 booleans) - support.attributes = assert(function( div ) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Support: IE<9 - support.getElementsByClassName = rnative.test( doc.getElementsByClassName ); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function( div ) { - docElem.appendChild( div ).id = expando; - return !doc.getElementsByName || !doc.getElementsByName( expando ).length; - }); - - // ID find and filter - if ( support.getById ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [ m ] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - // Support: IE6/7 - // getElementById is not reliable as a find shortcut - delete Expr.find["ID"]; - - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( tag ); - - // DocumentFragment nodes don't have gEBTN - } else if ( support.qsa ) { - return context.querySelectorAll( tag ); - } - } : - - function( tag, context ) { - var elem, - tmp = [], - i = 0, - // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { - if ( documentIsHTML ) { - return context.getElementsByClassName( className ); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See http://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - docElem.appendChild( div ).innerHTML = "<a id='" + expando + "'></a>" + - "<select id='" + expando + "-\f]' msallowcapture=''>" + - "<option selected=''></option></select>"; - - // Support: IE8, Opera 11-12.16 - // Nothing should be selected when empty strings follow ^= or $= or *= - // The test attribute must be unknown in Opera but "safe" for WinRT - // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section - if ( div.querySelectorAll("[msallowcapture^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); - } - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); - } - - // Support: Chrome<29, Android<4.2+, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.7+ - if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { - rbuggyQSA.push("~="); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - - // Support: Safari 8+, iOS 8+ - // https://bugs.webkit.org/show_bug.cgi?id=136851 - // In-page `selector#id sibing-combinator selector` fails - if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { - rbuggyQSA.push(".#.+[+~]"); - } - }); - - assert(function( div ) { - // Support: Windows 8 Native Apps - // The type and name attributes are restricted during .innerHTML assignment - var input = doc.createElement("input"); - input.setAttribute( "type", "hidden" ); - div.appendChild( input ).setAttribute( "name", "D" ); - - // Support: IE8 - // Enforce case-sensitivity of name attribute - if ( div.querySelectorAll("[name=d]").length ) { - rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || - docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); - - /* Contains - ---------------------------------------------------------------------- */ - hasCompare = rnative.test( docElem.compareDocumentPosition ); - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = hasCompare || rnative.test( docElem.contains ) ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = hasCompare ? - function( a, b ) { - - // Flag for duplicate removal - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - // Sort on method existence if only one input has compareDocumentPosition - var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; - if ( compare ) { - return compare; - } - - // Calculate position if both inputs belong to the same document - compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? - a.compareDocumentPosition( b ) : - - // Otherwise we know they are disconnected - 1; - - // Disconnected nodes - if ( compare & 1 || - (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { - - // Choose the first element that is related to our preferred document - if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { - return -1; - } - if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { - return 1; - } - - // Maintain original order - return sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - } - - return compare & 4 ? -1 : 1; - } : - function( a, b ) { - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Parentless nodes are either documents or disconnected - if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - if ( support.matchesSelector && documentIsHTML && - ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && - ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { - - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch (e) {} - } - - return Sizzle( expr, document, null, [ elem ] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - var fn = Expr.attrHandle[ name.toLowerCase() ], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? - fn( elem, name, !documentIsHTML ) : - undefined; - - return val !== undefined ? - val : - support.attributes || !documentIsHTML ? - elem.getAttribute( name ) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -/** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice( 0 ); - results.sort( sortOrder ); - - if ( hasDuplicate ) { - while ( (elem = results[i++]) ) { - if ( elem === results[ i ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - // Clear input after sorting to release objects - // See https://github.com/jquery/sizzle/pull/225 - sortInput = null; - - return results; -}; - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - while ( (node = elem[i++]) ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (jQuery #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[6] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[3] ) { - match[2] = match[4] || match[5] || ""; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeNameSelector ) { - var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); - return nodeNameSelector === "*" ? - function() { return true; } : - function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - // Don't keep the element (issue #299) - input[0] = null; - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - text = text.replace( runescape, funescape ); - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifier - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), - // but not by others (comment: 8; processing instruction: 7; etc.) - // nodeType < 6 works because attributes (2) do not appear as children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeType < 6 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - - // Support: IE<8 - // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -// Easy API for creating new setFilters -function setFilters() {} -setFilters.prototype = Expr.filters = Expr.pseudos; -Expr.setFilters = new setFilters(); - -tokenize = Sizzle.tokenize = function( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( (tokens = []) ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - }); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -}; - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var oldCache, outerCache, - newCache = [ dirruns, doneName ]; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (oldCache = outerCache[ dir ]) && - oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { - - // Assign to newCache so results back-propagate to previous elements - return (newCache[ 2 ] = oldCache[ 2 ]); - } else { - // Reuse newcache so results back-propagate to previous elements - outerCache[ dir ] = newCache; - - // A match means we're done; a fail means we have to keep checking - if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - // Avoid hanging onto element (issue #299) - checkContext = null; - return ret; - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) - ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - var bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, outermost ) { - var elem, j, matcher, - matchedCount = 0, - i = "0", - unmatched = seed && [], - setMatched = [], - contextBackup = outermostContext, - // We must always have either seed elements or outermost context - elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), - len = elems.length; - - if ( outermost ) { - outermostContext = context !== document && context; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - // Support: IE<9, Safari - // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id - for ( ; i !== len && (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !match ) { - match = tokenize( selector ); - } - i = match.length; - while ( i-- ) { - cached = matcherFromTokens( match[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - - // Save selector and tokenization - cached.selector = selector; - } - return cached; -}; - -/** - * A low-level selection function that works with Sizzle's compiled - * selector functions - * @param {String|Function} selector A selector or a pre-compiled - * selector function built with Sizzle.compile - * @param {Element} context - * @param {Array} [results] - * @param {Array} [seed] A set of elements to match against - */ -select = Sizzle.select = function( selector, context, results, seed ) { - var i, tokens, token, type, find, - compiled = typeof selector === "function" && selector, - match = !seed && tokenize( (selector = compiled.selector || selector) ); - - results = results || []; - - // Try to minimize operations if there is no seed and only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[ tokens[1].type ] ) { - - context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; - if ( !context ) { - return results; - - // Precompiled matchers will still verify ancestry, so step up a level - } else if ( compiled ) { - context = context.parentNode; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, seed ); - return results; - } - - break; - } - } - } - } - - // Compile and execute a filtering function if one is not provided - // Provide `match` to avoid retokenization if we modified the selector above - ( compiled || compile( selector, match ) )( - seed, - context, - !documentIsHTML, - results, - rsibling.test( selector ) && testContext( context.parentNode ) || context - ); - return results; -}; - -// One-time assignments - -// Sort stability -support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; - -// Support: Chrome 14-35+ -// Always assume duplicates if they aren't passed to the comparison function -support.detectDuplicates = !!hasDuplicate; - -// Initialize against the default document -setDocument(); - -// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) -// Detached nodes confoundingly follow *each other* -support.sortDetached = assert(function( div1 ) { - // Should return 1, but returns 4 (following) - return div1.compareDocumentPosition( document.createElement("div") ) & 1; -}); - -// Support: IE<8 -// Prevent attribute/property "interpolation" -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !assert(function( div ) { - div.innerHTML = "<a href='#'></a>"; - return div.firstChild.getAttribute("href") === "#" ; -}) ) { - addHandle( "type|href|height|width", function( elem, name, isXML ) { - if ( !isXML ) { - return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); - } - }); -} - -// Support: IE<9 -// Use defaultValue in place of getAttribute("value") -if ( !support.attributes || !assert(function( div ) { - div.innerHTML = "<input/>"; - div.firstChild.setAttribute( "value", "" ); - return div.firstChild.getAttribute( "value" ) === ""; -}) ) { - addHandle( "value", function( elem, name, isXML ) { - if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { - return elem.defaultValue; - } - }); -} - -// Support: IE<9 -// Use getAttributeNode to fetch booleans when getAttribute lies -if ( !assert(function( div ) { - return div.getAttribute("disabled") == null; -}) ) { - addHandle( booleans, function( elem, name, isXML ) { - var val; - if ( !isXML ) { - return elem[ name ] === true ? name.toLowerCase() : - (val = elem.getAttributeNode( name )) && val.specified ? - val.value : - null; - } - }); -} - -return Sizzle; - -})( window ); - - - -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - - -var rneedsContext = jQuery.expr.match.needsContext; - -var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); - - - -var risSimple = /^.[^:#\[\.,]*$/; - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, not ) { - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep( elements, function( elem, i ) { - /* jshint -W018 */ - return !!qualifier.call( elem, i, elem ) !== not; - }); - - } - - if ( qualifier.nodeType ) { - return jQuery.grep( elements, function( elem ) { - return ( elem === qualifier ) !== not; - }); - - } - - if ( typeof qualifier === "string" ) { - if ( risSimple.test( qualifier ) ) { - return jQuery.filter( qualifier, elements, not ); - } - - qualifier = jQuery.filter( qualifier, elements ); - } - - return jQuery.grep( elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; - }); -} - -jQuery.filter = function( expr, elems, not ) { - var elem = elems[ 0 ]; - - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : - jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { - return elem.nodeType === 1; - })); -}; - -jQuery.fn.extend({ - find: function( selector ) { - var i, - ret = [], - self = this, - len = self.length; - - if ( typeof selector !== "string" ) { - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, self[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, - filter: function( selector ) { - return this.pushStack( winnow(this, selector || [], false) ); - }, - not: function( selector ) { - return this.pushStack( winnow(this, selector || [], true) ); - }, - is: function( selector ) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test( selector ) ? - jQuery( selector ) : - selector || [], - false - ).length; - } -}); - - -// Initialize a jQuery object - - -// A central reference to the root jQuery(document) -var rootjQuery, - - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - - // A simple way to check for HTML strings - // Prioritize #id over <tag> to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - init = jQuery.fn.init = function( selector, context ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - // Intentionally let the error be thrown if parseHTML is not present - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return typeof rootjQuery.ready !== "undefined" ? - rootjQuery.ready( selector ) : - // Execute immediately if ready is not present - selector( jQuery ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }; - -// Give the init function the jQuery prototype for later instantiation -init.prototype = jQuery.fn; - -// Initialize central reference -rootjQuery = jQuery( document ); - - -var rparentsprev = /^(?:parents|prev(?:Until|All))/, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.extend({ - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -jQuery.fn.extend({ - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; - - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - matched = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { - // Always skip document fragments - if ( cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors)) ) { - - matched.push( cur ); - break; - } - } - } - - return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - return this.pushStack( - jQuery.unique( - jQuery.merge( this.get(), jQuery( selector, context ) ) - ) - ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); - - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( name.slice( -5 ) !== "Until" ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - if ( this.length > 1 ) { - // Remove duplicates - if ( !guaranteedUnique[ name ] ) { - ret = jQuery.unique( ret ); - } - - // Reverse order for parents* and prev-derivatives - if ( rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - } - - return this.pushStack( ret ); - }; -}); -var rnotwhite = (/\S+/g); - - - -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - if ( list && ( !fired || stack ) ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; - - -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; - if ( values === progressValues ) { - deferred.notifyWith( contexts, values ); - - } else if ( !(--remaining) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); - - -// The deferred used on DOM ready -var readyList; - -jQuery.fn.ready = function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; -}; - -jQuery.extend({ - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.triggerHandler ) { - jQuery( document ).triggerHandler( "ready" ); - jQuery( document ).off( "ready" ); - } - } -}); - -/** - * Clean-up method for dom ready events - */ -function detach() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } -} - -/** - * The ready event handler and self cleanup method - */ -function completed() { - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } -} - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - - -var strundefined = typeof undefined; - - - -// Support: IE<9 -// Iteration over object's inherited properties before its own -var i; -for ( i in jQuery( support ) ) { - break; -} -support.ownLast = i !== "0"; - -// Note: most support tests are defined in their respective modules. -// false until the test is run -support.inlineBlockNeedsLayout = false; - -// Execute ASAP in case we need to set body.style.zoom -jQuery(function() { - // Minified: var a,b,c,d - var val, div, body, container; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Return for frameset docs that don't have a body - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - if ( typeof div.style.zoom !== strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.style.cssText = "display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1"; - - support.inlineBlockNeedsLayout = val = div.offsetWidth === 3; - if ( val ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild( container ); -}); - - - - -(function() { - var div = document.createElement( "div" ); - - // Execute the test only if not already executed in another module. - if (support.deleteExpando == null) { - // Support: IE<9 - support.deleteExpando = true; - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - } - - // Null elements to avoid leaks in IE. - div = null; -})(); - - -/** - * Determines whether an object can have data - */ -jQuery.acceptData = function( elem ) { - var noData = jQuery.noData[ (elem.nodeName + " ").toLowerCase() ], - nodeType = +elem.nodeType || 1; - - // Do not set data on non-element DOM nodes because it will not be cleared (#8335). - return nodeType !== 1 && nodeType !== 9 ? - false : - - // Nodes accept data unless otherwise specified; rejection can be conditional - !noData || noData !== true && elem.getAttribute("classid") === noData; -}; - - -var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, - rmultiDash = /([A-Z])/g; - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} - -function internalData( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var ret, thisCache, - internalKey = jQuery.expando, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - id = elem[ internalKey ] = deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( typeof name === "string" ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, i, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - - i = name.length; - while ( i-- ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if ( support.deleteExpando || cache != cache.window ) { - /* jshint eqeqeq: true */ - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } -} - -jQuery.extend({ - cache: {}, - - // The following elements (space-suffixed to avoid Object.prototype collisions) - // throw uncatchable exceptions if you attempt to set expando properties - noData: { - "applet ": true, - "embed ": true, - // ...but Flash objects (which have this classid) *can* handle expandos - "object ": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, - - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, - - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var i, name, data, - elem = this[0], - attrs = elem && elem.attributes; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - i = attrs.length; - while ( i-- ) { - - // Support: IE11+ - // The attrs elements can be null (#14894) - if ( attrs[ i ] ) { - name = attrs[ i ].name; - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.slice(5) ); - dataAttr( elem, name, data[ name ] ); - } - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - return arguments.length > 1 ? - - // Sets one value - this.each(function() { - jQuery.data( this, key, value ); - }) : - - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : undefined; - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - - -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while ( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; - -var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; - -var isHidden = function( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); - }; - - - -// Multifunctional method to get and set values of a collection -// The value/s can optionally be executed if it's a function -var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; -}; -var rcheckableType = (/^(?:checkbox|radio)$/i); - - - -(function() { - // Minified: var a,b,c - var input = document.createElement( "input" ), - div = document.createElement( "div" ), - fragment = document.createDocumentFragment(); - - // Setup - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName( "tbody" ).length; - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !!div.getElementsByTagName( "link" ).length; - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = - document.createElement( "nav" ).cloneNode( true ).outerHTML !== "<:nav></:nav>"; - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - input.type = "checkbox"; - input.checked = true; - fragment.appendChild( input ); - support.appendChecked = input.checked; - - // Make sure textarea (and checkbox) defaultValue is properly cloned - // Support: IE6-IE11+ - div.innerHTML = "<textarea>x</textarea>"; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // #11217 - WebKit loses check when the name is after the checked attribute - fragment.appendChild( div ); - div.innerHTML = "<input type='radio' checked='checked' name='t'/>"; - - // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 - // old WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - support.noCloneEvent = true; - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Execute the test only if not already executed in another module. - if (support.deleteExpando == null) { - // Support: IE<9 - support.deleteExpando = true; - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - } -})(); - - -(function() { - var i, eventName, - div = document.createElement( "div" ); - - // Support: IE<9 (lack submit/change bubble), Firefox 23+ (lack focusin event) - for ( i in { submit: true, change: true, focusin: true }) { - eventName = "on" + i; - - if ( !(support[ i + "Bubbles" ] = eventName in window) ) { - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - div.setAttribute( eventName, "t" ); - support[ i + "Bubbles" ] = div.attributes[ eventName ].expando === false; - } - } - - // Null elements to avoid leaks in IE. - div = null; -})(); - - -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && jQuery.acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && - jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG <use> instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - /* jshint eqeqeq: false */ - for ( ; cur != this; cur = cur.parentNode || this ) { - /* jshint eqeqeq: true */ - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - // Support: IE < 9, Android < 4.0 - src.returnValue === false ? - returnTrue : - returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && e.stopImmediatePropagation ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - jQuery._removeData( doc, fix ); - } else { - jQuery._data( doc, fix, attaches ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); - - -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /<tbody/i, - rhtml = /<|&#?\w+;/, - rnoInnerhtml = /<(?:script|style|link)/i, - // checked="checked" or checked - rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, - rscriptType = /^$|\/(?:java|ecma)script/i, - rscriptTypeMasked = /^true\/(.*)/, - rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "<select multiple='multiple'>", "</select>" ], - legend: [ 1, "<fieldset>", "</fieldset>" ], - area: [ 1, "<map>", "</map>" ], - param: [ 1, "<object>", "</object>" ], - thead: [ 1, "<table>", "</table>" ], - tr: [ 2, "<table><tbody>", "</tbody></table>" ], - col: [ 2, "<table><tbody></tbody><colgroup>", "</colgroup></table>" ], - td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X<div>", "</div>" ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} - -// Support: IE<8 -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild( elem.ownerDocument.createElement("tbody") ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = (jQuery.find.attr( elem, "type" ) !== null) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!support.noCloneEvent || !support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment( context ), - - nodes = [], - i = 0; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = (rtagName.exec( elem ) || [ "", "" ])[ 1 ].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if ( !support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - - // Remove IE's autoinserted <tbody> from table fragments - if ( !support.tbody ) { - - // String was a <table>, *may* have spurious <tbody> - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare <thead> or <tfoot> - wrap[1] === "<table>" && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; - - } else if ( typeof elem.removeAttribute !== strundefined ) { - elem.removeAttribute( internalKey ); - - } else { - elem[ internalKey ] = null; - } - - deletedIds.push( id ); - } - } - } - } - } -}); - -jQuery.fn.extend({ - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - append: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip( arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - remove: function( selector, keepData /* Internal Use Only */ ) { - var elem, - elems = selector ? jQuery.filter( selector, this ) : this, - i = 0; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map(function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ (rtagName.exec( value ) || [ "", "" ])[ 1 ].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1></$2>" ); - - try { - for (; i < l; i++ ) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch(e) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var arg = arguments[ 0 ]; - - // Make the changes, replacing each context element with the new content - this.domManip( arguments, function( elem ) { - arg = this.parentNode; - - jQuery.cleanData( getAll( this ) ); - - if ( arg ) { - arg.replaceChild( elem, this ); - } - }); - - // Force removal if there was no new content (e.g., from empty arguments) - return arg && (arg.length || arg.nodeType) ? this : this.remove(); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, callback ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[0] = value.call( this, index, self.html() ); - } - self.domManip( args, callback ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( this[i], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); - } - } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } -}); - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - - -var iframe, - elemdisplay = {}; - -/** - * Retrieve the actual display of a element - * @param {String} name nodeName of the element - * @param {Object} doc Document object - */ -// Called only from within defaultDisplay -function actualDisplay( name, doc ) { - var style, - elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - - // getDefaultComputedStyle might be reliably used only on attached element - display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? - - // Use of this method is a temporary fix (more like optmization) until something better comes along, - // since it was removed from specification and supported only in FF - style.display : jQuery.css( elem[ 0 ], "display" ); - - // We don't have any data stored on the element, - // so use "detach" method as fast way to get rid of the element - elem.detach(); - - return display; -} - -/** - * Try to determine the default display value of an element - * @param {String} nodeName - */ -function defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - - // Use the already-created iframe if possible - iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement ); - - // Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse - doc = ( iframe[ 0 ].contentWindow || iframe[ 0 ].contentDocument ).document; - - // Support: IE - doc.write(); - doc.close(); - - display = actualDisplay( nodeName, doc ); - iframe.detach(); - } - - // Store the correct default display - elemdisplay[ nodeName ] = display; - } - - return display; -} - - -(function() { - var shrinkWrapBlocksVal; - - support.shrinkWrapBlocks = function() { - if ( shrinkWrapBlocksVal != null ) { - return shrinkWrapBlocksVal; - } - - // Will be changed later if needed. - shrinkWrapBlocksVal = false; - - // Minified: var b,c,d - var div, body, container; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Test fired too early or in an unsupported environment, exit. - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - if ( typeof div.style.zoom !== strundefined ) { - // Reset CSS: box-sizing; display; margin; border - div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + - "box-sizing:content-box;display:block;margin:0;border:0;" + - "padding:1px;width:1px;zoom:1"; - div.appendChild( document.createElement( "div" ) ).style.width = "5px"; - shrinkWrapBlocksVal = div.offsetWidth !== 3; - } - - body.removeChild( container ); - - return shrinkWrapBlocksVal; - }; - -})(); -var rmargin = (/^margin/); - -var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" ); - - - -var getStyles, curCSS, - rposition = /^(top|right|bottom|left)$/; - -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - // Support: IE<=11+, Firefox<=30+ (#15098, #14150) - // IE throws on elements created in popups - // FF meanwhile throws on frame elements through "defaultView.getComputedStyle" - if ( elem.ownerDocument.defaultView.opener ) { - return elem.ownerDocument.defaultView.getComputedStyle( elem, null ); - } - - return window.getComputedStyle( elem, null ); - }; - - curCSS = function( elem, name, computed ) { - var width, minWidth, maxWidth, ret, - style = elem.style; - - computed = computed || getStyles( elem ); - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined; - - if ( computed ) { - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - // Support: IE - // IE returns zIndex value as an integer. - return ret === undefined ? - ret : - ret + ""; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; - - curCSS = function( elem, name, computed ) { - var left, rs, rsLeft, ret, - style = elem.style; - - computed = computed || getStyles( elem ); - ret = computed ? computed[ name ] : undefined; - - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; - } - } - - // Support: IE - // IE returns zIndex value as an integer. - return ret === undefined ? - ret : - ret + "" || "auto"; - }; -} - - - - -function addGetHookIf( conditionFn, hookFn ) { - // Define the hook, we'll check on the first run if it's really needed. - return { - get: function() { - var condition = conditionFn(); - - if ( condition == null ) { - // The test was not ready at this point; screw the hook this time - // but check again when needed next time. - return; - } - - if ( condition ) { - // Hook not needed (or it's not possible to use it due to missing dependency), - // remove it. - // Since there are no other hooks for marginRight, remove the whole object. - delete this.get; - return; - } - - // Hook needed; redefine it so that the support test is not executed again. - - return (this.get = hookFn).apply( this, arguments ); - } - }; -} - - -(function() { - // Minified: var b,c,d,e,f,g, h,i - var div, style, a, pixelPositionVal, boxSizingReliableVal, - reliableHiddenOffsetsVal, reliableMarginRightVal; - - // Setup - div = document.createElement( "div" ); - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - a = div.getElementsByTagName( "a" )[ 0 ]; - style = a && a.style; - - // Finish early in limited (non-browser) environments - if ( !style ) { - return; - } - - style.cssText = "float:left;opacity:.5"; - - // Support: IE<9 - // Make sure that element opacity exists (as opposed to filter) - support.opacity = style.opacity === "0.5"; - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !!style.cssFloat; - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - support.boxSizing = style.boxSizing === "" || style.MozBoxSizing === "" || - style.WebkitBoxSizing === ""; - - jQuery.extend(support, { - reliableHiddenOffsets: function() { - if ( reliableHiddenOffsetsVal == null ) { - computeStyleTests(); - } - return reliableHiddenOffsetsVal; - }, - - boxSizingReliable: function() { - if ( boxSizingReliableVal == null ) { - computeStyleTests(); - } - return boxSizingReliableVal; - }, - - pixelPosition: function() { - if ( pixelPositionVal == null ) { - computeStyleTests(); - } - return pixelPositionVal; - }, - - // Support: Android 2.3 - reliableMarginRight: function() { - if ( reliableMarginRightVal == null ) { - computeStyleTests(); - } - return reliableMarginRightVal; - } - }); - - function computeStyleTests() { - // Minified: var b,c,d,j - var div, body, container, contents; - - body = document.getElementsByTagName( "body" )[ 0 ]; - if ( !body || !body.style ) { - // Test fired too early or in an unsupported environment, exit. - return; - } - - // Setup - div = document.createElement( "div" ); - container = document.createElement( "div" ); - container.style.cssText = "position:absolute;border:0;width:0;height:0;top:0;left:-9999px"; - body.appendChild( container ).appendChild( div ); - - div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" + - "box-sizing:border-box;display:block;margin-top:1%;top:1%;" + - "border:1px;padding:1px;width:4px;position:absolute"; - - // Support: IE<9 - // Assume reasonable values in the absence of getComputedStyle - pixelPositionVal = boxSizingReliableVal = false; - reliableMarginRightVal = true; - - // Check for getComputedStyle so that this code is not run in IE<9. - if ( window.getComputedStyle ) { - pixelPositionVal = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - boxSizingReliableVal = - ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Support: Android 2.3 - // Div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container (#3333) - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - contents = div.appendChild( document.createElement( "div" ) ); - - // Reset CSS: box-sizing; display; margin; border; padding - contents.style.cssText = div.style.cssText = - // Support: Firefox<29, Android 2.3 - // Vendor-prefix box-sizing - "-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" + - "box-sizing:content-box;display:block;margin:0;border:0;padding:0"; - contents.style.marginRight = contents.style.width = "0"; - div.style.width = "1px"; - - reliableMarginRightVal = - !parseFloat( ( window.getComputedStyle( contents, null ) || {} ).marginRight ); - - div.removeChild( contents ); - } - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "<table><tr><td></td><td>t</td></tr></table>"; - contents = div.getElementsByTagName( "td" ); - contents[ 0 ].style.cssText = "margin:0;border:0;padding:0;display:none"; - reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0; - if ( reliableHiddenOffsetsVal ) { - contents[ 0 ].style.display = ""; - contents[ 1 ].style.display = "none"; - reliableHiddenOffsetsVal = contents[ 0 ].offsetHeight === 0; - } - - body.removeChild( container ); - } - -})(); - - -// A method for quickly swapping in/out CSS properties to get correct calculations. -jQuery.swap = function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; -}; - - -var - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ), - - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: "0", - fontWeight: "400" - }, - - cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; - - -// return a css property mapped to a potentially vendor prefixed property -function vendorPropName( style, name ) { - - // shortcut for names that are not vendor prefixed - if ( name in style ) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in style ) { - return name; - } - } - - return origName; -} - -function showHide( elements, show ) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - values[ index ] = jQuery._data( elem, "olddisplay" ); - display = elem.style.display; - if ( show ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !values[ index ] && display === "none" ) { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", defaultDisplay(elem.nodeName) ); - } - } else { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( index = 0; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - if ( !show || elem.style.display === "none" || elem.style.display === "" ) { - elem.style.display = show ? values[ index ] || "" : "none"; - } - } - - return elements; -} - -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( support.boxSizingReliable() || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "flexGrow": true, - "flexShrink": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if ( type === "string" && (ret = rrelNum.exec( value )) ) { - value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that null and NaN values aren't set. See: #7116 - if ( value == null || value !== value ) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( type === "number" && !jQuery.cssNumber[ origName ] ) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if ( !support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Support: IE - // Swallow errors from 'invalid' CSS values (#5509) - try { - style[ name ] = value; - } catch(e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var num, val, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - //convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; - } - return val; - } -}); - -jQuery.each([ "height", "width" ], function( i, name ) { - jQuery.cssHooks[ name ] = { - get: function( elem, computed, extra ) { - if ( computed ) { - // certain elements can have dimension info if we invisibly show them - // however, it must have a current display style that would benefit from this - return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ? - jQuery.swap( elem, cssShow, function() { - return getWidthOrHeight( elem, name, extra ); - }) : - getWidthOrHeight( elem, name, extra ); - } - }, - - set: function( elem, value, extra ) { - var styles = extra && getStyles( elem ); - return setPositiveNumber( elem, value, extra ? - augmentWidthOrHeight( - elem, - name, - extra, - support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box", - styles - ) : 0 - ); - } - }; -}); - -if ( !support.opacity ) { - jQuery.cssHooks.opacity = { - get: function( elem, computed ) { - // IE uses filters for opacity - return ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || "" ) ? - ( 0.01 * parseFloat( RegExp.$1 ) ) + "" : - computed ? "1" : ""; - }, - - set: function( elem, value ) { - var style = elem.style, - currentStyle = elem.currentStyle, - opacity = jQuery.isNumeric( value ) ? "alpha(opacity=" + value * 100 + ")" : "", - filter = currentStyle && currentStyle.filter || style.filter || ""; - - // IE has trouble with opacity if it does not have layout - // Force it by setting the zoom level - style.zoom = 1; - - // if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652 - // if value === "", then remove inline opacity #12685 - if ( ( value >= 1 || value === "" ) && - jQuery.trim( filter.replace( ralpha, "" ) ) === "" && - style.removeAttribute ) { - - // Setting style.filter to null, "" & " " still leave "filter:" in the cssText - // if "filter:" is present at all, clearType is disabled, we want to avoid this - // style.removeAttribute is IE Only, but so apparently is this code path... - style.removeAttribute( "filter" ); - - // if there is no filter style applied in a css rule or unset inline opacity, we are done - if ( value === "" || currentStyle && !currentStyle.filter ) { - return; - } - } - - // otherwise, set new filter values - style.filter = ralpha.test( filter ) ? - filter.replace( ralpha, opacity ) : - filter + " " + opacity; - } - }; -} - -jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight, - function( elem, computed ) { - if ( computed ) { - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - // Work around by temporarily setting element display to inline-block - return jQuery.swap( elem, { "display": "inline-block" }, - curCSS, [ elem, "marginRight" ] ); - } - } -); - -// These hooks are used by animate to expand properties -jQuery.each({ - margin: "", - padding: "", - border: "Width" -}, function( prefix, suffix ) { - jQuery.cssHooks[ prefix + suffix ] = { - expand: function( value ) { - var i = 0, - expanded = {}, - - // assumes a single number if not a string - parts = typeof value === "string" ? value.split(" ") : [ value ]; - - for ( ; i < 4; i++ ) { - expanded[ prefix + cssExpand[ i ] + suffix ] = - parts[ i ] || parts[ i - 2 ] || parts[ 0 ]; - } - - return expanded; - } - }; - - if ( !rmargin.test( prefix ) ) { - jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber; - } -}); - -jQuery.fn.extend({ - css: function( name, value ) { - return access( this, function( elem, name, value ) { - var styles, len, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - if ( typeof state === "boolean" ) { - return state ? this.show() : this.hide(); - } - - return this.each(function() { - if ( isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - - -function Tween( elem, options, prop, end, easing ) { - return new Tween.prototype.init( elem, options, prop, end, easing ); -} -jQuery.Tween = Tween; - -Tween.prototype = { - constructor: Tween, - init: function( elem, options, prop, end, easing, unit ) { - this.elem = elem; - this.prop = prop; - this.easing = easing || "swing"; - this.options = options; - this.start = this.now = this.cur(); - this.end = end; - this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" ); - }, - cur: function() { - var hooks = Tween.propHooks[ this.prop ]; - - return hooks && hooks.get ? - hooks.get( this ) : - Tween.propHooks._default.get( this ); - }, - run: function( percent ) { - var eased, - hooks = Tween.propHooks[ this.prop ]; - - if ( this.options.duration ) { - this.pos = eased = jQuery.easing[ this.easing ]( - percent, this.options.duration * percent, 0, 1, this.options.duration - ); - } else { - this.pos = eased = percent; - } - this.now = ( this.end - this.start ) * eased + this.start; - - if ( this.options.step ) { - this.options.step.call( this.elem, this.now, this ); - } - - if ( hooks && hooks.set ) { - hooks.set( this ); - } else { - Tween.propHooks._default.set( this ); - } - return this; - } -}; - -Tween.prototype.init.prototype = Tween.prototype; - -Tween.propHooks = { - _default: { - get: function( tween ) { - var result; - - if ( tween.elem[ tween.prop ] != null && - (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) { - return tween.elem[ tween.prop ]; - } - - // passing an empty string as a 3rd parameter to .css will automatically - // attempt a parseFloat and fallback to a string if the parse fails - // so, simple values such as "10px" are parsed to Float. - // complex values such as "rotate(1rad)" are returned as is. - result = jQuery.css( tween.elem, tween.prop, "" ); - // Empty strings, null, undefined and "auto" are converted to 0. - return !result || result === "auto" ? 0 : result; - }, - set: function( tween ) { - // use step hook for back compat - use cssHook if its there - use .style if its - // available and use plain properties where available - if ( jQuery.fx.step[ tween.prop ] ) { - jQuery.fx.step[ tween.prop ]( tween ); - } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) { - jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); - } else { - tween.elem[ tween.prop ] = tween.now; - } - } - } -}; - -// Support: IE <=9 -// Panic based approach to setting things on disconnected nodes - -Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = { - set: function( tween ) { - if ( tween.elem.nodeType && tween.elem.parentNode ) { - tween.elem[ tween.prop ] = tween.now; - } - } -}; - -jQuery.easing = { - linear: function( p ) { - return p; - }, - swing: function( p ) { - return 0.5 - Math.cos( p * Math.PI ) / 2; - } -}; - -jQuery.fx = Tween.prototype.init; - -// Back Compat <1.8 extension point -jQuery.fx.step = {}; - - - - -var - fxNow, timerId, - rfxtypes = /^(?:toggle|show|hide)$/, - rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ), - rrun = /queueHooks$/, - animationPrefilters = [ defaultPrefilter ], - tweeners = { - "*": [ function( prop, value ) { - var tween = this.createTween( prop, value ), - target = tween.cur(), - parts = rfxnum.exec( value ), - unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), - - // Starting value computation is required for potential unit mismatches - start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) && - rfxnum.exec( jQuery.css( tween.elem, prop ) ), - scale = 1, - maxIterations = 20; - - if ( start && start[ 3 ] !== unit ) { - // Trust units reported by jQuery.css - unit = unit || start[ 3 ]; - - // Make sure we update the tween properties later on - parts = parts || []; - - // Iteratively approximate from a nonzero starting point - start = +target || 1; - - do { - // If previous iteration zeroed out, double until we get *something* - // Use a string for doubling factor so we don't accidentally see scale as unchanged below - scale = scale || ".5"; - - // Adjust and apply - start = start / scale; - jQuery.style( tween.elem, prop, start + unit ); - - // Update scale, tolerating zero or NaN from tween.cur() - // And breaking the loop if scale is unchanged or perfect, or if we've just had enough - } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations ); - } - - // Update tween properties - if ( parts ) { - start = tween.start = +start || +target || 0; - tween.unit = unit; - // If a +=/-= token was provided, we're doing a relative animation - tween.end = parts[ 1 ] ? - start + ( parts[ 1 ] + 1 ) * parts[ 2 ] : - +parts[ 2 ]; - } - - return tween; - } ] - }; - -// Animations created synchronously will run synchronously -function createFxNow() { - setTimeout(function() { - fxNow = undefined; - }); - return ( fxNow = jQuery.now() ); -} - -// Generate parameters to create a standard animation -function genFx( type, includeWidth ) { - var which, - attrs = { height: type }, - i = 0; - - // if we include width, step value is 1 to do all cssExpand values, - // if we don't include width, step value is 2 to skip over Left and Right - includeWidth = includeWidth ? 1 : 0; - for ( ; i < 4 ; i += 2 - includeWidth ) { - which = cssExpand[ i ]; - attrs[ "margin" + which ] = attrs[ "padding" + which ] = type; - } - - if ( includeWidth ) { - attrs.opacity = attrs.width = type; - } - - return attrs; -} - -function createTween( value, prop, animation ) { - var tween, - collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ), - index = 0, - length = collection.length; - for ( ; index < length; index++ ) { - if ( (tween = collection[ index ].call( animation, prop, value )) ) { - - // we're done with this property - return tween; - } - } -} - -function defaultPrefilter( elem, props, opts ) { - /* jshint validthis: true */ - var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay, - anim = this, - orig = {}, - style = elem.style, - hidden = elem.nodeType && isHidden( elem ), - dataShow = jQuery._data( elem, "fxshow" ); - - // handle queue: false promises - if ( !opts.queue ) { - hooks = jQuery._queueHooks( elem, "fx" ); - if ( hooks.unqueued == null ) { - hooks.unqueued = 0; - oldfire = hooks.empty.fire; - hooks.empty.fire = function() { - if ( !hooks.unqueued ) { - oldfire(); - } - }; - } - hooks.unqueued++; - - anim.always(function() { - // doing this makes sure that the complete handler will be called - // before this completes - anim.always(function() { - hooks.unqueued--; - if ( !jQuery.queue( elem, "fx" ).length ) { - hooks.empty.fire(); - } - }); - }); - } - - // height/width overflow pass - if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) { - // Make sure that nothing sneaks out - // Record all 3 overflow attributes because IE does not - // change the overflow attribute when overflowX and - // overflowY are set to the same value - opts.overflow = [ style.overflow, style.overflowX, style.overflowY ]; - - // Set display property to inline-block for height/width - // animations on inline elements that are having width/height animated - display = jQuery.css( elem, "display" ); - - // Test default display if display is currently "none" - checkDisplay = display === "none" ? - jQuery._data( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display; - - if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) { - - // inline-level elements accept inline-block; - // block-level elements need to be inline with layout - if ( !support.inlineBlockNeedsLayout || defaultDisplay( elem.nodeName ) === "inline" ) { - style.display = "inline-block"; - } else { - style.zoom = 1; - } - } - } - - if ( opts.overflow ) { - style.overflow = "hidden"; - if ( !support.shrinkWrapBlocks() ) { - anim.always(function() { - style.overflow = opts.overflow[ 0 ]; - style.overflowX = opts.overflow[ 1 ]; - style.overflowY = opts.overflow[ 2 ]; - }); - } - } - - // show/hide pass - for ( prop in props ) { - value = props[ prop ]; - if ( rfxtypes.exec( value ) ) { - delete props[ prop ]; - toggle = toggle || value === "toggle"; - if ( value === ( hidden ? "hide" : "show" ) ) { - - // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden - if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) { - hidden = true; - } else { - continue; - } - } - orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop ); - - // Any non-fx value stops us from restoring the original display value - } else { - display = undefined; - } - } - - if ( !jQuery.isEmptyObject( orig ) ) { - if ( dataShow ) { - if ( "hidden" in dataShow ) { - hidden = dataShow.hidden; - } - } else { - dataShow = jQuery._data( elem, "fxshow", {} ); - } - - // store state if its toggle - enables .stop().toggle() to "reverse" - if ( toggle ) { - dataShow.hidden = !hidden; - } - if ( hidden ) { - jQuery( elem ).show(); - } else { - anim.done(function() { - jQuery( elem ).hide(); - }); - } - anim.done(function() { - var prop; - jQuery._removeData( elem, "fxshow" ); - for ( prop in orig ) { - jQuery.style( elem, prop, orig[ prop ] ); - } - }); - for ( prop in orig ) { - tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim ); - - if ( !( prop in dataShow ) ) { - dataShow[ prop ] = tween.start; - if ( hidden ) { - tween.end = tween.start; - tween.start = prop === "width" || prop === "height" ? 1 : 0; - } - } - } - - // If this is a noop like .hide().hide(), restore an overwritten display value - } else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) { - style.display = display; - } -} - -function propFilter( props, specialEasing ) { - var index, name, easing, value, hooks; - - // camelCase, specialEasing and expand cssHook pass - for ( index in props ) { - name = jQuery.camelCase( index ); - easing = specialEasing[ name ]; - value = props[ index ]; - if ( jQuery.isArray( value ) ) { - easing = value[ 1 ]; - value = props[ index ] = value[ 0 ]; - } - - if ( index !== name ) { - props[ name ] = value; - delete props[ index ]; - } - - hooks = jQuery.cssHooks[ name ]; - if ( hooks && "expand" in hooks ) { - value = hooks.expand( value ); - delete props[ name ]; - - // not quite $.extend, this wont overwrite keys already present. - // also - reusing 'index' from above because we have the correct "name" - for ( index in value ) { - if ( !( index in props ) ) { - props[ index ] = value[ index ]; - specialEasing[ index ] = easing; - } - } - } else { - specialEasing[ name ] = easing; - } - } -} - -function Animation( elem, properties, options ) { - var result, - stopped, - index = 0, - length = animationPrefilters.length, - deferred = jQuery.Deferred().always( function() { - // don't match elem in the :animated selector - delete tick.elem; - }), - tick = function() { - if ( stopped ) { - return false; - } - var currentTime = fxNow || createFxNow(), - remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ), - // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497) - temp = remaining / animation.duration || 0, - percent = 1 - temp, - index = 0, - length = animation.tweens.length; - - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( percent ); - } - - deferred.notifyWith( elem, [ animation, percent, remaining ]); - - if ( percent < 1 && length ) { - return remaining; - } else { - deferred.resolveWith( elem, [ animation ] ); - return false; - } - }, - animation = deferred.promise({ - elem: elem, - props: jQuery.extend( {}, properties ), - opts: jQuery.extend( true, { specialEasing: {} }, options ), - originalProperties: properties, - originalOptions: options, - startTime: fxNow || createFxNow(), - duration: options.duration, - tweens: [], - createTween: function( prop, end ) { - var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); - animation.tweens.push( tween ); - return tween; - }, - stop: function( gotoEnd ) { - var index = 0, - // if we are going to the end, we want to run all the tweens - // otherwise we skip this part - length = gotoEnd ? animation.tweens.length : 0; - if ( stopped ) { - return this; - } - stopped = true; - for ( ; index < length ; index++ ) { - animation.tweens[ index ].run( 1 ); - } - - // resolve when we played the last frame - // otherwise, reject - if ( gotoEnd ) { - deferred.resolveWith( elem, [ animation, gotoEnd ] ); - } else { - deferred.rejectWith( elem, [ animation, gotoEnd ] ); - } - return this; - } - }), - props = animation.props; - - propFilter( props, animation.opts.specialEasing ); - - for ( ; index < length ; index++ ) { - result = animationPrefilters[ index ].call( animation, elem, props, animation.opts ); - if ( result ) { - return result; - } - } - - jQuery.map( props, createTween, animation ); - - if ( jQuery.isFunction( animation.opts.start ) ) { - animation.opts.start.call( elem, animation ); - } - - jQuery.fx.timer( - jQuery.extend( tick, { - elem: elem, - anim: animation, - queue: animation.opts.queue - }) - ); - - // attach callbacks from options - return animation.progress( animation.opts.progress ) - .done( animation.opts.done, animation.opts.complete ) - .fail( animation.opts.fail ) - .always( animation.opts.always ); -} - -jQuery.Animation = jQuery.extend( Animation, { - tweener: function( props, callback ) { - if ( jQuery.isFunction( props ) ) { - callback = props; - props = [ "*" ]; - } else { - props = props.split(" "); - } - - var prop, - index = 0, - length = props.length; - - for ( ; index < length ; index++ ) { - prop = props[ index ]; - tweeners[ prop ] = tweeners[ prop ] || []; - tweeners[ prop ].unshift( callback ); - } - }, - - prefilter: function( callback, prepend ) { - if ( prepend ) { - animationPrefilters.unshift( callback ); - } else { - animationPrefilters.push( callback ); - } - } -}); - -jQuery.speed = function( speed, easing, fn ) { - var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : { - complete: fn || !fn && easing || - jQuery.isFunction( speed ) && speed, - duration: speed, - easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing - }; - - opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration : - opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default; - - // normalize opt.queue - true/undefined/null -> "fx" - if ( opt.queue == null || opt.queue === true ) { - opt.queue = "fx"; - } - - // Queueing - opt.old = opt.complete; - - opt.complete = function() { - if ( jQuery.isFunction( opt.old ) ) { - opt.old.call( this ); - } - - if ( opt.queue ) { - jQuery.dequeue( this, opt.queue ); - } - }; - - return opt; -}; - -jQuery.fn.extend({ - fadeTo: function( speed, to, easing, callback ) { - - // show any hidden elements after setting opacity to 0 - return this.filter( isHidden ).css( "opacity", 0 ).show() - - // animate to the value specified - .end().animate({ opacity: to }, speed, easing, callback ); - }, - animate: function( prop, speed, easing, callback ) { - var empty = jQuery.isEmptyObject( prop ), - optall = jQuery.speed( speed, easing, callback ), - doAnimation = function() { - // Operate on a copy of prop so per-property easing won't be lost - var anim = Animation( this, jQuery.extend( {}, prop ), optall ); - - // Empty animations, or finishing resolves immediately - if ( empty || jQuery._data( this, "finish" ) ) { - anim.stop( true ); - } - }; - doAnimation.finish = doAnimation; - - return empty || optall.queue === false ? - this.each( doAnimation ) : - this.queue( optall.queue, doAnimation ); - }, - stop: function( type, clearQueue, gotoEnd ) { - var stopQueue = function( hooks ) { - var stop = hooks.stop; - delete hooks.stop; - stop( gotoEnd ); - }; - - if ( typeof type !== "string" ) { - gotoEnd = clearQueue; - clearQueue = type; - type = undefined; - } - if ( clearQueue && type !== false ) { - this.queue( type || "fx", [] ); - } - - return this.each(function() { - var dequeue = true, - index = type != null && type + "queueHooks", - timers = jQuery.timers, - data = jQuery._data( this ); - - if ( index ) { - if ( data[ index ] && data[ index ].stop ) { - stopQueue( data[ index ] ); - } - } else { - for ( index in data ) { - if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) { - stopQueue( data[ index ] ); - } - } - } - - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) { - timers[ index ].anim.stop( gotoEnd ); - dequeue = false; - timers.splice( index, 1 ); - } - } - - // start the next in the queue if the last step wasn't forced - // timers currently will call their complete callbacks, which will dequeue - // but only if they were gotoEnd - if ( dequeue || !gotoEnd ) { - jQuery.dequeue( this, type ); - } - }); - }, - finish: function( type ) { - if ( type !== false ) { - type = type || "fx"; - } - return this.each(function() { - var index, - data = jQuery._data( this ), - queue = data[ type + "queue" ], - hooks = data[ type + "queueHooks" ], - timers = jQuery.timers, - length = queue ? queue.length : 0; - - // enable finishing flag on private data - data.finish = true; - - // empty the queue first - jQuery.queue( this, type, [] ); - - if ( hooks && hooks.stop ) { - hooks.stop.call( this, true ); - } - - // look for any active animations, and finish them - for ( index = timers.length; index--; ) { - if ( timers[ index ].elem === this && timers[ index ].queue === type ) { - timers[ index ].anim.stop( true ); - timers.splice( index, 1 ); - } - } - - // look for any animations in the old queue and finish them - for ( index = 0; index < length; index++ ) { - if ( queue[ index ] && queue[ index ].finish ) { - queue[ index ].finish.call( this ); - } - } - - // turn off finishing flag - delete data.finish; - }); - } -}); - -jQuery.each([ "toggle", "show", "hide" ], function( i, name ) { - var cssFn = jQuery.fn[ name ]; - jQuery.fn[ name ] = function( speed, easing, callback ) { - return speed == null || typeof speed === "boolean" ? - cssFn.apply( this, arguments ) : - this.animate( genFx( name, true ), speed, easing, callback ); - }; -}); - -// Generate shortcuts for custom animations -jQuery.each({ - slideDown: genFx("show"), - slideUp: genFx("hide"), - slideToggle: genFx("toggle"), - fadeIn: { opacity: "show" }, - fadeOut: { opacity: "hide" }, - fadeToggle: { opacity: "toggle" } -}, function( name, props ) { - jQuery.fn[ name ] = function( speed, easing, callback ) { - return this.animate( props, speed, easing, callback ); - }; -}); - -jQuery.timers = []; -jQuery.fx.tick = function() { - var timer, - timers = jQuery.timers, - i = 0; - - fxNow = jQuery.now(); - - for ( ; i < timers.length; i++ ) { - timer = timers[ i ]; - // Checks the timer has not already been removed - if ( !timer() && timers[ i ] === timer ) { - timers.splice( i--, 1 ); - } - } - - if ( !timers.length ) { - jQuery.fx.stop(); - } - fxNow = undefined; -}; - -jQuery.fx.timer = function( timer ) { - jQuery.timers.push( timer ); - if ( timer() ) { - jQuery.fx.start(); - } else { - jQuery.timers.pop(); - } -}; - -jQuery.fx.interval = 13; - -jQuery.fx.start = function() { - if ( !timerId ) { - timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval ); - } -}; - -jQuery.fx.stop = function() { - clearInterval( timerId ); - timerId = null; -}; - -jQuery.fx.speeds = { - slow: 600, - fast: 200, - // Default speed - _default: 400 -}; - - -// Based off of the plugin by Clint Helfers, with permission. -// http://blindsignals.com/index.php/2009/07/jquery-delay/ -jQuery.fn.delay = function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); -}; - - -(function() { - // Minified: var a,b,c,d,e - var input, div, select, a, opt; - - // Setup - div = document.createElement( "div" ); - div.setAttribute( "className", "t" ); - div.innerHTML = " <link/><table></table><a href='/a'>a</a><input type='checkbox'/>"; - a = div.getElementsByTagName("a")[ 0 ]; - - // First batch of tests. - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px"; - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; - - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test( a.getAttribute("style") ); - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !!input.value; - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; - - // Tests for enctype support on a form (#6743) - support.enctype = !!document.createElement("form").enctype; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE8 only - // Check if we can trust getAttribute("value") - input = document.createElement( "input" ); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; -})(); - - -var rreturn = /\r/g; - -jQuery.fn.extend({ - val: function( value ) { - var hooks, ret, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, jQuery( this ).val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map( val, function( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - var val = jQuery.find.attr( elem, "value" ); - return val != null ? - val : - // Support: IE10-11+ - // option.text throws exceptions (#14686, #14858) - jQuery.trim( jQuery.text( elem ) ); - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray( value ), - i = options.length; - - while ( i-- ) { - option = options[ i ]; - - if ( jQuery.inArray( jQuery.valHooks.option.get( option ), values ) >= 0 ) { - - // Support: IE6 - // When new option element is added to select box we need to - // force reflow of newly added node in order to workaround delay - // of initialization properties - try { - option.selected = optionSet = true; - - } catch ( _ ) { - - // Will be executed only in IE6 - option.scrollHeight; - } - - } else { - option.selected = false; - } - } - - // Force browsers to behave consistently when non-matching value is set - if ( !optionSet ) { - elem.selectedIndex = -1; - } - - return options; - } - } - } -}); - -// Radios and checkboxes getter/setter -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }; - if ( !support.checkOn ) { - jQuery.valHooks[ this ].get = function( elem ) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } -}); - - - - -var nodeHook, boolHook, - attrHandle = jQuery.expr.attrHandle, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = support.getSetAttribute, - getSetInput = support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - } -}); - -jQuery.extend({ - attr: function( elem, name, value ) { - var hooks, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === strundefined ) { - return jQuery.prop( elem, name, value ); - } - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || - ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - ret = jQuery.find.attr( elem, name ); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( jQuery.expr.match.bool.test( name ) ) { - // Set corresponding property to false - if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - elem[ propName ] = false; - // Support: IE<9 - // Also clear defaultChecked/defaultSelected (if appropriate) - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - } -}); - -// Hook for boolean attributes -boolHook = { - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; - -// Retrieve booleans specially -jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { - - var getter = attrHandle[ name ] || jQuery.find.attr; - - attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? - function( elem, name, isXML ) { - var ret, handle; - if ( !isXML ) { - // Avoid an infinite loop by temporarily removing this function from the getter - handle = attrHandle[ name ]; - attrHandle[ name ] = ret; - ret = getter( elem, name, isXML ) != null ? - name.toLowerCase() : - null; - attrHandle[ name ] = handle; - } - return ret; - } : - function( elem, name, isXML ) { - if ( !isXML ) { - return elem[ jQuery.camelCase( "default-" + name ) ] ? - name.toLowerCase() : - null; - } - }; -}); - -// fix oldIE attroperties -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = { - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - if ( name === "value" || value === elem.getAttribute( name ) ) { - return value; - } - } - }; - - // Some attributes are constructed with empty-string values when not defined - attrHandle.id = attrHandle.name = attrHandle.coords = - function( elem, name, isXML ) { - var ret; - if ( !isXML ) { - return (ret = elem.getAttributeNode( name )) && ret.value !== "" ? - ret.value : - null; - } - }; - - // Fixing value retrieval on a button requires this module - jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - if ( ret && ret.specified ) { - return ret.value; - } - }, - set: nodeHook.set - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }; - }); -} - -if ( !support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} - - - - -var rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i; - -jQuery.fn.extend({ - prop: function( name, value ) { - return access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - } -}); - -jQuery.extend({ - propFix: { - "for": "htmlFor", - "class": "className" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? - ret : - ( elem[ name ] = value ); - - } else { - return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? - ret : - elem[ name ]; - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr( elem, "tabindex" ); - - return tabindex ? - parseInt( tabindex, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - -1; - } - } - } -}); - -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !support.hrefNormalized ) { - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} - -// Support: Safari, IE9+ -// mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !support.optSelected ) { - jQuery.propHooks.selected = { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }; -} - -jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" -], function() { - jQuery.propFix[ this.toLowerCase() ] = this; -}); - -// IE6/7 call enctype encoding -if ( !support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - - - - -var rclass = /[\t\r\n\f]/g; - -jQuery.fn.extend({ - addClass: function( value ) { - var classes, elem, cur, clazz, j, finalValue, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - - // only assign if different to avoid unneeded rendering. - finalValue = jQuery.trim( cur ); - if ( elem.className !== finalValue ) { - elem.className = finalValue; - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, finalValue, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - - // only assign if different to avoid unneeded rendering. - finalValue = value ? jQuery.trim( cur ) : ""; - if ( elem.className !== finalValue ) { - elem.className = finalValue; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value; - - if ( typeof stateVal === "boolean" && type === "string" ) { - return stateVal ? this.addClass( value ) : this.removeClass( value ); - } - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - classNames = value.match( rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - if ( self.hasClass( className ) ) { - self.removeClass( className ); - } else { - self.addClass( className ); - } - } - - // Toggle whole class name - } else if ( type === strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - } -}); - - - - -// Return jQuery for attributes-only inclusion - - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - return arguments.length > 0 ? - this.on( name, null, data, fn ) : - this.trigger( name ); - }; -}); - -jQuery.fn.extend({ - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); - } -}); - - -var nonce = jQuery.now(); - -var rquery = (/\?/); - - - -var rvalidtokens = /(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g; - -jQuery.parseJSON = function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - // Support: Android 2.3 - // Workaround failure to string-cast null input - return window.JSON.parse( data + "" ); - } - - var requireNonComma, - depth = null, - str = jQuery.trim( data + "" ); - - // Guard against invalid (and possibly dangerous) input by ensuring that nothing remains - // after removing valid tokens - return str && !jQuery.trim( str.replace( rvalidtokens, function( token, comma, open, close ) { - - // Force termination if we see a misplaced comma - if ( requireNonComma && comma ) { - depth = 0; - } - - // Perform no more replacements after returning to outermost depth - if ( depth === 0 ) { - return token; - } - - // Commas must not follow "[", "{", or "," - requireNonComma = open || comma; - - // Determine new depth - // array/object open ("[" or "{"): depth += true - false (increment) - // array/object close ("]" or "}"): depth += false - true (decrement) - // other cases ("," or primitive): depth += true - true (numeric cast) - depth += !close - !open; - - // Remove this token - return ""; - }) ) ? - ( Function( "return " + str ) )() : - jQuery.error( "Invalid JSON: " + data ); -}; - - -// Cross-browser xml parsing -jQuery.parseXML = function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data, "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; -}; - - -var - // Document location - ajaxLocParts, - ajaxLocation, - - rhash = /#.*$/, - rts = /([?&])_=[^&]*/, - rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, // IE leaves an \r character at EOL - // #7653, #8125, #8152: local protocol detection - rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/, - rnoContent = /^(?:GET|HEAD)$/, - rprotocol = /^\/\//, - rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/, - - /* Prefilters - * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example) - * 2) These are called: - * - BEFORE asking for a transport - * - AFTER param serialization (s.data is a string if s.processData is true) - * 3) key is the dataType - * 4) the catchall symbol "*" can be used - * 5) execution will start with transport dataType and THEN continue down to "*" if needed - */ - prefilters = {}, - - /* Transports bindings - * 1) key is the dataType - * 2) the catchall symbol "*" can be used - * 3) selection will start with transport dataType and THEN go to "*" if needed - */ - transports = {}, - - // Avoid comment-prolog char sequence (#10098); must appease lint and evade compression - allTypes = "*/".concat("*"); - -// #8138, IE may throw an exception when accessing -// a field from window.location if document.domain has been set -try { - ajaxLocation = location.href; -} catch( e ) { - // Use the href attribute of an A element - // since IE will modify it given document.location - ajaxLocation = document.createElement( "a" ); - ajaxLocation.href = ""; - ajaxLocation = ajaxLocation.href; -} - -// Segment location into parts -ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || []; - -// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport -function addToPrefiltersOrTransports( structure ) { - - // dataTypeExpression is optional and defaults to "*" - return function( dataTypeExpression, func ) { - - if ( typeof dataTypeExpression !== "string" ) { - func = dataTypeExpression; - dataTypeExpression = "*"; - } - - var dataType, - i = 0, - dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || []; - - if ( jQuery.isFunction( func ) ) { - // For each dataType in the dataTypeExpression - while ( (dataType = dataTypes[i++]) ) { - // Prepend if requested - if ( dataType.charAt( 0 ) === "+" ) { - dataType = dataType.slice( 1 ) || "*"; - (structure[ dataType ] = structure[ dataType ] || []).unshift( func ); - - // Otherwise append - } else { - (structure[ dataType ] = structure[ dataType ] || []).push( func ); - } - } - } - }; -} - -// Base inspection function for prefilters and transports -function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) { - - var inspected = {}, - seekingTransport = ( structure === transports ); - - function inspect( dataType ) { - var selected; - inspected[ dataType ] = true; - jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) { - var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR ); - if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) { - options.dataTypes.unshift( dataTypeOrTransport ); - inspect( dataTypeOrTransport ); - return false; - } else if ( seekingTransport ) { - return !( selected = dataTypeOrTransport ); - } - }); - return selected; - } - - return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" ); -} - -// A special extend for ajax options -// that takes "flat" options (not to be deep extended) -// Fixes #9887 -function ajaxExtend( target, src ) { - var deep, key, - flatOptions = jQuery.ajaxSettings.flatOptions || {}; - - for ( key in src ) { - if ( src[ key ] !== undefined ) { - ( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ]; - } - } - if ( deep ) { - jQuery.extend( true, target, deep ); - } - - return target; -} - -/* Handles responses to an ajax request: - * - finds the right dataType (mediates between content-type and expected dataType) - * - returns the corresponding response - */ -function ajaxHandleResponses( s, jqXHR, responses ) { - var firstDataType, ct, finalDataType, type, - contents = s.contents, - dataTypes = s.dataTypes; - - // Remove auto dataType and get content-type in the process - while ( dataTypes[ 0 ] === "*" ) { - dataTypes.shift(); - if ( ct === undefined ) { - ct = s.mimeType || jqXHR.getResponseHeader("Content-Type"); - } - } - - // Check if we're dealing with a known content-type - if ( ct ) { - for ( type in contents ) { - if ( contents[ type ] && contents[ type ].test( ct ) ) { - dataTypes.unshift( type ); - break; - } - } - } - - // Check to see if we have a response for the expected dataType - if ( dataTypes[ 0 ] in responses ) { - finalDataType = dataTypes[ 0 ]; - } else { - // Try convertible dataTypes - for ( type in responses ) { - if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) { - finalDataType = type; - break; - } - if ( !firstDataType ) { - firstDataType = type; - } - } - // Or just use first one - finalDataType = finalDataType || firstDataType; - } - - // If we found a dataType - // We add the dataType to the list if needed - // and return the corresponding response - if ( finalDataType ) { - if ( finalDataType !== dataTypes[ 0 ] ) { - dataTypes.unshift( finalDataType ); - } - return responses[ finalDataType ]; - } -} - -/* Chain conversions given the request and the original response - * Also sets the responseXXX fields on the jqXHR instance - */ -function ajaxConvert( s, response, jqXHR, isSuccess ) { - var conv2, current, conv, tmp, prev, - converters = {}, - // Work with a copy of dataTypes in case we need to modify it for conversion - dataTypes = s.dataTypes.slice(); - - // Create converters map with lowercased keys - if ( dataTypes[ 1 ] ) { - for ( conv in s.converters ) { - converters[ conv.toLowerCase() ] = s.converters[ conv ]; - } - } - - current = dataTypes.shift(); - - // Convert to each sequential dataType - while ( current ) { - - if ( s.responseFields[ current ] ) { - jqXHR[ s.responseFields[ current ] ] = response; - } - - // Apply the dataFilter if provided - if ( !prev && isSuccess && s.dataFilter ) { - response = s.dataFilter( response, s.dataType ); - } - - prev = current; - current = dataTypes.shift(); - - if ( current ) { - - // There's only work to do if current dataType is non-auto - if ( current === "*" ) { - - current = prev; - - // Convert response if prev dataType is non-auto and differs from current - } else if ( prev !== "*" && prev !== current ) { - - // Seek a direct converter - conv = converters[ prev + " " + current ] || converters[ "* " + current ]; - - // If none found, seek a pair - if ( !conv ) { - for ( conv2 in converters ) { - - // If conv2 outputs current - tmp = conv2.split( " " ); - if ( tmp[ 1 ] === current ) { - - // If prev can be converted to accepted input - conv = converters[ prev + " " + tmp[ 0 ] ] || - converters[ "* " + tmp[ 0 ] ]; - if ( conv ) { - // Condense equivalence converters - if ( conv === true ) { - conv = converters[ conv2 ]; - - // Otherwise, insert the intermediate dataType - } else if ( converters[ conv2 ] !== true ) { - current = tmp[ 0 ]; - dataTypes.unshift( tmp[ 1 ] ); - } - break; - } - } - } - } - - // Apply converter (if not an equivalence) - if ( conv !== true ) { - - // Unless errors are allowed to bubble, catch and return them - if ( conv && s[ "throws" ] ) { - response = conv( response ); - } else { - try { - response = conv( response ); - } catch ( e ) { - return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current }; - } - } - } - } - } - } - - return { state: "success", data: response }; -} - -jQuery.extend({ - - // Counter for holding the number of active queries - active: 0, - - // Last-Modified header cache for next request - lastModified: {}, - etag: {}, - - ajaxSettings: { - url: ajaxLocation, - type: "GET", - isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ), - global: true, - processData: true, - async: true, - contentType: "application/x-www-form-urlencoded; charset=UTF-8", - /* - timeout: 0, - data: null, - dataType: null, - username: null, - password: null, - cache: null, - throws: false, - traditional: false, - headers: {}, - */ - - accepts: { - "*": allTypes, - text: "text/plain", - html: "text/html", - xml: "application/xml, text/xml", - json: "application/json, text/javascript" - }, - - contents: { - xml: /xml/, - html: /html/, - json: /json/ - }, - - responseFields: { - xml: "responseXML", - text: "responseText", - json: "responseJSON" - }, - - // Data converters - // Keys separate source (or catchall "*") and destination types with a single space - converters: { - - // Convert anything to text - "* text": String, - - // Text to html (true = no transformation) - "text html": true, - - // Evaluate text as a json expression - "text json": jQuery.parseJSON, - - // Parse text as xml - "text xml": jQuery.parseXML - }, - - // For options that shouldn't be deep extended: - // you can add your own custom options here if - // and when you create one that shouldn't be - // deep extended (see ajaxExtend) - flatOptions: { - url: true, - context: true - } - }, - - // Creates a full fledged settings object into target - // with both ajaxSettings and settings fields. - // If target is omitted, writes into ajaxSettings. - ajaxSetup: function( target, settings ) { - return settings ? - - // Building a settings object - ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) : - - // Extending ajaxSettings - ajaxExtend( jQuery.ajaxSettings, target ); - }, - - ajaxPrefilter: addToPrefiltersOrTransports( prefilters ), - ajaxTransport: addToPrefiltersOrTransports( transports ), - - // Main method - ajax: function( url, options ) { - - // If url is an object, simulate pre-1.5 signature - if ( typeof url === "object" ) { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - var // Cross-domain detection vars - parts, - // Loop variable - i, - // URL without anti-cache param - cacheURL, - // Response headers as string - responseHeadersString, - // timeout handle - timeoutTimer, - - // To know if global events are to be dispatched - fireGlobals, - - transport, - // Response headers - responseHeaders, - // Create the final options object - s = jQuery.ajaxSetup( {}, options ), - // Callbacks context - callbackContext = s.context || s, - // Context for global events is callbackContext if it is a DOM node or jQuery collection - globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, - // Deferreds - deferred = jQuery.Deferred(), - completeDeferred = jQuery.Callbacks("once memory"), - // Status-dependent callbacks - statusCode = s.statusCode || {}, - // Headers (they are sent all at once) - requestHeaders = {}, - requestHeadersNames = {}, - // The jqXHR state - state = 0, - // Default abort message - strAbort = "canceled", - // Fake xhr - jqXHR = { - readyState: 0, - - // Builds headers hashtable if needed - getResponseHeader: function( key ) { - var match; - if ( state === 2 ) { - if ( !responseHeaders ) { - responseHeaders = {}; - while ( (match = rheaders.exec( responseHeadersString )) ) { - responseHeaders[ match[1].toLowerCase() ] = match[ 2 ]; - } - } - match = responseHeaders[ key.toLowerCase() ]; - } - return match == null ? null : match; - }, - - // Raw string - getAllResponseHeaders: function() { - return state === 2 ? responseHeadersString : null; - }, - - // Caches the header - setRequestHeader: function( name, value ) { - var lname = name.toLowerCase(); - if ( !state ) { - name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name; - requestHeaders[ name ] = value; - } - return this; - }, - - // Overrides response content-type header - overrideMimeType: function( type ) { - if ( !state ) { - s.mimeType = type; - } - return this; - }, - - // Status-dependent callbacks - statusCode: function( map ) { - var code; - if ( map ) { - if ( state < 2 ) { - for ( code in map ) { - // Lazy-add the new callback in a way that preserves old ones - statusCode[ code ] = [ statusCode[ code ], map[ code ] ]; - } - } else { - // Execute the appropriate callbacks - jqXHR.always( map[ jqXHR.status ] ); - } - } - return this; - }, - - // Cancel the request - abort: function( statusText ) { - var finalText = statusText || strAbort; - if ( transport ) { - transport.abort( finalText ); - } - done( 0, finalText ); - return this; - } - }; - - // Attach deferreds - deferred.promise( jqXHR ).complete = completeDeferred.add; - jqXHR.success = jqXHR.done; - jqXHR.error = jqXHR.fail; - - // Remove hash character (#7531: and string promotion) - // Add protocol if not provided (#5866: IE7 issue with protocol-less urls) - // Handle falsy url in the settings object (#10093: consistency with old signature) - // We also use the url parameter if available - s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" ); - - // Alias method option to type as per ticket #12004 - s.type = options.method || options.type || s.method || s.type; - - // Extract dataTypes list - s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ]; - - // A cross-domain request is in order when we have a protocol:host:port mismatch - if ( s.crossDomain == null ) { - parts = rurl.exec( s.url.toLowerCase() ); - s.crossDomain = !!( parts && - ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] || - ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !== - ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) ) - ); - } - - // Convert data if not already a string - if ( s.data && s.processData && typeof s.data !== "string" ) { - s.data = jQuery.param( s.data, s.traditional ); - } - - // Apply prefilters - inspectPrefiltersOrTransports( prefilters, s, options, jqXHR ); - - // If request was aborted inside a prefilter, stop there - if ( state === 2 ) { - return jqXHR; - } - - // We can fire global events as of now if asked to - // Don't fire events if jQuery.event is undefined in an AMD-usage scenario (#15118) - fireGlobals = jQuery.event && s.global; - - // Watch for a new set of requests - if ( fireGlobals && jQuery.active++ === 0 ) { - jQuery.event.trigger("ajaxStart"); - } - - // Uppercase the type - s.type = s.type.toUpperCase(); - - // Determine if request has content - s.hasContent = !rnoContent.test( s.type ); - - // Save the URL in case we're toying with the If-Modified-Since - // and/or If-None-Match header later on - cacheURL = s.url; - - // More options handling for requests with no content - if ( !s.hasContent ) { - - // If data is available, append data to url - if ( s.data ) { - cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data ); - // #9682: remove data so that it's not used in an eventual retry - delete s.data; - } - - // Add anti-cache in url if needed - if ( s.cache === false ) { - s.url = rts.test( cacheURL ) ? - - // If there is already a '_' parameter, set its value - cacheURL.replace( rts, "$1_=" + nonce++ ) : - - // Otherwise add one to the end - cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++; - } - } - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - if ( jQuery.lastModified[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] ); - } - if ( jQuery.etag[ cacheURL ] ) { - jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] ); - } - } - - // Set the correct header, if data is being sent - if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) { - jqXHR.setRequestHeader( "Content-Type", s.contentType ); - } - - // Set the Accepts header for the server, depending on the dataType - jqXHR.setRequestHeader( - "Accept", - s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ? - s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) : - s.accepts[ "*" ] - ); - - // Check for headers option - for ( i in s.headers ) { - jqXHR.setRequestHeader( i, s.headers[ i ] ); - } - - // Allow custom headers/mimetypes and early abort - if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) { - // Abort if not done already and return - return jqXHR.abort(); - } - - // aborting is no longer a cancellation - strAbort = "abort"; - - // Install callbacks on deferreds - for ( i in { success: 1, error: 1, complete: 1 } ) { - jqXHR[ i ]( s[ i ] ); - } - - // Get transport - transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR ); - - // If no transport, we auto-abort - if ( !transport ) { - done( -1, "No Transport" ); - } else { - jqXHR.readyState = 1; - - // Send global event - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] ); - } - // Timeout - if ( s.async && s.timeout > 0 ) { - timeoutTimer = setTimeout(function() { - jqXHR.abort("timeout"); - }, s.timeout ); - } - - try { - state = 1; - transport.send( requestHeaders, done ); - } catch ( e ) { - // Propagate exception as error if not done - if ( state < 2 ) { - done( -1, e ); - // Simply rethrow otherwise - } else { - throw e; - } - } - } - - // Callback for when everything is done - function done( status, nativeStatusText, responses, headers ) { - var isSuccess, success, error, response, modified, - statusText = nativeStatusText; - - // Called once - if ( state === 2 ) { - return; - } - - // State is "done" now - state = 2; - - // Clear timeout if it exists - if ( timeoutTimer ) { - clearTimeout( timeoutTimer ); - } - - // Dereference transport for early garbage collection - // (no matter how long the jqXHR object will be used) - transport = undefined; - - // Cache response headers - responseHeadersString = headers || ""; - - // Set readyState - jqXHR.readyState = status > 0 ? 4 : 0; - - // Determine if successful - isSuccess = status >= 200 && status < 300 || status === 304; - - // Get response data - if ( responses ) { - response = ajaxHandleResponses( s, jqXHR, responses ); - } - - // Convert no matter what (that way responseXXX fields are always set) - response = ajaxConvert( s, response, jqXHR, isSuccess ); - - // If successful, handle type chaining - if ( isSuccess ) { - - // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode. - if ( s.ifModified ) { - modified = jqXHR.getResponseHeader("Last-Modified"); - if ( modified ) { - jQuery.lastModified[ cacheURL ] = modified; - } - modified = jqXHR.getResponseHeader("etag"); - if ( modified ) { - jQuery.etag[ cacheURL ] = modified; - } - } - - // if no content - if ( status === 204 || s.type === "HEAD" ) { - statusText = "nocontent"; - - // if not modified - } else if ( status === 304 ) { - statusText = "notmodified"; - - // If we have data, let's convert it - } else { - statusText = response.state; - success = response.data; - error = response.error; - isSuccess = !error; - } - } else { - // We extract error from statusText - // then normalize statusText and status for non-aborts - error = statusText; - if ( status || !statusText ) { - statusText = "error"; - if ( status < 0 ) { - status = 0; - } - } - } - - // Set data for the fake xhr object - jqXHR.status = status; - jqXHR.statusText = ( nativeStatusText || statusText ) + ""; - - // Success/Error - if ( isSuccess ) { - deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] ); - } else { - deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] ); - } - - // Status-dependent callbacks - jqXHR.statusCode( statusCode ); - statusCode = undefined; - - if ( fireGlobals ) { - globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError", - [ jqXHR, s, isSuccess ? success : error ] ); - } - - // Complete - completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] ); - - if ( fireGlobals ) { - globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] ); - // Handle the global AJAX counter - if ( !( --jQuery.active ) ) { - jQuery.event.trigger("ajaxStop"); - } - } - } - - return jqXHR; - }, - - getJSON: function( url, data, callback ) { - return jQuery.get( url, data, callback, "json" ); - }, - - getScript: function( url, callback ) { - return jQuery.get( url, undefined, callback, "script" ); - } -}); - -jQuery.each( [ "get", "post" ], function( i, method ) { - jQuery[ method ] = function( url, data, callback, type ) { - // shift arguments if data argument was omitted - if ( jQuery.isFunction( data ) ) { - type = type || callback; - callback = data; - data = undefined; - } - - return jQuery.ajax({ - url: url, - type: method, - dataType: type, - data: data, - success: callback - }); - }; -}); - - -jQuery._evalUrl = function( url ) { - return jQuery.ajax({ - url: url, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); -}; - - -jQuery.fn.extend({ - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - } -}); - - -jQuery.expr.filters.hidden = function( elem ) { - // Support: Opera <= 12.12 - // Opera reports offsetWidths and offsetHeights less than zero on some elements - return elem.offsetWidth <= 0 && elem.offsetHeight <= 0 || - (!support.reliableHiddenOffsets() && - ((elem.style && elem.style.display) || jQuery.css( elem, "display" )) === "none"); -}; - -jQuery.expr.filters.visible = function( elem ) { - return !jQuery.expr.filters.hidden( elem ); -}; - - - - -var r20 = /%20/g, - rbracket = /\[\]$/, - rCRLF = /\r?\n/g, - rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i, - rsubmittable = /^(?:input|select|textarea|keygen)/i; - -function buildParams( prefix, obj, traditional, add ) { - var name; - - if ( jQuery.isArray( obj ) ) { - // Serialize array item. - jQuery.each( obj, function( i, v ) { - if ( traditional || rbracket.test( prefix ) ) { - // Treat each array item as a scalar. - add( prefix, v ); - - } else { - // Item is non-scalar (array or object), encode its numeric index. - buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add ); - } - }); - - } else if ( !traditional && jQuery.type( obj ) === "object" ) { - // Serialize object item. - for ( name in obj ) { - buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add ); - } - - } else { - // Serialize scalar item. - add( prefix, obj ); - } -} - -// Serialize an array of form elements or a set of -// key/values into a query string -jQuery.param = function( a, traditional ) { - var prefix, - s = [], - add = function( key, value ) { - // If value is a function, invoke it and return its value - value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value ); - s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value ); - }; - - // Set traditional to true for jQuery <= 1.3.2 behavior. - if ( traditional === undefined ) { - traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional; - } - - // If an array was passed in, assume that it is an array of form elements. - if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) { - // Serialize the form elements - jQuery.each( a, function() { - add( this.name, this.value ); - }); - - } else { - // If traditional, encode the "old" way (the way 1.3.2 or older - // did it), otherwise encode params recursively. - for ( prefix in a ) { - buildParams( prefix, a[ prefix ], traditional, add ); - } - } - - // Return the resulting serialization - return s.join( "&" ).replace( r20, "+" ); -}; - -jQuery.fn.extend({ - serialize: function() { - return jQuery.param( this.serializeArray() ); - }, - serializeArray: function() { - return this.map(function() { - // Can add propHook for "elements" to filter or add form elements - var elements = jQuery.prop( this, "elements" ); - return elements ? jQuery.makeArray( elements ) : this; - }) - .filter(function() { - var type = this.type; - // Use .is(":disabled") so that fieldset[disabled] works - return this.name && !jQuery( this ).is( ":disabled" ) && - rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && - ( this.checked || !rcheckableType.test( type ) ); - }) - .map(function( i, elem ) { - var val = jQuery( this ).val(); - - return val == null ? - null : - jQuery.isArray( val ) ? - jQuery.map( val, function( val ) { - return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }) : - { name: elem.name, value: val.replace( rCRLF, "\r\n" ) }; - }).get(); - } -}); - - -// Create the request object -// (This is still attached to ajaxSettings for backward compatibility) -jQuery.ajaxSettings.xhr = window.ActiveXObject !== undefined ? - // Support: IE6+ - function() { - - // XHR cannot access local files, always use ActiveX for that case - return !this.isLocal && - - // Support: IE7-8 - // oldIE XHR does not support non-RFC2616 methods (#13240) - // See http://msdn.microsoft.com/en-us/library/ie/ms536648(v=vs.85).aspx - // and http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9 - // Although this check for six methods instead of eight - // since IE also does not support "trace" and "connect" - /^(get|post|head|put|delete|options)$/i.test( this.type ) && - - createStandardXHR() || createActiveXHR(); - } : - // For all other browsers, use the standard XMLHttpRequest object - createStandardXHR; - -var xhrId = 0, - xhrCallbacks = {}, - xhrSupported = jQuery.ajaxSettings.xhr(); - -// Support: IE<10 -// Open requests must be manually aborted on unload (#5280) -// See https://support.microsoft.com/kb/2856746 for more info -if ( window.attachEvent ) { - window.attachEvent( "onunload", function() { - for ( var key in xhrCallbacks ) { - xhrCallbacks[ key ]( undefined, true ); - } - }); -} - -// Determine support properties -support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported ); -xhrSupported = support.ajax = !!xhrSupported; - -// Create transport if the browser can provide an xhr -if ( xhrSupported ) { - - jQuery.ajaxTransport(function( options ) { - // Cross domain only allowed if supported through XMLHttpRequest - if ( !options.crossDomain || support.cors ) { - - var callback; - - return { - send: function( headers, complete ) { - var i, - xhr = options.xhr(), - id = ++xhrId; - - // Open the socket - xhr.open( options.type, options.url, options.async, options.username, options.password ); - - // Apply custom fields if provided - if ( options.xhrFields ) { - for ( i in options.xhrFields ) { - xhr[ i ] = options.xhrFields[ i ]; - } - } - - // Override mime type if needed - if ( options.mimeType && xhr.overrideMimeType ) { - xhr.overrideMimeType( options.mimeType ); - } - - // X-Requested-With header - // For cross-domain requests, seeing as conditions for a preflight are - // akin to a jigsaw puzzle, we simply never set it to be sure. - // (it can always be set on a per-request basis or even using ajaxSetup) - // For same-domain requests, won't change header if already provided. - if ( !options.crossDomain && !headers["X-Requested-With"] ) { - headers["X-Requested-With"] = "XMLHttpRequest"; - } - - // Set headers - for ( i in headers ) { - // Support: IE<9 - // IE's ActiveXObject throws a 'Type Mismatch' exception when setting - // request header to a null-value. - // - // To keep consistent with other XHR implementations, cast the value - // to string and ignore `undefined`. - if ( headers[ i ] !== undefined ) { - xhr.setRequestHeader( i, headers[ i ] + "" ); - } - } - - // Do send the request - // This may raise an exception which is actually - // handled in jQuery.ajax (so no try/catch here) - xhr.send( ( options.hasContent && options.data ) || null ); - - // Listener - callback = function( _, isAbort ) { - var status, statusText, responses; - - // Was never called and is aborted or complete - if ( callback && ( isAbort || xhr.readyState === 4 ) ) { - // Clean up - delete xhrCallbacks[ id ]; - callback = undefined; - xhr.onreadystatechange = jQuery.noop; - - // Abort manually if needed - if ( isAbort ) { - if ( xhr.readyState !== 4 ) { - xhr.abort(); - } - } else { - responses = {}; - status = xhr.status; - - // Support: IE<10 - // Accessing binary-data responseText throws an exception - // (#11426) - if ( typeof xhr.responseText === "string" ) { - responses.text = xhr.responseText; - } - - // Firefox throws an exception when accessing - // statusText for faulty cross-domain requests - try { - statusText = xhr.statusText; - } catch( e ) { - // We normalize with Webkit giving an empty statusText - statusText = ""; - } - - // Filter status for non standard behaviors - - // If the request is local and we have data: assume a success - // (success with no data won't get notified, that's the best we - // can do given current implementations) - if ( !status && options.isLocal && !options.crossDomain ) { - status = responses.text ? 200 : 404; - // IE - #1450: sometimes returns 1223 when it should be 204 - } else if ( status === 1223 ) { - status = 204; - } - } - } - - // Call complete if needed - if ( responses ) { - complete( status, statusText, responses, xhr.getAllResponseHeaders() ); - } - }; - - if ( !options.async ) { - // if we're in sync mode we fire the callback - callback(); - } else if ( xhr.readyState === 4 ) { - // (IE6 & IE7) if it's in cache and has been - // retrieved directly we need to fire the callback - setTimeout( callback ); - } else { - // Add to the list of active xhr callbacks - xhr.onreadystatechange = xhrCallbacks[ id ] = callback; - } - }, - - abort: function() { - if ( callback ) { - callback( undefined, true ); - } - } - }; - } - }); -} - -// Functions to create xhrs -function createStandardXHR() { - try { - return new window.XMLHttpRequest(); - } catch( e ) {} -} - -function createActiveXHR() { - try { - return new window.ActiveXObject( "Microsoft.XMLHTTP" ); - } catch( e ) {} -} - - - - -// Install script dataType -jQuery.ajaxSetup({ - accepts: { - script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript" - }, - contents: { - script: /(?:java|ecma)script/ - }, - converters: { - "text script": function( text ) { - jQuery.globalEval( text ); - return text; - } - } -}); - -// Handle cache's special case and global -jQuery.ajaxPrefilter( "script", function( s ) { - if ( s.cache === undefined ) { - s.cache = false; - } - if ( s.crossDomain ) { - s.type = "GET"; - s.global = false; - } -}); - -// Bind script tag hack transport -jQuery.ajaxTransport( "script", function(s) { - - // This transport only deals with cross domain requests - if ( s.crossDomain ) { - - var script, - head = document.head || jQuery("head")[0] || document.documentElement; - - return { - - send: function( _, callback ) { - - script = document.createElement("script"); - - script.async = true; - - if ( s.scriptCharset ) { - script.charset = s.scriptCharset; - } - - script.src = s.url; - - // Attach handlers for all browsers - script.onload = script.onreadystatechange = function( _, isAbort ) { - - if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) { - - // Handle memory leak in IE - script.onload = script.onreadystatechange = null; - - // Remove the script - if ( script.parentNode ) { - script.parentNode.removeChild( script ); - } - - // Dereference the script - script = null; - - // Callback if not abort - if ( !isAbort ) { - callback( 200, "success" ); - } - } - }; - - // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending - // Use native DOM manipulation to avoid our domManip AJAX trickery - head.insertBefore( script, head.firstChild ); - }, - - abort: function() { - if ( script ) { - script.onload( undefined, true ); - } - } - }; - } -}); - - - - -var oldCallbacks = [], - rjsonp = /(=)\?(?=&|$)|\?\?/; - -// Default jsonp settings -jQuery.ajaxSetup({ - jsonp: "callback", - jsonpCallback: function() { - var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) ); - this[ callback ] = true; - return callback; - } -}); - -// Detect, normalize options and install callbacks for jsonp requests -jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) { - - var callbackName, overwritten, responseContainer, - jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ? - "url" : - typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data" - ); - - // Handle iff the expected data type is "jsonp" or we have a parameter to set - if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) { - - // Get callback name, remembering preexisting value associated with it - callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ? - s.jsonpCallback() : - s.jsonpCallback; - - // Insert callback into url or form data - if ( jsonProp ) { - s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName ); - } else if ( s.jsonp !== false ) { - s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName; - } - - // Use data converter to retrieve json after script execution - s.converters["script json"] = function() { - if ( !responseContainer ) { - jQuery.error( callbackName + " was not called" ); - } - return responseContainer[ 0 ]; - }; - - // force json dataType - s.dataTypes[ 0 ] = "json"; - - // Install callback - overwritten = window[ callbackName ]; - window[ callbackName ] = function() { - responseContainer = arguments; - }; - - // Clean-up function (fires after converters) - jqXHR.always(function() { - // Restore preexisting value - window[ callbackName ] = overwritten; - - // Save back as free - if ( s[ callbackName ] ) { - // make sure that re-using the options doesn't screw things around - s.jsonpCallback = originalSettings.jsonpCallback; - - // save the callback name for future use - oldCallbacks.push( callbackName ); - } - - // Call if it was a function and we have a response - if ( responseContainer && jQuery.isFunction( overwritten ) ) { - overwritten( responseContainer[ 0 ] ); - } - - responseContainer = overwritten = undefined; - }); - - // Delegate to script - return "script"; - } -}); - - - - -// data: string of html -// context (optional): If specified, the fragment will be created in this context, defaults to document -// keepScripts (optional): If true, will include scripts passed in the html string -jQuery.parseHTML = function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - - if ( scripts && scripts.length ) { - jQuery( scripts ).remove(); - } - - return jQuery.merge( [], parsed.childNodes ); -}; - - -// Keep a copy of the old load method -var _load = jQuery.fn.load; - -/** - * Load a url into a page - */ -jQuery.fn.load = function( url, params, callback ) { - if ( typeof url !== "string" && _load ) { - return _load.apply( this, arguments ); - } - - var selector, response, type, - self = this, - off = url.indexOf(" "); - - if ( off >= 0 ) { - selector = jQuery.trim( url.slice( off, url.length ) ); - url = url.slice( 0, off ); - } - - // If it's a function - if ( jQuery.isFunction( params ) ) { - - // We assume that it's the callback - callback = params; - params = undefined; - - // Otherwise, build a param string - } else if ( params && typeof params === "object" ) { - type = "POST"; - } - - // If we have elements to modify, make the request - if ( self.length > 0 ) { - jQuery.ajax({ - url: url, - - // if "type" variable is undefined, then "GET" method will be used - type: type, - dataType: "html", - data: params - }).done(function( responseText ) { - - // Save response for use in complete callback - response = arguments; - - self.html( selector ? - - // If a selector was specified, locate the right elements in a dummy div - // Exclude scripts to avoid IE 'Permission Denied' errors - jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) : - - // Otherwise use the full result - responseText ); - - }).complete( callback && function( jqXHR, status ) { - self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] ); - }); - } - - return this; -}; - - - - -// Attach a bunch of functions for handling common AJAX events -jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) { - jQuery.fn[ type ] = function( fn ) { - return this.on( type, fn ); - }; -}); - - - - -jQuery.expr.filters.animated = function( elem ) { - return jQuery.grep(jQuery.timers, function( fn ) { - return elem === fn.elem; - }).length; -}; - - - - - -var docElem = window.document.documentElement; - -/** - * Gets a window from an element - */ -function getWindow( elem ) { - return jQuery.isWindow( elem ) ? - elem : - elem.nodeType === 9 ? - elem.defaultView || elem.parentWindow : - false; -} - -jQuery.offset = { - setOffset: function( elem, options, i ) { - var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition, - position = jQuery.css( elem, "position" ), - curElem = jQuery( elem ), - props = {}; - - // set position first, in-case top/left are set even on static elem - if ( position === "static" ) { - elem.style.position = "relative"; - } - - curOffset = curElem.offset(); - curCSSTop = jQuery.css( elem, "top" ); - curCSSLeft = jQuery.css( elem, "left" ); - calculatePosition = ( position === "absolute" || position === "fixed" ) && - jQuery.inArray("auto", [ curCSSTop, curCSSLeft ] ) > -1; - - // need to be able to calculate position if either top or left is auto and position is either absolute or fixed - if ( calculatePosition ) { - curPosition = curElem.position(); - curTop = curPosition.top; - curLeft = curPosition.left; - } else { - curTop = parseFloat( curCSSTop ) || 0; - curLeft = parseFloat( curCSSLeft ) || 0; - } - - if ( jQuery.isFunction( options ) ) { - options = options.call( elem, i, curOffset ); - } - - if ( options.top != null ) { - props.top = ( options.top - curOffset.top ) + curTop; - } - if ( options.left != null ) { - props.left = ( options.left - curOffset.left ) + curLeft; - } - - if ( "using" in options ) { - options.using.call( elem, props ); - } else { - curElem.css( props ); - } - } -}; - -jQuery.fn.extend({ - offset: function( options ) { - if ( arguments.length ) { - return options === undefined ? - this : - this.each(function( i ) { - jQuery.offset.setOffset( this, options, i ); - }); - } - - var docElem, win, - box = { top: 0, left: 0 }, - elem = this[ 0 ], - doc = elem && elem.ownerDocument; - - if ( !doc ) { - return; - } - - docElem = doc.documentElement; - - // Make sure it's not a disconnected DOM node - if ( !jQuery.contains( docElem, elem ) ) { - return box; - } - - // If we don't have gBCR, just use 0,0 rather than error - // BlackBerry 5, iOS 3 (original iPhone) - if ( typeof elem.getBoundingClientRect !== strundefined ) { - box = elem.getBoundingClientRect(); - } - win = getWindow( doc ); - return { - top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ), - left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 ) - }; - }, - - position: function() { - if ( !this[ 0 ] ) { - return; - } - - var offsetParent, offset, - parentOffset = { top: 0, left: 0 }, - elem = this[ 0 ]; - - // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent - if ( jQuery.css( elem, "position" ) === "fixed" ) { - // we assume that getBoundingClientRect is available when computed position is fixed - offset = elem.getBoundingClientRect(); - } else { - // Get *real* offsetParent - offsetParent = this.offsetParent(); - - // Get correct offsets - offset = this.offset(); - if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) { - parentOffset = offsetParent.offset(); - } - - // Add offsetParent borders - parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true ); - parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true ); - } - - // Subtract parent offsets and element margins - // note: when an element has margin: auto the offsetLeft and marginLeft - // are the same in Safari causing offset.left to incorrectly be 0 - return { - top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ), - left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true) - }; - }, - - offsetParent: function() { - return this.map(function() { - var offsetParent = this.offsetParent || docElem; - - while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) { - offsetParent = offsetParent.offsetParent; - } - return offsetParent || docElem; - }); - } -}); - -// Create scrollLeft and scrollTop methods -jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) { - var top = /Y/.test( prop ); - - jQuery.fn[ method ] = function( val ) { - return access( this, function( elem, method, val ) { - var win = getWindow( elem ); - - if ( val === undefined ) { - return win ? (prop in win) ? win[ prop ] : - win.document.documentElement[ method ] : - elem[ method ]; - } - - if ( win ) { - win.scrollTo( - !top ? val : jQuery( win ).scrollLeft(), - top ? val : jQuery( win ).scrollTop() - ); - - } else { - elem[ method ] = val; - } - }, method, val, arguments.length, null ); - }; -}); - -// Add the top/left cssHooks using jQuery.fn.position -// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084 -// getComputedStyle returns percent when specified for top/left/bottom/right -// rather than make the css module depend on the offset module, we just check for it here -jQuery.each( [ "top", "left" ], function( i, prop ) { - jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition, - function( elem, computed ) { - if ( computed ) { - computed = curCSS( elem, prop ); - // if curCSS returns percentage, fallback to offset - return rnumnonpx.test( computed ) ? - jQuery( elem ).position()[ prop ] + "px" : - computed; - } - } - ); -}); - - -// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods -jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { - jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) { - // margin is only for outerHeight, outerWidth - jQuery.fn[ funcName ] = function( margin, value ) { - var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ), - extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" ); - - return access( this, function( elem, type, value ) { - var doc; - - if ( jQuery.isWindow( elem ) ) { - // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there - // isn't a whole lot we can do. See pull request at this URL for discussion: - // https://github.com/jquery/jquery/pull/764 - return elem.document.documentElement[ "client" + name ]; - } - - // Get document width or height - if ( elem.nodeType === 9 ) { - doc = elem.documentElement; - - // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest - // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it. - return Math.max( - elem.body[ "scroll" + name ], doc[ "scroll" + name ], - elem.body[ "offset" + name ], doc[ "offset" + name ], - doc[ "client" + name ] - ); - } - - return value === undefined ? - // Get width or height on the element, requesting but not forcing parseFloat - jQuery.css( elem, type, extra ) : - - // Set width or height on the element - jQuery.style( elem, type, value, extra ); - }, type, chainable ? margin : undefined, chainable, null ); - }; - }); -}); - - -// The number of elements contained in the matched element set -jQuery.fn.size = function() { - return this.length; -}; - -jQuery.fn.andSelf = jQuery.fn.addBack; - - - - -// Register as a named AMD module, since jQuery can be concatenated with other -// files that may use define, but not via a proper concatenation script that -// understands anonymous AMD modules. A named AMD is safest and most robust -// way to register. Lowercase jquery is used because AMD module names are -// derived from file names, and jQuery is normally delivered in a lowercase -// file name. Do this after creating the global so that if an AMD module wants -// to call noConflict to hide this version of jQuery, it will work. - -// Note that for maximum portability, libraries that are not jQuery should -// declare themselves as anonymous modules, and avoid setting a global if an -// AMD loader is present. jQuery is a special case. For more information, see -// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon - -if ( typeof define === "function" && define.amd ) { - define( "jquery", [], function() { - return jQuery; - }); -} - - - - -var - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$; - -jQuery.noConflict = function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; -}; - -// Expose jQuery and $ identifiers, even in -// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557) -// and CommonJS for browser emulators (#13566) -if ( typeof noGlobal === strundefined ) { - window.jQuery = window.$ = jQuery; -} - - - - -return jQuery; - -})); diff --git a/src/UI/JsLibraries/jquery.knob.js b/src/UI/JsLibraries/jquery.knob.js deleted file mode 100644 index a657773d4..000000000 --- a/src/UI/JsLibraries/jquery.knob.js +++ /dev/null @@ -1,672 +0,0 @@ -/*!jQuery Knob*/ -/** - * Downward compatible, touchable dial - * - * Version: 1.2.0 (15/07/2012) - * Requires: jQuery v1.7+ - * - * Copyright (c) 2012 Anthony Terrien - * Under MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * Thanks to vor, eskimoblood, spiffistan, FabrizioC - */ -(function($) { - - /** - * Kontrol library - */ - "use strict"; - - /** - * Definition of globals and core - */ - var k = {}, // kontrol - max = Math.max, - min = Math.min; - - k.c = {}; - k.c.d = $(document); - k.c.t = function (e) { - return e.originalEvent.touches.length - 1; - }; - - /** - * Kontrol Object - * - * Definition of an abstract UI control - * - * Each concrete component must call this one. - * <code> - * k.o.call(this); - * </code> - */ - k.o = function () { - var s = this; - - this.o = null; // array of options - this.$ = null; // jQuery wrapped element - this.i = null; // mixed HTMLInputElement or array of HTMLInputElement - this.g = null; // 2D graphics context for 'pre-rendering' - this.v = null; // value ; mixed array or integer - this.cv = null; // change value ; not commited value - this.x = 0; // canvas x position - this.y = 0; // canvas y position - this.$c = null; // jQuery canvas element - this.c = null; // rendered canvas context - this.t = 0; // touches index - this.isInit = false; - this.fgColor = null; // main color - this.pColor = null; // previous color - this.dH = null; // draw hook - this.cH = null; // change hook - this.eH = null; // cancel hook - this.rH = null; // release hook - - this.run = function () { - var cf = function (e, conf) { - var k; - for (k in conf) { - s.o[k] = conf[k]; - } - s.init(); - s._configure() - ._draw(); - }; - - if(this.$.data('kontroled')) return; - this.$.data('kontroled', true); - - this.extend(); - this.o = $.extend( - { - // Config - min : this.$.data('min') || 0, - max : this.$.data('max') || 100, - stopper : true, - readOnly : this.$.data('readonly'), - - // UI - cursor : (this.$.data('cursor') === true && 30) - || this.$.data('cursor') - || 0, - thickness : this.$.data('thickness') || 0.35, - lineCap : this.$.data('linecap') || 'butt', - width : this.$.data('width') || 200, - height : this.$.data('height') || 200, - displayInput : this.$.data('displayinput') == null || this.$.data('displayinput'), - displayPrevious : this.$.data('displayprevious'), - fgColor : this.$.data('fgcolor') || '#87CEEB', - inputColor: this.$.data('inputcolor') || this.$.data('fgcolor') || '#87CEEB', - inline : false, - step : this.$.data('step') || 1, - - // Hooks - draw : null, // function () {} - change : null, // function (value) {} - cancel : null, // function () {} - release : null, // function (value) {} - error : null // function () {} - }, this.o - ); - - // routing value - if(this.$.is('fieldset')) { - - // fieldset = array of integer - this.v = {}; - this.i = this.$.find('input') - this.i.each(function(k) { - var $this = $(this); - s.i[k] = $this; - s.v[k] = $this.val(); - - $this.bind( - 'change' - , function () { - var val = {}; - val[k] = $this.val(); - s.val(val); - } - ); - }); - this.$.find('legend').remove(); - - } else { - // input = integer - this.i = this.$; - this.v = this.$.val(); - (this.v == '') && (this.v = this.o.min); - - this.$.bind( - 'change' - , function () { - s.val(s._validate(s.$.val())); - } - ); - } - - (!this.o.displayInput) && this.$.hide(); - - this.$c = $('<canvas width="' + - this.o.width + 'px" height="' + - this.o.height + 'px"></canvas>'); - - this.c = this.$c[0].getContext? this.$c[0].getContext('2d') : null; - - if (!this.c) { - this.o.error && this.o.error(); - return; - } - - this.$ - .wrap($('<div style="' + (this.o.inline ? 'display:inline;' : '') + - 'width:' + this.o.width + 'px;height:' + - this.o.height + 'px;"></div>')) - .before(this.$c); - - if (this.v instanceof Object) { - this.cv = {}; - this.copy(this.v, this.cv); - } else { - this.cv = this.v; - } - - this.$ - .bind("configure", cf) - .parent() - .bind("configure", cf); - - this._listen() - ._configure() - ._xy() - .init(); - - this.isInit = true; - - this._draw(); - - return this; - }; - - this._draw = function () { - - // canvas pre-rendering - var d = true, - c = document.createElement('canvas'); - - c.width = s.o.width; - c.height = s.o.height; - s.g = c.getContext('2d'); - - s.clear(); - - s.dH - && (d = s.dH()); - - (d !== false) && s.draw(); - - s.c.drawImage(c, 0, 0); - c = null; - }; - - this._touch = function (e) { - - var touchMove = function (e) { - - var v = s.xy2val( - e.originalEvent.touches[s.t].pageX, - e.originalEvent.touches[s.t].pageY - ); - - if (v == s.cv) return; - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - - s.change(s._validate(v)); - s.$.trigger('change', v); - s._draw(); - }; - - // get touches index - this.t = k.c.t(e); - - // First touch - touchMove(e); - - // Touch events listeners - k.c.d - .bind("touchmove.k", touchMove) - .bind( - "touchend.k" - , function () { - k.c.d.unbind('touchmove.k touchend.k'); - - if ( - s.rH - && (s.rH(s.cv) === false) - ) return; - - s.val(s.cv); - } - ); - - return this; - }; - - this._mouse = function (e) { - - var mouseMove = function (e) { - var v = s.xy2val(e.pageX, e.pageY); - if (v == s.cv) return; - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - s.change(s._validate(v)); - s.$.trigger('change', v); - s._draw(); - }; - - // First click - mouseMove(e); - - // Mouse events listeners - k.c.d - .bind("mousemove.k", mouseMove) - .bind( - // Escape key cancel current change - "keyup.k" - , function (e) { - if (e.keyCode === 27) { - k.c.d.unbind("mouseup.k mousemove.k keyup.k"); - - if ( - s.eH - && (s.eH() === false) - ) return; - - s.cancel(); - } - } - ) - .bind( - "mouseup.k" - , function (e) { - k.c.d.unbind('mousemove.k mouseup.k keyup.k'); - - if ( - s.rH - && (s.rH(s.cv) === false) - ) return; - - s.val(s.cv); - } - ); - - return this; - }; - - this._xy = function () { - var o = this.$c.offset(); - this.x = o.left; - this.y = o.top; - return this; - }; - - this._listen = function () { - - if (!this.o.readOnly) { - this.$c - .bind( - "mousedown" - , function (e) { - e.preventDefault(); - s._xy()._mouse(e); - } - ) - .bind( - "touchstart" - , function (e) { - e.preventDefault(); - s._xy()._touch(e); - } - ); - this.listen(); - } else { - this.$.attr('readonly', 'readonly'); - } - - return this; - }; - - this._configure = function () { - - // Hooks - if (this.o.draw) this.dH = this.o.draw; - if (this.o.change) this.cH = this.o.change; - if (this.o.cancel) this.eH = this.o.cancel; - if (this.o.release) this.rH = this.o.release; - - if (this.o.displayPrevious) { - this.pColor = this.h2rgba(this.o.fgColor, "0.4"); - this.fgColor = this.h2rgba(this.o.fgColor, "0.6"); - } else { - this.fgColor = this.o.fgColor; - } - - return this; - }; - - this._clear = function () { - this.$c[0].width = this.$c[0].width; - }; - - this._validate = function(v) { - return (~~ (((v < 0) ? -0.5 : 0.5) + (v/this.o.step))) * this.o.step; - }; - - // Abstract methods - this.listen = function () {}; // on start, one time - this.extend = function () {}; // each time configure triggered - this.init = function () {}; // each time configure triggered - this.change = function (v) {}; // on change - this.val = function (v) {}; // on release - this.xy2val = function (x, y) {}; // - this.draw = function () {}; // on change / on release - this.clear = function () { this._clear(); }; - - // Utils - this.h2rgba = function (h, a) { - var rgb; - h = h.substring(1,7) - rgb = [parseInt(h.substring(0,2),16) - ,parseInt(h.substring(2,4),16) - ,parseInt(h.substring(4,6),16)]; - return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")"; - }; - - this.copy = function (f, t) { - for (var i in f) { t[i] = f[i]; } - }; - }; - - - /** - * k.Dial - */ - k.Dial = function () { - k.o.call(this); - - this.startAngle = null; - this.xy = null; - this.radius = null; - this.lineWidth = null; - this.cursorExt = null; - this.w2 = null; - this.PI2 = 2*Math.PI; - - this.extend = function () { - this.o = $.extend( - { - bgColor : this.$.data('bgcolor') || '#EEEEEE', - angleOffset : this.$.data('angleoffset') || 0, - angleArc : this.$.data('anglearc') || 360, - inline : true - }, this.o - ); - }; - - this.val = function (v) { - if (null != v) { - this.cv = this.o.stopper ? max(min(v, this.o.max), this.o.min) : v; - this.v = this.cv; - this.$.val(this.v); - this._draw(); - } else { - return this.v; - } - }; - - this.xy2val = function (x, y) { - var a, ret; - - a = Math.atan2( - x - (this.x + this.w2) - , - (y - this.y - this.w2) - ) - this.angleOffset; - - if(this.angleArc != this.PI2 && (a < 0) && (a > -0.5)) { - // if isset angleArc option, set to min if .5 under min - a = 0; - } else if (a < 0) { - a += this.PI2; - } - - ret = ~~ (0.5 + (a * (this.o.max - this.o.min) / this.angleArc)) - + this.o.min; - - this.o.stopper - && (ret = max(min(ret, this.o.max), this.o.min)); - - return ret; - }; - - this.listen = function () { - // bind MouseWheel - var s = this, - mw = function (e) { - e.preventDefault(); - var ori = e.originalEvent - ,deltaX = ori.detail || ori.wheelDeltaX - ,deltaY = ori.detail || ori.wheelDeltaY - ,v = parseInt(s.$.val()) + (deltaX>0 || deltaY>0 ? s.o.step : deltaX<0 || deltaY<0 ? -s.o.step : 0); - - if ( - s.cH - && (s.cH(v) === false) - ) return; - - s.val(v); - s.$.trigger('change', v); - } - , kval, to, m = 1, kv = {37:-s.o.step, 38:s.o.step, 39:s.o.step, 40:-s.o.step}; - - this.$ - .bind( - "keydown" - ,function (e) { - var kc = e.keyCode; - - // numpad support - if(kc >= 96 && kc <= 105) { - kc = e.keyCode = kc - 48; - } - - kval = parseInt(String.fromCharCode(kc)); - - if (isNaN(kval)) { - - (kc !== 13) // enter - && (kc !== 8) // bs - && (kc !== 9) // tab - && (kc !== 189) // - - && e.preventDefault(); - - // arrows - if ($.inArray(kc,[37,38,39,40]) > -1) { - e.preventDefault(); - - var v = parseInt(s.$.val()) + kv[kc] * m; - - s.o.stopper - && (v = max(min(v, s.o.max), s.o.min)); - - s.change(v); - s.$.trigger('change', v); - s._draw(); - - // long time keydown speed-up - to = window.setTimeout( - function () { m*=2; } - ,30 - ); - } - } - } - ) - .bind( - "keyup" - ,function (e) { - if (isNaN(kval)) { - if (to) { - window.clearTimeout(to); - to = null; - m = 1; - s.val(s.$.val()); - } - } else { - // kval postcond - (s.$.val() > s.o.max && s.$.val(s.o.max)) - || (s.$.val() < s.o.min && s.$.val(s.o.min)); - } - - } - ); - - this.$c.bind("mousewheel DOMMouseScroll", mw); - this.$.bind("mousewheel DOMMouseScroll", mw) - }; - - this.init = function () { - - if ( - this.v < this.o.min - || this.v > this.o.max - ) this.v = this.o.min; - - this.$.val(this.v); - this.w2 = this.o.width / 2; - this.cursorExt = this.o.cursor / 100; - this.xy = this.w2; - this.lineWidth = this.xy * this.o.thickness; - this.lineCap = this.o.lineCap; - this.radius = this.xy - this.lineWidth / 2; - - this.o.angleOffset - && (this.o.angleOffset = isNaN(this.o.angleOffset) ? 0 : this.o.angleOffset); - - this.o.angleArc - && (this.o.angleArc = isNaN(this.o.angleArc) ? this.PI2 : this.o.angleArc); - - // deg to rad - this.angleOffset = this.o.angleOffset * Math.PI / 180; - this.angleArc = this.o.angleArc * Math.PI / 180; - - // compute start and end angles - this.startAngle = 1.5 * Math.PI + this.angleOffset; - this.endAngle = 1.5 * Math.PI + this.angleOffset + this.angleArc; - - var s = max( - String(Math.abs(this.o.max)).length - , String(Math.abs(this.o.min)).length - , 2 - ) + 2; - - this.o.displayInput - && this.i.css({ - 'width' : ((this.o.width / 2 + 4) >> 0) + 'px' - ,'height' : ((this.o.width / 3) >> 0) + 'px' - ,'position' : 'absolute' - ,'vertical-align' : 'middle' - ,'margin-top' : ((this.o.width / 3) >> 0) + 'px' - ,'margin-left' : '-' + ((this.o.width * 3 / 4 + 2) >> 0) + 'px' - ,'border' : 0 - ,'background' : 'none' - ,'font' : 'bold ' + ((this.o.width / s) >> 0) + 'px Arial' - ,'text-align' : 'center' - ,'color' : this.o.inputColor || this.o.fgColor - ,'padding' : '0px' - ,'-webkit-appearance': 'none' - }) - || this.i.css({ - 'width' : '0px' - ,'visibility' : 'hidden' - }); - }; - - this.change = function (v) { - this.cv = v; - this.$.val(v); - }; - - this.angle = function (v) { - return (v - this.o.min) * this.angleArc / (this.o.max - this.o.min); - }; - - this.draw = function () { - - var c = this.g, // context - a = this.angle(this.cv) // Angle - , sat = this.startAngle // Start angle - , eat = sat + a // End angle - , sa, ea // Previous angles - , r = 1; - - c.lineWidth = this.lineWidth; - - c.lineCap = this.lineCap; - - this.o.cursor - && (sat = eat - this.cursorExt) - && (eat = eat + this.cursorExt); - - c.beginPath(); - c.strokeStyle = this.o.bgColor; - c.arc(this.xy, this.xy, this.radius, this.endAngle, this.startAngle, true); - c.stroke(); - - if (this.o.displayPrevious) { - ea = this.startAngle + this.angle(this.v); - sa = this.startAngle; - this.o.cursor - && (sa = ea - this.cursorExt) - && (ea = ea + this.cursorExt); - - c.beginPath(); - c.strokeStyle = this.pColor; - c.arc(this.xy, this.xy, this.radius, sa, ea, false); - c.stroke(); - r = (this.cv == this.v); - } - - c.beginPath(); - c.strokeStyle = r ? this.o.fgColor : this.fgColor ; - c.arc(this.xy, this.xy, this.radius, sat, eat, false); - c.stroke(); - }; - - this.cancel = function () { - this.val(this.v); - }; - }; - - $.fn.dial = $.fn.knob = function (o) { - return this.each( - function () { - var d = new k.Dial(); - d.o = o; - d.$ = $(this); - d.run(); - } - ).parent(); - }; - -})(jQuery); \ No newline at end of file diff --git a/src/UI/JsLibraries/jquery.signalR.js b/src/UI/JsLibraries/jquery.signalR.js deleted file mode 100644 index fcacbc371..000000000 --- a/src/UI/JsLibraries/jquery.signalR.js +++ /dev/null @@ -1,2193 +0,0 @@ -/* jquery.signalR.core.js */ -/*global window:false */ -/*! - * ASP.NET SignalR JavaScript Library v1.1.3 - * http://signalr.net/ - * - * Copyright Microsoft Open Technologies, Inc. All rights reserved. - * Licensed under the Apache 2.0 - * https://github.com/SignalR/SignalR/blob/master/LICENSE.md - * - */ - -/// <reference path="Scripts/jquery-1.6.4.js" /> -(function ($, window) { - "use strict"; - - if (typeof ($) !== "function") { - // no jQuery! - throw new Error("SignalR: jQuery not found. Please ensure jQuery is referenced before the SignalR.js file."); - } - - if (!window.JSON) { - // no JSON! - throw new Error("SignalR: No JSON parser found. Please ensure json2.js is referenced before the SignalR.js file if you need to support clients without native JSON parsing support, e.g. IE<8."); - } - - var signalR, - _connection, - _pageLoaded = (window.document.readyState === "complete"), - _pageWindow = $(window), - - events = { - onStart: "onStart", - onStarting: "onStarting", - onReceived: "onReceived", - onError: "onError", - onConnectionSlow: "onConnectionSlow", - onReconnecting: "onReconnecting", - onReconnect: "onReconnect", - onStateChanged: "onStateChanged", - onDisconnect: "onDisconnect" - }, - - log = function (msg, logging) { - if (logging === false) { - return; - } - var m; - if (typeof (window.console) === "undefined") { - return; - } - m = "[" + new Date().toTimeString() + "] SignalR: " + msg; - if (window.console.debug) { - window.console.debug(m); - } else if (window.console.log) { - window.console.log(m); - } - }, - - changeState = function (connection, expectedState, newState) { - if (expectedState === connection.state) { - connection.state = newState; - - $(connection).triggerHandler(events.onStateChanged, [{ oldState: expectedState, newState: newState }]); - return true; - } - - return false; - }, - - isDisconnecting = function (connection) { - return connection.state === signalR.connectionState.disconnected; - }, - - configureStopReconnectingTimeout = function (connection) { - var stopReconnectingTimeout, - onReconnectTimeout; - - // Check if this connection has already been configured to stop reconnecting after a specified timeout. - // Without this check if a connection is stopped then started events will be bound multiple times. - if (!connection._.configuredStopReconnectingTimeout) { - onReconnectTimeout = function (connection) { - connection.log("Couldn't reconnect within the configured timeout (" + connection.disconnectTimeout + "ms), disconnecting."); - connection.stop(/* async */ false, /* notifyServer */ false); - }; - - connection.reconnecting(function () { - var connection = this; - - // Guard against state changing in a previous user defined even handler - if (connection.state === signalR.connectionState.reconnecting) { - stopReconnectingTimeout = window.setTimeout(function () { onReconnectTimeout(connection); }, connection.disconnectTimeout); - } - }); - - connection.stateChanged(function (data) { - if (data.oldState === signalR.connectionState.reconnecting) { - // Clear the pending reconnect timeout check - window.clearTimeout(stopReconnectingTimeout); - } - }); - - connection._.configuredStopReconnectingTimeout = true; - } - }; - - signalR = function (url, qs, logging) { - /// <summary>Creates a new SignalR connection for the given url</summary> - /// <param name="url" type="String">The URL of the long polling endpoint</param> - /// <param name="qs" type="Object"> - /// [Optional] Custom querystring parameters to add to the connection URL. - /// If an object, every non-function member will be added to the querystring. - /// If a string, it's added to the QS as specified. - /// </param> - /// <param name="logging" type="Boolean"> - /// [Optional] A flag indicating whether connection logging is enabled to the browser - /// console/log. Defaults to false. - /// </param> - - return new signalR.fn.init(url, qs, logging); - }; - - signalR._ = { - defaultContentType: "application/x-www-form-urlencoded; charset=UTF-8", - ieVersion: (function () { - var version, - matches; - - if (window.navigator.appName === 'Microsoft Internet Explorer') { - // Check if the user agent has the pattern "MSIE (one or more numbers).(one or more numbers)"; - matches = /MSIE ([0-9]+\.[0-9]+)/.exec(window.navigator.userAgent); - - if (matches) { - version = window.parseFloat(matches[1]); - } - } - - // undefined value means not IE - return version; - })() - }; - - signalR.events = events; - - signalR.changeState = changeState; - - signalR.isDisconnecting = isDisconnecting; - - signalR.connectionState = { - connecting: 0, - connected: 1, - reconnecting: 2, - disconnected: 4 - }; - - signalR.hub = { - start: function () { - // This will get replaced with the real hub connection start method when hubs is referenced correctly - throw new Error("SignalR: Error loading hubs. Ensure your hubs reference is correct, e.g. <script src='/signalr/hubs'></script>."); - } - }; - - _pageWindow.load(function () { _pageLoaded = true; }); - - function validateTransport(requestedTransport, connection) { - /// <summary>Validates the requested transport by cross checking it with the pre-defined signalR.transports</summary> - /// <param name="requestedTransport" type="Object">The designated transports that the user has specified.</param> - /// <param name="connection" type="signalR">The connection that will be using the requested transports. Used for logging purposes.</param> - /// <returns type="Object" /> - - if ($.isArray(requestedTransport)) { - // Go through transport array and remove an "invalid" tranports - for (var i = requestedTransport.length - 1; i >= 0; i--) { - var transport = requestedTransport[i]; - if ($.type(requestedTransport) !== "object" && ($.type(transport) !== "string" || !signalR.transports[transport])) { - connection.log("Invalid transport: " + transport + ", removing it from the transports list."); - requestedTransport.splice(i, 1); - } - } - - // Verify we still have transports left, if we dont then we have invalid transports - if (requestedTransport.length === 0) { - connection.log("No transports remain within the specified transport array."); - requestedTransport = null; - } - } else if ($.type(requestedTransport) !== "object" && !signalR.transports[requestedTransport] && requestedTransport !== "auto") { - connection.log("Invalid transport: " + requestedTransport.toString()); - requestedTransport = null; - } - else if (requestedTransport === "auto" && signalR._.ieVersion <= 8) - { - // If we're doing an auto transport and we're IE8 then force longPolling, #1764 - return ["longPolling"]; - - } - - return requestedTransport; - } - - function getDefaultPort(protocol) { - if(protocol === "http:") { - return 80; - } - else if (protocol === "https:") { - return 443; - } - } - - function addDefaultPort(protocol, url) { - // Remove ports from url. We have to check if there's a / or end of line - // following the port in order to avoid removing ports such as 8080. - if(url.match(/:\d+$/)) { - return url; - } else { - return url + ":" + getDefaultPort(protocol); - } - } - - signalR.fn = signalR.prototype = { - init: function (url, qs, logging) { - this.url = url; - this.qs = qs; - this._ = {}; - if (typeof (logging) === "boolean") { - this.logging = logging; - } - }, - - isCrossDomain: function (url, against) { - /// <summary>Checks if url is cross domain</summary> - /// <param name="url" type="String">The base URL</param> - /// <param name="against" type="Object"> - /// An optional argument to compare the URL against, if not specified it will be set to window.location. - /// If specified it must contain a protocol and a host property. - /// </param> - var link; - - url = $.trim(url); - if (url.indexOf("http") !== 0) { - return false; - } - - against = against || window.location; - - // Create an anchor tag. - link = window.document.createElement("a"); - link.href = url; - - // When checking for cross domain we have to special case port 80 because the window.location will remove the - return link.protocol + addDefaultPort(link.protocol, link.host) !== against.protocol + addDefaultPort(against.protocol, against.host); - }, - - ajaxDataType: "json", - - contentType: "application/json; charset=UTF-8", - - logging: false, - - state: signalR.connectionState.disconnected, - - keepAliveData: {}, - - reconnectDelay: 2000, - - disconnectTimeout: 30000, // This should be set by the server in response to the negotiate request (30s default) - - keepAliveWarnAt: 2 / 3, // Warn user of slow connection if we breach the X% mark of the keep alive timeout - - start: function (options, callback) { - /// <summary>Starts the connection</summary> - /// <param name="options" type="Object">Options map</param> - /// <param name="callback" type="Function">A callback function to execute when the connection has started</param> - var connection = this, - config = { - waitForPageLoad: true, - transport: "auto", - jsonp: false - }, - initialize, - deferred = connection._deferral || $.Deferred(), // Check to see if there is a pre-existing deferral that's being built on, if so we want to keep using it - parser = window.document.createElement("a"); - - if ($.type(options) === "function") { - // Support calling with single callback parameter - callback = options; - } else if ($.type(options) === "object") { - $.extend(config, options); - if ($.type(config.callback) === "function") { - callback = config.callback; - } - } - - config.transport = validateTransport(config.transport, connection); - - // If the transport is invalid throw an error and abort start - if (!config.transport) { - throw new Error("SignalR: Invalid transport(s) specified, aborting start."); - } - - // Check to see if start is being called prior to page load - // If waitForPageLoad is true we then want to re-direct function call to the window load event - if (!_pageLoaded && config.waitForPageLoad === true) { - _pageWindow.load(function () { - connection._deferral = deferred; - connection.start(options, callback); - }); - return deferred.promise(); - } - - configureStopReconnectingTimeout(connection); - - if (changeState(connection, - signalR.connectionState.disconnected, - signalR.connectionState.connecting) === false) { - // Already started, just return - deferred.resolve(connection); - return deferred.promise(); - } - - // Resolve the full url - parser.href = connection.url; - if (!parser.protocol || parser.protocol === ":") { - connection.protocol = window.document.location.protocol; - connection.host = window.document.location.host; - connection.baseUrl = connection.protocol + "//" + connection.host; - } - else { - connection.protocol = parser.protocol; - connection.host = parser.host; - connection.baseUrl = parser.protocol + "//" + parser.host; - } - - // Set the websocket protocol - connection.wsProtocol = connection.protocol === "https:" ? "wss://" : "ws://"; - - // If jsonp with no/auto transport is specified, then set the transport to long polling - // since that is the only transport for which jsonp really makes sense. - // Some developers might actually choose to specify jsonp for same origin requests - // as demonstrated by Issue #623. - if (config.transport === "auto" && config.jsonp === true) { - config.transport = "longPolling"; - } - - if (this.isCrossDomain(connection.url)) { - connection.log("Auto detected cross domain url."); - - if (config.transport === "auto") { - // Try webSockets and longPolling since SSE doesn't support CORS - // TODO: Support XDM with foreverFrame - config.transport = ["webSockets", "longPolling"]; - } - - // Determine if jsonp is the only choice for negotiation, ajaxSend and ajaxAbort. - // i.e. if the browser doesn't supports CORS - // If it is, ignore any preference to the contrary, and switch to jsonp. - if (!config.jsonp) { - config.jsonp = !$.support.cors; - - if (config.jsonp) { - connection.log("Using jsonp because this browser doesn't support CORS"); - } - } - - connection.contentType = signalR._.defaultContentType; - } - - connection.ajaxDataType = config.jsonp ? "jsonp" : "json"; - - $(connection).bind(events.onStart, function (e, data) { - if ($.type(callback) === "function") { - callback.call(connection); - } - deferred.resolve(connection); - }); - - initialize = function (transports, index) { - index = index || 0; - if (index >= transports.length) { - if (!connection.transport) { - // No transport initialized successfully - $(connection).triggerHandler(events.onError, ["SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."]); - deferred.reject("SignalR: No transport could be initialized successfully. Try specifying a different transport or none at all for auto initialization."); - // Stop the connection if it has connected and move it into the disconnected state - connection.stop(); - } - return; - } - - var transportName = transports[index], - transport = $.type(transportName) === "object" ? transportName : signalR.transports[transportName]; - - if (transportName.indexOf("_") === 0) { - // Private member - initialize(transports, index + 1); - return; - } - - transport.start(connection, function () { // success - if (transport.supportsKeepAlive && connection.keepAliveData.activated) { - signalR.transports._logic.monitorKeepAlive(connection); - } - - connection.transport = transport; - - changeState(connection, - signalR.connectionState.connecting, - signalR.connectionState.connected); - - $(connection).triggerHandler(events.onStart); - - _pageWindow.unload(function () { // failure - connection.stop(false /* async */); - }); - - }, function () { - initialize(transports, index + 1); - }); - }; - - var url = connection.url + "/negotiate"; - - url = signalR.transports._logic.addQs(url, connection); - - connection.log("Negotiating with '" + url + "'."); - $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - contentType: connection.contentType, - data: {}, - dataType: connection.ajaxDataType, - error: function (error) { - $(connection).triggerHandler(events.onError, [error.responseText]); - deferred.reject("SignalR: Error during negotiation request: " + error.responseText); - // Stop the connection if negotiate failed - connection.stop(); - }, - success: function (res) { - var keepAliveData = connection.keepAliveData; - - connection.appRelativeUrl = res.Url; - connection.id = res.ConnectionId; - connection.token = res.ConnectionToken; - connection.webSocketServerUrl = res.WebSocketServerUrl; - - // Once the server has labeled the PersistentConnection as Disconnected, we should stop attempting to reconnect - // after res.DisconnectTimeout seconds. - connection.disconnectTimeout = res.DisconnectTimeout * 1000; // in ms - - - // If we have a keep alive - if (res.KeepAliveTimeout) { - // Register the keep alive data as activated - keepAliveData.activated = true; - - // Timeout to designate when to force the connection into reconnecting converted to milliseconds - keepAliveData.timeout = res.KeepAliveTimeout * 1000; - - // Timeout to designate when to warn the developer that the connection may be dead or is not responding. - keepAliveData.timeoutWarning = keepAliveData.timeout * connection.keepAliveWarnAt; - - // Instantiate the frequency in which we check the keep alive. It must be short in order to not miss/pick up any changes - keepAliveData.checkInterval = (keepAliveData.timeout - keepAliveData.timeoutWarning) / 3; - } - else { - keepAliveData.activated = false; - } - - if (!res.ProtocolVersion || res.ProtocolVersion !== "1.2") { - $(connection).triggerHandler(events.onError, ["You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."]); - deferred.reject("You are using a version of the client that isn't compatible with the server. Client version 1.2, server version " + res.ProtocolVersion + "."); - return; - } - - $(connection).triggerHandler(events.onStarting); - - var transports = [], - supportedTransports = []; - - $.each(signalR.transports, function (key) { - if (key === "webSockets" && !res.TryWebSockets) { - // Server said don't even try WebSockets, but keep processing the loop - return true; - } - supportedTransports.push(key); - }); - - if ($.isArray(config.transport)) { - // ordered list provided - $.each(config.transport, function () { - var transport = this; - if ($.type(transport) === "object" || ($.type(transport) === "string" && $.inArray("" + transport, supportedTransports) >= 0)) { - transports.push($.type(transport) === "string" ? "" + transport : transport); - } - }); - } else if ($.type(config.transport) === "object" || - $.inArray(config.transport, supportedTransports) >= 0) { - // specific transport provided, as object or a named transport, e.g. "longPolling" - transports.push(config.transport); - } else { // default "auto" - transports = supportedTransports; - } - initialize(transports); - } - }); - - return deferred.promise(); - }, - - starting: function (callback) { - /// <summary>Adds a callback that will be invoked before anything is sent over the connection</summary> - /// <param name="callback" type="Function">A callback function to execute before each time data is sent on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onStarting, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - send: function (data) { - /// <summary>Sends data over the connection</summary> - /// <param name="data" type="String">The data to send over the connection</param> - /// <returns type="signalR" /> - var connection = this; - - if (connection.state === signalR.connectionState.disconnected) { - // Connection hasn't been started yet - throw new Error("SignalR: Connection must be started before data can be sent. Call .start() before .send()"); - } - - if (connection.state === signalR.connectionState.connecting) { - // Connection hasn't been started yet - throw new Error("SignalR: Connection has not been fully initialized. Use .start().done() or .start().fail() to run logic after the connection has started."); - } - - connection.transport.send(connection, data); - // REVIEW: Should we return deferred here? - return connection; - }, - - received: function (callback) { - /// <summary>Adds a callback that will be invoked after anything is received over the connection</summary> - /// <param name="callback" type="Function">A callback function to execute when any data is received on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReceived, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - stateChanged: function (callback) { - /// <summary>Adds a callback that will be invoked when the connection state changes</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection state changes</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onStateChanged, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - error: function (callback) { - /// <summary>Adds a callback that will be invoked after an error occurs with the connection</summary> - /// <param name="callback" type="Function">A callback function to execute when an error occurs on the connection</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onError, function (e, data) { - callback.call(connection, data); - }); - return connection; - }, - - disconnected: function (callback) { - /// <summary>Adds a callback that will be invoked when the client disconnects</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is broken</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onDisconnect, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - connectionSlow: function (callback) { - /// <summary>Adds a callback that will be invoked when the client detects a slow connection</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is slow</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onConnectionSlow, function(e, data) { - callback.call(connection); - }); - - return connection; - }, - - reconnecting: function (callback) { - /// <summary>Adds a callback that will be invoked when the underlying transport begins reconnecting</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection enters a reconnecting state</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReconnecting, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - reconnected: function (callback) { - /// <summary>Adds a callback that will be invoked when the underlying transport reconnects</summary> - /// <param name="callback" type="Function">A callback function to execute when the connection is restored</param> - /// <returns type="signalR" /> - var connection = this; - $(connection).bind(events.onReconnect, function (e, data) { - callback.call(connection); - }); - return connection; - }, - - stop: function (async, notifyServer) { - /// <summary>Stops listening</summary> - /// <param name="async" type="Boolean">Whether or not to asynchronously abort the connection</param> - /// <param name="notifyServer" type="Boolean">Whether we want to notify the server that we are aborting the connection</param> - /// <returns type="signalR" /> - var connection = this; - - if (connection.state === signalR.connectionState.disconnected) { - return; - } - - try { - if (connection.transport) { - if (notifyServer !== false) { - connection.transport.abort(connection, async); - } - - if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { - signalR.transports._logic.stopMonitoringKeepAlive(connection); - } - - connection.transport.stop(connection); - connection.transport = null; - } - - // Trigger the disconnect event - $(connection).triggerHandler(events.onDisconnect); - - delete connection.messageId; - delete connection.groupsToken; - - // Remove the ID and the deferral on stop, this is to ensure that if a connection is restarted it takes on a new id/deferral. - delete connection.id; - delete connection._deferral; - } - finally { - changeState(connection, connection.state, signalR.connectionState.disconnected); - } - - return connection; - }, - - log: function (msg) { - log(msg, this.logging); - } - }; - - signalR.fn.init.prototype = signalR.fn; - - signalR.noConflict = function () { - /// <summary>Reinstates the original value of $.connection and returns the signalR object for manual assignment</summary> - /// <returns type="signalR" /> - if ($.connection === signalR) { - $.connection = _connection; - } - return signalR; - }; - - if ($.connection) { - _connection = $.connection; - } - - $.connection = $.signalR = signalR; - -}(window.jQuery, window)); -/* jquery.signalR.transports.common.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState; - - signalR.transports = {}; - - function checkIfAlive(connection) { - var keepAliveData = connection.keepAliveData, - diff, - timeElapsed; - - // Only check if we're connected - if (connection.state === signalR.connectionState.connected) { - diff = new Date(); - - diff.setTime(diff - keepAliveData.lastKeepAlive); - timeElapsed = diff.getTime(); - - // Check if the keep alive has completely timed out - if (timeElapsed >= keepAliveData.timeout) { - connection.log("Keep alive timed out. Notifying transport that connection has been lost."); - - // Notify transport that the connection has been lost - connection.transport.lostConnection(connection); - } - else if (timeElapsed >= keepAliveData.timeoutWarning) { - // This is to assure that the user only gets a single warning - if (!keepAliveData.userNotified) { - connection.log("Keep alive has been missed, connection may be dead/slow."); - $(connection).triggerHandler(events.onConnectionSlow); - keepAliveData.userNotified = true; - } - } - else { - keepAliveData.userNotified = false; - } - } - - // Verify we're monitoring the keep alive - // We don't want this as a part of the inner if statement above because we want keep alives to continue to be checked - // in the event that the server comes back online (if it goes offline). - if (keepAliveData.monitoring) { - window.setTimeout(function () { - checkIfAlive(connection); - }, keepAliveData.checkInterval); - } - } - - function isConnectedOrReconnecting(connection) { - return connection.state === signalR.connectionState.connected || - connection.state === signalR.connectionState.reconnecting; - } - - signalR.transports._logic = { - pingServer: function (connection, transport) { - /// <summary>Pings the server</summary> - /// <param name="connection" type="signalr">Connection associated with the server ping</param> - /// <returns type="signalR" /> - var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, - url = baseUrl + connection.appRelativeUrl + "/ping", - deferral = $.Deferred(); - - url = this.addQs(url, connection); - - $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - contentType: connection.contentType, - data: {}, - dataType: connection.ajaxDataType, - success: function (data) { - if (data.Response === "pong") { - deferral.resolve(); - } - else { - deferral.reject("SignalR: Invalid ping response when pinging server: " + (data.responseText || data.statusText)); - } - }, - error: function (data) { - deferral.reject("SignalR: Error pinging server: " + (data.responseText || data.statusText)); - } - }); - - return deferral.promise(); - }, - - addQs: function (url, connection) { - var appender = url.indexOf("?") !== -1 ? "&" : "?", - firstChar; - - if (!connection.qs) { - return url; - } - - if (typeof (connection.qs) === "object") { - return url + appender + $.param(connection.qs); - } - - if (typeof (connection.qs) === "string") { - firstChar = connection.qs.charAt(0); - - if (firstChar === "?" || firstChar === "&") { - appender = ""; - } - - return url + appender + connection.qs; - } - - throw new Error("Connections query string property must be either a string or object."); - }, - - getUrl: function (connection, transport, reconnecting, poll) { - /// <summary>Gets the url for making a GET based connect request</summary> - var baseUrl = transport === "webSockets" ? "" : connection.baseUrl, - url = baseUrl + connection.appRelativeUrl, - qs = "transport=" + transport + "&connectionToken=" + window.encodeURIComponent(connection.token); - - if (connection.data) { - qs += "&connectionData=" + window.encodeURIComponent(connection.data); - } - - if (connection.groupsToken) { - qs += "&groupsToken=" + window.encodeURIComponent(connection.groupsToken); - } - - if (!reconnecting) { - url += "/connect"; - } else { - if (poll) { - // longPolling transport specific - url += "/poll"; - } else { - url += "/reconnect"; - } - - if (connection.messageId) { - qs += "&messageId=" + window.encodeURIComponent(connection.messageId); - } - } - url += "?" + qs; - url = this.addQs(url, connection); - url += "&tid=" + Math.floor(Math.random() * 11); - return url; - }, - - maximizePersistentResponse: function (minPersistentResponse) { - return { - MessageId: minPersistentResponse.C, - Messages: minPersistentResponse.M, - Disconnect: typeof (minPersistentResponse.D) !== "undefined" ? true : false, - TimedOut: typeof (minPersistentResponse.T) !== "undefined" ? true : false, - LongPollDelay: minPersistentResponse.L, - GroupsToken: minPersistentResponse.G - }; - }, - - updateGroups: function (connection, groupsToken) { - if (groupsToken) { - connection.groupsToken = groupsToken; - } - }, - - ajaxSend: function (connection, data) { - var url = connection.url + "/send" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); - url = this.addQs(url, connection); - return $.ajax({ - url: url, - global: true, - type: connection.ajaxDataType === "jsonp" ? "GET" : "POST", - contentType: signalR._.defaultContentType, - dataType: connection.ajaxDataType, - data: { - data: data - }, - success: function (result) { - if (result) { - $(connection).triggerHandler(events.onReceived, [result]); - } - }, - error: function (errData, textStatus) { - if (textStatus === "abort" || textStatus === "parsererror") { - // The parsererror happens for sends that don't return any data, and hence - // don't write the jsonp callback to the response. This is harder to fix on the server - // so just hack around it on the client for now. - return; - } - $(connection).triggerHandler(events.onError, [errData, data]); - } - }); - }, - - ajaxAbort: function (connection, async) { - if (typeof (connection.transport) === "undefined") { - return; - } - - // Async by default unless explicitly overidden - async = typeof async === "undefined" ? true : async; - - var url = connection.url + "/abort" + "?transport=" + connection.transport.name + "&connectionToken=" + window.encodeURIComponent(connection.token); - url = this.addQs(url, connection); - $.ajax({ - url: url, - async: async, - timeout: 1000, - global: true, - type: "POST", - contentType: connection.contentType, - dataType: connection.ajaxDataType, - data: {} - }); - - connection.log("Fired ajax abort async = " + async); - }, - - processMessages: function (connection, minData) { - var data; - // Transport can be null if we've just closed the connection - if (connection.transport) { - var $connection = $(connection); - - // If our transport supports keep alive then we need to update the last keep alive time stamp. - // Very rarely the transport can be null. - if (connection.transport.supportsKeepAlive && connection.keepAliveData.activated) { - this.updateKeepAlive(connection); - } - - if (!minData) { - return; - } - - data = this.maximizePersistentResponse(minData); - - if (data.Disconnect) { - connection.log("Disconnect command received from server"); - - // Disconnected by the server - connection.stop(false, false); - return; - } - - this.updateGroups(connection, data.GroupsToken); - - if (data.Messages) { - $.each(data.Messages, function (index, message) { - $connection.triggerHandler(events.onReceived, [message]); - }); - } - - if (data.MessageId) { - connection.messageId = data.MessageId; - } - } - }, - - monitorKeepAlive: function (connection) { - var keepAliveData = connection.keepAliveData, - that = this; - - // If we haven't initiated the keep alive timeouts then we need to - if (!keepAliveData.monitoring) { - keepAliveData.monitoring = true; - - // Initialize the keep alive time stamp ping - that.updateKeepAlive(connection); - - // Save the function so we can unbind it on stop - connection.keepAliveData.reconnectKeepAliveUpdate = function () { - that.updateKeepAlive(connection); - }; - - // Update Keep alive on reconnect - $(connection).bind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); - - connection.log("Now monitoring keep alive with a warning timeout of " + keepAliveData.timeoutWarning + " and a connection lost timeout of " + keepAliveData.timeout); - // Start the monitoring of the keep alive - checkIfAlive(connection); - } - else { - connection.log("Tried to monitor keep alive but it's already being monitored"); - } - }, - - stopMonitoringKeepAlive: function (connection) { - var keepAliveData = connection.keepAliveData; - - // Only attempt to stop the keep alive monitoring if its being monitored - if (keepAliveData.monitoring) { - // Stop monitoring - keepAliveData.monitoring = false; - - // Remove the updateKeepAlive function from the reconnect event - $(connection).unbind(events.onReconnect, connection.keepAliveData.reconnectKeepAliveUpdate); - - // Clear all the keep alive data - connection.keepAliveData = {}; - connection.log("Stopping the monitoring of the keep alive"); - } - }, - - updateKeepAlive: function (connection) { - connection.keepAliveData.lastKeepAlive = new Date(); - }, - - ensureReconnectingState: function (connection) { - if (changeState(connection, - signalR.connectionState.connected, - signalR.connectionState.reconnecting) === true) { - $(connection).triggerHandler(events.onReconnecting); - } - return connection.state === signalR.connectionState.reconnecting; - }, - - clearReconnectTimeout: function (connection) { - if (connection && connection._.reconnectTimeout) { - window.clearTimeout(connection._.reconnectTimeout); - delete connection._.reconnectTimeout; - } - }, - - reconnect: function (connection, transportName) { - var transport = signalR.transports[transportName], - that = this; - - // We should only set a reconnectTimeout if we are currently connected - // and a reconnectTimeout isn't already set. - if (isConnectedOrReconnecting(connection) && !connection._.reconnectTimeout) { - - connection._.reconnectTimeout = window.setTimeout(function () { - transport.stop(connection); - - if (that.ensureReconnectingState(connection)) { - connection.log(transportName + " reconnecting"); - transport.start(connection); - } - }, connection.reconnectDelay); - } - }, - - foreverFrame: { - count: 0, - connections: {} - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.webSockets.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic; - - signalR.transports.webSockets = { - name: "webSockets", - - supportsKeepAlive: true, - - send: function (connection, data) { - connection.socket.send(data); - }, - - start: function (connection, onSuccess, onFailed) { - var url, - opened = false, - that = this, - reconnecting = !onSuccess, - $connection = $(connection); - - if (!window.WebSocket) { - onFailed(); - return; - } - - if (!connection.socket) { - if (connection.webSocketServerUrl) { - url = connection.webSocketServerUrl; - } - else { - url = connection.wsProtocol + connection.host; - } - - url += transportLogic.getUrl(connection, this.name, reconnecting); - - connection.log("Connecting to websocket endpoint '" + url + "'"); - connection.socket = new window.WebSocket(url); - connection.socket.onopen = function () { - opened = true; - connection.log("Websocket opened"); - - transportLogic.clearReconnectTimeout(connection); - - if (onSuccess) { - onSuccess(); - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - $connection.triggerHandler(events.onReconnect); - } - }; - - connection.socket.onclose = function (event) { - // Only handle a socket close if the close is from the current socket. - // Sometimes on disconnect the server will push down an onclose event - // to an expired socket. - if (this === connection.socket) { - if (!opened) { - if (onFailed) { - onFailed(); - } - else if (reconnecting) { - that.reconnect(connection); - } - return; - } - else if (typeof event.wasClean !== "undefined" && event.wasClean === false) { - // Ideally this would use the websocket.onerror handler (rather than checking wasClean in onclose) but - // I found in some circumstances Chrome won't call onerror. This implementation seems to work on all browsers. - $(connection).triggerHandler(events.onError, [event.reason]); - connection.log("Unclean disconnect from websocket." + event.reason); - } - else { - connection.log("Websocket closed"); - } - - that.reconnect(connection); - } - }; - - connection.socket.onmessage = function (event) { - var data = window.JSON.parse(event.data), - $connection = $(connection); - - if (data) { - // data.M is PersistentResponse.Messages - if ($.isEmptyObject(data) || data.M) { - transportLogic.processMessages(connection, data); - } else { - // For websockets we need to trigger onReceived - // for callbacks to outgoing hub calls. - $connection.triggerHandler(events.onReceived, [data]); - } - } - }; - } - }, - - reconnect: function (connection) { - transportLogic.reconnect(connection, this.name); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - - }, - - stop: function (connection) { - // Don't trigger a reconnect after stopping - transportLogic.clearReconnectTimeout(connection); - - if (connection.socket !== null) { - connection.log("Closing the Websocket"); - connection.socket.close(); - connection.socket = null; - } - }, - - abort: function (connection) { - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.serverSentEvents.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic; - - signalR.transports.serverSentEvents = { - name: "serverSentEvents", - - supportsKeepAlive: true, - - timeOut: 3000, - - start: function (connection, onSuccess, onFailed) { - var that = this, - opened = false, - $connection = $(connection), - reconnecting = !onSuccess, - url, - connectTimeOut; - - if (connection.eventSource) { - connection.log("The connection already has an event source. Stopping it."); - connection.stop(); - } - - if (!window.EventSource) { - if (onFailed) { - connection.log("This browser doesn't support SSE."); - onFailed(); - } - return; - } - - url = transportLogic.getUrl(connection, this.name, reconnecting); - - try { - connection.log("Attempting to connect to SSE endpoint '" + url + "'"); - connection.eventSource = new window.EventSource(url); - } - catch (e) { - connection.log("EventSource failed trying to connect with error " + e.Message); - if (onFailed) { - // The connection failed, call the failed callback - onFailed(); - } - else { - $connection.triggerHandler(events.onError, [e]); - if (reconnecting) { - // If we were reconnecting, rather than doing initial connect, then try reconnect again - that.reconnect(connection); - } - } - return; - } - - // After connecting, if after the specified timeout there's no response stop the connection - // and raise on failed - connectTimeOut = window.setTimeout(function () { - if (opened === false) { - connection.log("EventSource timed out trying to connect"); - connection.log("EventSource readyState: " + connection.eventSource.readyState); - - if (!reconnecting) { - that.stop(connection); - } - - if (reconnecting) { - // If we're reconnecting and the event source is attempting to connect, - // don't keep retrying. This causes duplicate connections to spawn. - if (connection.eventSource.readyState !== window.EventSource.CONNECTING && - connection.eventSource.readyState !== window.EventSource.OPEN) { - // If we were reconnecting, rather than doing initial connect, then try reconnect again - that.reconnect(connection); - } - } else if (onFailed) { - onFailed(); - } - } - }, - that.timeOut); - - connection.eventSource.addEventListener("open", function (e) { - connection.log("EventSource connected"); - - if (connectTimeOut) { - window.clearTimeout(connectTimeOut); - } - - transportLogic.clearReconnectTimeout(connection); - - if (opened === false) { - opened = true; - - if (onSuccess) { - onSuccess(); - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // If there's no onSuccess handler we assume this is a reconnect - $connection.triggerHandler(events.onReconnect); - } - } - }, false); - - connection.eventSource.addEventListener("message", function (e) { - // process messages - if (e.data === "initialized") { - return; - } - - transportLogic.processMessages(connection, window.JSON.parse(e.data)); - }, false); - - connection.eventSource.addEventListener("error", function (e) { - // Only handle an error if the error is from the current Event Source. - // Sometimes on disconnect the server will push down an error event - // to an expired Event Source. - if (this === connection.eventSource) { - if (!opened) { - if (onFailed) { - onFailed(); - } - - return; - } - - connection.log("EventSource readyState: " + connection.eventSource.readyState); - - if (e.eventPhase === window.EventSource.CLOSED) { - // We don't use the EventSource's native reconnect function as it - // doesn't allow us to change the URL when reconnecting. We need - // to change the URL to not include the /connect suffix, and pass - // the last message id we received. - connection.log("EventSource reconnecting due to the server connection ending"); - that.reconnect(connection); - } else { - // connection error - connection.log("EventSource error"); - $connection.triggerHandler(events.onError); - } - } - }, false); - }, - - reconnect: function (connection) { - transportLogic.reconnect(connection, this.name); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - stop: function (connection) { - // Don't trigger a reconnect after stopping - transportLogic.clearReconnectTimeout(connection); - - if (connection && connection.eventSource) { - connection.log("EventSource calling close()"); - connection.eventSource.close(); - connection.eventSource = null; - delete connection.eventSource; - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.foreverFrame.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - transportLogic = signalR.transports._logic, - // Used to prevent infinite loading icon spins in older versions of ie - // We build this object inside a closure so we don't pollute the rest of - // the foreverFrame transport with unnecessary functions/utilities. - loadPreventer = (function () { - var loadingFixIntervalId = null, - loadingFixInterval = 1000, - attachedTo = 0; - - return { - prevent: function () { - // Prevent additional iframe removal procedures from newer browsers - if (signalR._.ieVersion <= 8) { - // We only ever want to set the interval one time, so on the first attachedTo - if (attachedTo === 0) { - // Create and destroy iframe every 3 seconds to prevent loading icon, super hacky - loadingFixIntervalId = window.setInterval(function () { - var tempFrame = $("<iframe style='position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;' src=''></iframe>"); - - $("body").append(tempFrame); - tempFrame.remove(); - tempFrame = null; - }, loadingFixInterval); - } - - attachedTo++; - } - }, - cancel: function () { - // Only clear the interval if there's only one more object that the loadPreventer is attachedTo - if (attachedTo === 1) { - window.clearInterval(loadingFixIntervalId); - } - - if (attachedTo > 0) { - attachedTo--; - } - } - }; - })(); - - signalR.transports.foreverFrame = { - name: "foreverFrame", - - supportsKeepAlive: true, - - timeOut: 3000, - - start: function (connection, onSuccess, onFailed) { - var that = this, - frameId = (transportLogic.foreverFrame.count += 1), - url, - frame = $("<iframe data-signalr-connection-id='" + connection.id + "' style='position:absolute;top:0;left:0;width:0;height:0;visibility:hidden;' src=''></iframe>"); - - if (window.EventSource) { - // If the browser supports SSE, don't use Forever Frame - if (onFailed) { - connection.log("This browser supports SSE, skipping Forever Frame."); - onFailed(); - } - return; - } - - // Start preventing loading icon - // This will only perform work if the loadPreventer is not attached to another connection. - loadPreventer.prevent(); - - // Build the url - url = transportLogic.getUrl(connection, this.name); - url += "&frameId=" + frameId; - - // Set body prior to setting URL to avoid caching issues. - $("body").append(frame); - - frame.prop("src", url); - transportLogic.foreverFrame.connections[frameId] = connection; - - connection.log("Binding to iframe's readystatechange event."); - frame.bind("readystatechange", function () { - if ($.inArray(this.readyState, ["loaded", "complete"]) >= 0) { - connection.log("Forever frame iframe readyState changed to " + this.readyState + ", reconnecting"); - - that.reconnect(connection); - } - }); - - connection.frame = frame[0]; - connection.frameId = frameId; - - if (onSuccess) { - connection.onSuccess = onSuccess; - } - - // After connecting, if after the specified timeout there's no response stop the connection - // and raise on failed - window.setTimeout(function () { - if (connection.onSuccess) { - connection.log("Failed to connect using forever frame source, it timed out after " + that.timeOut + "ms."); - that.stop(connection); - - if (onFailed) { - onFailed(); - } - } - }, that.timeOut); - }, - - reconnect: function (connection) { - var that = this; - window.setTimeout(function () { - if (connection.frame && transportLogic.ensureReconnectingState(connection)) { - var frame = connection.frame, - src = transportLogic.getUrl(connection, that.name, true) + "&frameId=" + connection.frameId; - connection.log("Updating iframe src to '" + src + "'."); - frame.src = src; - } - }, connection.reconnectDelay); - }, - - lostConnection: function (connection) { - this.reconnect(connection); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - receive: function (connection, data) { - var cw; - - transportLogic.processMessages(connection, data); - // Delete the script & div elements - connection.frameMessageCount = (connection.frameMessageCount || 0) + 1; - if (connection.frameMessageCount > 50) { - connection.frameMessageCount = 0; - cw = connection.frame.contentWindow || connection.frame.contentDocument; - if (cw && cw.document) { - $("body", cw.document).empty(); - } - } - }, - - stop: function (connection) { - var cw = null; - - // Stop attempting to prevent loading icon - loadPreventer.cancel(); - - if (connection.frame) { - if (connection.frame.stop) { - connection.frame.stop(); - } else { - try { - cw = connection.frame.contentWindow || connection.frame.contentDocument; - if (cw.document && cw.document.execCommand) { - cw.document.execCommand("Stop"); - } - } - catch (e) { - connection.log("SignalR: Error occured when stopping foreverFrame transport. Message = " + e.message); - } - } - $(connection.frame).remove(); - delete transportLogic.foreverFrame.connections[connection.frameId]; - connection.frame = null; - connection.frameId = null; - delete connection.frame; - delete connection.frameId; - connection.log("Stopping forever frame"); - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - }, - - getConnection: function (id) { - return transportLogic.foreverFrame.connections[id]; - }, - - started: function (connection) { - if (connection.onSuccess) { - connection.onSuccess(); - connection.onSuccess = null; - delete connection.onSuccess; - } else if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // If there's no onSuccess handler we assume this is a reconnect - $(connection).triggerHandler(events.onReconnect); - } - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.transports.longPolling.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.transports.common.js" /> - -(function ($, window) { - "use strict"; - - var signalR = $.signalR, - events = $.signalR.events, - changeState = $.signalR.changeState, - isDisconnecting = $.signalR.isDisconnecting, - transportLogic = signalR.transports._logic; - - signalR.transports.longPolling = { - name: "longPolling", - - supportsKeepAlive: false, - - reconnectDelay: 3000, - - init: function (connection, onComplete) { - /// <summary>Pings the server to ensure availability</summary> - /// <param name="connection" type="signalr">Connection associated with the server ping</param> - /// <param name="onComplete" type="Function">Callback to call once initialization has completed</param> - - var that = this, - pingLoop, - // pingFail is used to loop the re-ping behavior. When we fail we want to re-try. - pingFail = function (reason) { - if (isDisconnecting(connection) === false) { - connection.log("SignalR: Server ping failed because '" + reason + "', re-trying ping."); - window.setTimeout(pingLoop, that.reconnectDelay); - } - }; - - connection.log("SignalR: Initializing long polling connection with server."); - pingLoop = function () { - // Ping the server, on successful ping call the onComplete method, otherwise if we fail call the pingFail - transportLogic.pingServer(connection, that.name).done(onComplete).fail(pingFail); - }; - - pingLoop(); - }, - - start: function (connection, onSuccess, onFailed) { - /// <summary>Starts the long polling connection</summary> - /// <param name="connection" type="signalR">The SignalR connection to start</param> - var that = this, - initialConnectedFired = false, - fireConnect = function () { - if (initialConnectedFired) { - return; - } - initialConnectedFired = true; - onSuccess(); - connection.log("Longpolling connected"); - }, - reconnectErrors = 0, - reconnectTimeoutId = null, - fireReconnected = function (instance) { - window.clearTimeout(reconnectTimeoutId); - reconnectTimeoutId = null; - - if (changeState(connection, - signalR.connectionState.reconnecting, - signalR.connectionState.connected) === true) { - // Successfully reconnected! - connection.log("Raising the reconnect event"); - $(instance).triggerHandler(events.onReconnect); - } - }, - // 1 hour - maxFireReconnectedTimeout = 3600000; - - if (connection.pollXhr) { - connection.log("Polling xhr requests already exists, aborting."); - connection.stop(); - } - - // We start with an initialization procedure which pings the server to verify that it is there. - // On scucessful initialization we'll then proceed with starting the transport. - that.init(connection, function () { - connection.messageId = null; - - window.setTimeout(function () { - (function poll(instance, raiseReconnect) { - var messageId = instance.messageId, - connect = (messageId === null), - reconnecting = !connect, - polling = !raiseReconnect, - url = transportLogic.getUrl(instance, that.name, reconnecting, polling); - - // If we've disconnected during the time we've tried to re-instantiate the poll then stop. - if (isDisconnecting(instance) === true) { - return; - } - - connection.log("Attempting to connect to '" + url + "' using longPolling."); - instance.pollXhr = $.ajax({ - url: url, - global: true, - cache: false, - type: "GET", - dataType: connection.ajaxDataType, - contentType: connection.contentType, - success: function (minData) { - var delay = 0, - data; - - // Reset our reconnect errors so if we transition into a reconnecting state again we trigger - // reconnected quickly - reconnectErrors = 0; - - // If there's currently a timeout to trigger reconnect, fire it now before processing messages - if (reconnectTimeoutId !== null) { - fireReconnected(); - } - - fireConnect(); - - if (minData) { - data = transportLogic.maximizePersistentResponse(minData); - } - - transportLogic.processMessages(instance, minData); - - if (data && - $.type(data.LongPollDelay) === "number") { - delay = data.LongPollDelay; - } - - if (data && data.Disconnect) { - return; - } - - if (isDisconnecting(instance) === true) { - return; - } - - // We never want to pass a raiseReconnect flag after a successful poll. This is handled via the error function - if (delay > 0) { - window.setTimeout(function () { - poll(instance, false); - }, delay); - } else { - poll(instance, false); - } - }, - - error: function (data, textStatus) { - // Stop trying to trigger reconnect, connection is in an error state - // If we're not in the reconnect state this will noop - window.clearTimeout(reconnectTimeoutId); - reconnectTimeoutId = null; - - if (textStatus === "abort") { - connection.log("Aborted xhr requst."); - return; - } - - // Increment our reconnect errors, we assume all errors to be reconnect errors - // In the case that it's our first error this will cause Reconnect to be fired - // after 1 second due to reconnectErrors being = 1. - reconnectErrors++; - - if (connection.state !== signalR.connectionState.reconnecting) { - connection.log("An error occurred using longPolling. Status = " + textStatus + ". " + data.responseText); - $(instance).triggerHandler(events.onError, [data.responseText]); - } - - // Transition into the reconnecting state - transportLogic.ensureReconnectingState(instance); - - // If we've errored out we need to verify that the server is still there, so re-start initialization process - // This will ping the server until it successfully gets a response. - that.init(instance, function () { - // Call poll with the raiseReconnect flag as true - poll(instance, true); - }); - } - }); - - - // This will only ever pass after an error has occured via the poll ajax procedure. - if (reconnecting && raiseReconnect === true) { - // We wait to reconnect depending on how many times we've failed to reconnect. - // This is essentially a heuristic that will exponentially increase in wait time before - // triggering reconnected. This depends on the "error" handler of Poll to cancel this - // timeout if it triggers before the Reconnected event fires. - // The Math.min at the end is to ensure that the reconnect timeout does not overflow. - reconnectTimeoutId = window.setTimeout(function () { fireReconnected(instance); }, Math.min(1000 * (Math.pow(2, reconnectErrors) - 1), maxFireReconnectedTimeout)); - } - }(connection)); - - // Set an arbitrary timeout to trigger onSuccess, this will alot for enough time on the server to wire up the connection. - // Will be fixed by #1189 and this code can be modified to not be a timeout - window.setTimeout(function () { - // Trigger the onSuccess() method because we've now instantiated a connection - fireConnect(); - }, 250); - }, 250); // Have to delay initial poll so Chrome doesn't show loader spinner in tab - }); - }, - - lostConnection: function (connection) { - throw new Error("Lost Connection not handled for LongPolling"); - }, - - send: function (connection, data) { - transportLogic.ajaxSend(connection, data); - }, - - stop: function (connection) { - /// <summary>Stops the long polling connection</summary> - /// <param name="connection" type="signalR">The SignalR connection to stop</param> - if (connection.pollXhr) { - connection.pollXhr.abort(); - connection.pollXhr = null; - delete connection.pollXhr; - } - }, - - abort: function (connection, async) { - transportLogic.ajaxAbort(connection, async); - } - }; - -}(window.jQuery, window)); -/* jquery.signalR.hubs.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> - -(function ($, window) { - "use strict"; - - // we use a global id for tracking callbacks so the server doesn't have to send extra info like hub name - var eventNamespace = ".hubProxy"; - - function makeEventName(event) { - return event + eventNamespace; - } - - // Equivalent to Array.prototype.map - function map(arr, fun, thisp) { - var i, - length = arr.length, - result = []; - for (i = 0; i < length; i += 1) { - if (arr.hasOwnProperty(i)) { - result[i] = fun.call(thisp, arr[i], i, arr); - } - } - return result; - } - - function getArgValue(a) { - return $.isFunction(a) ? null : ($.type(a) === "undefined" ? null : a); - } - - function hasMembers(obj) { - for (var key in obj) { - // If we have any properties in our callback map then we have callbacks and can exit the loop via return - if (obj.hasOwnProperty(key)) { - return true; - } - } - - return false; - } - - function clearInvocationCallbacks(connection, error) { - /// <param name="connection" type="hubConnection" /> - var callbacks = connection._.invocationCallbacks, - callback; - - connection.log("Clearing hub invocation callbacks with error: " + error); - - // Reset the callback cache now as we have a local var referencing it - connection._.invocationCallbackId = 0; - delete connection._.invocationCallbacks; - connection._.invocationCallbacks = {}; - - // Loop over the callbacks and invoke them. - // We do this using a local var reference and *after* we've cleared the cache - // so that if a fail callback itself tries to invoke another method we don't - // end up with its callback in the list we're looping over. - for (var callbackId in callbacks) { - callback = callbacks[callbackId]; - callback.method.call(callback.scope, { E: error }); - } - } - - // hubProxy - function hubProxy(hubConnection, hubName) { - /// <summary> - /// Creates a new proxy object for the given hub connection that can be used to invoke - /// methods on server hubs and handle client method invocation requests from the server. - /// </summary> - return new hubProxy.fn.init(hubConnection, hubName); - } - - hubProxy.fn = hubProxy.prototype = { - init: function (connection, hubName) { - this.state = {}; - this.connection = connection; - this.hubName = hubName; - this._ = { - callbackMap: {} - }; - }, - - hasSubscriptions: function () { - return hasMembers(this._.callbackMap); - }, - - on: function (eventName, callback) { - /// <summary>Wires up a callback to be invoked when a invocation request is received from the server hub.</summary> - /// <param name="eventName" type="String">The name of the hub event to register the callback for.</param> - /// <param name="callback" type="Function">The callback to be invoked.</param> - var self = this, - callbackMap = self._.callbackMap; - - // Normalize the event name to lowercase - eventName = eventName.toLowerCase(); - - // If there is not an event registered for this callback yet we want to create its event space in the callback map. - if (!callbackMap[eventName]) { - callbackMap[eventName] = {}; - } - - // Map the callback to our encompassed function - callbackMap[eventName][callback] = function (e, data) { - callback.apply(self, data); - }; - - $(self).bind(makeEventName(eventName), callbackMap[eventName][callback]); - - return self; - }, - - off: function (eventName, callback) { - /// <summary>Removes the callback invocation request from the server hub for the given event name.</summary> - /// <param name="eventName" type="String">The name of the hub event to unregister the callback for.</param> - /// <param name="callback" type="Function">The callback to be invoked.</param> - var self = this, - callbackMap = self._.callbackMap, - callbackSpace; - - // Normalize the event name to lowercase - eventName = eventName.toLowerCase(); - - callbackSpace = callbackMap[eventName]; - - // Verify that there is an event space to unbind - if (callbackSpace) { - // Only unbind if there's an event bound with eventName and a callback with the specified callback - if (callbackSpace[callback]) { - $(self).unbind(makeEventName(eventName), callbackSpace[callback]); - - // Remove the callback from the callback map - delete callbackSpace[callback]; - - // Check if there are any members left on the event, if not we need to destroy it. - if (!hasMembers(callbackSpace)) { - delete callbackMap[eventName]; - } - } - else if (!callback) { // Check if we're removing the whole event and we didn't error because of an invalid callback - $(self).unbind(makeEventName(eventName)); - - delete callbackMap[eventName]; - } - } - - return self; - }, - - invoke: function (methodName) { - /// <summary>Invokes a server hub method with the given arguments.</summary> - /// <param name="methodName" type="String">The name of the server hub method.</param> - - var self = this, - connection = self.connection, - args = $.makeArray(arguments).slice(1), - argValues = map(args, getArgValue), - data = { H: self.hubName, M: methodName, A: argValues, I: connection._.invocationCallbackId }, - d = $.Deferred(), - callback = function (minResult) { - var result = self._maximizeHubResponse(minResult); - - // Update the hub state - $.extend(self.state, result.State); - - if (result.Error) { - // Server hub method threw an exception, log it & reject the deferred - if (result.StackTrace) { - connection.log(result.Error + "\n" + result.StackTrace); - } - d.rejectWith(self, [result.Error]); - } else { - // Server invocation succeeded, resolve the deferred - d.resolveWith(self, [result.Result]); - } - }; - - connection._.invocationCallbacks[connection._.invocationCallbackId.toString()] = { scope: self, method: callback }; - connection._.invocationCallbackId += 1; - - if (!$.isEmptyObject(self.state)) { - data.S = self.state; - } - - connection.send(window.JSON.stringify(data)); - - return d.promise(); - }, - - _maximizeHubResponse: function (minHubResponse) { - return { - State: minHubResponse.S, - Result: minHubResponse.R, - Id: minHubResponse.I, - Error: minHubResponse.E, - StackTrace: minHubResponse.T - }; - } - }; - - hubProxy.fn.init.prototype = hubProxy.fn; - - // hubConnection - function hubConnection(url, options) { - /// <summary>Creates a new hub connection.</summary> - /// <param name="url" type="String">[Optional] The hub route url, defaults to "/signalr".</param> - /// <param name="options" type="Object">[Optional] Settings to use when creating the hubConnection.</param> - var settings = { - qs: null, - logging: false, - useDefaultPath: true - }; - - $.extend(settings, options); - - if (!url || settings.useDefaultPath) { - url = (url || "") + "/signalr"; - } - return new hubConnection.fn.init(url, settings); - } - - hubConnection.fn = hubConnection.prototype = $.connection(); - - hubConnection.fn.init = function (url, options) { - var settings = { - qs: null, - logging: false, - useDefaultPath: true - }, - connection = this; - - $.extend(settings, options); - - // Call the base constructor - $.signalR.fn.init.call(connection, url, settings.qs, settings.logging); - - // Object to store hub proxies for this connection - connection.proxies = {}; - - connection._.invocationCallbackId = 0; - connection._.invocationCallbacks = {}; - - // Wire up the received handler - connection.received(function (minData) { - var data, proxy, dataCallbackId, callback, hubName, eventName; - if (!minData) { - return; - } - - if (typeof (minData.I) !== "undefined") { - // We received the return value from a server method invocation, look up callback by id and call it - dataCallbackId = minData.I.toString(); - callback = connection._.invocationCallbacks[dataCallbackId]; - if (callback) { - // Delete the callback from the proxy - connection._.invocationCallbacks[dataCallbackId] = null; - delete connection._.invocationCallbacks[dataCallbackId]; - - // Invoke the callback - callback.method.call(callback.scope, minData); - } - } else { - data = this._maximizeClientHubInvocation(minData); - - // We received a client invocation request, i.e. broadcast from server hub - connection.log("Triggering client hub event '" + data.Method + "' on hub '" + data.Hub + "'."); - - // Normalize the names to lowercase - hubName = data.Hub.toLowerCase(); - eventName = data.Method.toLowerCase(); - - // Trigger the local invocation event - proxy = this.proxies[hubName]; - - // Update the hub state - $.extend(proxy.state, data.State); - $(proxy).triggerHandler(makeEventName(eventName), [data.Args]); - } - }); - - connection.error(function (errData, origData) { - var data, callbackId, callback; - - if (connection.transport && connection.transport.name === "webSockets") { - // WebSockets connections have all callbacks removed on reconnect instead - // as WebSockets sends are fire & forget - return; - } - - if (!origData) { - // No original data passed so this is not a send error - return; - } - - try { - data = window.JSON.parse(origData); - if (!data.I) { - // The original data doesn't have a callback ID so not a send error - return; - } - } catch (e) { - // The original data is not a JSON payload so this is not a send error - return; - } - - callbackId = data.I; - callback = connection._.invocationCallbacks[callbackId]; - - // Invoke the callback with an error to reject the promise - callback.method.call(callback.scope, { E: errData }); - - // Delete the callback - connection._.invocationCallbacks[callbackId] = null; - delete connection._.invocationCallbacks[callbackId]; - }); - - connection.reconnecting(function () { - if (connection.transport && connection.transport.name === "webSockets") { - clearInvocationCallbacks(connection, "Connection started reconnecting before invocation result was received."); - } - }); - - connection.disconnected(function () { - clearInvocationCallbacks(connection, "Connection was disconnected before invocation result was received."); - }); - }; - - hubConnection.fn._maximizeClientHubInvocation = function (minClientHubInvocation) { - return { - Hub: minClientHubInvocation.H, - Method: minClientHubInvocation.M, - Args: minClientHubInvocation.A, - State: minClientHubInvocation.S - }; - }; - - hubConnection.fn._registerSubscribedHubs = function () { - /// <summary> - /// Sets the starting event to loop through the known hubs and register any new hubs - /// that have been added to the proxy. - /// </summary> - - if (!this._subscribedToHubs) { - this._subscribedToHubs = true; - this.starting(function () { - // Set the connection's data object with all the hub proxies with active subscriptions. - // These proxies will receive notifications from the server. - var subscribedHubs = []; - - $.each(this.proxies, function (key) { - if (this.hasSubscriptions()) { - subscribedHubs.push({ name: key }); - } - }); - - this.data = window.JSON.stringify(subscribedHubs); - }); - } - }; - - hubConnection.fn.createHubProxy = function (hubName) { - /// <summary> - /// Creates a new proxy object for the given hub connection that can be used to invoke - /// methods on server hubs and handle client method invocation requests from the server. - /// </summary> - /// <param name="hubName" type="String"> - /// The name of the hub on the server to create the proxy for. - /// </param> - - // Normalize the name to lowercase - hubName = hubName.toLowerCase(); - - var proxy = this.proxies[hubName]; - if (!proxy) { - proxy = hubProxy(this, hubName); - this.proxies[hubName] = proxy; - } - - this._registerSubscribedHubs(); - - return proxy; - }; - - hubConnection.fn.init.prototype = hubConnection.fn; - - $.hubConnection = hubConnection; - -}(window.jQuery, window)); -/* jquery.signalR.version.js */ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. - -/*global window:false */ -/// <reference path="jquery.signalR.core.js" /> -(function ($) { - $.signalR.version = "1.1.3"; -}(window.jQuery)); diff --git a/src/UI/JsLibraries/locale/placeholder.txt b/src/UI/JsLibraries/locale/placeholder.txt deleted file mode 100644 index 89326d0d4..000000000 --- a/src/UI/JsLibraries/locale/placeholder.txt +++ /dev/null @@ -1 +0,0 @@ -//Need this directory for moment/webpack, but git doesn't like empty directories. \ No newline at end of file diff --git a/src/UI/JsLibraries/lodash.underscore.js b/src/UI/JsLibraries/lodash.underscore.js deleted file mode 100644 index 02fc342c5..000000000 --- a/src/UI/JsLibraries/lodash.underscore.js +++ /dev/null @@ -1,4619 +0,0 @@ -/** - * @license - * Lo-Dash 1.3.1 (Custom Build) <http://lodash.com/> - * Build: `lodash underscore exports="amd,commonjs,global,node" -o ./dist/lodash.underscore.js` - * Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/> - * Based on Underscore.js 1.5.1 <http://underscorejs.org/LICENSE> - * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors - * Available under MIT license <http://lodash.com/license> - */ -;(function(window) { - - /** Used as a safe reference for `undefined` in pre ES5 environments */ - var undefined; - - /** Used to generate unique IDs */ - var idCounter = 0; - - /** Used internally to indicate various things */ - var indicatorObject = {}; - - /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ - var keyPrefix = +new Date + ''; - - /** Used to match "interpolate" template delimiters */ - var reInterpolate = /<%=([\s\S]+?)%>/g; - - /** Used to ensure capturing order of template delimiters */ - var reNoMatch = /($^)/; - - /** Used to match unescaped characters in compiled string literals */ - var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; - - /** `Object#toString` result shortcuts */ - var argsClass = '[object Arguments]', - arrayClass = '[object Array]', - boolClass = '[object Boolean]', - dateClass = '[object Date]', - funcClass = '[object Function]', - numberClass = '[object Number]', - objectClass = '[object Object]', - regexpClass = '[object RegExp]', - stringClass = '[object String]'; - - /** Used to determine if values are of the language type Object */ - var objectTypes = { - 'boolean': false, - 'function': true, - 'object': true, - 'number': false, - 'string': false, - 'undefined': false - }; - - /** Used to escape characters for inclusion in compiled string literals */ - var stringEscapes = { - '\\': '\\', - "'": "'", - '\n': 'n', - '\r': 'r', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - /** Detect free variable `exports` */ - var freeExports = objectTypes[typeof exports] && exports; - - /** Detect free variable `module` */ - var freeModule = objectTypes[typeof module] && module && module.exports == freeExports && module; - - /** Detect free variable `global` from Node.js or Browserified code and use it as `window` */ - var freeGlobal = objectTypes[typeof global] && global; - if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { - window = freeGlobal; - } - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.indexOf` without support for binary searches - * or `fromIndex` constraints. - * - * @private - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Number} [fromIndex=0] The index to search from. - * @returns {Number} Returns the index of the matched value or `-1`. - */ - function baseIndexOf(array, value, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0; - - while (++index < length) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Used by `sortBy` to compare transformed `collection` elements, stable sorting - * them in ascending order. - * - * @private - * @param {Object} a The object to compare to `b`. - * @param {Object} b The object to compare to `a`. - * @returns {Number} Returns the sort order indicator of `1` or `-1`. - */ - function compareAscending(a, b) { - var ac = a.criteria, - bc = b.criteria; - - // ensure a stable sort in V8 and other engines - // http://code.google.com/p/v8/issues/detail?id=90 - if (ac !== bc) { - if (ac > bc || typeof ac == 'undefined') { - return 1; - } - if (ac < bc || typeof bc == 'undefined') { - return -1; - } - } - // The JS engine embedded in Adobe applications like InDesign has a buggy - // `Array#sort` implementation that causes it, under certain circumstances, - // to return the same value for `a` and `b`. - // See https://github.com/jashkenas/underscore/pull/1247 - return a.index - b.index; - } - - /** - * Used by `template` to escape characters for inclusion in compiled - * string literals. - * - * @private - * @param {String} match The matched character to escape. - * @returns {String} Returns the escaped character. - */ - function escapeStringChar(match) { - return '\\' + stringEscapes[match]; - } - - /** - * A no-operation function. - * - * @private - */ - function noop() { - // no operation performed - } - - /*--------------------------------------------------------------------------*/ - - /** - * Used for `Array` method references. - * - * Normally `Array.prototype` would suffice, however, using an array literal - * avoids issues in Narwhal. - */ - var arrayRef = []; - - /** Used for native method references */ - var objectProto = Object.prototype; - - /** Used to restore the original `_` reference in `noConflict` */ - var oldDash = window._; - - /** Used to detect if a method is native */ - var reNative = RegExp('^' + - String(objectProto.valueOf) - .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - .replace(/valueOf|for [^\]]+/g, '.+?') + '$' - ); - - /** Native method shortcuts */ - var ceil = Math.ceil, - floor = Math.floor, - hasOwnProperty = objectProto.hasOwnProperty, - push = arrayRef.push, - toString = objectProto.toString, - unshift = arrayRef.unshift; - - /* Native method shortcuts for methods with the same name as other `lodash` methods */ - var nativeBind = reNative.test(nativeBind = toString.bind) && nativeBind, - nativeCreate = reNative.test(nativeCreate = Object.create) && nativeCreate, - nativeIsArray = reNative.test(nativeIsArray = Array.isArray) && nativeIsArray, - nativeIsFinite = window.isFinite, - nativeIsNaN = window.isNaN, - nativeKeys = reNative.test(nativeKeys = Object.keys) && nativeKeys, - nativeMax = Math.max, - nativeMin = Math.min, - nativeRandom = Math.random, - nativeSlice = arrayRef.slice; - - /** Detect various environments */ - var isIeOpera = reNative.test(window.attachEvent), - isV8 = nativeBind && !/\n|true/.test(nativeBind + isIeOpera); - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object which wraps the given value to enable method - * chaining. - * - * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: - * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, - * and `unshift` - * - * Chaining is supported in custom builds as long as the `value` method is - * implicitly or explicitly included in the build. - * - * The chainable wrapper functions are: - * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, - * `compose`, `concat`, `countBy`, `createCallback`, `curry`, `debounce`, - * `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, `forEach`, - * `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, `functions`, - * `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, `invoke`, `keys`, - * `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, `once`, `pairs`, - * `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, `range`, `reject`, - * `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, `sortBy`, `splice`, - * `tap`, `throttle`, `times`, `toArray`, `transform`, `union`, `uniq`, `unshift`, - * `unzip`, `values`, `where`, `without`, `wrap`, and `zip` - * - * The non-chainable wrapper functions are: - * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, - * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, - * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, - * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, - * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, - * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, - * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, - * `template`, `unescape`, `uniqueId`, and `value` - * - * The wrapper functions `first` and `last` return wrapped values when `n` is - * provided, otherwise they return unwrapped values. - * - * @name _ - * @constructor - * @category Chaining - * @param {Mixed} value The value to wrap in a `lodash` instance. - * @returns {Object} Returns a `lodash` instance. - * @example - * - * var wrapped = _([1, 2, 3]); - * - * // returns an unwrapped value - * wrapped.reduce(function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * // returns a wrapped value - * var squares = wrapped.map(function(num) { - * return num * num; - * }); - * - * _.isArray(squares); - * // => false - * - * _.isArray(squares.value()); - * // => true - */ - function lodash(value) { - return (value instanceof lodash) - ? value - : new lodashWrapper(value); - } - - /** - * A fast path for creating `lodash` wrapper objects. - * - * @private - * @param {Mixed} value The value to wrap in a `lodash` instance. - * @param {Boolean} chainAll A flag to enable chaining for all methods - * @returns {Object} Returns a `lodash` instance. - */ - function lodashWrapper(value, chainAll) { - this.__chain__ = !!chainAll; - this.__wrapped__ = value; - } - // ensure `new lodashWrapper` is an instance of `lodash` - lodashWrapper.prototype = lodash.prototype; - - /** - * An object used to flag environments features. - * - * @static - * @memberOf _ - * @type Object - */ - var support = {}; - - (function() { - var object = { '0': 1, 'length': 1 }; - - /** - * Detect if `Function#bind` exists and is inferred to be fast (all but V8). - * - * @memberOf _.support - * @type Boolean - */ - support.fastBind = nativeBind && !isV8; - - /** - * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. - * - * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` - * and `splice()` functions that fail to remove the last element, `value[0]`, - * of array-like objects even though the `length` property is set to `0`. - * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` - * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. - * - * @memberOf _.support - * @type Boolean - */ - support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); - }(1)); - - /** - * By default, the template delimiters used by Lo-Dash are similar to those in - * embedded Ruby (ERB). Change the following template settings to use alternative - * delimiters. - * - * @static - * @memberOf _ - * @type Object - */ - lodash.templateSettings = { - - /** - * Used to detect `data` property values to be HTML-escaped. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'escape': /<%-([\s\S]+?)%>/g, - - /** - * Used to detect code to be evaluated. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'evaluate': /<%([\s\S]+?)%>/g, - - /** - * Used to detect `data` property values to inject. - * - * @memberOf _.templateSettings - * @type RegExp - */ - 'interpolate': reInterpolate, - - /** - * Used to reference the data object in the template text. - * - * @memberOf _.templateSettings - * @type String - */ - 'variable': '' - }; - - /*--------------------------------------------------------------------------*/ - - /** - * The base implementation of `_.createCallback` without support for creating - * "_.pluck" or "_.where" style callbacks. - * - * @private - * @param {Mixed} [func=identity] The value to convert to a callback. - * @param {Mixed} [thisArg] The `this` binding of the created callback. - * @param {Number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - */ - function baseCreateCallback(func, thisArg, argCount) { - if (typeof func != 'function') { - return identity; - } - // exit early if there is no `thisArg` - if (typeof thisArg == 'undefined') { - return func; - } - switch (argCount) { - case 1: return function(value) { - return func.call(thisArg, value); - }; - case 2: return function(a, b) { - return func.call(thisArg, a, b); - }; - case 3: return function(value, index, collection) { - return func.call(thisArg, value, index, collection); - }; - case 4: return function(accumulator, value, index, collection) { - return func.call(thisArg, accumulator, value, index, collection); - }; - } - return bind(func, thisArg); - } - - /** - * The base implementation of `_.flatten` without support for callback - * shorthands or `thisArg` binding. - * - * @private - * @param {Array} array The array to flatten. - * @param {Boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {Boolean} [isArgArrays=false] A flag to restrict flattening to arrays and `arguments` objects. - * @param {Number} [fromIndex=0] The index to start from. - * @returns {Array} Returns a new flattened array. - */ - function baseFlatten(array, isShallow, isArgArrays, fromIndex) { - var index = (fromIndex || 0) - 1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - // recursively flatten arrays (susceptible to call stack limits) - if (value && typeof value == 'object' && (isArray(value) || isArguments(value))) { - push.apply(result, isShallow ? value : baseFlatten(value, isShallow, isArgArrays)); - } else if (!isArgArrays) { - result.push(value); - } - } - return result; - } - - /** - * The base implementation of `_.isEqual`, without support for `thisArg` binding, - * that allows partial "_.where" style comparisons. - * - * @private - * @param {Mixed} a The value to compare. - * @param {Mixed} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. - * @param {Array} [stackA=[]] Tracks traversed `a` objects. - * @param {Array} [stackB=[]] Tracks traversed `b` objects. - * @returns {Boolean} Returns `true` if the values are equivalent, else `false`. - */ - function baseIsEqual(a, b, stackA, stackB) { - if (a === b) { - return a !== 0 || (1 / a == 1 / b); - } - var type = typeof a, - otherType = typeof b; - - if (a === a && - !(a && objectTypes[type]) && - !(b && objectTypes[otherType])) { - return false; - } - if (a == null || b == null) { - return a === b; - } - var className = toString.call(a), - otherClass = toString.call(b); - - if (className != otherClass) { - return false; - } - switch (className) { - case boolClass: - case dateClass: - return +a == +b; - - case numberClass: - return a != +a - ? b != +b - : (a == 0 ? (1 / a == 1 / b) : a == +b); - - case regexpClass: - case stringClass: - return a == String(b); - } - var isArr = className == arrayClass; - if (!isArr) { - if (hasOwnProperty.call(a, '__wrapped__ ') || b instanceof lodash) { - return baseIsEqual(a.__wrapped__ || a, b.__wrapped__ || b, stackA, stackB); - } - if (className != objectClass) { - return false; - } - var ctorA = a.constructor, - ctorB = b.constructor; - - if (ctorA != ctorB && !( - isFunction(ctorA) && ctorA instanceof ctorA && - isFunction(ctorB) && ctorB instanceof ctorB - )) { - return false; - } - } - stackA || (stackA = []); - stackB || (stackB = []); - - var length = stackA.length; - while (length--) { - if (stackA[length] == a) { - return stackB[length] == b; - } - } - var result = true, - size = 0; - - stackA.push(a); - stackB.push(b); - - if (isArr) { - size = b.length; - result = size == a.length; - - if (result) { - while (size--) { - if (!(result = baseIsEqual(a[size], b[size], stackA, stackB))) { - break; - } - } - } - return result; - } - forIn(b, function(value, key, b) { - if (hasOwnProperty.call(b, key)) { - size++; - return !(result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, stackA, stackB)) && indicatorObject; - } - }); - - if (result) { - forIn(a, function(value, key, a) { - if (hasOwnProperty.call(a, key)) { - return !(result = --size > -1) && indicatorObject; - } - }); - } - return result; - } - - /** - * The base implementation of `_.uniq` without support for callback shorthands - * or `thisArg` binding. - * - * @private - * @param {Array} array The array to process. - * @param {Boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function} [callback] The function called per iteration. - * @returns {Array} Returns a duplicate-value-free array. - */ - function baseUniq(array, isSorted, callback) { - var index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - result = [], - seen = callback ? [] : result; - - while (++index < length) { - var value = array[index], - computed = callback ? callback(value, index, array) : value; - - if (isSorted - ? !index || seen[seen.length - 1] !== computed - : indexOf(seen, computed) < 0 - ) { - if (callback) { - seen.push(computed); - } - result.push(value); - } - } - return result; - } - - /** - * Creates a function that aggregates a collection, creating an object composed - * of keys generated from the results of running each element of the collection - * through a callback. The given `setter` function sets the keys and values - * of the composed object. - * - * @private - * @param {Function} setter The setter function. - * @returns {Function} Returns the new aggregator function. - */ - function createAggregator(setter) { - return function(collection, callback, thisArg) { - var result = {}; - callback = createCallback(callback, thisArg, 3); - forEach(collection, function(value, key, collection) { - key = String(callback(value, key, collection)); - setter(result, value, key, collection); - }); - return result; - }; - } - - /** - * Creates a function that, when called, either curries or invokes `func` - * with an optional `this` binding and partially applied arguments. - * - * @private - * @param {Function|String} func The function or method name to reference. - * @param {Number} bitmask The bitmask of method flags to compose. - * The bitmask may be composed of the following flags: - * 1 - `_.bind` - * 2 - `_.bindKey` - * 4 - `_.curry` - * 8 - `_.curry` (bound) - * 16 - `_.partial` - * 32 - `_.partialRight` - * @param {Array} [partialArgs] An array of arguments to prepend to those - * provided to the new function. - * @param {Array} [partialRightArgs] An array of arguments to append to those - * provided to the new function. - * @param {Mixed} [thisArg] The `this` binding of `func`. - * @param {Number} [arity] The arity of `func`. - * @returns {Function} Returns the new bound function. - */ - function createBound(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { - var isBind = bitmask & 1, - isBindKey = bitmask & 2, - isCurry = bitmask & 4, - isCurryBound = bitmask & 8, - isPartial = bitmask & 16, - isPartialRight = bitmask & 32; - - if (!isBindKey && !isFunction(func)) { - throw new TypeError; - } - // use `Function#bind` if it exists and is fast - // (in V8 `Function#bind` is slower except when partially applied) - if (isBind && !(isBindKey || isCurry || isPartialRight) && - (support.fastBind || (nativeBind && partialArgs.length))) { - var args = [func, thisArg]; - push.apply(args, partialArgs); - var bound = nativeBind.call.apply(nativeBind, args); - } - else { - bound = function() { - // `Function#bind` spec - // http://es5.github.io/#x15.3.4.5 - var args = arguments, - thisBinding = isBind ? thisArg : this; - - if (partialArgs) { - unshift.apply(args, partialArgs); - } - if (partialRightArgs) { - push.apply(args, partialRightArgs); - } - if (isCurry && args.length < arity) { - bitmask |= 16 & ~32 - return createBound(func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity); - } - if (isBindKey) { - func = thisBinding[key]; - } - if (this instanceof bound) { - // ensure `new bound` is an instance of `func` - thisBinding = createObject(func.prototype); - - // mimic the constructor's `return` behavior - // http://es5.github.io/#x13.2.2 - var result = func.apply(thisBinding, args); - return isObject(result) ? result : thisBinding; - } - return func.apply(thisBinding, args); - }; - } - if (isBindKey) { - var key = thisArg; - thisArg = func; - } - return bound; - } - - /** - * Creates a new object with the specified `prototype`. - * - * @private - * @param {Object} prototype The prototype object. - * @returns {Object} Returns the new object. - */ - function createObject(prototype) { - return isObject(prototype) ? nativeCreate(prototype) : {}; - } - // fallback for browsers without `Object.create` - if (!nativeCreate) { - createObject = function(prototype) { - if (isObject(prototype)) { - noop.prototype = prototype; - var result = new noop; - noop.prototype = null; - } - return result || {}; - }; - } - - /** - * Used by `escape` to convert characters to HTML entities. - * - * @private - * @param {String} match The matched character to escape. - * @returns {String} Returns the escaped character. - */ - function escapeHtmlChar(match) { - return htmlEscapes[match]; - } - - /** - * Gets the appropriate "indexOf" function. If the `_.indexOf` method is - * customized, this method returns the custom method, otherwise it returns - * the `baseIndexOf` function. - * - * @private - * @returns {Function} Returns the "indexOf" function. - */ - function getIndexOf() { - var result = (result = lodash.indexOf) === indexOf ? baseIndexOf : result; - return result; - } - - /** - * Used by `unescape` to convert HTML entities to characters. - * - * @private - * @param {String} match The matched character to unescape. - * @returns {String} Returns the unescaped character. - */ - function unescapeHtmlChar(match) { - return htmlUnescapes[match]; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Checks if `value` is an `arguments` object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an `arguments` object, else `false`. - * @example - * - * (function() { return _.isArguments(arguments); })(1, 2, 3); - * // => true - * - * _.isArguments([1, 2, 3]); - * // => false - */ - function isArguments(value) { - return (value && typeof value == 'object') ? toString.call(value) == argsClass : false; - } - // fallback for browsers that can't detect `arguments` objects by [[Class]] - if (!isArguments(arguments)) { - isArguments = function(value) { - return (value && typeof value == 'object') ? hasOwnProperty.call(value, 'callee') : false; - }; - } - - /** - * Checks if `value` is an array. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an array, else `false`. - * @example - * - * (function() { return _.isArray(arguments); })(); - * // => false - * - * _.isArray([1, 2, 3]); - * // => true - */ - var isArray = nativeIsArray || function(value) { - return (value && typeof value == 'object') ? toString.call(value) == arrayClass : false; - }; - - /** - * A fallback implementation of `Object.keys` which produces an array of the - * given object's own enumerable property names. - * - * @private - * @type Function - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - */ - var shimKeys = function(object) { - var index, iterable = object, result = []; - if (!iterable) return result; - if (!(objectTypes[typeof object])) return result; - for (index in iterable) { - if (hasOwnProperty.call(iterable, index)) { - result.push(index); - } - } - return result - }; - - /** - * Creates an array composed of the own enumerable property names of an object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names. - * @example - * - * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); - * // => ['one', 'two', 'three'] (order is not guaranteed) - */ - var keys = !nativeKeys ? shimKeys : function(object) { - if (!isObject(object)) { - return []; - } - return nativeKeys(object); - }; - - /** - * Used to convert characters to HTML entities: - * - * Though the `>` character is escaped for symmetry, characters like `>` and `/` - * don't require escaping in HTML and have no special meaning unless they're part - * of a tag or an unquoted attribute value. - * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") - */ - var htmlEscapes = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '/': '/' - }; - - /** Used to convert HTML entities to characters */ - var htmlUnescapes = invert(htmlEscapes); - - /** Used to match HTML entities and HTML characters */ - var reEscapedHtml = RegExp('(' + keys(htmlUnescapes).join('|') + ')', 'g'), - reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); - - /*--------------------------------------------------------------------------*/ - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object. Subsequent sources will overwrite property assignments of previous - * sources. If a callback is provided it will be executed to produce the - * assigned values. The callback is bound to `thisArg` and invoked with two - * arguments; (objectValue, sourceValue). - * - * @static - * @memberOf _ - * @type Function - * @alias extend - * @category Objects - * @param {Object} object The destination object. - * @param {Object} [source1, source2, ...] The source objects. - * @param {Function} [callback] The function to customize assigning values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the destination object. - * @example - * - * _.assign({ 'name': 'moe' }, { 'age': 40 }); - * // => { 'name': 'moe', 'age': 40 } - * - * var defaults = _.partialRight(_.assign, function(a, b) { - * return typeof a == 'undefined' ? b : a; - * }); - * - * var food = { 'name': 'apple' }; - * defaults(food, { 'name': 'banana', 'type': 'fruit' }); - * // => { 'name': 'apple', 'type': 'fruit' } - */ - function assign(object) { - if (!object) { - return object; - } - for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { - var iterable = arguments[argsIndex]; - if (iterable) { - for (var key in iterable) { - object[key] = iterable[key]; - } - } - } - return object; - } - - /** - * Creates a clone of `value`. If `deep` is `true` nested objects will also - * be cloned, otherwise they will be assigned by reference. If a callback - * is provided it will be executed to produce the cloned values. If the - * callback returns `undefined` cloning will be handled by the method instead. - * The callback is bound to `thisArg` and invoked with one argument; (value). - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to clone. - * @param {Boolean} [deep=false] A flag to indicate a deep clone. - * @param {Function} [callback] The function to customize cloning values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the cloned `value`. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * var shallow = _.clone(stooges); - * shallow[0] === stooges[0]; - * // => true - * - * var deep = _.clone(stooges, true); - * deep[0] === stooges[0]; - * // => false - * - * _.mixin({ - * 'clone': _.partialRight(_.clone, function(value) { - * return _.isElement(value) ? value.cloneNode(false) : undefined; - * }) - * }); - * - * var clone = _.clone(document.body); - * clone.childNodes.length; - * // => 0 - */ - function clone(value) { - return isObject(value) - ? (isArray(value) ? nativeSlice.call(value) : assign({}, value)) - : value; - } - - /** - * Assigns own enumerable properties of source object(s) to the destination - * object for all destination properties that resolve to `undefined`. Once a - * property is set, additional defaults of the same property will be ignored. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The destination object. - * @param {Object} [source1, source2, ...] The source objects. - * @param- {Object} [guard] Allows working with `_.reduce` without using - * their `key` and `object` arguments as sources. - * @returns {Object} Returns the destination object. - * @example - * - * var food = { 'name': 'apple' }; - * _.defaults(food, { 'name': 'banana', 'type': 'fruit' }); - * // => { 'name': 'apple', 'type': 'fruit' } - */ - function defaults(object) { - if (!object) { - return object; - } - for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) { - var iterable = arguments[argsIndex]; - if (iterable) { - for (var key in iterable) { - if (typeof object[key] == 'undefined') { - object[key] = iterable[key]; - } - } - } - } - return object; - } - - /** - * Iterates over own and inherited enumerable properties of an object, - * executing the callback for each property. The callback is bound to `thisArg` - * and invoked with three arguments; (value, key, object). Callbacks may exit - * iteration early by explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * function Dog(name) { - * this.name = name; - * } - * - * Dog.prototype.bark = function() { - * console.log('Woof, woof!'); - * }; - * - * _.forIn(new Dog('Dagny'), function(value, key) { - * console.log(key); - * }); - * // => logs 'bark' and 'name' (order is not guaranteed) - */ - var forIn = function(collection, callback) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - for (index in iterable) { - if (callback(iterable[index], index, collection) === indicatorObject) return result; - } - return result - }; - - /** - * Iterates over own enumerable properties of an object, executing the callback - * for each property. The callback is bound to `thisArg` and invoked with three - * arguments; (value, key, object). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @type Function - * @category Objects - * @param {Object} object The object to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns `object`. - * @example - * - * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { - * console.log(key); - * }); - * // => logs '0', '1', and 'length' (order is not guaranteed) - */ - var forOwn = function(collection, callback) { - var index, iterable = collection, result = iterable; - if (!iterable) return result; - if (!objectTypes[typeof iterable]) return result; - for (index in iterable) { - if (hasOwnProperty.call(iterable, index)) { - if (callback(iterable[index], index, collection) === indicatorObject) return result; - } - } - return result - }; - - /** - * Creates a sorted array of property names of all enumerable properties, - * own and inherited, of `object` that have function values. - * - * @static - * @memberOf _ - * @alias methods - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property names that have function values. - * @example - * - * _.functions(_); - * // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] - */ - function functions(object) { - var result = []; - forIn(object, function(value, key) { - if (isFunction(value)) { - result.push(key); - } - }); - return result.sort(); - } - - /** - * Checks if the specified object `property` exists and is a direct property, - * instead of an inherited property. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to check. - * @param {String} property The property to check for. - * @returns {Boolean} Returns `true` if key is a direct property, else `false`. - * @example - * - * _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); - * // => true - */ - function has(object, property) { - return object ? hasOwnProperty.call(object, property) : false; - } - - /** - * Creates an object composed of the inverted keys and values of the given object. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to invert. - * @returns {Object} Returns the created inverted object. - * @example - * - * _.invert({ 'first': 'moe', 'second': 'larry' }); - * // => { 'moe': 'first', 'larry': 'second' } - */ - function invert(object) { - var index = -1, - props = keys(object), - length = props.length, - result = {}; - - while (++index < length) { - var key = props[index]; - result[object[key]] = key; - } - return result; - } - - /** - * Checks if `value` is a boolean value. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a boolean value, else `false`. - * @example - * - * _.isBoolean(null); - * // => false - */ - function isBoolean(value) { - return value === true || value === false || toString.call(value) == boolClass; - } - - /** - * Checks if `value` is a date. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a date, else `false`. - * @example - * - * _.isDate(new Date); - * // => true - */ - function isDate(value) { - return value ? (typeof value == 'object' && toString.call(value) == dateClass) : false; - } - - /** - * Checks if `value` is a DOM element. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a DOM element, else `false`. - * @example - * - * _.isElement(document.body); - * // => true - */ - function isElement(value) { - return value ? value.nodeType === 1 : false; - } - - /** - * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a - * length of `0` and objects with no own enumerable properties are considered - * "empty". - * - * @static - * @memberOf _ - * @category Objects - * @param {Array|Object|String} value The value to inspect. - * @returns {Boolean} Returns `true` if the `value` is empty, else `false`. - * @example - * - * _.isEmpty([1, 2, 3]); - * // => false - * - * _.isEmpty({}); - * // => true - * - * _.isEmpty(''); - * // => true - */ - function isEmpty(value) { - if (!value) { - return true; - } - if (isArray(value) || isString(value)) { - return !value.length; - } - for (var key in value) { - if (hasOwnProperty.call(value, key)) { - return false; - } - } - return true; - } - - /** - * Performs a deep comparison between two values to determine if they are - * equivalent to each other. If a callback is provided it will be executed - * to compare values. If the callback returns `undefined` comparisons will - * be handled by the method instead. The callback is bound to `thisArg` and - * invoked with two arguments; (a, b). - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} a The value to compare. - * @param {Mixed} b The other value to compare. - * @param {Function} [callback] The function to customize comparing values. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if the values are equivalent, else `false`. - * @example - * - * var moe = { 'name': 'moe', 'age': 40 }; - * var copy = { 'name': 'moe', 'age': 40 }; - * - * moe == copy; - * // => false - * - * _.isEqual(moe, copy); - * // => true - * - * var words = ['hello', 'goodbye']; - * var otherWords = ['hi', 'goodbye']; - * - * _.isEqual(words, otherWords, function(a, b) { - * var reGreet = /^(?:hello|hi)$/i, - * aGreet = _.isString(a) && reGreet.test(a), - * bGreet = _.isString(b) && reGreet.test(b); - * - * return (aGreet || bGreet) ? (aGreet == bGreet) : undefined; - * }); - * // => true - */ - function isEqual(a, b) { - return baseIsEqual(a, b); - } - - /** - * Checks if `value` is, or can be coerced to, a finite number. - * - * Note: This is not the same as native `isFinite` which will return true for - * booleans and empty strings. See http://es5.github.io/#x15.1.2.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is finite, else `false`. - * @example - * - * _.isFinite(-101); - * // => true - * - * _.isFinite('10'); - * // => true - * - * _.isFinite(true); - * // => false - * - * _.isFinite(''); - * // => false - * - * _.isFinite(Infinity); - * // => false - */ - function isFinite(value) { - return nativeIsFinite(value) && !nativeIsNaN(parseFloat(value)); - } - - /** - * Checks if `value` is a function. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a function, else `false`. - * @example - * - * _.isFunction(_); - * // => true - */ - function isFunction(value) { - return typeof value == 'function'; - } - // fallback for older versions of Chrome and Safari - if (isFunction(/x/)) { - isFunction = function(value) { - return typeof value == 'function' && toString.call(value) == funcClass; - }; - } - - /** - * Checks if `value` is the language type of Object. - * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is an object, else `false`. - * @example - * - * _.isObject({}); - * // => true - * - * _.isObject([1, 2, 3]); - * // => true - * - * _.isObject(1); - * // => false - */ - function isObject(value) { - // check if the value is the ECMAScript language type of Object - // http://es5.github.io/#x8 - // and avoid a V8 bug - // http://code.google.com/p/v8/issues/detail?id=2291 - return !!(value && objectTypes[typeof value]); - } - - /** - * Checks if `value` is `NaN`. - * - * Note: This is not the same as native `isNaN` which will return `true` for - * `undefined` and other non-numeric values. See http://es5.github.io/#x15.1.2.4. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `NaN`, else `false`. - * @example - * - * _.isNaN(NaN); - * // => true - * - * _.isNaN(new Number(NaN)); - * // => true - * - * isNaN(undefined); - * // => true - * - * _.isNaN(undefined); - * // => false - */ - function isNaN(value) { - // `NaN` as a primitive is the only value that is not equal to itself - // (perform the [[Class]] check first to avoid errors with some host objects in IE) - return isNumber(value) && value != +value; - } - - /** - * Checks if `value` is `null`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `null`, else `false`. - * @example - * - * _.isNull(null); - * // => true - * - * _.isNull(undefined); - * // => false - */ - function isNull(value) { - return value === null; - } - - /** - * Checks if `value` is a number. - * - * Note: `NaN` is considered a number. See http://es5.github.io/#x8.5. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a number, else `false`. - * @example - * - * _.isNumber(8.4 * 5); - * // => true - */ - function isNumber(value) { - return typeof value == 'number' || toString.call(value) == numberClass; - } - - /** - * Checks if `value` is a regular expression. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a regular expression, else `false`. - * @example - * - * _.isRegExp(/moe/); - * // => true - */ - function isRegExp(value) { - return (value && objectTypes[typeof value]) ? toString.call(value) == regexpClass : false; - } - - /** - * Checks if `value` is a string. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is a string, else `false`. - * @example - * - * _.isString('moe'); - * // => true - */ - function isString(value) { - return typeof value == 'string' || toString.call(value) == stringClass; - } - - /** - * Checks if `value` is `undefined`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Mixed} value The value to check. - * @returns {Boolean} Returns `true` if the `value` is `undefined`, else `false`. - * @example - * - * _.isUndefined(void 0); - * // => true - */ - function isUndefined(value) { - return typeof value == 'undefined'; - } - - /** - * Creates a shallow clone of `object` excluding the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` omitting the properties the callback returns truthy - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Function|String} callback|[prop1, prop2, ...] The properties to omit - * or the function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object without the omitted properties. - * @example - * - * _.omit({ 'name': 'moe', 'age': 40 }, 'age'); - * // => { 'name': 'moe' } - * - * _.omit({ 'name': 'moe', 'age': 40 }, function(value) { - * return typeof value == 'number'; - * }); - * // => { 'name': 'moe' } - */ - function omit(object) { - var indexOf = getIndexOf(), - props = baseFlatten(arguments, true, false, 1), - result = {}; - - forIn(object, function(value, key) { - if (indexOf(props, key) < 0) { - result[key] = value; - } - }); - return result; - } - - /** - * Creates a two dimensional array of an object's key-value pairs, - * i.e. `[[key1, value1], [key2, value2]]`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns new array of key-value pairs. - * @example - * - * _.pairs({ 'moe': 30, 'larry': 40 }); - * // => [['moe', 30], ['larry', 40]] (order is not guaranteed) - */ - function pairs(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - var key = props[index]; - result[index] = [key, object[key]]; - } - return result; - } - - /** - * Creates a shallow clone of `object` composed of the specified properties. - * Property names may be specified as individual arguments or as arrays of - * property names. If a callback is provided it will be executed for each - * property of `object` picking the properties the callback returns truthy - * for. The callback is bound to `thisArg` and invoked with three arguments; - * (value, key, object). - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The source object. - * @param {Array|Function|String} callback|[prop1, prop2, ...] The function - * called per iteration or property names to pick, specified as individual - * property names or arrays of property names. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns an object composed of the picked properties. - * @example - * - * _.pick({ 'name': 'moe', '_userid': 'moe1' }, 'name'); - * // => { 'name': 'moe' } - * - * _.pick({ 'name': 'moe', '_userid': 'moe1' }, function(value, key) { - * return key.charAt(0) != '_'; - * }); - * // => { 'name': 'moe' } - */ - function pick(object) { - var index = -1, - props = baseFlatten(arguments, true, false, 1), - length = props.length, - result = {}; - - while (++index < length) { - var prop = props[index]; - if (prop in object) { - result[prop] = object[prop]; - } - } - return result; - } - - /** - * Creates an array composed of the own enumerable property values of `object`. - * - * @static - * @memberOf _ - * @category Objects - * @param {Object} object The object to inspect. - * @returns {Array} Returns an array of property values. - * @example - * - * _.values({ 'one': 1, 'two': 2, 'three': 3 }); - * // => [1, 2, 3] (order is not guaranteed) - */ - function values(object) { - var index = -1, - props = keys(object), - length = props.length, - result = Array(length); - - while (++index < length) { - result[index] = object[props[index]]; - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Checks if a given value is present in a collection using strict equality - * for comparisons, i.e. `===`. If `fromIndex` is negative, it is used as the - * offset from the end of the collection. - * - * @static - * @memberOf _ - * @alias include - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Mixed} target The value to check for. - * @param {Number} [fromIndex=0] The index to search from. - * @returns {Boolean} Returns `true` if the `target` element is found, else `false`. - * @example - * - * _.contains([1, 2, 3], 1); - * // => true - * - * _.contains([1, 2, 3], 1, 2); - * // => false - * - * _.contains({ 'name': 'moe', 'age': 40 }, 'moe'); - * // => true - * - * _.contains('curly', 'ur'); - * // => true - */ - function contains(collection, target) { - var indexOf = getIndexOf(), - length = collection ? collection.length : 0, - result = false; - if (length && typeof length == 'number') { - result = indexOf(collection, target) > -1; - } else { - forOwn(collection, function(value) { - return (result = value === target) && indicatorObject; - }); - } - return result; - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of `collection` through the callback. The corresponding value - * of each key is the number of times the key was returned by the callback. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': 1, '6': 2 } - * - * _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': 1, '6': 2 } - * - * _.countBy(['one', 'two', 'three'], 'length'); - * // => { '3': 2, '5': 1 } - */ - var countBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); - }); - - /** - * Checks if the given callback returns truthy value for **all** elements of - * a collection. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias all - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if all elements passed the callback check, - * else `false`. - * @example - * - * _.every([true, 1, null, 'yes'], Boolean); - * // => false - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.every(stooges, 'age'); - * // => true - * - * // using "_.where" callback shorthand - * _.every(stooges, { 'age': 50 }); - * // => false - */ - function every(collection, callback, thisArg) { - var result = true; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - if (!(result = !!callback(collection[index], index, collection))) { - break; - } - } - } else { - forOwn(collection, function(value, index, collection) { - return !(result = !!callback(value, index, collection)) && indicatorObject; - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning an array of all elements - * the callback returns truthy for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias select - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that passed the callback check. - * @example - * - * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [2, 4, 6] - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.filter(food, 'organic'); - * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] - * - * // using "_.where" callback shorthand - * _.filter(food, { 'type': 'fruit' }); - * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] - */ - function filter(collection, callback, thisArg) { - var result = []; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - result.push(value); - } - } - } else { - forOwn(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result.push(value); - } - }); - } - return result; - } - - /** - * Iterates over elements of a collection, returning the first element that - * the callback returns truthy for. The callback is bound to `thisArg` and - * invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias detect, findWhere - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the found element, else `undefined`. - * @example - * - * _.find([1, 2, 3, 4], function(num) { - * return num % 2 == 0; - * }); - * // => 2 - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'banana', 'organic': true, 'type': 'fruit' }, - * { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.find(food, { 'type': 'vegetable' }); - * // => { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * - * // using "_.pluck" callback shorthand - * _.find(food, 'organic'); - * // => { 'name': 'banana', 'organic': true, 'type': 'fruit' } - */ - function find(collection, callback, thisArg) { - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (callback(value, index, collection)) { - return value; - } - } - } else { - var result; - forOwn(collection, function(value, index, collection) { - if (callback(value, index, collection)) { - result = value; - return indicatorObject; - } - }); - return result; - } - } - - /** - * Examines each element in a `collection`, returning the first that - * has the given `properties`. When checking `properties`, this method - * performs a deep comparison between values to determine if they are - * equivalent to each other. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Object} properties The object of property values to filter by. - * @returns {Mixed} Returns the found element, else `undefined`. - * @example - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'banana', 'organic': true, 'type': 'fruit' }, - * { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - * ]; - * - * _.findWhere(food, { 'type': 'vegetable' }); - * // => { 'name': 'beet', 'organic': false, 'type': 'vegetable' } - */ - function findWhere(object, properties) { - return where(object, properties, true); - } - - /** - * Iterates over elements of a collection, executing the callback for each - * element. The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). Callbacks may exit iteration early by - * explicitly returning `false`. - * - * @static - * @memberOf _ - * @alias each - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|String} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); - * // => logs each number and returns '1,2,3' - * - * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); - * // => logs each number value and returns the object (order is not guaranteed) - */ - function forEach(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0; - - callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); - if (typeof length == 'number') { - while (++index < length) { - if (callback(collection[index], index, collection) === indicatorObject) { - break; - } - } - } else { - forOwn(collection, callback); - } - } - - /** - * This method is like `_.forEach` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias eachRight - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array|Object|String} Returns `collection`. - * @example - * - * _([1, 2, 3]).forEachRight(function(num) { console.log(num); }).join(','); - * // => logs each number from right to left and returns '3,2,1' - */ - function forEachRight(collection, callback) { - var iterable = collection, - length = collection ? collection.length : 0; - - if (typeof length != 'number') { - var props = keys(collection); - length = props.length; - } - forEach(collection, function(value, index, collection) { - index = props ? props[--length] : --length; - return callback(iterable[index], index, collection) === false && indicatorObject; - }); - } - - /** - * Creates an object composed of keys generated from the results of running - * each element of a collection through the callback. The corresponding value - * of each key is an array of the elements responsible for generating the key. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false` - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Object} Returns the composed aggregate object. - * @example - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); - * // => { '4': [4.2], '6': [6.1, 6.4] } - * - * // using "_.pluck" callback shorthand - * _.groupBy(['one', 'two', 'three'], 'length'); - * // => { '3': ['one', 'two'], '5': ['three'] } - */ - var groupBy = createAggregator(function(result, value, key) { - (hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); - }); - - /** - * Invokes the method named by `methodName` on each element in the `collection` - * returning an array of the results of each invoked method. Additional arguments - * will be provided to each invoked method. If `methodName` is a function it - * will be invoked for, and `this` bound to, each element in the `collection`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|String} methodName The name of the method to invoke or - * the function invoked per iteration. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the method with. - * @returns {Array} Returns a new array of the results of each invoked method. - * @example - * - * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); - * // => [[1, 5, 7], [1, 2, 3]] - * - * _.invoke([123, 456], String.prototype.split, ''); - * // => [['1', '2', '3'], ['4', '5', '6']] - */ - function invoke(collection, methodName) { - var args = nativeSlice.call(arguments, 2), - index = -1, - isFunc = typeof methodName == 'function', - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - result[++index] = (isFunc ? methodName : value[methodName]).apply(value, args); - }); - return result; - } - - /** - * Creates an array of values by running each element in the collection - * through the callback. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias collect - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of the results of each `callback` execution. - * @example - * - * _.map([1, 2, 3], function(num) { return num * 3; }); - * // => [3, 6, 9] - * - * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); - * // => [3, 6, 9] (order is not guaranteed) - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // using "_.pluck" callback shorthand - * _.map(stooges, 'name'); - * // => ['moe', 'larry'] - */ - function map(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0; - - callback = createCallback(callback, thisArg, 3); - if (typeof length == 'number') { - var result = Array(length); - while (++index < length) { - result[index] = callback(collection[index], index, collection); - } - } else { - result = []; - forOwn(collection, function(value, key, collection) { - result[++index] = callback(value, key, collection); - }); - } - return result; - } - - /** - * Retrieves the maximum value of an array. If a callback is provided it - * will be executed for each value in the array to generate the criterion by - * which the value is ranked. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the maximum value. - * @example - * - * _.max([4, 2, 8, 6]); - * // => 8 - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.max(stooges, function(stooge) { return stooge.age; }); - * // => { 'name': 'larry', 'age': 50 }; - * - * // using "_.pluck" callback shorthand - * _.max(stooges, 'age'); - * // => { 'name': 'larry', 'age': 50 }; - */ - function max(collection, callback, thisArg) { - var computed = -Infinity, - result = computed; - - var index = -1, - length = collection ? collection.length : 0; - - if (!callback && typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (value > result) { - result = value; - } - } - } else { - callback = createCallback(callback, thisArg, 3); - - forEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current > computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the minimum value of an array. If a callback is provided it - * will be executed for each value in the array to generate the criterion by - * which the value is ranked. The callback is bound to `thisArg` and invoked - * with three arguments; (value, index, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the minimum value. - * @example - * - * _.min([4, 2, 8, 6]); - * // => 2 - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.min(stooges, function(stooge) { return stooge.age; }); - * // => { 'name': 'moe', 'age': 40 }; - * - * // using "_.pluck" callback shorthand - * _.min(stooges, 'age'); - * // => { 'name': 'moe', 'age': 40 }; - */ - function min(collection, callback, thisArg) { - var computed = Infinity, - result = computed; - - var index = -1, - length = collection ? collection.length : 0; - - if (!callback && typeof length == 'number') { - while (++index < length) { - var value = collection[index]; - if (value < result) { - result = value; - } - } - } else { - callback = createCallback(callback, thisArg, 3); - - forEach(collection, function(value, index, collection) { - var current = callback(value, index, collection); - if (current < computed) { - computed = current; - result = value; - } - }); - } - return result; - } - - /** - * Retrieves the value of a specified property from all elements in the `collection`. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {String} property The property to pluck. - * @returns {Array} Returns a new array of property values. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * _.pluck(stooges, 'name'); - * // => ['moe', 'larry'] - */ - function pluck(collection, property) { - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - var result = Array(length); - while (++index < length) { - result[index] = collection[index][property]; - } - } - return result || map(collection, property); - } - - /** - * Reduces a collection to a value which is the accumulated result of running - * each element in the collection through the callback, where each successive - * callback execution consumes the return value of the previous execution. If - * `accumulator` is not provided the first element of the collection will be - * used as the initial `accumulator` value. The callback is bound to `thisArg` - * and invoked with four arguments; (accumulator, value, index|key, collection). - * - * @static - * @memberOf _ - * @alias foldl, inject - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [accumulator] Initial value of the accumulator. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the accumulated value. - * @example - * - * var sum = _.reduce([1, 2, 3], function(sum, num) { - * return sum + num; - * }); - * // => 6 - * - * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { - * result[key] = num * 3; - * return result; - * }, {}); - * // => { 'a': 3, 'b': 6, 'c': 9 } - */ - function reduce(collection, callback, accumulator, thisArg) { - if (!collection) return accumulator; - var noaccum = arguments.length < 3; - callback = baseCreateCallback(callback, thisArg, 4); - - var index = -1, - length = collection.length; - - if (typeof length == 'number') { - if (noaccum) { - accumulator = collection[++index]; - } - while (++index < length) { - accumulator = callback(accumulator, collection[index], index, collection); - } - } else { - forOwn(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection) - }); - } - return accumulator; - } - - /** - * This method is like `_.reduce` except that it iterates over elements - * of a `collection` from right to left. - * - * @static - * @memberOf _ - * @alias foldr - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function} [callback=identity] The function called per iteration. - * @param {Mixed} [accumulator] Initial value of the accumulator. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the accumulated value. - * @example - * - * var list = [[0, 1], [2, 3], [4, 5]]; - * var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); - * // => [4, 5, 2, 3, 0, 1] - */ - function reduceRight(collection, callback, accumulator, thisArg) { - var noaccum = arguments.length < 3; - callback = baseCreateCallback(callback, thisArg, 4); - forEachRight(collection, function(value, index, collection) { - accumulator = noaccum - ? (noaccum = false, value) - : callback(accumulator, value, index, collection); - }); - return accumulator; - } - - /** - * The opposite of `_.filter` this method returns the elements of a - * collection that the callback does **not** return truthy for. - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of elements that failed the callback check. - * @example - * - * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); - * // => [1, 3, 5] - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.reject(food, 'organic'); - * // => [{ 'name': 'apple', 'organic': false, 'type': 'fruit' }] - * - * // using "_.where" callback shorthand - * _.reject(food, { 'type': 'fruit' }); - * // => [{ 'name': 'carrot', 'organic': true, 'type': 'vegetable' }] - */ - function reject(collection, callback, thisArg) { - callback = createCallback(callback, thisArg, 3); - return filter(collection, function(value, index, collection) { - return !callback(value, index, collection); - }); - } - - /** - * Creates an array of shuffled values, using a version of the Fisher-Yates - * shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to shuffle. - * @returns {Array} Returns a new shuffled collection. - * @example - * - * _.shuffle([1, 2, 3, 4, 5, 6]); - * // => [4, 1, 6, 3, 5, 2] - */ - function shuffle(collection) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - forEach(collection, function(value) { - var rand = floor(nativeRandom() * (++index + 1)); - result[index] = result[rand]; - result[rand] = value; - }); - return result; - } - - /** - * Gets the size of the `collection` by returning `collection.length` for arrays - * and array-like objects or the number of own enumerable properties for objects. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to inspect. - * @returns {Number} Returns `collection.length` or number of own enumerable properties. - * @example - * - * _.size([1, 2]); - * // => 2 - * - * _.size({ 'one': 1, 'two': 2, 'three': 3 }); - * // => 3 - * - * _.size('curly'); - * // => 5 - */ - function size(collection) { - var length = collection ? collection.length : 0; - return typeof length == 'number' ? length : keys(collection).length; - } - - /** - * Checks if the callback returns a truthy value for **any** element of a - * collection. The function returns as soon as it finds a passing value and - * does not iterate over the entire collection. The callback is bound to - * `thisArg` and invoked with three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias any - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Boolean} Returns `true` if any element passed the callback check, - * else `false`. - * @example - * - * _.some([null, 0, 'yes', false], Boolean); - * // => true - * - * var food = [ - * { 'name': 'apple', 'organic': false, 'type': 'fruit' }, - * { 'name': 'carrot', 'organic': true, 'type': 'vegetable' } - * ]; - * - * // using "_.pluck" callback shorthand - * _.some(food, 'organic'); - * // => true - * - * // using "_.where" callback shorthand - * _.some(food, { 'type': 'meat' }); - * // => false - */ - function some(collection, callback, thisArg) { - var result; - callback = createCallback(callback, thisArg, 3); - - var index = -1, - length = collection ? collection.length : 0; - - if (typeof length == 'number') { - while (++index < length) { - if ((result = callback(collection[index], index, collection))) { - break; - } - } - } else { - forOwn(collection, function(value, index, collection) { - return (result = callback(value, index, collection)) && indicatorObject; - }); - } - return !!result; - } - - /** - * Creates an array of elements, sorted in ascending order by the results of - * running each element in a collection through the callback. This method - * performs a stable sort, that is, it will preserve the original sort order - * of equal elements. The callback is bound to `thisArg` and invoked with - * three arguments; (value, index|key, collection). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new array of sorted elements. - * @example - * - * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); - * // => [3, 1, 2] - * - * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); - * // => [3, 1, 2] - * - * // using "_.pluck" callback shorthand - * _.sortBy(['banana', 'strawberry', 'apple'], 'length'); - * // => ['apple', 'banana', 'strawberry'] - */ - function sortBy(collection, callback, thisArg) { - var index = -1, - length = collection ? collection.length : 0, - result = Array(typeof length == 'number' ? length : 0); - - callback = createCallback(callback, thisArg, 3); - forEach(collection, function(value, key, collection) { - result[++index] = { - 'criteria': callback(value, key, collection), - 'index': index, - 'value': value - }; - }); - - length = result.length; - result.sort(compareAscending); - while (length--) { - result[length] = result[length].value; - } - return result; - } - - /** - * Converts the `collection` to an array. - * - * @static - * @memberOf _ - * @category Collections - * @param {Array|Object|String} collection The collection to convert. - * @returns {Array} Returns the new converted array. - * @example - * - * (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); - * // => [2, 3, 4] - */ - function toArray(collection) { - if (isArray(collection)) { - return nativeSlice.call(collection); - } - if (collection && typeof collection.length == 'number') { - return map(collection); - } - return values(collection); - } - - /** - * Performs a deep comparison of each element in a `collection` to the given - * `properties` object, returning an array of all elements that have equivalent - * property values. - * - * @static - * @memberOf _ - * @type Function - * @category Collections - * @param {Array|Object|String} collection The collection to iterate over. - * @param {Object} properties The object of property values to filter by. - * @returns {Array} Returns a new array of elements that have the given `properties`. - * @example - * - * var stooges = [ - * { 'name': 'curly', 'age': 30, 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }, - * { 'name': 'moe', 'age': '40', 'quotes': ['Spread out!', 'You knucklehead!'] } - * ]; - * - * _.where(stooges, { 'age': 40 }); - * // => [{ 'name': 'moe', 'age': '40', 'quotes': ['Spread out!', 'You knucklehead!'] }] - * - * _.where(stooges, { 'quotes': ['Poifect!'] }); - * // => [{ 'name': 'curly', 'age': 30, 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }] - */ - function where(collection, properties, first) { - return (first && isEmpty(properties)) - ? undefined - : (first ? find : filter)(collection, properties); - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates an array with all falsey values removed. The values `false`, `null`, - * `0`, `""`, `undefined`, and `NaN` are all falsey. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to compact. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.compact([0, 1, false, 2, '', 3]); - * // => [1, 2, 3] - */ - function compact(array) { - var index = -1, - length = array ? array.length : 0, - result = []; - - while (++index < length) { - var value = array[index]; - if (value) { - result.push(value); - } - } - return result; - } - - /** - * Creates an array excluding all values of the provided arrays using strict - * equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to process. - * @param {Array} [array1, array2, ...] The arrays of values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.difference([1, 2, 3, 4, 5], [5, 2, 10]); - * // => [1, 3, 4] - */ - function difference(array) { - var index = -1, - indexOf = getIndexOf(), - length = array.length, - flattened = baseFlatten(arguments, true, true, 1), - result = []; - - while (++index < length) { - var value = array[index]; - if (indexOf(flattened, value) < 0) { - result.push(value); - } - } - return result; - } - - /** - * Gets the first element of an array. If a number `n` is provided the first - * `n` elements of the array are returned. If a callback is provided elements - * at the beginning of the array are returned as long as the callback returns - * truthy. The callback is bound to `thisArg` and invoked with three arguments; - * (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias head, take - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the first element(s) of `array`. - * @example - * - * _.first([1, 2, 3]); - * // => 1 - * - * _.first([1, 2, 3], 2); - * // => [1, 2] - * - * _.first([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [1, 2] - * - * var food = [ - * { 'name': 'banana', 'organic': true }, - * { 'name': 'beet', 'organic': false }, - * ]; - * - * // using "_.pluck" callback shorthand - * _.first(food, 'organic'); - * // => [{ 'name': 'banana', 'organic': true }] - * - * var food = [ - * { 'name': 'apple', 'type': 'fruit' }, - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.first(food, { 'type': 'fruit' }); - * // => [{ 'name': 'apple', 'type': 'fruit' }, { 'name': 'banana', 'type': 'fruit' }] - */ - function first(array, callback, thisArg) { - if (array) { - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = -1; - callback = createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array[0]; - } - } - return nativeSlice.call(array, 0, nativeMin(nativeMax(0, n), length)); - } - } - - /** - * Flattens a nested array (the nesting can be to any depth). If `isShallow` - * is truthy, the array will only be flattened a single level. If a callback - * is provided each element of the array is passed through the callback before - * flattening. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to flatten. - * @param {Boolean} [isShallow=false] A flag to restrict flattening to a single level. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a new flattened array. - * @example - * - * _.flatten([1, [2], [3, [[4]]]]); - * // => [1, 2, 3, 4]; - * - * _.flatten([1, [2], [3, [[4]]]], true); - * // => [1, 2, 3, [[4]]]; - * - * var stooges = [ - * { 'name': 'curly', 'quotes': ['Oh, a wise guy, eh?', 'Poifect!'] }, - * { 'name': 'moe', 'quotes': ['Spread out!', 'You knucklehead!'] } - * ]; - * - * // using "_.pluck" callback shorthand - * _.flatten(stooges, 'quotes'); - * // => ['Oh, a wise guy, eh?', 'Poifect!', 'Spread out!', 'You knucklehead!'] - */ - function flatten(array, isShallow) { - return baseFlatten(array, isShallow); - } - - /** - * Gets the index at which the first occurrence of `value` is found using - * strict equality for comparisons, i.e. `===`. If the array is already sorted - * providing `true` for `fromIndex` will run a faster binary search. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Boolean|Number} [fromIndex=0] The index to search from or `true` - * to perform a binary search on a sorted array. - * @returns {Number} Returns the index of the matched value or `-1`. - * @example - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2); - * // => 1 - * - * _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 4 - * - * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); - * // => 2 - */ - function indexOf(array, value, fromIndex) { - if (typeof fromIndex == 'number') { - var length = array ? array.length : 0; - fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0); - } else if (fromIndex) { - var index = sortedIndex(array, value); - return array[index] === value ? index : -1; - } - return array ? baseIndexOf(array, value, fromIndex) : -1; - } - - /** - * Gets all but the last element of an array. If a number `n` is provided - * the last `n` elements are excluded from the result. If a callback is - * provided elements at the end of the array are excluded from the result - * as long as the callback returns truthy. The callback is bound to `thisArg` - * and invoked with three arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.initial([1, 2, 3]); - * // => [1, 2] - * - * _.initial([1, 2, 3], 2); - * // => [1] - * - * _.initial([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [1] - * - * var food = [ - * { 'name': 'beet', 'organic': false }, - * { 'name': 'carrot', 'organic': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.initial(food, 'organic'); - * // => [{ 'name': 'beet', 'organic': false }] - * - * var food = [ - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' }, - * { 'name': 'carrot', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.initial(food, { 'type': 'vegetable' }); - * // => [{ 'name': 'banana', 'type': 'fruit' }] - */ - function initial(array, callback, thisArg) { - if (!array) { - return []; - } - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : callback || n; - } - return nativeSlice.call(array, 0, nativeMin(nativeMax(0, length - n), length)); - } - - /** - * Creates an array of unique values present in all provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} [array1, array2, ...] The arrays to inspect. - * @returns {Array} Returns an array of composite values. - * @example - * - * _.intersection([1, 2, 3], [101, 2, 1, 10], [2, 1]); - * // => [1, 2] - */ - function intersection(array) { - var args = arguments, - argsLength = args.length, - index = -1, - indexOf = getIndexOf(), - length = array ? array.length : 0, - result = []; - - outer: - while (++index < length) { - var value = array[index]; - if (indexOf(result, value) < 0) { - var argsIndex = argsLength; - while (--argsIndex) { - if (indexOf(args[argsIndex], value) < 0) { - continue outer; - } - } - result.push(value); - } - } - return result; - } - - /** - * Gets the last element of an array. If a number `n` is provided the last - * `n` elements of the array are returned. If a callback is provided elements - * at the end of the array are returned as long as the callback returns truthy. - * The callback is bound to `thisArg` and invoked with three arguments; - * (value, index, array). - * - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n] The function called - * per element or the number of elements to return. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Mixed} Returns the last element(s) of `array`. - * @example - * - * _.last([1, 2, 3]); - * // => 3 - * - * _.last([1, 2, 3], 2); - * // => [2, 3] - * - * _.last([1, 2, 3], function(num) { - * return num > 1; - * }); - * // => [2, 3] - * - * var food = [ - * { 'name': 'beet', 'organic': false }, - * { 'name': 'carrot', 'organic': true } - * ]; - * - * // using "_.pluck" callback shorthand - * _.last(food, 'organic'); - * // => [{ 'name': 'carrot', 'organic': true }] - * - * var food = [ - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' }, - * { 'name': 'carrot', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.last(food, { 'type': 'vegetable' }); - * // => [{ 'name': 'beet', 'type': 'vegetable' }, { 'name': 'carrot', 'type': 'vegetable' }] - */ - function last(array, callback, thisArg) { - if (array) { - var n = 0, - length = array.length; - - if (typeof callback != 'number' && callback != null) { - var index = length; - callback = createCallback(callback, thisArg, 3); - while (index-- && callback(array[index], index, array)) { - n++; - } - } else { - n = callback; - if (n == null || thisArg) { - return array[length - 1]; - } - } - return nativeSlice.call(array, nativeMax(0, length - n)); - } - } - - /** - * Gets the index at which the last occurrence of `value` is found using strict - * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used - * as the offset from the end of the collection. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to search. - * @param {Mixed} value The value to search for. - * @param {Number} [fromIndex=array.length-1] The index to search from. - * @returns {Number} Returns the index of the matched value or `-1`. - * @example - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); - * // => 4 - * - * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); - * // => 1 - */ - function lastIndexOf(array, value, fromIndex) { - var index = array ? array.length : 0; - if (typeof fromIndex == 'number') { - index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; - } - while (index--) { - if (array[index] === value) { - return index; - } - } - return -1; - } - - /** - * Creates an array of numbers (positive and/or negative) progressing from - * `start` up to but not including `end`. If `start` is less than `stop` a - * zero-length range is created unless a negative `step` is specified. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Number} [start=0] The start of the range. - * @param {Number} end The end of the range. - * @param {Number} [step=1] The value to increment or decrement by. - * @returns {Array} Returns a new range array. - * @example - * - * _.range(10); - * // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - * - * _.range(1, 11); - * // => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - * - * _.range(0, 30, 5); - * // => [0, 5, 10, 15, 20, 25] - * - * _.range(0, -10, -1); - * // => [0, -1, -2, -3, -4, -5, -6, -7, -8, -9] - * - * _.range(1, 4, 0); - * // => [1, 1, 1] - * - * _.range(0); - * // => [] - */ - function range(start, end, step) { - start = +start || 0; - step = (+step || 1); - - if (end == null) { - end = start; - start = 0; - } - // use `Array(length)` so engines, like Chakra and V8, avoid slower modes - // http://youtu.be/XAqIpGU8ZZk#t=17m25s - var index = -1, - length = nativeMax(0, ceil((end - start) / step)), - result = Array(length); - - while (++index < length) { - result[index] = start; - start += step; - } - return result; - } - - /** - * The opposite of `_.initial` this method gets all but the first value of - * an array. If a number `n` is provided the first `n` values are excluded - * from the result. If a callback function is provided elements at the beginning - * of the array are excluded from the result as long as the callback returns - * truthy. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias drop, tail - * @category Arrays - * @param {Array} array The array to query. - * @param {Function|Object|Number|String} [callback|n=1] The function called - * per element or the number of elements to exclude. If a property name or - * object is provided it will be used to create a "_.pluck" or "_.where" - * style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a slice of `array`. - * @example - * - * _.rest([1, 2, 3]); - * // => [2, 3] - * - * _.rest([1, 2, 3], 2); - * // => [3] - * - * _.rest([1, 2, 3], function(num) { - * return num < 3; - * }); - * // => [3] - * - * var food = [ - * { 'name': 'banana', 'organic': true }, - * { 'name': 'beet', 'organic': false }, - * ]; - * - * // using "_.pluck" callback shorthand - * _.rest(food, 'organic'); - * // => [{ 'name': 'beet', 'organic': false }] - * - * var food = [ - * { 'name': 'apple', 'type': 'fruit' }, - * { 'name': 'banana', 'type': 'fruit' }, - * { 'name': 'beet', 'type': 'vegetable' } - * ]; - * - * // using "_.where" callback shorthand - * _.rest(food, { 'type': 'fruit' }); - * // => [{ 'name': 'beet', 'type': 'vegetable' }] - */ - function rest(array, callback, thisArg) { - if (typeof callback != 'number' && callback != null) { - var n = 0, - index = -1, - length = array ? array.length : 0; - - callback = createCallback(callback, thisArg, 3); - while (++index < length && callback(array[index], index, array)) { - n++; - } - } else { - n = (callback == null || thisArg) ? 1 : nativeMax(0, callback); - } - return nativeSlice.call(array, n); - } - - /** - * Uses a binary search to determine the smallest index at which a value - * should be inserted into a given sorted array in order to maintain the sort - * order of the array. If a callback is provided it will be executed for - * `value` and each element of `array` to compute their sort ranking. The - * callback is bound to `thisArg` and invoked with one argument; (value). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to inspect. - * @param {Mixed} value The value to evaluate. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Number} Returns the index at which `value` should be inserted - * into `array`. - * @example - * - * _.sortedIndex([20, 30, 50], 40); - * // => 2 - * - * // using "_.pluck" callback shorthand - * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); - * // => 2 - * - * var dict = { - * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } - * }; - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return dict.wordToNumber[word]; - * }); - * // => 2 - * - * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { - * return this.wordToNumber[word]; - * }, dict); - * // => 2 - */ - function sortedIndex(array, value, callback, thisArg) { - var low = 0, - high = array ? array.length : low; - - // explicitly reference `identity` for better inlining in Firefox - callback = callback ? createCallback(callback, thisArg, 1) : identity; - value = callback(value); - - while (low < high) { - var mid = (low + high) >>> 1; - (callback(array[mid]) < value) - ? low = mid + 1 - : high = mid; - } - return low; - } - - /** - * Creates an array of unique values, in order, of the provided arrays using - * strict equality for comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} [array1, array2, ...] The arrays to inspect. - * @returns {Array} Returns an array of composite values. - * @example - * - * _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); - * // => [1, 2, 3, 101, 10] - */ - function union(array) { - return baseUniq(baseFlatten(arguments, true, true)); - } - - /** - * Creates a duplicate-value-free version of an array using strict equality - * for comparisons, i.e. `===`. If the array is sorted, providing - * `true` for `isSorted` will use a faster algorithm. If a callback is provided - * each element of `array` is passed through the callback before uniqueness - * is computed. The callback is bound to `thisArg` and invoked with three - * arguments; (value, index, array). - * - * If a property name is provided for `callback` the created "_.pluck" style - * callback will return the property value of the given element. - * - * If an object is provided for `callback` the created "_.where" style callback - * will return `true` for elements that have the properties of the given object, - * else `false`. - * - * @static - * @memberOf _ - * @alias unique - * @category Arrays - * @param {Array} array The array to process. - * @param {Boolean} [isSorted=false] A flag to indicate that `array` is sorted. - * @param {Function|Object|String} [callback=identity] The function called - * per iteration. If a property name or object is provided it will be used - * to create a "_.pluck" or "_.where" style callback, respectively. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns a duplicate-value-free array. - * @example - * - * _.uniq([1, 2, 1, 3, 1]); - * // => [1, 2, 3] - * - * _.uniq([1, 1, 2, 2, 3], true); - * // => [1, 2, 3] - * - * _.uniq(['A', 'b', 'C', 'a', 'B', 'c'], function(letter) { return letter.toLowerCase(); }); - * // => ['A', 'b', 'C'] - * - * _.uniq([1, 2.5, 3, 1.5, 2, 3.5], function(num) { return this.floor(num); }, Math); - * // => [1, 2.5, 3] - * - * // using "_.pluck" callback shorthand - * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); - * // => [{ 'x': 1 }, { 'x': 2 }] - */ - function uniq(array, isSorted, callback, thisArg) { - // juggle arguments - if (typeof isSorted != 'boolean' && isSorted != null) { - thisArg = callback; - callback = !(thisArg && thisArg[isSorted] === array) ? isSorted : undefined; - isSorted = false; - } - if (callback != null) { - callback = createCallback(callback, thisArg, 3); - } - return baseUniq(array, isSorted, callback); - } - - /** - * Creates an array excluding all provided values using strict equality for - * comparisons, i.e. `===`. - * - * @static - * @memberOf _ - * @category Arrays - * @param {Array} array The array to filter. - * @param {Mixed} [value1, value2, ...] The values to exclude. - * @returns {Array} Returns a new array of filtered values. - * @example - * - * _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); - * // => [2, 3, 4] - */ - function without(array) { - return difference(array, nativeSlice.call(arguments, 1)); - } - - /** - * Creates an array of grouped elements, the first of which contains the first - * elements of the given arrays, the second of which contains the second - * elements of the given arrays, and so on. - * - * @static - * @memberOf _ - * @alias unzip - * @category Arrays - * @param {Array} [array1, array2, ...] Arrays to process. - * @returns {Array} Returns a new array of grouped elements. - * @example - * - * _.zip(['moe', 'larry'], [30, 40], [true, false]); - * // => [['moe', 30, true], ['larry', 40, false]] - */ - function zip() { - var index = -1, - length = max(pluck(arguments, 'length')), - result = Array(length < 0 ? 0 : length); - - while (++index < length) { - result[index] = pluck(arguments, index); - } - return result; - } - - /** - * Creates an object composed from arrays of `keys` and `values`. Provide - * either a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]` - * or two arrays, one of `keys` and one of corresponding `values`. - * - * @static - * @memberOf _ - * @alias object - * @category Arrays - * @param {Array} keys The array of keys. - * @param {Array} [values=[]] The array of values. - * @returns {Object} Returns an object composed of the given keys and - * corresponding values. - * @example - * - * _.zipObject(['moe', 'larry'], [30, 40]); - * // => { 'moe': 30, 'larry': 40 } - */ - function zipObject(keys, values) { - var index = -1, - length = keys ? keys.length : 0, - result = {}; - - while (++index < length) { - var key = keys[index]; - if (values) { - result[key] = values[index]; - } else if (key) { - result[key[0]] = key[1]; - } - } - return result; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a function this is restricted to executing `func` with the `this` - * binding and arguments of the created function, only after it is called `n` times. - * - * @static - * @memberOf _ - * @category Functions - * @param {Number} n The number of times the function must be called before - * `func` is executed. - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var renderNotes = _.after(notes.length, render); - * _.forEach(notes, function(note) { - * note.asyncSave({ 'success': renderNotes }); - * }); - * // `renderNotes` is run once, after all notes have saved - */ - function after(n, func) { - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (--n < 1) { - return func.apply(this, arguments); - } - }; - } - - /** - * Creates a function that, when called, invokes `func` with the `this` - * binding of `thisArg` and prepends any additional `bind` arguments to those - * provided to the bound function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to bind. - * @param {Mixed} [thisArg] The `this` binding of `func`. - * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. - * @returns {Function} Returns the new bound function. - * @example - * - * var func = function(greeting) { - * return greeting + ' ' + this.name; - * }; - * - * func = _.bind(func, { 'name': 'moe' }, 'hi'); - * func(); - * // => 'hi moe' - */ - function bind(func, thisArg) { - return createBound(func, 17, nativeSlice.call(arguments, 2), null, thisArg); - } - - /** - * Binds methods of an object to the object itself, overwriting the existing - * method. Method names may be specified as individual arguments or as arrays - * of method names. If no method names are provided all the function properties - * of `object` will be bound. - * - * @static - * @memberOf _ - * @category Functions - * @param {Object} object The object to bind and assign the bound methods to. - * @param {String} [methodName1, methodName2, ...] The object method names to - * bind, specified as individual method names or arrays of method names. - * @returns {Object} Returns `object`. - * @example - * - * var view = { - * 'label': 'docs', - * 'onClick': function() { console.log('clicked ' + this.label); } - * }; - * - * _.bindAll(view); - * jQuery('#docs').on('click', view.onClick); - * // => logs 'clicked docs', when the button is clicked - */ - function bindAll(object) { - var funcs = arguments.length > 1 ? baseFlatten(arguments, true, false, 1) : functions(object), - index = -1, - length = funcs.length; - - while (++index < length) { - var key = funcs[index]; - object[key] = bind(object[key], object); - } - return object; - } - - /** - * Creates a function that is the composition of the provided functions, - * where each function consumes the return value of the function that follows. - * For example, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. - * Each function is executed with the `this` binding of the composed function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} [func1, func2, ...] Functions to compose. - * @returns {Function} Returns the new composed function. - * @example - * - * var realNameMap = { - * 'curly': 'jerome' - * }; - * - * var format = function(name) { - * name = realNameMap[name.toLowerCase()] || name; - * return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); - * }; - * - * var greet = function(formatted) { - * return 'Hiya ' + formatted + '!'; - * }; - * - * var welcome = _.compose(greet, format); - * welcome('curly'); - * // => 'Hiya Jerome!' - */ - function compose() { - var funcs = arguments, - length = funcs.length || 1; - - while (length--) { - if (!isFunction(funcs[length])) { - throw new TypeError; - } - } - return function() { - var args = arguments, - length = funcs.length; - - while (length--) { - args = [funcs[length].apply(this, args)]; - } - return args[0]; - }; - } - - /** - * Produces a callback bound to an optional `thisArg`. If `func` is a property - * name the created callback will return the property value for a given element. - * If `func` is an object the created callback will return `true` for elements - * that contain the equivalent object properties, otherwise it will return `false`. - * - * @static - * @memberOf _ - * @category Functions - * @param {Mixed} [func=identity] The value to convert to a callback. - * @param {Mixed} [thisArg] The `this` binding of the created callback. - * @param {Number} [argCount] The number of arguments the callback accepts. - * @returns {Function} Returns a callback function. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 } - * ]; - * - * // wrap to create custom callback shorthands - * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { - * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); - * return !match ? func(callback, thisArg) : function(object) { - * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; - * }; - * }); - * - * _.filter(stooges, 'age__gt45'); - * // => [{ 'name': 'larry', 'age': 50 }] - */ - function createCallback(func, thisArg, argCount) { - var type = typeof func; - if (func == null || type == 'function') { - return baseCreateCallback(func, thisArg, argCount); - } - // handle "_.pluck" style callback shorthands - if (type != 'object') { - return function(object) { - return object[func]; - }; - } - var props = keys(func); - return function(object) { - var length = props.length, - result = false; - - while (length--) { - if (!(result = object[props[length]] === func[props[length]])) { - break; - } - } - return result; - }; - } - - /** - * Creates a function that will delay the execution of `func` until after - * `wait` milliseconds have elapsed since the last time it was invoked. - * Provide an options object to indicate that `func` should be invoked on - * the leading and/or trailing edge of the `wait` timeout. Subsequent calls - * to the debounced function will return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the debounced function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to debounce. - * @param {Number} wait The number of milliseconds to delay. - * @param {Object} options The options object. - * [leading=false] A boolean to specify execution on the leading edge of the timeout. - * [maxWait] The maximum time `func` is allowed to be delayed before it's called. - * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new debounced function. - * @example - * - * // avoid costly calculations while the window size is in flux - * var lazyLayout = _.debounce(calculateLayout, 150); - * jQuery(window).on('resize', lazyLayout); - * - * // execute `sendMail` when the click event is fired, debouncing subsequent calls - * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { - * 'leading': true, - * 'trailing': false - * }); - * - * // ensure `batchLog` is executed once after 1 second of debounced calls - * var source = new EventSource('/stream'); - * source.addEventListener('message', _.debounce(batchLog, 250, { - * 'maxWait': 1000 - * }, false); - */ - function debounce(func, wait, options) { - var args, - result, - thisArg, - callCount = 0, - lastCalled = 0, - maxWait = false, - maxTimeoutId = null, - timeoutId = null, - trailing = true; - - if (!isFunction(func)) { - throw new TypeError; - } - wait = nativeMax(0, wait || 0); - if (options === true) { - var leading = true; - trailing = false; - } else if (isObject(options)) { - leading = options.leading; - maxWait = 'maxWait' in options && nativeMax(wait, options.maxWait || 0); - trailing = 'trailing' in options ? options.trailing : trailing; - } - var clear = function() { - clearTimeout(maxTimeoutId); - clearTimeout(timeoutId); - callCount = 0; - maxTimeoutId = timeoutId = null; - }; - - var delayed = function() { - var isCalled = trailing && (!leading || callCount > 1); - clear(); - if (isCalled) { - if (maxWait !== false) { - lastCalled = +new Date; - } - result = func.apply(thisArg, args); - } - }; - - var maxDelayed = function() { - clear(); - if (trailing || (maxWait !== wait)) { - lastCalled = +new Date; - result = func.apply(thisArg, args); - } - }; - - return function() { - args = arguments; - thisArg = this; - callCount++; - - // avoid issues with Titanium and `undefined` timeout ids - // https://github.com/appcelerator/titanium_mobile/blob/3_1_0_GA/android/titanium/src/java/ti/modules/titanium/TitaniumModule.java#L185-L192 - clearTimeout(timeoutId); - - if (maxWait === false) { - if (leading && callCount < 2) { - result = func.apply(thisArg, args); - } - } else { - var stamp = +new Date; - if (!maxTimeoutId && !leading) { - lastCalled = stamp; - } - var remaining = maxWait - (stamp - lastCalled); - if (remaining <= 0) { - clearTimeout(maxTimeoutId); - maxTimeoutId = null; - lastCalled = stamp; - result = func.apply(thisArg, args); - } - else if (!maxTimeoutId) { - maxTimeoutId = setTimeout(maxDelayed, remaining); - } - } - if (wait !== maxWait) { - timeoutId = setTimeout(delayed, wait); - } - return result; - }; - } - - /** - * Defers executing the `func` function until the current call stack has cleared. - * Additional arguments will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to defer. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. - * @returns {Number} Returns the timer id. - * @example - * - * _.defer(function() { console.log('deferred'); }); - * // returns from the function before 'deferred' is logged - */ - function defer(func) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = nativeSlice.call(arguments, 1); - return setTimeout(function() { func.apply(undefined, args); }, 1); - } - - /** - * Executes the `func` function after `wait` milliseconds. Additional arguments - * will be provided to `func` when it is invoked. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to delay. - * @param {Number} wait The number of milliseconds to delay execution. - * @param {Mixed} [arg1, arg2, ...] Arguments to invoke the function with. - * @returns {Number} Returns the timer id. - * @example - * - * var log = _.bind(console.log, console); - * _.delay(log, 1000, 'logged later'); - * // => 'logged later' (Appears after one second.) - */ - function delay(func, wait) { - if (!isFunction(func)) { - throw new TypeError; - } - var args = nativeSlice.call(arguments, 2); - return setTimeout(function() { func.apply(undefined, args); }, wait); - } - - /** - * Creates a function that memoizes the result of `func`. If `resolver` is - * provided it will be used to determine the cache key for storing the result - * based on the arguments provided to the memoized function. By default, the - * first argument provided to the memoized function is used as the cache key. - * The `func` is executed with the `this` binding of the memoized function. - * The result cache is exposed as the `cache` property on the memoized function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to have its output memoized. - * @param {Function} [resolver] A function used to resolve the cache key. - * @returns {Function} Returns the new memoizing function. - * @example - * - * var fibonacci = _.memoize(function(n) { - * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); - * }); - */ - function memoize(func, resolver) { - var cache = {}; - return function() { - var key = keyPrefix + (resolver ? resolver.apply(this, arguments) : arguments[0]); - return hasOwnProperty.call(cache, key) - ? cache[key] - : (cache[key] = func.apply(this, arguments)); - }; - } - - /** - * Creates a function that is restricted to execute `func` once. Repeat calls to - * the function will return the value of the first call. The `func` is executed - * with the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to restrict. - * @returns {Function} Returns the new restricted function. - * @example - * - * var initialize = _.once(createApplication); - * initialize(); - * initialize(); - * // `initialize` executes `createApplication` once - */ - function once(func) { - var ran, - result; - - if (!isFunction(func)) { - throw new TypeError; - } - return function() { - if (ran) { - return result; - } - ran = true; - result = func.apply(this, arguments); - - // clear the `func` variable so the function may be garbage collected - func = null; - return result; - }; - } - - /** - * Creates a function that, when called, invokes `func` with any additional - * `partial` arguments prepended to those provided to the new function. This - * method is similar to `_.bind` except it does **not** alter the `this` binding. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to partially apply arguments to. - * @param {Mixed} [arg1, arg2, ...] Arguments to be partially applied. - * @returns {Function} Returns the new partially applied function. - * @example - * - * var greet = function(greeting, name) { return greeting + ' ' + name; }; - * var hi = _.partial(greet, 'hi'); - * hi('moe'); - * // => 'hi moe' - */ - function partial(func) { - return createBound(func, 16, nativeSlice.call(arguments, 1)); - } - - /** - * Creates a function that, when executed, will only call the `func` function - * at most once per every `wait` milliseconds. Provide an options object to - * indicate that `func` should be invoked on the leading and/or trailing edge - * of the `wait` timeout. Subsequent calls to the throttled function will - * return the result of the last `func` call. - * - * Note: If `leading` and `trailing` options are `true` `func` will be called - * on the trailing edge of the timeout only if the the throttled function is - * invoked more than once during the `wait` timeout. - * - * @static - * @memberOf _ - * @category Functions - * @param {Function} func The function to throttle. - * @param {Number} wait The number of milliseconds to throttle executions to. - * @param {Object} options The options object. - * [leading=true] A boolean to specify execution on the leading edge of the timeout. - * [trailing=true] A boolean to specify execution on the trailing edge of the timeout. - * @returns {Function} Returns the new throttled function. - * @example - * - * // avoid excessively updating the position while scrolling - * var throttled = _.throttle(updatePosition, 100); - * jQuery(window).on('scroll', throttled); - * - * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes - * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { - * 'trailing': false - * })); - */ - function throttle(func, wait, options) { - var leading = true, - trailing = true; - - if (options === false) { - leading = false; - } else if (isObject(options)) { - leading = 'leading' in options ? options.leading : leading; - trailing = 'trailing' in options ? options.trailing : trailing; - } - options = {}; - options.leading = leading; - options.maxWait = wait; - options.trailing = trailing; - - return debounce(func, wait, options); - } - - /** - * Creates a function that provides `value` to the wrapper function as its - * first argument. Additional arguments provided to the function are appended - * to those provided to the wrapper function. The wrapper is executed with - * the `this` binding of the created function. - * - * @static - * @memberOf _ - * @category Functions - * @param {Mixed} value The value to wrap. - * @param {Function} wrapper The wrapper function. - * @returns {Function} Returns the new function. - * @example - * - * var hello = function(name) { return 'hello ' + name; }; - * hello = _.wrap(hello, function(func) { - * return 'before, ' + func('moe') + ', after'; - * }); - * hello(); - * // => 'before, hello moe, after' - */ - function wrap(value, wrapper) { - if (!isFunction(wrapper)) { - throw new TypeError; - } - return function() { - var args = [value]; - push.apply(args, arguments); - return wrapper.apply(this, args); - }; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding HTML entities. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} string The string to escape. - * @returns {String} Returns the escaped string. - * @example - * - * _.escape('Moe, Larry & Curly'); - * // => 'Moe, Larry & Curly' - */ - function escape(string) { - return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); - } - - /** - * This method returns the first argument provided to it. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Mixed} value Any value. - * @returns {Mixed} Returns `value`. - * @example - * - * var moe = { 'name': 'moe' }; - * moe === _.identity(moe); - * // => true - */ - function identity(value) { - return value; - } - - /** - * Adds function properties of a source object to the `lodash` function and - * chainable wrapper. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} object The object of function properties to add to `lodash`. - * @param {Object} object The object of function properties to add to `lodash`. - * @example - * - * _.mixin({ - * 'capitalize': function(string) { - * return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); - * } - * }); - * - * _.capitalize('moe'); - * // => 'Moe' - * - * _('moe').capitalize(); - * // => 'Moe' - */ - function mixin(object) { - forEach(functions(object), function(methodName) { - var func = lodash[methodName] = object[methodName]; - - lodash.prototype[methodName] = function() { - var args = [this.__wrapped__]; - push.apply(args, arguments); - - var result = func.apply(lodash, args); - if (this.__chain__) { - result = new lodashWrapper(result); - result.__chain__ = true; - } - return result; - }; - }); - } - - /** - * Reverts the '_' variable to its previous value and returns a reference to - * the `lodash` function. - * - * @static - * @memberOf _ - * @category Utilities - * @returns {Function} Returns the `lodash` function. - * @example - * - * var lodash = _.noConflict(); - */ - function noConflict() { - window._ = oldDash; - return this; - } - - /** - * Produces a random number between `min` and `max` (inclusive). If only one - * argument is provided a number between `0` and the given number will be - * returned. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Number} [min=0] The minimum possible value. - * @param {Number} [max=1] The maximum possible value. - * @returns {Number} Returns a random number. - * @example - * - * _.random(0, 5); - * // => a number between 0 and 5 - * - * _.random(5); - * // => also a number between 0 and 5 - */ - function random(min, max) { - if (min == null && max == null) { - max = 1; - } - min = +min || 0; - if (max == null) { - max = min; - min = 0; - } else { - max = +max || 0; - } - var rand = nativeRandom(); - return (min % 1 || max % 1) - ? min + nativeMin(rand * (max - min + parseFloat('1e-' + ((rand +'').length - 1))), max) - : min + floor(rand * (max - min + 1)); - } - - /** - * Resolves the value of `property` on `object`. If `property` is a function - * it will be invoked with the `this` binding of `object` and its result returned, - * else the property value is returned. If `object` is falsey then `undefined` - * is returned. - * - * @static - * @memberOf _ - * @category Utilities - * @param {Object} object The object to inspect. - * @param {String} property The property to get the value of. - * @returns {Mixed} Returns the resolved value. - * @example - * - * var object = { - * 'cheese': 'crumpets', - * 'stuff': function() { - * return 'nonsense'; - * } - * }; - * - * _.result(object, 'cheese'); - * // => 'crumpets' - * - * _.result(object, 'stuff'); - * // => 'nonsense' - */ - function result(object, property) { - var value = object ? object[property] : undefined; - return isFunction(value) ? object[property]() : value; - } - - /** - * A micro-templating method that handles arbitrary delimiters, preserves - * whitespace, and correctly escapes quotes within interpolated code. - * - * Note: In the development build, `_.template` utilizes sourceURLs for easier - * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl - * - * For more information on precompiling templates see: - * http://lodash.com/#custom-builds - * - * For more information on Chrome extension sandboxes see: - * http://developer.chrome.com/stable/extensions/sandboxingEval.html - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} text The template text. - * @param {Object} data The data object used to populate the text. - * @param {Object} options The options object. - * escape - The "escape" delimiter regexp. - * evaluate - The "evaluate" delimiter regexp. - * imports - An object of properties to import into the compiled template as local variables. - * interpolate - The "interpolate" delimiter regexp. - * sourceURL - The sourceURL of the template's compiled source. - * variable - The data object variable name. - * @returns {Function|String} Returns a compiled function when no `data` object - * is given, else it returns the interpolated text. - * @example - * - * // using a compiled template - * var compiled = _.template('hello <%= name %>'); - * compiled({ 'name': 'moe' }); - * // => 'hello moe' - * - * // using the "escape" delimiter to escape HTML in data property values - * _.template('<b><%- value %></b>', { 'value': '<script>' }); - * // => '<b><script></b>' - * - * // using the "evaluate" delimiter to generate HTML - * var list = '<% _.forEach(people, function(name) { %><li><%= name %></li><% }); %>'; - * _.template(list, { 'people': ['moe', 'larry'] }); - * // => '<li>moe</li><li>larry</li>' - * - * // using the ES6 delimiter as an alternative to the default "interpolate" delimiter - * _.template('hello ${ name }', { 'name': 'curly' }); - * // => 'hello curly' - * - * // using the internal `print` function in "evaluate" delimiters - * _.template('<% print("hello " + epithet); %>!', { 'epithet': 'stooge' }); - * // => 'hello stooge!' - * - * // using a custom template delimiters - * _.templateSettings = { - * 'interpolate': /{{([\s\S]+?)}}/g - * }; - * - * _.template('hello {{ name }}!', { 'name': 'mustache' }); - * // => 'hello mustache!' - * - * // using the `imports` option to import jQuery - * var list = '<% $.each(people, function(name) { %><li><%= name %></li><% }); %>'; - * _.template(list, { 'people': ['moe', 'larry'] }, { 'imports': { '$': jQuery }); - * // => '<li>moe</li><li>larry</li>' - * - * // using the `sourceURL` option to specify a custom sourceURL for the template - * var compiled = _.template('hello <%= name %>', null, { 'sourceURL': '/basic/greeting.jst' }); - * compiled(data); - * // => find the source of "greeting.jst" under the Sources tab or Resources panel of the web inspector - * - * // using the `variable` option to ensure a with-statement isn't used in the compiled template - * var compiled = _.template('hi <%= data.name %>!', null, { 'variable': 'data' }); - * compiled.source; - * // => function(data) { - * var __t, __p = '', __e = _.escape; - * __p += 'hi ' + ((__t = ( data.name )) == null ? '' : __t) + '!'; - * return __p; - * } - * - * // using the `source` property to inline compiled templates for meaningful - * // line numbers in error messages and a stack trace - * fs.writeFileSync(path.join(cwd, 'jst.js'), '\ - * var JST = {\ - * "main": ' + _.template(mainText).source + '\ - * };\ - * '); - */ - function template(text, data, options) { - var _ = lodash, - settings = _.templateSettings; - - text || (text = ''); - options = defaults({}, options, settings); - - var index = 0, - source = "__p += '", - variable = options.variable; - - var reDelimiters = RegExp( - (options.escape || reNoMatch).source + '|' + - (options.interpolate || reNoMatch).source + '|' + - (options.evaluate || reNoMatch).source + '|$' - , 'g'); - - text.replace(reDelimiters, function(match, escapeValue, interpolateValue, evaluateValue, offset) { - source += text.slice(index, offset).replace(reUnescapedString, escapeStringChar); - if (escapeValue) { - source += "' +\n_.escape(" + escapeValue + ") +\n'"; - } - if (evaluateValue) { - source += "';\n" + evaluateValue + ";\n__p += '"; - } - if (interpolateValue) { - source += "' +\n((__t = (" + interpolateValue + ")) == null ? '' : __t) +\n'"; - } - index = offset + match.length; - return match; - }); - - source += "';\n"; - if (!variable) { - variable = 'obj'; - source = 'with (' + variable + ' || {}) {\n' + source + '\n}\n'; - } - source = 'function(' + variable + ') {\n' + - "var __t, __p = '', __j = Array.prototype.join;\n" + - "function print() { __p += __j.call(arguments, '') }\n" + - source + - 'return __p\n}'; - - try { - var result = Function('_', 'return ' + source)(_); - } catch(e) { - e.source = source; - throw e; - } - if (data) { - return result(data); - } - result.source = source; - return result; - } - - /** - * Executes the callback `n` times, returning an array of the results - * of each callback execution. The callback is bound to `thisArg` and invoked - * with one argument; (index). - * - * @static - * @memberOf _ - * @category Utilities - * @param {Number} n The number of times to execute the callback. - * @param {Function} callback The function called per iteration. - * @param {Mixed} [thisArg] The `this` binding of `callback`. - * @returns {Array} Returns an array of the results of each `callback` execution. - * @example - * - * var diceRolls = _.times(3, _.partial(_.random, 1, 6)); - * // => [3, 6, 4] - * - * _.times(3, function(n) { mage.castSpell(n); }); - * // => calls `mage.castSpell(n)` three times, passing `n` of `0`, `1`, and `2` respectively - * - * _.times(3, function(n) { this.cast(n); }, mage); - * // => also calls `mage.castSpell(n)` three times - */ - function times(n, callback, thisArg) { - var index = -1, - result = Array(n > -1 ? n : 0); - - while (++index < n) { - result[index] = callback.call(thisArg, index); - } - return result; - } - - /** - * The inverse of `_.escape` this method converts the HTML entities - * `&`, `<`, `>`, `"`, and `'` in `string` to their - * corresponding characters. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} string The string to unescape. - * @returns {String} Returns the unescaped string. - * @example - * - * _.unescape('Moe, Larry & Curly'); - * // => 'Moe, Larry & Curly' - */ - function unescape(string) { - return string == null ? '' : String(string).replace(reEscapedHtml, unescapeHtmlChar); - } - - /** - * Generates a unique ID. If `prefix` is provided the ID will be appended to it. - * - * @static - * @memberOf _ - * @category Utilities - * @param {String} [prefix] The value to prefix the ID with. - * @returns {String} Returns the unique ID. - * @example - * - * _.uniqueId('contact_'); - * // => 'contact_104' - * - * _.uniqueId(); - * // => '105' - */ - function uniqueId(prefix) { - var id = ++idCounter + ''; - return prefix ? prefix + id : id; - } - - /*--------------------------------------------------------------------------*/ - - /** - * Creates a `lodash` object that wraps the given `value`. - * - * @static - * @memberOf _ - * @category Chaining - * @param {Mixed} value The value to wrap. - * @returns {Object} Returns the wrapper object. - * @example - * - * var stooges = [ - * { 'name': 'moe', 'age': 40 }, - * { 'name': 'larry', 'age': 50 }, - * { 'name': 'curly', 'age': 60 } - * ]; - * - * var youngest = _.chain(stooges) - * .sortBy(function(stooge) { return stooge.age; }) - * .map(function(stooge) { return stooge.name + ' is ' + stooge.age; }) - * .first(); - * // => 'moe is 40' - */ - function chain(value) { - value = new lodashWrapper(value); - value.__chain__ = true; - return value; - } - - /** - * Invokes `interceptor` with the `value` as the first argument and then - * returns `value`. The purpose of this method is to "tap into" a method - * chain in order to perform operations on intermediate results within - * the chain. - * - * @static - * @memberOf _ - * @category Chaining - * @param {Mixed} value The value to provide to `interceptor`. - * @param {Function} interceptor The function to invoke. - * @returns {Mixed} Returns `value`. - * @example - * - * _([1, 2, 3, 4]) - * .filter(function(num) { return num % 2 == 0; }) - * .tap(function(array) { console.log(array); }) - * .map(function(num) { return num * num; }) - * .value(); - * // => // [2, 4] (logged) - * // => [4, 16] - */ - function tap(value, interceptor) { - interceptor(value); - return value; - } - - /** - * Enables method chaining on the wrapper object. - * - * @name chain - * @memberOf _ - * @category Chaining - * @returns {Mixed} Returns the wrapper object. - * @example - * - * var sum = _([1, 2, 3]) - * .chain() - * .reduce(function(sum, num) { return sum + num; }) - * .value() - * // => 6` - */ - function wrapperChain() { - this.__chain__ = true; - return this; - } - - /** - * Extracts the wrapped value. - * - * @name valueOf - * @memberOf _ - * @alias value - * @category Chaining - * @returns {Mixed} Returns the wrapped value. - * @example - * - * _([1, 2, 3]).valueOf(); - * // => [1, 2, 3] - */ - function wrapperValueOf() { - return this.__wrapped__; - } - - /*--------------------------------------------------------------------------*/ - - // add functions that return wrapped values when chaining - lodash.after = after; - lodash.bind = bind; - lodash.bindAll = bindAll; - lodash.chain = chain; - lodash.compact = compact; - lodash.compose = compose; - lodash.countBy = countBy; - lodash.debounce = debounce; - lodash.defaults = defaults; - lodash.defer = defer; - lodash.delay = delay; - lodash.difference = difference; - lodash.filter = filter; - lodash.flatten = flatten; - lodash.forEach = forEach; - lodash.functions = functions; - lodash.groupBy = groupBy; - lodash.initial = initial; - lodash.intersection = intersection; - lodash.invert = invert; - lodash.invoke = invoke; - lodash.keys = keys; - lodash.map = map; - lodash.max = max; - lodash.memoize = memoize; - lodash.min = min; - lodash.omit = omit; - lodash.once = once; - lodash.pairs = pairs; - lodash.partial = partial; - lodash.pick = pick; - lodash.pluck = pluck; - lodash.range = range; - lodash.reject = reject; - lodash.rest = rest; - lodash.shuffle = shuffle; - lodash.sortBy = sortBy; - lodash.tap = tap; - lodash.throttle = throttle; - lodash.times = times; - lodash.toArray = toArray; - lodash.union = union; - lodash.uniq = uniq; - lodash.values = values; - lodash.where = where; - lodash.without = without; - lodash.wrap = wrap; - lodash.zip = zip; - - // add aliases - lodash.collect = map; - lodash.drop = rest; - lodash.each = forEach; - lodash.extend = assign; - lodash.methods = functions; - lodash.object = zipObject; - lodash.select = filter; - lodash.tail = rest; - lodash.unique = uniq; - - /*--------------------------------------------------------------------------*/ - - // add functions that return unwrapped values when chaining - lodash.clone = clone; - lodash.contains = contains; - lodash.escape = escape; - lodash.every = every; - lodash.find = find; - lodash.has = has; - lodash.identity = identity; - lodash.indexOf = indexOf; - lodash.isArguments = isArguments; - lodash.isArray = isArray; - lodash.isBoolean = isBoolean; - lodash.isDate = isDate; - lodash.isElement = isElement; - lodash.isEmpty = isEmpty; - lodash.isEqual = isEqual; - lodash.isFinite = isFinite; - lodash.isFunction = isFunction; - lodash.isNaN = isNaN; - lodash.isNull = isNull; - lodash.isNumber = isNumber; - lodash.isObject = isObject; - lodash.isRegExp = isRegExp; - lodash.isString = isString; - lodash.isUndefined = isUndefined; - lodash.lastIndexOf = lastIndexOf; - lodash.mixin = mixin; - lodash.noConflict = noConflict; - lodash.random = random; - lodash.reduce = reduce; - lodash.reduceRight = reduceRight; - lodash.result = result; - lodash.size = size; - lodash.some = some; - lodash.sortedIndex = sortedIndex; - lodash.template = template; - lodash.unescape = unescape; - lodash.uniqueId = uniqueId; - - // add aliases - lodash.all = every; - lodash.any = some; - lodash.detect = find; - lodash.findWhere = findWhere; - lodash.foldl = reduce; - lodash.foldr = reduceRight; - lodash.include = contains; - lodash.inject = reduce; - - /*--------------------------------------------------------------------------*/ - - // add functions capable of returning wrapped and unwrapped values when chaining - lodash.first = first; - lodash.last = last; - - // add aliases - lodash.take = first; - lodash.head = first; - - /*--------------------------------------------------------------------------*/ - - // add functions to `lodash.prototype` - mixin(lodash); - - /** - * The semantic version number. - * - * @static - * @memberOf _ - * @type String - */ - lodash.VERSION = '1.3.1'; - - // add "Chaining" functions to the wrapper - lodash.prototype.chain = wrapperChain; - lodash.prototype.value = wrapperValueOf; - - // add `Array` mutator functions to the wrapper - forEach(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(methodName) { - var func = arrayRef[methodName]; - lodash.prototype[methodName] = function() { - var value = this.__wrapped__; - func.apply(value, arguments); - - // avoid array-like object bugs with `Array#shift` and `Array#splice` - // in Firefox < 10 and IE < 9 - if (!support.spliceObjects && value.length === 0) { - delete value[0]; - } - return this; - }; - }); - - // add `Array` accessor functions to the wrapper - forEach(['concat', 'join', 'slice'], function(methodName) { - var func = arrayRef[methodName]; - lodash.prototype[methodName] = function() { - var value = this.__wrapped__, - result = func.apply(value, arguments); - - if (this.__chain__) { - result = new lodashWrapper(result); - result.__chain__ = true; - } - return result; - }; - }); - - /*--------------------------------------------------------------------------*/ - - // some AMD build optimizers, like r.js, check for specific condition patterns like the following: - if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { - // Expose Lo-Dash to the global object even when an AMD loader is present in - // case Lo-Dash was injected by a third-party script and not intended to be - // loaded as a module. The global assignment can be reverted in the Lo-Dash - // module via its `noConflict()` method. - window._ = lodash; - - // define as an anonymous module so, through path mapping, it can be - // referenced as the "underscore" module - define(function() { - return lodash; - }); - } - // check for `exports` after `define` in case a build optimizer adds an `exports` object - else if (freeExports && !freeExports.nodeType) { - // in Node.js or RingoJS v0.8.0+ - if (freeModule) { - (freeModule.exports = lodash)._ = lodash; - } - // in Narwhal or RingoJS v0.7.0- - else { - freeExports._ = lodash; - } - } - else { - // in a browser or Rhino - window._ = lodash; - } -}(this)); diff --git a/src/UI/JsLibraries/messenger.js b/src/UI/JsLibraries/messenger.js deleted file mode 100644 index 8acdbcff3..000000000 --- a/src/UI/JsLibraries/messenger.js +++ /dev/null @@ -1,1263 +0,0 @@ -/*! messenger 1.4.1 */ -/* - * This file begins the output concatenated into messenger.js - * - * It establishes the Messenger object while preserving whatever it was before - * (for noConflict), and making it a callable function. - */ - -(function(){ - var _prevMessenger = window.Messenger; - var localMessenger; - - localMessenger = window.Messenger = function(){ - return localMessenger._call.apply(this, arguments); - } - - window.Messenger.noConflict = function(){ - window.Messenger = _prevMessenger; - - return localMessenger; - } -})(); - -/* - * This file contains shims for when Underscore and Backbone - * are not included. - * - * Portions taken from Underscore.js and Backbone.js - * Both of which are Copyright (c) 2009-2013 Jeremy Ashkenas, DocumentCloud - */ -window.Messenger._ = (function() { - if (window._) - return window._ - - var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; - - // Create quick reference variables for speed access to core prototypes. - var push = ArrayProto.push, - slice = ArrayProto.slice, - concat = ArrayProto.concat, - toString = ObjProto.toString, - hasOwnProperty = ObjProto.hasOwnProperty; - - // All **ECMAScript 5** native function implementations that we hope to use - // are declared here. - var - nativeForEach = ArrayProto.forEach, - nativeMap = ArrayProto.map, - nativeReduce = ArrayProto.reduce, - nativeReduceRight = ArrayProto.reduceRight, - nativeFilter = ArrayProto.filter, - nativeEvery = ArrayProto.every, - nativeSome = ArrayProto.some, - nativeIndexOf = ArrayProto.indexOf, - nativeLastIndexOf = ArrayProto.lastIndexOf, - nativeIsArray = Array.isArray, - nativeKeys = Object.keys, - nativeBind = FuncProto.bind; - - // Create a safe reference to the Underscore object for use below. - var _ = {}; - - // Establish the object that gets returned to break out of a loop iteration. - var breaker = {}; - - var each = _.each = _.forEach = function(obj, iterator, context) { - if (obj == null) return; - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === breaker) return; - } - } else { - for (var key in obj) { - if (_.has(obj, key)) { - if (iterator.call(context, obj[key], key, obj) === breaker) return; - } - } - } - }; - - _.result = function(object, property) { - if (object == null) return null; - var value = object[property]; - return _.isFunction(value) ? value.call(object) : value; - }; - - _.once = function(func) { - var ran = false, memo; - return function() { - if (ran) return memo; - ran = true; - memo = func.apply(this, arguments); - func = null; - return memo; - }; - }; - - var idCounter = 0; - _.uniqueId = function(prefix) { - var id = ++idCounter + ''; - return prefix ? prefix + id : id; - }; - - _.filter = _.select = function(obj, iterator, context) { - var results = []; - if (obj == null) return results; - if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); - each(obj, function(value, index, list) { - if (iterator.call(context, value, index, list)) results[results.length] = value; - }); - return results; - }; - - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { - _['is' + name] = function(obj) { - return toString.call(obj) == '[object ' + name + ']'; - }; - }); - - _.defaults = function(obj) { - each(slice.call(arguments, 1), function(source) { - if (source) { - for (var prop in source) { - if (obj[prop] == null) obj[prop] = source[prop]; - } - } - }); - return obj; - }; - - _.extend = function(obj) { - each(slice.call(arguments, 1), function(source) { - if (source) { - for (var prop in source) { - obj[prop] = source[prop]; - } - } - }); - return obj; - }; - - _.keys = nativeKeys || function(obj) { - if (obj !== Object(obj)) throw new TypeError('Invalid object'); - var keys = []; - for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; - return keys; - }; - - _.bind = function(func, context) { - if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); - var args = slice.call(arguments, 2); - return function() { - return func.apply(context, args.concat(slice.call(arguments))); - }; - }; - - _.isObject = function(obj) { - return obj === Object(obj); - }; - - return _; -})(); - -window.Messenger.Events = (function() { - if (window.Backbone && Backbone.Events) { - return Backbone.Events; - } - - var eventsShim = function() { - var eventSplitter = /\s+/; - - var eventsApi = function(obj, action, name, rest) { - if (!name) return true; - if (typeof name === 'object') { - for (var key in name) { - obj[action].apply(obj, [key, name[key]].concat(rest)); - } - } else if (eventSplitter.test(name)) { - var names = name.split(eventSplitter); - for (var i = 0, l = names.length; i < l; i++) { - obj[action].apply(obj, [names[i]].concat(rest)); - } - } else { - return true; - } - }; - - var triggerEvents = function(events, args) { - var ev, i = -1, l = events.length; - switch (args.length) { - case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); - return; - case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0]); - return; - case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1]); - return; - case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, args[0], args[1], args[2]); - return; - default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); - } - }; - - var Events = { - - on: function(name, callback, context) { - if (!(eventsApi(this, 'on', name, [callback, context]) && callback)) return this; - this._events || (this._events = {}); - var list = this._events[name] || (this._events[name] = []); - list.push({callback: callback, context: context, ctx: context || this}); - return this; - }, - - once: function(name, callback, context) { - if (!(eventsApi(this, 'once', name, [callback, context]) && callback)) return this; - var self = this; - var once = _.once(function() { - self.off(name, once); - callback.apply(this, arguments); - }); - once._callback = callback; - this.on(name, once, context); - return this; - }, - - off: function(name, callback, context) { - var list, ev, events, names, i, l, j, k; - if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; - if (!name && !callback && !context) { - this._events = {}; - return this; - } - - names = name ? [name] : _.keys(this._events); - for (i = 0, l = names.length; i < l; i++) { - name = names[i]; - if (list = this._events[name]) { - events = []; - if (callback || context) { - for (j = 0, k = list.length; j < k; j++) { - ev = list[j]; - if ((callback && callback !== ev.callback && - callback !== ev.callback._callback) || - (context && context !== ev.context)) { - events.push(ev); - } - } - } - this._events[name] = events; - } - } - - return this; - }, - - trigger: function(name) { - if (!this._events) return this; - var args = Array.prototype.slice.call(arguments, 1); - if (!eventsApi(this, 'trigger', name, args)) return this; - var events = this._events[name]; - var allEvents = this._events.all; - if (events) triggerEvents(events, args); - if (allEvents) triggerEvents(allEvents, arguments); - return this; - }, - - listenTo: function(obj, name, callback) { - var listeners = this._listeners || (this._listeners = {}); - var id = obj._listenerId || (obj._listenerId = _.uniqueId('l')); - listeners[id] = obj; - obj.on(name, typeof name === 'object' ? this : callback, this); - return this; - }, - - stopListening: function(obj, name, callback) { - var listeners = this._listeners; - if (!listeners) return; - if (obj) { - obj.off(name, typeof name === 'object' ? this : callback, this); - if (!name && !callback) delete listeners[obj._listenerId]; - } else { - if (typeof name === 'object') callback = this; - for (var id in listeners) { - listeners[id].off(name, callback, this); - } - this._listeners = {}; - } - return this; - } - }; - - Events.bind = Events.on; - Events.unbind = Events.off; - return Events; - }; - return eventsShim(); -})(); - -(function() { - var $, ActionMessenger, BaseView, Events, RetryingMessage, _, _Message, _Messenger, _ref, _ref1, _ref2, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - __slice = [].slice, - __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - - $ = jQuery; - - _ = (_ref = window._) != null ? _ref : window.Messenger._; - - Events = (_ref1 = typeof Backbone !== "undefined" && Backbone !== null ? Backbone.Events : void 0) != null ? _ref1 : window.Messenger.Events; - - BaseView = (function() { - - function BaseView(options) { - $.extend(this, Events); - if (_.isObject(options)) { - if (options.el) { - this.setElement(options.el); - } - this.model = options.model; - } - this.initialize.apply(this, arguments); - } - - BaseView.prototype.setElement = function(el) { - this.$el = $(el); - return this.el = this.$el[0]; - }; - - BaseView.prototype.delegateEvents = function(events) { - var delegateEventSplitter, eventName, key, match, method, selector, _results; - if (!(events || (events = _.result(this, "events")))) { - return; - } - this.undelegateEvents(); - delegateEventSplitter = /^(\S+)\s*(.*)$/; - _results = []; - for (key in events) { - method = events[key]; - if (!_.isFunction(method)) { - method = this[events[key]]; - } - if (!method) { - throw new Error("Method \"" + events[key] + "\" does not exist"); - } - match = key.match(delegateEventSplitter); - eventName = match[1]; - selector = match[2]; - method = _.bind(method, this); - eventName += ".delegateEvents" + this.cid; - if (selector === '') { - _results.push(this.jqon(eventName, method)); - } else { - _results.push(this.jqon(eventName, selector, method)); - } - } - return _results; - }; - - BaseView.prototype.jqon = function(eventName, selector, method) { - var _ref2; - if (this.$el.on != null) { - return (_ref2 = this.$el).on.apply(_ref2, arguments); - } else { - if (!(method != null)) { - method = selector; - selector = void 0; - } - if (selector != null) { - return this.$el.delegate(selector, eventName, method); - } else { - return this.$el.bind(eventName, method); - } - } - }; - - BaseView.prototype.jqoff = function(eventName) { - var _ref2; - if (this.$el.off != null) { - return (_ref2 = this.$el).off.apply(_ref2, arguments); - } else { - this.$el.undelegate(); - return this.$el.unbind(eventName); - } - }; - - BaseView.prototype.undelegateEvents = function() { - return this.jqoff(".delegateEvents" + this.cid); - }; - - BaseView.prototype.remove = function() { - this.undelegateEvents(); - return this.$el.remove(); - }; - - return BaseView; - - })(); - - _Message = (function(_super) { - - __extends(_Message, _super); - - function _Message() { - return _Message.__super__.constructor.apply(this, arguments); - } - - _Message.prototype.defaults = { - hideAfter: 10, - scroll: true, - closeButtonText: "×" - }; - - _Message.prototype.initialize = function(opts) { - if (opts == null) { - opts = {}; - } - this.shown = false; - this.rendered = false; - this.messenger = opts.messenger; - return this.options = $.extend({}, this.options, opts, this.defaults); - }; - - _Message.prototype.show = function() { - var wasShown; - if (!this.rendered) { - this.render(); - } - this.$message.removeClass('messenger-hidden'); - wasShown = this.shown; - this.shown = true; - if (!wasShown) { - return this.trigger('show'); - } - }; - - _Message.prototype.hide = function() { - var wasShown; - if (!this.rendered) { - return; - } - this.$message.addClass('messenger-hidden'); - wasShown = this.shown; - this.shown = false; - if (wasShown) { - return this.trigger('hide'); - } - }; - - _Message.prototype.cancel = function() { - return this.hide(); - }; - - _Message.prototype.update = function(opts) { - var _ref2, - _this = this; - if (_.isString(opts)) { - opts = { - message: opts - }; - } - $.extend(this.options, opts); - this.lastUpdate = new Date(); - this.rendered = false; - this.events = (_ref2 = this.options.events) != null ? _ref2 : {}; - this.render(); - this.actionsToEvents(); - this.delegateEvents(); - this.checkClickable(); - if (this.options.hideAfter) { - this.$message.addClass('messenger-will-hide-after'); - if (this._hideTimeout != null) { - clearTimeout(this._hideTimeout); - } - this._hideTimeout = setTimeout(function() { - return _this.hide(); - }, this.options.hideAfter * 1000); - } else { - this.$message.removeClass('messenger-will-hide-after'); - } - if (this.options.hideOnNavigate) { - this.$message.addClass('messenger-will-hide-on-navigate'); - if ((typeof Backbone !== "undefined" && Backbone !== null ? Backbone.history : void 0) != null) { - Backbone.history.on('route', function() { - return _this.hide(); - }); - } - } else { - this.$message.removeClass('messenger-will-hide-on-navigate'); - } - return this.trigger('update', this); - }; - - _Message.prototype.scrollTo = function() { - if (!this.options.scroll) { - return; - } - return $.scrollTo(this.$el, { - duration: 400, - offset: { - left: 0, - top: -20 - } - }); - }; - - _Message.prototype.timeSinceUpdate = function() { - if (this.lastUpdate) { - return (new Date) - this.lastUpdate; - } else { - return null; - } - }; - - _Message.prototype.actionsToEvents = function() { - var act, name, _ref2, _results, - _this = this; - _ref2 = this.options.actions; - _results = []; - for (name in _ref2) { - act = _ref2[name]; - _results.push(this.events["click [data-action=\"" + name + "\"] a"] = (function(act) { - return function(e) { - e.preventDefault(); - e.stopPropagation(); - _this.trigger("action:" + name, act, e); - return act.action.call(_this, e, _this); - }; - })(act)); - } - return _results; - }; - - _Message.prototype.checkClickable = function() { - var evt, name, _ref2, _results; - _ref2 = this.events; - _results = []; - for (name in _ref2) { - evt = _ref2[name]; - if (name === 'click') { - _results.push(this.$message.addClass('messenger-clickable')); - } else { - _results.push(void 0); - } - } - return _results; - }; - - _Message.prototype.undelegateEvents = function() { - var _ref2; - _Message.__super__.undelegateEvents.apply(this, arguments); - return (_ref2 = this.$message) != null ? _ref2.removeClass('messenger-clickable') : void 0; - }; - - _Message.prototype.parseActions = function() { - var act, actions, n_act, name, _ref2, _ref3; - actions = []; - _ref2 = this.options.actions; - for (name in _ref2) { - act = _ref2[name]; - n_act = $.extend({}, act); - n_act.name = name; - if ((_ref3 = n_act.label) == null) { - n_act.label = name; - } - actions.push(n_act); - } - return actions; - }; - - _Message.prototype.template = function(opts) { - var $action, $actions, $cancel, $link, $message, $text, action, _i, _len, _ref2, - _this = this; - $message = $("<div class='messenger-message message alert " + opts.type + " message-" + opts.type + " alert-" + opts.type + "'>"); - if (opts.showCloseButton) { - $cancel = $('<button type="button" class="messenger-close" data-dismiss="alert">'); - $cancel.html(opts.closeButtonText); - $cancel.click(function() { - _this.cancel(); - return true; - }); - $message.append($cancel); - } - $text = $("<div class=\"messenger-message-inner\">" + opts.message + "</div>"); - $message.append($text); - if (opts.actions.length) { - $actions = $('<div class="messenger-actions">'); - } - _ref2 = opts.actions; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - action = _ref2[_i]; - $action = $('<span>'); - $action.attr('data-action', "" + action.name); - $link = $('<a>'); - $link.html(action.label); - $action.append($('<span class="messenger-phrase">')); - $action.append($link); - $actions.append($action); - } - $message.append($actions); - return $message; - }; - - _Message.prototype.render = function() { - var opts; - if (this.rendered) { - return; - } - if (!this._hasSlot) { - this.setElement(this.messenger._reserveMessageSlot(this)); - this._hasSlot = true; - } - opts = $.extend({}, this.options, { - actions: this.parseActions() - }); - this.$message = $(this.template(opts)); - this.$el.html(this.$message); - this.shown = true; - this.rendered = true; - return this.trigger('render'); - }; - - return _Message; - - })(BaseView); - - RetryingMessage = (function(_super) { - - __extends(RetryingMessage, _super); - - function RetryingMessage() { - return RetryingMessage.__super__.constructor.apply(this, arguments); - } - - RetryingMessage.prototype.initialize = function() { - RetryingMessage.__super__.initialize.apply(this, arguments); - return this._timers = {}; - }; - - RetryingMessage.prototype.cancel = function() { - this.clearTimers(); - this.hide(); - if ((this._actionInstance != null) && (this._actionInstance.abort != null)) { - return this._actionInstance.abort(); - } - }; - - RetryingMessage.prototype.clearTimers = function() { - var name, timer, _ref2, _ref3; - _ref2 = this._timers; - for (name in _ref2) { - timer = _ref2[name]; - clearTimeout(timer); - } - this._timers = {}; - return (_ref3 = this.$message) != null ? _ref3.removeClass('messenger-retry-soon messenger-retry-later') : void 0; - }; - - RetryingMessage.prototype.render = function() { - var action, name, _ref2, _results; - RetryingMessage.__super__.render.apply(this, arguments); - this.clearTimers(); - _ref2 = this.options.actions; - _results = []; - for (name in _ref2) { - action = _ref2[name]; - if (action.auto) { - _results.push(this.startCountdown(name, action)); - } else { - _results.push(void 0); - } - } - return _results; - }; - - RetryingMessage.prototype.renderPhrase = function(action, time) { - var phrase; - phrase = action.phrase.replace('TIME', this.formatTime(time)); - return phrase; - }; - - RetryingMessage.prototype.formatTime = function(time) { - var pluralize; - pluralize = function(num, str) { - num = Math.floor(num); - if (num !== 1) { - str = str + 's'; - } - return 'in ' + num + ' ' + str; - }; - if (Math.floor(time) === 0) { - return 'now...'; - } - if (time < 60) { - return pluralize(time, 'second'); - } - time /= 60; - if (time < 60) { - return pluralize(time, 'minute'); - } - time /= 60; - return pluralize(time, 'hour'); - }; - - RetryingMessage.prototype.startCountdown = function(name, action) { - var $phrase, remaining, tick, _ref2, - _this = this; - if (this._timers[name] != null) { - return; - } - $phrase = this.$message.find("[data-action='" + name + "'] .messenger-phrase"); - remaining = (_ref2 = action.delay) != null ? _ref2 : 3; - if (remaining <= 10) { - this.$message.removeClass('messenger-retry-later'); - this.$message.addClass('messenger-retry-soon'); - } else { - this.$message.removeClass('messenger-retry-soon'); - this.$message.addClass('messenger-retry-later'); - } - tick = function() { - var delta; - $phrase.text(_this.renderPhrase(action, remaining)); - if (remaining > 0) { - delta = Math.min(remaining, 1); - remaining -= delta; - return _this._timers[name] = setTimeout(tick, delta * 1000); - } else { - _this.$message.removeClass('messenger-retry-soon messenger-retry-later'); - delete _this._timers[name]; - return action.action(); - } - }; - return tick(); - }; - - return RetryingMessage; - - })(_Message); - - _Messenger = (function(_super) { - - __extends(_Messenger, _super); - - function _Messenger() { - return _Messenger.__super__.constructor.apply(this, arguments); - } - - _Messenger.prototype.tagName = 'ul'; - - _Messenger.prototype.className = 'messenger'; - - _Messenger.prototype.messageDefaults = { - type: 'info' - }; - - _Messenger.prototype.initialize = function(options) { - this.options = options != null ? options : {}; - this.history = []; - return this.messageDefaults = $.extend({}, this.messageDefaults, this.options.messageDefaults); - }; - - _Messenger.prototype.render = function() { - return this.updateMessageSlotClasses(); - }; - - _Messenger.prototype.findById = function(id) { - return _.filter(this.history, function(rec) { - return rec.msg.options.id === id; - }); - }; - - _Messenger.prototype._reserveMessageSlot = function(msg) { - var $slot, dmsg, - _this = this; - $slot = $('<li>'); - $slot.addClass('messenger-message-slot'); - this.$el.prepend($slot); - this.history.push({ - msg: msg, - $slot: $slot - }); - this._enforceIdConstraint(msg); - msg.on('update', function() { - return _this._enforceIdConstraint(msg); - }); - while (this.options.maxMessages && this.history.length > this.options.maxMessages) { - dmsg = this.history.shift(); - dmsg.msg.remove(); - dmsg.$slot.remove(); - } - return $slot; - }; - - _Messenger.prototype._enforceIdConstraint = function(msg) { - var entry, _i, _len, _msg, _ref2; - if (msg.options.id == null) { - return; - } - _ref2 = this.history; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - entry = _ref2[_i]; - _msg = entry.msg; - if ((_msg.options.id != null) && _msg.options.id === msg.options.id && msg !== _msg) { - if (msg.options.singleton) { - msg.hide(); - return; - } else { - _msg.hide(); - } - } - } - }; - - _Messenger.prototype.newMessage = function(opts) { - var msg, _ref2, _ref3, _ref4, - _this = this; - if (opts == null) { - opts = {}; - } - opts.messenger = this; - _Message = (_ref2 = (_ref3 = Messenger.themes[(_ref4 = opts.theme) != null ? _ref4 : this.options.theme]) != null ? _ref3.Message : void 0) != null ? _ref2 : RetryingMessage; - msg = new _Message(opts); - msg.on('show', function() { - if (opts.scrollTo && _this.$el.css('position') !== 'fixed') { - return msg.scrollTo(); - } - }); - msg.on('hide show render', this.updateMessageSlotClasses, this); - return msg; - }; - - _Messenger.prototype.updateMessageSlotClasses = function() { - var anyShown, last, rec, willBeFirst, _i, _len, _ref2; - willBeFirst = true; - last = null; - anyShown = false; - _ref2 = this.history; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - rec = _ref2[_i]; - rec.$slot.removeClass('messenger-first messenger-last messenger-shown'); - if (rec.msg.shown && rec.msg.rendered) { - rec.$slot.addClass('messenger-shown'); - anyShown = true; - last = rec; - if (willBeFirst) { - willBeFirst = false; - rec.$slot.addClass('messenger-first'); - } - } - } - if (last != null) { - last.$slot.addClass('messenger-last'); - } - return this.$el["" + (anyShown ? 'remove' : 'add') + "Class"]('messenger-empty'); - }; - - _Messenger.prototype.hideAll = function() { - var rec, _i, _len, _ref2, _results; - _ref2 = this.history; - _results = []; - for (_i = 0, _len = _ref2.length; _i < _len; _i++) { - rec = _ref2[_i]; - _results.push(rec.msg.hide()); - } - return _results; - }; - - _Messenger.prototype.post = function(opts) { - var msg; - if (_.isString(opts)) { - opts = { - message: opts - }; - } - opts = $.extend(true, {}, this.messageDefaults, opts); - msg = this.newMessage(opts); - msg.update(opts); - return msg; - }; - - return _Messenger; - - })(BaseView); - - ActionMessenger = (function(_super) { - - __extends(ActionMessenger, _super); - - function ActionMessenger() { - return ActionMessenger.__super__.constructor.apply(this, arguments); - } - - ActionMessenger.prototype.doDefaults = { - progressMessage: null, - successMessage: null, - errorMessage: "Error connecting to the server.", - showSuccessWithoutError: true, - retry: { - auto: true, - allow: true - }, - action: $.ajax - }; - - ActionMessenger.prototype.hookBackboneAjax = function(msgr_opts) { - var _ajax, - _this = this; - if (msgr_opts == null) { - msgr_opts = {}; - } - if (!(window.Backbone != null)) { - throw 'Expected Backbone to be defined'; - } - msgr_opts = _.defaults(msgr_opts, { - id: 'BACKBONE_ACTION', - errorMessage: false, - successMessage: "Request completed successfully.", - showSuccessWithoutError: false - }); - _ajax = function(options) { - var sync_msgr_opts; - sync_msgr_opts = _.extend({}, msgr_opts, options.messenger); - return _this["do"](sync_msgr_opts, options); - }; - if (Backbone.ajax != null) { - if (Backbone.ajax._withoutMessenger) { - Backbone.ajax = Backbone.ajax._withoutMessenger; - } - if (!(msgr_opts.action != null) || msgr_opts.action === this.doDefaults.action) { - msgr_opts.action = Backbone.ajax; - } - _ajax._withoutMessenger = Backbone.ajax; - return Backbone.ajax = _ajax; - } else { - return Backbone.sync = _.wrap(Backbone.sync, function() { - var args, _old_ajax, _old_sync; - _old_sync = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - _old_ajax = $.ajax; - $.ajax = _ajax; - _old_sync.call.apply(_old_sync, [this].concat(__slice.call(args))); - return $.ajax = _old_ajax; - }); - } - }; - - ActionMessenger.prototype._getHandlerResponse = function(returnVal) { - if (returnVal === false) { - return false; - } - if (returnVal === true || !(returnVal != null)) { - return true; - } - return returnVal; - }; - - ActionMessenger.prototype._parseEvents = function(events) { - var desc, firstSpace, func, label, out, type, _ref2; - if (events == null) { - events = {}; - } - out = {}; - for (label in events) { - func = events[label]; - firstSpace = label.indexOf(' '); - type = label.substring(0, firstSpace); - desc = label.substring(firstSpace + 1); - if ((_ref2 = out[type]) == null) { - out[type] = {}; - } - out[type][desc] = func; - } - return out; - }; - - ActionMessenger.prototype._normalizeResponse = function() { - var data, elem, resp, type, xhr, _i, _len; - resp = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - type = null; - xhr = null; - data = null; - for (_i = 0, _len = resp.length; _i < _len; _i++) { - elem = resp[_i]; - if (elem === 'success' || elem === 'timeout' || elem === 'abort') { - type = elem; - } else if (((elem != null ? elem.readyState : void 0) != null) && ((elem != null ? elem.responseText : void 0) != null)) { - xhr = elem; - } else if (_.isObject(elem)) { - data = elem; - } - } - return [type, data, xhr]; - }; - - ActionMessenger.prototype.run = function() { - var args, events, getMessageText, handler, handlers, m_opts, msg, old, opts, type, _ref2, - _this = this; - m_opts = arguments[0], opts = arguments[1], args = 3 <= arguments.length ? __slice.call(arguments, 2) : []; - if (opts == null) { - opts = {}; - } - m_opts = $.extend(true, {}, this.messageDefaults, this.doDefaults, m_opts != null ? m_opts : {}); - events = this._parseEvents(m_opts.events); - getMessageText = function(type, xhr) { - var message; - message = m_opts[type + 'Message']; - if (_.isFunction(message)) { - return message.call(_this, type, xhr); - } - return message; - }; - msg = (_ref2 = m_opts.messageInstance) != null ? _ref2 : this.newMessage(m_opts); - if (m_opts.id != null) { - msg.options.id = m_opts.id; - } - if (m_opts.progressMessage != null) { - msg.update($.extend({}, m_opts, { - message: getMessageText('progress', null), - type: 'info' - })); - } - handlers = {}; - _.each(['error', 'success'], function(type) { - var originalHandler; - originalHandler = opts[type]; - return handlers[type] = function() { - var data, defaultOpts, handlerResp, msgOpts, reason, resp, responseOpts, xhr, _ref3, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9; - resp = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - _ref3 = _this._normalizeResponse.apply(_this, resp), reason = _ref3[0], data = _ref3[1], xhr = _ref3[2]; - if (type === 'success' && !(msg.errorCount != null) && m_opts.showSuccessWithoutError === false) { - m_opts['successMessage'] = null; - } - if (type === 'error') { - if ((_ref4 = m_opts.errorCount) == null) { - m_opts.errorCount = 0; - } - m_opts.errorCount += 1; - } - handlerResp = m_opts.returnsPromise ? resp[0] : typeof originalHandler === "function" ? originalHandler.apply(null, resp) : void 0; - responseOpts = _this._getHandlerResponse(handlerResp); - if (_.isString(responseOpts)) { - responseOpts = { - message: responseOpts - }; - } - if (type === 'error' && ((xhr != null ? xhr.status : void 0) === 0 || reason === 'abort')) { - msg.hide(); - return; - } - if (type === 'error' && ((m_opts.ignoredErrorCodes != null) && (_ref5 = xhr != null ? xhr.status : void 0, __indexOf.call(m_opts.ignoredErrorCodes, _ref5) >= 0))) { - msg.hide(); - return; - } - defaultOpts = { - message: getMessageText(type, xhr), - type: type, - events: (_ref6 = events[type]) != null ? _ref6 : {}, - hideOnNavigate: type === 'success' - }; - msgOpts = $.extend({}, m_opts, defaultOpts, responseOpts); - if (typeof ((_ref7 = msgOpts.retry) != null ? _ref7.allow : void 0) === 'number') { - msgOpts.retry.allow--; - } - if (type === 'error' && (xhr != null ? xhr.status : void 0) >= 500 && ((_ref8 = msgOpts.retry) != null ? _ref8.allow : void 0)) { - if (msgOpts.retry.delay == null) { - if (msgOpts.errorCount < 4) { - msgOpts.retry.delay = 10; - } else { - msgOpts.retry.delay = 5 * 60; - } - } - if (msgOpts.hideAfter) { - if ((_ref9 = msgOpts._hideAfter) == null) { - msgOpts._hideAfter = msgOpts.hideAfter; - } - msgOpts.hideAfter = msgOpts._hideAfter + msgOpts.retry.delay; - } - msgOpts._retryActions = true; - msgOpts.actions = { - retry: { - label: 'retry now', - phrase: 'Retrying TIME', - auto: msgOpts.retry.auto, - delay: msgOpts.retry.delay, - action: function() { - msgOpts.messageInstance = msg; - return setTimeout(function() { - return _this["do"].apply(_this, [msgOpts, opts].concat(__slice.call(args))); - }, 0); - } - }, - cancel: { - action: function() { - return msg.cancel(); - } - } - }; - } else if (msgOpts._retryActions) { - delete msgOpts.actions.retry; - delete msgOpts.actions.cancel; - delete m_opts._retryActions; - } - msg.update(msgOpts); - if (responseOpts && msgOpts.message) { - Messenger(_.extend({}, _this.options, { - instance: _this - })); - return msg.show(); - } else { - return msg.hide(); - } - }; - }); - if (!m_opts.returnsPromise) { - for (type in handlers) { - handler = handlers[type]; - old = opts[type]; - opts[type] = handler; - } - } - msg._actionInstance = m_opts.action.apply(m_opts, [opts].concat(__slice.call(args))); - if (m_opts.returnsPromise) { - msg._actionInstance.then(handlers.success, handlers.error); - } - return msg; - }; - - ActionMessenger.prototype["do"] = ActionMessenger.prototype.run; - - ActionMessenger.prototype.ajax = function() { - var args, m_opts; - m_opts = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - m_opts.action = $.ajax; - return this.run.apply(this, [m_opts].concat(__slice.call(args))); - }; - - ActionMessenger.prototype.expectPromise = function(action, m_opts) { - m_opts = _.extend({}, m_opts, { - action: action, - returnsPromise: true - }); - return this.run(m_opts); - }; - - ActionMessenger.prototype.error = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'error'; - return this.post(m_opts); - }; - - ActionMessenger.prototype.info = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'info'; - return this.post(m_opts); - }; - - ActionMessenger.prototype.success = function(m_opts) { - if (m_opts == null) { - m_opts = {}; - } - if (typeof m_opts === 'string') { - m_opts = { - message: m_opts - }; - } - m_opts.type = 'success'; - return this.post(m_opts); - }; - - return ActionMessenger; - - })(_Messenger); - - $.fn.messenger = function() { - var $el, args, func, instance, opts, _ref2, _ref3, _ref4; - func = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; - if (func == null) { - func = {}; - } - $el = this; - if (!(func != null) || !_.isString(func)) { - opts = func; - if (!($el.data('messenger') != null)) { - _Messenger = (_ref2 = (_ref3 = Messenger.themes[opts.theme]) != null ? _ref3.Messenger : void 0) != null ? _ref2 : ActionMessenger; - $el.data('messenger', instance = new _Messenger($.extend({ - el: $el - }, opts))); - instance.render(); - } - return $el.data('messenger'); - } else { - return (_ref4 = $el.data('messenger'))[func].apply(_ref4, args); - } - }; - - window.Messenger._call = function(opts) { - var $el, $parent, choosen_loc, chosen_loc, classes, defaultOpts, inst, loc, locations, _i, _len; - defaultOpts = { - extraClasses: 'messenger-fixed messenger-on-bottom messenger-on-right', - theme: 'future', - maxMessages: 9, - parentLocations: ['body'] - }; - opts = $.extend(defaultOpts, $._messengerDefaults, Messenger.options, opts); - if (opts.theme != null) { - opts.extraClasses += " messenger-theme-" + opts.theme; - } - inst = opts.instance || Messenger.instance; - if (opts.instance == null) { - locations = opts.parentLocations; - $parent = null; - choosen_loc = null; - for (_i = 0, _len = locations.length; _i < _len; _i++) { - loc = locations[_i]; - $parent = $(loc); - if ($parent.length) { - chosen_loc = loc; - break; - } - } - if (!inst) { - $el = $('<ul>'); - $parent.prepend($el); - inst = $el.messenger(opts); - inst._location = chosen_loc; - Messenger.instance = inst; - } else if (!$(inst._location).is($(chosen_loc))) { - inst.$el.detach(); - $parent.prepend(inst.$el); - } - } - if (inst._addedClasses != null) { - inst.$el.removeClass(inst._addedClasses); - } - inst.$el.addClass(classes = "" + inst.className + " " + opts.extraClasses); - inst._addedClasses = classes; - return inst; - }; - - $.extend(Messenger, { - Message: RetryingMessage, - Messenger: ActionMessenger, - themes: (_ref2 = Messenger.themes) != null ? _ref2 : {} - }); - - $.globalMessenger = window.Messenger = Messenger; - -}).call(this); diff --git a/src/UI/JsLibraries/moment.js b/src/UI/JsLibraries/moment.js deleted file mode 100644 index 275a3c324..000000000 --- a/src/UI/JsLibraries/moment.js +++ /dev/null @@ -1,3111 +0,0 @@ -//! moment.js -//! version : 2.10.3 -//! authors : Tim Wood, Iskren Chernev, Moment.js contributors -//! license : MIT -//! momentjs.com - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - global.moment = factory() -}(this, function () { 'use strict'; - - var hookCallback; - - function utils_hooks__hooks () { - return hookCallback.apply(null, arguments); - } - - // This is done to register the method called with moment() - // without creating circular dependencies. - function setHookCallback (callback) { - hookCallback = callback; - } - - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - - function isDate(input) { - return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; - } - - function map(arr, fn) { - var res = [], i; - for (i = 0; i < arr.length; ++i) { - res.push(fn(arr[i], i)); - } - return res; - } - - function hasOwnProp(a, b) { - return Object.prototype.hasOwnProperty.call(a, b); - } - - function extend(a, b) { - for (var i in b) { - if (hasOwnProp(b, i)) { - a[i] = b[i]; - } - } - - if (hasOwnProp(b, 'toString')) { - a.toString = b.toString; - } - - if (hasOwnProp(b, 'valueOf')) { - a.valueOf = b.valueOf; - } - - return a; - } - - function create_utc__createUTC (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, true).utc(); - } - - function defaultParsingFlags() { - // We need to deep clone this object. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso : false - }; - } - - function getParsingFlags(m) { - if (m._pf == null) { - m._pf = defaultParsingFlags(); - } - return m._pf; - } - - function valid__isValid(m) { - if (m._isValid == null) { - var flags = getParsingFlags(m); - m._isValid = !isNaN(m._d.getTime()) && - flags.overflow < 0 && - !flags.empty && - !flags.invalidMonth && - !flags.nullInput && - !flags.invalidFormat && - !flags.userInvalidated; - - if (m._strict) { - m._isValid = m._isValid && - flags.charsLeftOver === 0 && - flags.unusedTokens.length === 0 && - flags.bigHour === undefined; - } - } - return m._isValid; - } - - function valid__createInvalid (flags) { - var m = create_utc__createUTC(NaN); - if (flags != null) { - extend(getParsingFlags(m), flags); - } - else { - getParsingFlags(m).userInvalidated = true; - } - - return m; - } - - var momentProperties = utils_hooks__hooks.momentProperties = []; - - function copyConfig(to, from) { - var i, prop, val; - - if (typeof from._isAMomentObject !== 'undefined') { - to._isAMomentObject = from._isAMomentObject; - } - if (typeof from._i !== 'undefined') { - to._i = from._i; - } - if (typeof from._f !== 'undefined') { - to._f = from._f; - } - if (typeof from._l !== 'undefined') { - to._l = from._l; - } - if (typeof from._strict !== 'undefined') { - to._strict = from._strict; - } - if (typeof from._tzm !== 'undefined') { - to._tzm = from._tzm; - } - if (typeof from._isUTC !== 'undefined') { - to._isUTC = from._isUTC; - } - if (typeof from._offset !== 'undefined') { - to._offset = from._offset; - } - if (typeof from._pf !== 'undefined') { - to._pf = getParsingFlags(from); - } - if (typeof from._locale !== 'undefined') { - to._locale = from._locale; - } - - if (momentProperties.length > 0) { - for (i in momentProperties) { - prop = momentProperties[i]; - val = from[prop]; - if (typeof val !== 'undefined') { - to[prop] = val; - } - } - } - - return to; - } - - var updateInProgress = false; - - // Moment prototype object - function Moment(config) { - copyConfig(this, config); - this._d = new Date(+config._d); - // Prevent infinite loop in case updateOffset creates new moment - // objects. - if (updateInProgress === false) { - updateInProgress = true; - utils_hooks__hooks.updateOffset(this); - updateInProgress = false; - } - } - - function isMoment (obj) { - return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); - } - - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; - - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); - } else { - value = Math.ceil(coercedNumber); - } - } - - return value; - } - - function compareArrays(array1, array2, dontConvert) { - var len = Math.min(array1.length, array2.length), - lengthDiff = Math.abs(array1.length - array2.length), - diffs = 0, - i; - for (i = 0; i < len; i++) { - if ((dontConvert && array1[i] !== array2[i]) || - (!dontConvert && toInt(array1[i]) !== toInt(array2[i]))) { - diffs++; - } - } - return diffs + lengthDiff; - } - - function Locale() { - } - - var locales = {}; - var globalLocale; - - function normalizeLocale(key) { - return key ? key.toLowerCase().replace('_', '-') : key; - } - - // pick the locale from the array - // try ['en-au', 'en-gb'] as 'en-au', 'en-gb', 'en', as in move through the list trying each - // substring from most specific to least, but move to the next array item if it's a more specific variant than the current root - function chooseLocale(names) { - var i = 0, j, next, locale, split; - - while (i < names.length) { - split = normalizeLocale(names[i]).split('-'); - j = split.length; - next = normalizeLocale(names[i + 1]); - next = next ? next.split('-') : null; - while (j > 0) { - locale = loadLocale(split.slice(0, j).join('-')); - if (locale) { - return locale; - } - if (next && next.length >= j && compareArrays(split, next, true) >= j - 1) { - //the next array item is better than a shallower substring of this one - break; - } - j--; - } - i++; - } - return null; - } - - function loadLocale(name) { - var oldLocale = null; - // TODO: Find a better way to register and load all the locales in Node - if (!locales[name] && typeof module !== 'undefined' && - module && module.exports) { - try { - oldLocale = globalLocale._abbr; - require('./locale/' + name); - // because defineLocale currently also sets the global locale, we - // want to undo that for lazy loaded locales - locale_locales__getSetGlobalLocale(oldLocale); - } catch (e) { } - } - return locales[name]; - } - - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - function locale_locales__getSetGlobalLocale (key, values) { - var data; - if (key) { - if (typeof values === 'undefined') { - data = locale_locales__getLocale(key); - } - else { - data = defineLocale(key, values); - } - - if (data) { - // moment.duration._locale = moment._locale = data; - globalLocale = data; - } - } - - return globalLocale._abbr; - } - - function defineLocale (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); - - // backwards compat for now: also set the locale - locale_locales__getSetGlobalLocale(name); - - return locales[name]; - } else { - // useful for testing - delete locales[name]; - return null; - } - } - - // returns locale data - function locale_locales__getLocale (key) { - var locale; - - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; - } - - if (!key) { - return globalLocale; - } - - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } - - return chooseLocale(key); - } - - var aliases = {}; - - function addUnitAlias (unit, shorthand) { - var lowerCase = unit.toLowerCase(); - aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; - } - - function normalizeUnits(units) { - return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; - } - - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; - - for (prop in inputObject) { - if (hasOwnProp(inputObject, prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } - } - } - - return normalizedInput; - } - - function makeGetSet (unit, keepTime) { - return function (value) { - if (value != null) { - get_set__set(this, unit, value); - utils_hooks__hooks.updateOffset(this, keepTime); - return this; - } else { - return get_set__get(this, unit); - } - }; - } - - function get_set__get (mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } - - function get_set__set (mom, unit, value) { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); - } - - // MOMENTS - - function getSet (units, value) { - var unit; - if (typeof units === 'object') { - for (unit in units) { - this.set(unit, units[unit]); - } - } else { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - return this[units](value); - } - } - return this; - } - - function zeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; - - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } - - var formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|x|X|zz?|ZZ?|.)/g; - - var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; - - var formatFunctions = {}; - - var formatTokenFunctions = {}; - - // token: 'M' - // padded: ['MM', 2] - // ordinal: 'Mo' - // callback: function () { this.month() + 1 } - function addFormatToken (token, padded, ordinal, callback) { - var func = callback; - if (typeof callback === 'string') { - func = function () { - return this[callback](); - }; - } - if (token) { - formatTokenFunctions[token] = func; - } - if (padded) { - formatTokenFunctions[padded[0]] = function () { - return zeroFill(func.apply(this, arguments), padded[1], padded[2]); - }; - } - if (ordinal) { - formatTokenFunctions[ordinal] = function () { - return this.localeData().ordinal(func.apply(this, arguments), token); - }; - } - } - - function removeFormattingTokens(input) { - if (input.match(/\[[\s\S]/)) { - return input.replace(/^\[|\]$/g, ''); - } - return input.replace(/\\/g, ''); - } - - function makeFormatFunction(format) { - var array = format.match(formattingTokens), i, length; - - for (i = 0, length = array.length; i < length; i++) { - if (formatTokenFunctions[array[i]]) { - array[i] = formatTokenFunctions[array[i]]; - } else { - array[i] = removeFormattingTokens(array[i]); - } - } - - return function (mom) { - var output = ''; - for (i = 0; i < length; i++) { - output += array[i] instanceof Function ? array[i].call(mom, format) : array[i]; - } - return output; - }; - } - - // format date using native date object - function formatMoment(m, format) { - if (!m.isValid()) { - return m.localeData().invalidDate(); - } - - format = expandFormat(format, m.localeData()); - - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } - - return formatFunctions[format](m); - } - - function expandFormat(format, locale) { - var i = 5; - - function replaceLongDateFormatTokens(input) { - return locale.longDateFormat(input) || input; - } - - localFormattingTokens.lastIndex = 0; - while (i >= 0 && localFormattingTokens.test(format)) { - format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); - localFormattingTokens.lastIndex = 0; - i -= 1; - } - - return format; - } - - var match1 = /\d/; // 0 - 9 - var match2 = /\d\d/; // 00 - 99 - var match3 = /\d{3}/; // 000 - 999 - var match4 = /\d{4}/; // 0000 - 9999 - var match6 = /[+-]?\d{6}/; // -999999 - 999999 - var match1to2 = /\d\d?/; // 0 - 99 - var match1to3 = /\d{1,3}/; // 0 - 999 - var match1to4 = /\d{1,4}/; // 0 - 9999 - var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - - var matchUnsigned = /\d+/; // 0 - inf - var matchSigned = /[+-]?\d+/; // -inf - inf - - var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z - - var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 - - // any word (or two) characters or numbers including two/three word month in arabic. - var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; - - var regexes = {}; - - function addRegexToken (token, regex, strictRegex) { - regexes[token] = typeof regex === 'function' ? regex : function (isStrict) { - return (isStrict && strictRegex) ? strictRegex : regex; - }; - } - - function getParseRegexForToken (token, config) { - if (!hasOwnProp(regexes, token)) { - return new RegExp(unescapeFormat(token)); - } - - return regexes[token](config._strict, config._locale); - } - - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function unescapeFormat(s) { - return s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); - } - - var tokens = {}; - - function addParseToken (token, callback) { - var i, func = callback; - if (typeof token === 'string') { - token = [token]; - } - if (typeof callback === 'number') { - func = function (input, array) { - array[callback] = toInt(input); - }; - } - for (i = 0; i < token.length; i++) { - tokens[token[i]] = func; - } - } - - function addWeekParseToken (token, callback) { - addParseToken(token, function (input, array, config, token) { - config._w = config._w || {}; - callback(input, config._w, config, token); - }); - } - - function addTimeToArrayFromToken(token, input, config) { - if (input != null && hasOwnProp(tokens, token)) { - tokens[token](input, config._a, config, token); - } - } - - var YEAR = 0; - var MONTH = 1; - var DATE = 2; - var HOUR = 3; - var MINUTE = 4; - var SECOND = 5; - var MILLISECOND = 6; - - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - // FORMATTING - - addFormatToken('M', ['MM', 2], 'Mo', function () { - return this.month() + 1; - }); - - addFormatToken('MMM', 0, 0, function (format) { - return this.localeData().monthsShort(this, format); - }); - - addFormatToken('MMMM', 0, 0, function (format) { - return this.localeData().months(this, format); - }); - - // ALIASES - - addUnitAlias('month', 'M'); - - // PARSING - - addRegexToken('M', match1to2); - addRegexToken('MM', match1to2, match2); - addRegexToken('MMM', matchWord); - addRegexToken('MMMM', matchWord); - - addParseToken(['M', 'MM'], function (input, array) { - array[MONTH] = toInt(input) - 1; - }); - - addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { - var month = config._locale.monthsParse(input, token, config._strict); - // if we didn't find a month name, mark the date as invalid. - if (month != null) { - array[MONTH] = month; - } else { - getParsingFlags(config).invalidMonth = input; - } - }); - - // LOCALES - - var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); - function localeMonths (m) { - return this._months[m.month()]; - } - - var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); - function localeMonthsShort (m) { - return this._monthsShort[m.month()]; - } - - function localeMonthsParse (monthName, format, strict) { - var i, mom, regex; - - if (!this._monthsParse) { - this._monthsParse = []; - this._longMonthsParse = []; - this._shortMonthsParse = []; - } - - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - mom = create_utc__createUTC([2000, i]); - if (strict && !this._longMonthsParse[i]) { - this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); - this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); - } - if (!strict && !this._monthsParse[i]) { - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { - return i; - } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { - return i; - } else if (!strict && this._monthsParse[i].test(monthName)) { - return i; - } - } - } - - // MOMENTS - - function setMonth (mom, value) { - var dayOfMonth; - - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } - - dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function getSetMonth (value) { - if (value != null) { - setMonth(this, value); - utils_hooks__hooks.updateOffset(this, true); - return this; - } else { - return get_set__get(this, 'Month'); - } - } - - function getDaysInMonth () { - return daysInMonth(this.year(), this.month()); - } - - function checkOverflow (m) { - var overflow; - var a = m._a; - - if (a && getParsingFlags(m).overflow === -2) { - overflow = - a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : - a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : - a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : - a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : - a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : - a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : - -1; - - if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; - } - - getParsingFlags(m).overflow = overflow; - } - - return m; - } - - function warn(msg) { - if (utils_hooks__hooks.suppressDeprecationWarnings === false && typeof console !== 'undefined' && console.warn) { - console.warn('Deprecation warning: ' + msg); - } - } - - function deprecate(msg, fn) { - var firstTime = true, - msgWithStack = msg + '\n' + (new Error()).stack; - - return extend(function () { - if (firstTime) { - warn(msgWithStack); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } - - var deprecations = {}; - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - warn(msg); - deprecations[name] = true; - } - } - - utils_hooks__hooks.suppressDeprecationWarnings = false; - - var from_string__isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/; - - var isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ]; - - // iso time formats and regexes - var isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ]; - - var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; - - // date from iso format - function configFromISO(config) { - var i, l, - string = config._i, - match = from_string__isoRegex.exec(string); - - if (match) { - getParsingFlags(config).iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be 'T' or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(matchOffset)) { - config._f += 'Z'; - } - configFromStringAndFormat(config); - } else { - config._isValid = false; - } - } - - // date from iso format or fallback - function configFromString(config) { - var matched = aspNetJsonRegex.exec(config._i); - - if (matched !== null) { - config._d = new Date(+matched[1]); - return; - } - - configFromISO(config); - if (config._isValid === false) { - delete config._isValid; - utils_hooks__hooks.createFromInputFallback(config); - } - } - - utils_hooks__hooks.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); - } - ); - - function createDate (y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); - - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - - function createUTCDate (y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } - - addFormatToken(0, ['YY', 2], 0, function () { - return this.year() % 100; - }); - - addFormatToken(0, ['YYYY', 4], 0, 'year'); - addFormatToken(0, ['YYYYY', 5], 0, 'year'); - addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); - - // ALIASES - - addUnitAlias('year', 'y'); - - // PARSING - - addRegexToken('Y', matchSigned); - addRegexToken('YY', match1to2, match2); - addRegexToken('YYYY', match1to4, match4); - addRegexToken('YYYYY', match1to6, match6); - addRegexToken('YYYYYY', match1to6, match6); - - addParseToken(['YYYY', 'YYYYY', 'YYYYYY'], YEAR); - addParseToken('YY', function (input, array) { - array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); - }); - - // HELPERS - - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } - - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } - - // HOOKS - - utils_hooks__hooks.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; - - // MOMENTS - - var getSetYear = makeGetSet('FullYear', false); - - function getIsLeapYear () { - return isLeapYear(this.year()); - } - - addFormatToken('w', ['ww', 2], 'wo', 'week'); - addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); - - // ALIASES - - addUnitAlias('week', 'w'); - addUnitAlias('isoWeek', 'W'); - - // PARSING - - addRegexToken('w', match1to2); - addRegexToken('ww', match1to2, match2); - addRegexToken('W', match1to2); - addRegexToken('WW', match1to2, match2); - - addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { - week[token.substr(0, 1)] = toInt(input); - }); - - // HELPERS - - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; - - - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; - } - - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; - } - - adjustedMoment = local__createLocal(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; - } - - // LOCALES - - function localeWeek (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - } - - var defaultLocaleWeek = { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }; - - function localeFirstDayOfWeek () { - return this._week.dow; - } - - function localeFirstDayOfYear () { - return this._week.doy; - } - - // MOMENTS - - function getSetWeek (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - } - - function getSetISOWeek (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - } - - addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); - - // ALIASES - - addUnitAlias('dayOfYear', 'DDD'); - - // PARSING - - addRegexToken('DDD', match1to3); - addRegexToken('DDDD', match3); - addParseToken(['DDD', 'DDDD'], function (input, array, config) { - config._dayOfYear = toInt(input); - }); - - // HELPERS - - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = createUTCDate(year, 0, 1).getUTCDay(); - var daysToAdd; - var dayOfYear; - - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - - return { - year : dayOfYear > 0 ? year : year - 1, - dayOfYear : dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } - - // MOMENTS - - function getSetDayOfYear (input) { - var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - } - - // Pick the first defined of two or three arguments. - function defaults(a, b, c) { - if (a != null) { - return a; - } - if (b != null) { - return b; - } - return c; - } - - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()]; - } - return [now.getFullYear(), now.getMonth(), now.getDate()]; - } - - // convert an array to a date. - // the array should mirror the parameters below - // note: all values past the year are optional and will default to the lowest possible value. - // [year, month, day , hour, minute, second, millisecond] - function configFromArray (config) { - var i, date, input = [], currentDate, yearToUse; - - if (config._d) { - return; - } - - currentDate = currentDateArray(config); - - //compute day of the year from weeks and weekdays - if (config._w && config._a[DATE] == null && config._a[MONTH] == null) { - dayOfYearFromWeekInfo(config); - } - - //if the day of the year is set, figure out what it is - if (config._dayOfYear) { - yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); - - if (config._dayOfYear > daysInYear(yearToUse)) { - getParsingFlags(config)._overflowDayOfYear = true; - } - - date = createUTCDate(yearToUse, 0, config._dayOfYear); - config._a[MONTH] = date.getUTCMonth(); - config._a[DATE] = date.getUTCDate(); - } - - // Default to current date. - // * if no year, month, day of month are given, default to today - // * if day of month is given, default month and year - // * if month is given, default only year - // * if year is given, don't default anything - for (i = 0; i < 3 && config._a[i] == null; ++i) { - config._a[i] = input[i] = currentDate[i]; - } - - // Zero out whatever was not defaulted, including time - for (; i < 7; i++) { - config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; - } - - // Check for 24:00:00.000 - if (config._a[HOUR] === 24 && - config._a[MINUTE] === 0 && - config._a[SECOND] === 0 && - config._a[MILLISECOND] === 0) { - config._nextDay = true; - config._a[HOUR] = 0; - } - - config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); - // Apply timezone offset from input. The actual utcOffset can be changed - // with parseZone. - if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); - } - - if (config._nextDay) { - config._a[HOUR] = 24; - } - } - - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); - week = defaults(w.W, 1); - weekday = defaults(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; - - weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); - week = defaults(w.w, 1); - - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } - } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; - } - - utils_hooks__hooks.ISO_8601 = function () {}; - - // date from string and format string - function configFromStringAndFormat(config) { - // TODO: Move this to another part of the creation flow to prevent circular deps - if (config._f === utils_hooks__hooks.ISO_8601) { - configFromISO(config); - return; - } - - config._a = []; - getParsingFlags(config).empty = true; - - // This array is used to make a Date, either with `new Date` or `Date.UTC` - var string = '' + config._i, - i, parsedInput, tokens, token, skipped, - stringLength = string.length, - totalParsedInputLength = 0; - - tokens = expandFormat(config._f, config._locale).match(formattingTokens) || []; - - for (i = 0; i < tokens.length; i++) { - token = tokens[i]; - parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; - if (parsedInput) { - skipped = string.substr(0, string.indexOf(parsedInput)); - if (skipped.length > 0) { - getParsingFlags(config).unusedInput.push(skipped); - } - string = string.slice(string.indexOf(parsedInput) + parsedInput.length); - totalParsedInputLength += parsedInput.length; - } - // don't parse if it's not a known token - if (formatTokenFunctions[token]) { - if (parsedInput) { - getParsingFlags(config).empty = false; - } - else { - getParsingFlags(config).unusedTokens.push(token); - } - addTimeToArrayFromToken(token, parsedInput, config); - } - else if (config._strict && !parsedInput) { - getParsingFlags(config).unusedTokens.push(token); - } - } - - // add remaining unparsed input length to the string - getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; - if (string.length > 0) { - getParsingFlags(config).unusedInput.push(string); - } - - // clear _12h flag if hour is <= 12 - if (getParsingFlags(config).bigHour === true && - config._a[HOUR] <= 12 && - config._a[HOUR] > 0) { - getParsingFlags(config).bigHour = undefined; - } - // handle meridiem - config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - - configFromArray(config); - checkOverflow(config); - } - - - function meridiemFixWrap (locale, hour, meridiem) { - var isPm; - - if (meridiem == null) { - // nothing to do - return hour; - } - if (locale.meridiemHour != null) { - return locale.meridiemHour(hour, meridiem); - } else if (locale.isPM != null) { - // Fallback - isPm = locale.isPM(meridiem); - if (isPm && hour < 12) { - hour += 12; - } - if (!isPm && hour === 12) { - hour = 0; - } - return hour; - } else { - // this is not supposed to happen - return hour; - } - } - - function configFromStringAndArray(config) { - var tempConfig, - bestMoment, - - scoreToBeat, - i, - currentScore; - - if (config._f.length === 0) { - getParsingFlags(config).invalidFormat = true; - config._d = new Date(NaN); - return; - } - - for (i = 0; i < config._f.length; i++) { - currentScore = 0; - tempConfig = copyConfig({}, config); - if (config._useUTC != null) { - tempConfig._useUTC = config._useUTC; - } - tempConfig._f = config._f[i]; - configFromStringAndFormat(tempConfig); - - if (!valid__isValid(tempConfig)) { - continue; - } - - // if there is any input that was not parsed add a penalty for that format - currentScore += getParsingFlags(tempConfig).charsLeftOver; - - //or tokens - currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - - getParsingFlags(tempConfig).score = currentScore; - - if (scoreToBeat == null || currentScore < scoreToBeat) { - scoreToBeat = currentScore; - bestMoment = tempConfig; - } - } - - extend(config, bestMoment || tempConfig); - } - - function configFromObject(config) { - if (config._d) { - return; - } - - var i = normalizeObjectUnits(config._i); - config._a = [i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond]; - - configFromArray(config); - } - - function createFromConfig (config) { - var input = config._i, - format = config._f, - res; - - config._locale = config._locale || locale_locales__getLocale(config._l); - - if (input === null || (format === undefined && input === '')) { - return valid__createInvalid({nullInput: true}); - } - - if (typeof input === 'string') { - config._i = input = config._locale.preparse(input); - } - - if (isMoment(input)) { - return new Moment(checkOverflow(input)); - } else if (isArray(format)) { - configFromStringAndArray(config); - } else if (format) { - configFromStringAndFormat(config); - } else if (isDate(input)) { - config._d = input; - } else { - configFromInput(config); - } - - res = new Moment(checkOverflow(config)); - if (res._nextDay) { - // Adding is smart enough around DST - res.add(1, 'd'); - res._nextDay = undefined; - } - - return res; - } - - function configFromInput(config) { - var input = config._i; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if (typeof input === 'string') { - configFromString(config); - } else if (isArray(input)) { - config._a = map(input.slice(0), function (obj) { - return parseInt(obj, 10); - }); - configFromArray(config); - } else if (typeof(input) === 'object') { - configFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - utils_hooks__hooks.createFromInputFallback(config); - } - } - - function createLocalOrUTC (input, format, locale, strict, isUTC) { - var c = {}; - - if (typeof(locale) === 'boolean') { - strict = locale; - locale = undefined; - } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c._isAMomentObject = true; - c._useUTC = c._isUTC = isUTC; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - - return createFromConfig(c); - } - - function local__createLocal (input, format, locale, strict) { - return createLocalOrUTC(input, format, locale, strict, false); - } - - var prototypeMin = deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other < this ? this : other; - } - ); - - var prototypeMax = deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function () { - var other = local__createLocal.apply(null, arguments); - return other > this ? this : other; - } - ); - - // Pick a moment m from moments so that m[fn](other) is true for all - // other. This relies on the function fn to be transitive. - // - // moments should either be an array of moment objects or an array, whose - // first element is an array of moment objects. - function pickBy(fn, moments) { - var res, i; - if (moments.length === 1 && isArray(moments[0])) { - moments = moments[0]; - } - if (!moments.length) { - return local__createLocal(); - } - res = moments[0]; - for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { - res = moments[i]; - } - } - return res; - } - - // TODO: Use [].sort instead? - function min () { - var args = [].slice.call(arguments, 0); - - return pickBy('isBefore', args); - } - - function max () { - var args = [].slice.call(arguments, 0); - - return pickBy('isAfter', args); - } - - function Duration (duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; - - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; - - this._data = {}; - - this._locale = locale_locales__getLocale(); - - this._bubble(); - } - - function isDuration (obj) { - return obj instanceof Duration; - } - - function offset (token, separator) { - addFormatToken(token, 0, 0, function () { - var offset = this.utcOffset(); - var sign = '+'; - if (offset < 0) { - offset = -offset; - sign = '-'; - } - return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); - }); - } - - offset('Z', ':'); - offset('ZZ', ''); - - // PARSING - - addRegexToken('Z', matchOffset); - addRegexToken('ZZ', matchOffset); - addParseToken(['Z', 'ZZ'], function (input, array, config) { - config._useUTC = true; - config._tzm = offsetFromString(input); - }); - - // HELPERS - - // timezone chunker - // '+10:00' > ['10', '00'] - // '-1530' > ['-15', '30'] - var chunkOffset = /([\+\-]|\d\d)/gi; - - function offsetFromString(string) { - var matches = ((string || '').match(matchOffset) || []); - var chunk = matches[matches.length - 1] || []; - var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; - var minutes = +(parts[1] * 60) + toInt(parts[2]); - - return parts[0] === '+' ? minutes : -minutes; - } - - // Return a moment from input, that is local/utc/zone equivalent to model. - function cloneWithOffset(input, model) { - var res, diff; - if (model._isUTC) { - res = model.clone(); - diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); - // Use low-level api, because this fn is low-level api. - res._d.setTime(+res._d + diff); - utils_hooks__hooks.updateOffset(res, false); - return res; - } else { - return local__createLocal(input).local(); - } - return model._isUTC ? local__createLocal(input).zone(model._offset || 0) : local__createLocal(input).local(); - } - - function getDateOffset (m) { - // On Firefox.24 Date#getTimezoneOffset returns a floating point. - // https://github.com/moment/moment/pull/1871 - return -Math.round(m._d.getTimezoneOffset() / 15) * 15; - } - - // HOOKS - - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - utils_hooks__hooks.updateOffset = function () {}; - - // MOMENTS - - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - function getSetOffset (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = offsetFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = getDateOffset(this); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.add(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - utils_hooks__hooks.updateOffset(this, true); - this._changeInProgress = null; - } - } - return this; - } else { - return this._isUTC ? offset : getDateOffset(this); - } - } - - function getSetZone (input, keepLocalTime) { - if (input != null) { - if (typeof input !== 'string') { - input = -input; - } - - this.utcOffset(input, keepLocalTime); - - return this; - } else { - return -this.utcOffset(); - } - } - - function setOffsetToUTC (keepLocalTime) { - return this.utcOffset(0, keepLocalTime); - } - - function setOffsetToLocal (keepLocalTime) { - if (this._isUTC) { - this.utcOffset(0, keepLocalTime); - this._isUTC = false; - - if (keepLocalTime) { - this.subtract(getDateOffset(this), 'm'); - } - } - return this; - } - - function setOffsetToParsedOffset () { - if (this._tzm) { - this.utcOffset(this._tzm); - } else if (typeof this._i === 'string') { - this.utcOffset(offsetFromString(this._i)); - } - return this; - } - - function hasAlignedHourOffset (input) { - if (!input) { - input = 0; - } - else { - input = local__createLocal(input).utcOffset(); - } - - return (this.utcOffset() - input) % 60 === 0; - } - - function isDaylightSavingTime () { - return ( - this.utcOffset() > this.clone().month(0).utcOffset() || - this.utcOffset() > this.clone().month(5).utcOffset() - ); - } - - function isDaylightSavingTimeShifted () { - if (this._a) { - var other = this._isUTC ? create_utc__createUTC(this._a) : local__createLocal(this._a); - return this.isValid() && compareArrays(this._a, other.toArray()) > 0; - } - - return false; - } - - function isLocal () { - return !this._isUTC; - } - - function isUtcOffset () { - return this._isUTC; - } - - function isUtc () { - return this._isUTC && this._offset === 0; - } - - var aspNetRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/; - - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - var create__isoRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/; - - function create__createDuration (input, key) { - var duration = input, - // matching against regexp is expensive, do it on demand - match = null, - sign, - ret, - diffRes; - - if (isDuration(input)) { - duration = { - ms : input._milliseconds, - d : input._days, - M : input._months - }; - } else if (typeof input === 'number') { - duration = {}; - if (key) { - duration[key] = input; - } else { - duration.milliseconds = input; - } - } else if (!!(match = aspNetRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : 0, - d : toInt(match[DATE]) * sign, - h : toInt(match[HOUR]) * sign, - m : toInt(match[MINUTE]) * sign, - s : toInt(match[SECOND]) * sign, - ms : toInt(match[MILLISECOND]) * sign - }; - } else if (!!(match = create__isoRegex.exec(input))) { - sign = (match[1] === '-') ? -1 : 1; - duration = { - y : parseIso(match[2], sign), - M : parseIso(match[3], sign), - d : parseIso(match[4], sign), - h : parseIso(match[5], sign), - m : parseIso(match[6], sign), - s : parseIso(match[7], sign), - w : parseIso(match[8], sign) - }; - } else if (duration == null) {// checks for null or undefined - duration = {}; - } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); - - duration = {}; - duration.ms = diffRes.milliseconds; - duration.M = diffRes.months; - } - - ret = new Duration(duration); - - if (isDuration(input) && hasOwnProp(input, '_locale')) { - ret._locale = input._locale; - } - - return ret; - } - - create__createDuration.fn = Duration.prototype; - - function parseIso (inp, sign) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - } - - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; - } - - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; - } - - function momentsDifference(base, other) { - var res; - other = cloneWithOffset(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } - - return res; - } - - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = create__createDuration(val, period); - add_subtract__addSubtract(this, dur, direction); - return this; - }; - } - - function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; - - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); - } - if (months) { - setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - utils_hooks__hooks.updateOffset(mom, days || months); - } - } - - var add_subtract__add = createAdder(1, 'add'); - var add_subtract__subtract = createAdder(-1, 'subtract'); - - function moment_calendar__calendar (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're local/utc/offset or not. - var now = time || local__createLocal(), - sod = cloneWithOffset(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this, local__createLocal(now))); - } - - function clone () { - return new Moment(this); - } - - function isAfter (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this > +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return inputMs < +this.clone().startOf(units); - } - } - - function isBefore (input, units) { - var inputMs; - units = normalizeUnits(typeof units !== 'undefined' ? units : 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this < +input; - } else { - inputMs = isMoment(input) ? +input : +local__createLocal(input); - return +this.clone().endOf(units) < inputMs; - } - } - - function isBetween (from, to, units) { - return this.isAfter(from, units) && this.isBefore(to, units); - } - - function isSame (input, units) { - var inputMs; - units = normalizeUnits(units || 'millisecond'); - if (units === 'millisecond') { - input = isMoment(input) ? input : local__createLocal(input); - return +this === +input; - } else { - inputMs = +local__createLocal(input); - return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); - } - } - - function absFloor (number) { - if (number < 0) { - return Math.ceil(number); - } else { - return Math.floor(number); - } - } - - function diff (input, units, asFloat) { - var that = cloneWithOffset(input, this), - zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4, - delta, output; - - units = normalizeUnits(units); - - if (units === 'year' || units === 'month' || units === 'quarter') { - output = monthDiff(this, that); - if (units === 'quarter') { - output = output / 3; - } else if (units === 'year') { - output = output / 12; - } - } else { - delta = this - that; - output = units === 'second' ? delta / 1e3 : // 1000 - units === 'minute' ? delta / 6e4 : // 1000 * 60 - units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - delta; - } - return asFloat ? output : absFloor(output); - } - - function monthDiff (a, b) { - // difference in months - var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), - // b is in (anchor - 1 month, anchor + 1 month) - anchor = a.clone().add(wholeMonthDiff, 'months'), - anchor2, adjust; - - if (b - anchor < 0) { - anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor - anchor2); - } else { - anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); - // linear across the month - adjust = (b - anchor) / (anchor2 - anchor); - } - - return -(wholeMonthDiff + adjust); - } - - utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; - - function toString () { - return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); - } - - function moment_format__toISOString () { - var m = this.clone().utc(); - if (0 < m.year() && m.year() <= 9999) { - if ('function' === typeof Date.prototype.toISOString) { - // native implementation is ~50x faster, use it when we can - return this.toDate().toISOString(); - } else { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - } - - function format (inputString) { - var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); - return this.localeData().postformat(output); - } - - function from (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - } - - function fromNow (withoutSuffix) { - return this.from(local__createLocal(), withoutSuffix); - } - - function to (time, withoutSuffix) { - if (!this.isValid()) { - return this.localeData().invalidDate(); - } - return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); - } - - function toNow (withoutSuffix) { - return this.to(local__createLocal(), withoutSuffix); - } - - function locale (key) { - var newLocaleData; - - if (key === undefined) { - return this._locale._abbr; - } else { - newLocaleData = locale_locales__getLocale(key); - if (newLocaleData != null) { - this._locale = newLocaleData; - } - return this; - } - } - - var lang = deprecate( - 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - return this.locale(key); - } - } - ); - - function localeData () { - return this._locale; - } - - function startOf (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } - if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - - return this; - } - - function endOf (units) { - units = normalizeUnits(units); - if (units === undefined || units === 'millisecond') { - return this; - } - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - } - - function to_type__valueOf () { - return +this._d - ((this._offset || 0) * 60000); - } - - function unix () { - return Math.floor(+this / 1000); - } - - function toDate () { - return this._offset ? new Date(+this) : this._d; - } - - function toArray () { - var m = this; - return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; - } - - function moment_valid__isValid () { - return valid__isValid(this); - } - - function parsingFlags () { - return extend({}, getParsingFlags(this)); - } - - function invalidAt () { - return getParsingFlags(this).overflow; - } - - addFormatToken(0, ['gg', 2], 0, function () { - return this.weekYear() % 100; - }); - - addFormatToken(0, ['GG', 2], 0, function () { - return this.isoWeekYear() % 100; - }); - - function addWeekYearFormatToken (token, getter) { - addFormatToken(0, [token, token.length], 0, getter); - } - - addWeekYearFormatToken('gggg', 'weekYear'); - addWeekYearFormatToken('ggggg', 'weekYear'); - addWeekYearFormatToken('GGGG', 'isoWeekYear'); - addWeekYearFormatToken('GGGGG', 'isoWeekYear'); - - // ALIASES - - addUnitAlias('weekYear', 'gg'); - addUnitAlias('isoWeekYear', 'GG'); - - // PARSING - - addRegexToken('G', matchSigned); - addRegexToken('g', matchSigned); - addRegexToken('GG', match1to2, match2); - addRegexToken('gg', match1to2, match2); - addRegexToken('GGGG', match1to4, match4); - addRegexToken('gggg', match1to4, match4); - addRegexToken('GGGGG', match1to6, match6); - addRegexToken('ggggg', match1to6, match6); - - addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { - week[token.substr(0, 2)] = toInt(input); - }); - - addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { - week[token] = utils_hooks__hooks.parseTwoDigitYear(input); - }); - - // HELPERS - - function weeksInYear(year, dow, doy) { - return weekOfYear(local__createLocal([year, 11, 31 + dow - doy]), dow, doy).week; - } - - // MOMENTS - - function getSetWeekYear (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); - } - - function getSetISOWeekYear (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - } - - function getISOWeeksInYear () { - return weeksInYear(this.year(), 1, 4); - } - - function getWeeksInYear () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - } - - addFormatToken('Q', 0, 0, 'quarter'); - - // ALIASES - - addUnitAlias('quarter', 'Q'); - - // PARSING - - addRegexToken('Q', match1); - addParseToken('Q', function (input, array) { - array[MONTH] = (toInt(input) - 1) * 3; - }); - - // MOMENTS - - function getSetQuarter (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - } - - addFormatToken('D', ['DD', 2], 'Do', 'date'); - - // ALIASES - - addUnitAlias('date', 'D'); - - // PARSING - - addRegexToken('D', match1to2); - addRegexToken('DD', match1to2, match2); - addRegexToken('Do', function (isStrict, locale) { - return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; - }); - - addParseToken(['D', 'DD'], DATE); - addParseToken('Do', function (input, array) { - array[DATE] = toInt(input.match(match1to2)[0], 10); - }); - - // MOMENTS - - var getSetDayOfMonth = makeGetSet('Date', true); - - addFormatToken('d', 0, 'do', 'day'); - - addFormatToken('dd', 0, 0, function (format) { - return this.localeData().weekdaysMin(this, format); - }); - - addFormatToken('ddd', 0, 0, function (format) { - return this.localeData().weekdaysShort(this, format); - }); - - addFormatToken('dddd', 0, 0, function (format) { - return this.localeData().weekdays(this, format); - }); - - addFormatToken('e', 0, 0, 'weekday'); - addFormatToken('E', 0, 0, 'isoWeekday'); - - // ALIASES - - addUnitAlias('day', 'd'); - addUnitAlias('weekday', 'e'); - addUnitAlias('isoWeekday', 'E'); - - // PARSING - - addRegexToken('d', match1to2); - addRegexToken('e', match1to2); - addRegexToken('E', match1to2); - addRegexToken('dd', matchWord); - addRegexToken('ddd', matchWord); - addRegexToken('dddd', matchWord); - - addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config) { - var weekday = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (weekday != null) { - week.d = weekday; - } else { - getParsingFlags(config).invalidWeekday = input; - } - }); - - addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { - week[token] = toInt(input); - }); - - // HELPERS - - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } - - // LOCALES - - var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); - function localeWeekdays (m) { - return this._weekdays[m.day()]; - } - - var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); - function localeWeekdaysShort (m) { - return this._weekdaysShort[m.day()]; - } - - var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); - function localeWeekdaysMin (m) { - return this._weekdaysMin[m.day()]; - } - - function localeWeekdaysParse (weekdayName) { - var i, mom, regex; - - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } - - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = local__createLocal([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - } - - // MOMENTS - - function getSetDayOfWeek (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - } - - function getSetLocaleDayOfWeek (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - } - - function getSetISODayOfWeek (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - } - - addFormatToken('H', ['HH', 2], 0, 'hour'); - addFormatToken('h', ['hh', 2], 0, function () { - return this.hours() % 12 || 12; - }); - - function meridiem (token, lowercase) { - addFormatToken(token, 0, 0, function () { - return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); - }); - } - - meridiem('a', true); - meridiem('A', false); - - // ALIASES - - addUnitAlias('hour', 'h'); - - // PARSING - - function matchMeridiem (isStrict, locale) { - return locale._meridiemParse; - } - - addRegexToken('a', matchMeridiem); - addRegexToken('A', matchMeridiem); - addRegexToken('H', match1to2); - addRegexToken('h', match1to2); - addRegexToken('HH', match1to2, match2); - addRegexToken('hh', match1to2, match2); - - addParseToken(['H', 'HH'], HOUR); - addParseToken(['a', 'A'], function (input, array, config) { - config._isPm = config._locale.isPM(input); - config._meridiem = input; - }); - addParseToken(['h', 'hh'], function (input, array, config) { - array[HOUR] = toInt(input); - getParsingFlags(config).bigHour = true; - }); - - // LOCALES - - function localeIsPM (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - } - - var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; - function localeMeridiem (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - } - - - // MOMENTS - - // Setting the hour should keep the time, because the user explicitly - // specified which hour he wants. So trying to maintain the same hour (in - // a new timezone) makes sense. Adding/subtracting hours does not follow - // this rule. - var getSetHour = makeGetSet('Hours', true); - - addFormatToken('m', ['mm', 2], 0, 'minute'); - - // ALIASES - - addUnitAlias('minute', 'm'); - - // PARSING - - addRegexToken('m', match1to2); - addRegexToken('mm', match1to2, match2); - addParseToken(['m', 'mm'], MINUTE); - - // MOMENTS - - var getSetMinute = makeGetSet('Minutes', false); - - addFormatToken('s', ['ss', 2], 0, 'second'); - - // ALIASES - - addUnitAlias('second', 's'); - - // PARSING - - addRegexToken('s', match1to2); - addRegexToken('ss', match1to2, match2); - addParseToken(['s', 'ss'], SECOND); - - // MOMENTS - - var getSetSecond = makeGetSet('Seconds', false); - - addFormatToken('S', 0, 0, function () { - return ~~(this.millisecond() / 100); - }); - - addFormatToken(0, ['SS', 2], 0, function () { - return ~~(this.millisecond() / 10); - }); - - function millisecond__milliseconds (token) { - addFormatToken(0, [token, 3], 0, 'millisecond'); - } - - millisecond__milliseconds('SSS'); - millisecond__milliseconds('SSSS'); - - // ALIASES - - addUnitAlias('millisecond', 'ms'); - - // PARSING - - addRegexToken('S', match1to3, match1); - addRegexToken('SS', match1to3, match2); - addRegexToken('SSS', match1to3, match3); - addRegexToken('SSSS', matchUnsigned); - addParseToken(['S', 'SS', 'SSS', 'SSSS'], function (input, array) { - array[MILLISECOND] = toInt(('0.' + input) * 1000); - }); - - // MOMENTS - - var getSetMillisecond = makeGetSet('Milliseconds', false); - - addFormatToken('z', 0, 0, 'zoneAbbr'); - addFormatToken('zz', 0, 0, 'zoneName'); - - // MOMENTS - - function getZoneAbbr () { - return this._isUTC ? 'UTC' : ''; - } - - function getZoneName () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - } - - var momentPrototype__proto = Moment.prototype; - - momentPrototype__proto.add = add_subtract__add; - momentPrototype__proto.calendar = moment_calendar__calendar; - momentPrototype__proto.clone = clone; - momentPrototype__proto.diff = diff; - momentPrototype__proto.endOf = endOf; - momentPrototype__proto.format = format; - momentPrototype__proto.from = from; - momentPrototype__proto.fromNow = fromNow; - momentPrototype__proto.to = to; - momentPrototype__proto.toNow = toNow; - momentPrototype__proto.get = getSet; - momentPrototype__proto.invalidAt = invalidAt; - momentPrototype__proto.isAfter = isAfter; - momentPrototype__proto.isBefore = isBefore; - momentPrototype__proto.isBetween = isBetween; - momentPrototype__proto.isSame = isSame; - momentPrototype__proto.isValid = moment_valid__isValid; - momentPrototype__proto.lang = lang; - momentPrototype__proto.locale = locale; - momentPrototype__proto.localeData = localeData; - momentPrototype__proto.max = prototypeMax; - momentPrototype__proto.min = prototypeMin; - momentPrototype__proto.parsingFlags = parsingFlags; - momentPrototype__proto.set = getSet; - momentPrototype__proto.startOf = startOf; - momentPrototype__proto.subtract = add_subtract__subtract; - momentPrototype__proto.toArray = toArray; - momentPrototype__proto.toDate = toDate; - momentPrototype__proto.toISOString = moment_format__toISOString; - momentPrototype__proto.toJSON = moment_format__toISOString; - momentPrototype__proto.toString = toString; - momentPrototype__proto.unix = unix; - momentPrototype__proto.valueOf = to_type__valueOf; - - // Year - momentPrototype__proto.year = getSetYear; - momentPrototype__proto.isLeapYear = getIsLeapYear; - - // Week Year - momentPrototype__proto.weekYear = getSetWeekYear; - momentPrototype__proto.isoWeekYear = getSetISOWeekYear; - - // Quarter - momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; - - // Month - momentPrototype__proto.month = getSetMonth; - momentPrototype__proto.daysInMonth = getDaysInMonth; - - // Week - momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; - momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; - momentPrototype__proto.weeksInYear = getWeeksInYear; - momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; - - // Day - momentPrototype__proto.date = getSetDayOfMonth; - momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; - momentPrototype__proto.weekday = getSetLocaleDayOfWeek; - momentPrototype__proto.isoWeekday = getSetISODayOfWeek; - momentPrototype__proto.dayOfYear = getSetDayOfYear; - - // Hour - momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; - - // Minute - momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; - - // Second - momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; - - // Millisecond - momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; - - // Offset - momentPrototype__proto.utcOffset = getSetOffset; - momentPrototype__proto.utc = setOffsetToUTC; - momentPrototype__proto.local = setOffsetToLocal; - momentPrototype__proto.parseZone = setOffsetToParsedOffset; - momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; - momentPrototype__proto.isDST = isDaylightSavingTime; - momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; - momentPrototype__proto.isLocal = isLocal; - momentPrototype__proto.isUtcOffset = isUtcOffset; - momentPrototype__proto.isUtc = isUtc; - momentPrototype__proto.isUTC = isUtc; - - // Timezone - momentPrototype__proto.zoneAbbr = getZoneAbbr; - momentPrototype__proto.zoneName = getZoneName; - - // Deprecations - momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); - momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); - momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); - momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); - - var momentPrototype = momentPrototype__proto; - - function moment__createUnix (input) { - return local__createLocal(input * 1000); - } - - function moment__createInZone () { - return local__createLocal.apply(null, arguments).parseZone(); - } - - var defaultCalendar = { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }; - - function locale_calendar__calendar (key, mom, now) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.call(mom, now) : output; - } - - var defaultLongDateFormat = { - LTS : 'h:mm:ss A', - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }; - - function longDateFormat (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - } - - var defaultInvalidDate = 'Invalid date'; - - function invalidDate () { - return this._invalidDate; - } - - var defaultOrdinal = '%d'; - var defaultOrdinalParse = /\d{1,2}/; - - function ordinal (number) { - return this._ordinal.replace('%d', number); - } - - function preParsePostFormat (string) { - return string; - } - - var defaultRelativeTime = { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }; - - function relative__relativeTime (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); - } - - function pastFuture (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - } - - function locale_set__set (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; - } else { - this['_' + i] = prop; - } - } - // Lenient ordinal parsing accepts just a number in addition to - // number + (possibly) stuff coming from _ordinalParseLenient. - this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); - } - - var prototype__proto = Locale.prototype; - - prototype__proto._calendar = defaultCalendar; - prototype__proto.calendar = locale_calendar__calendar; - prototype__proto._longDateFormat = defaultLongDateFormat; - prototype__proto.longDateFormat = longDateFormat; - prototype__proto._invalidDate = defaultInvalidDate; - prototype__proto.invalidDate = invalidDate; - prototype__proto._ordinal = defaultOrdinal; - prototype__proto.ordinal = ordinal; - prototype__proto._ordinalParse = defaultOrdinalParse; - prototype__proto.preparse = preParsePostFormat; - prototype__proto.postformat = preParsePostFormat; - prototype__proto._relativeTime = defaultRelativeTime; - prototype__proto.relativeTime = relative__relativeTime; - prototype__proto.pastFuture = pastFuture; - prototype__proto.set = locale_set__set; - - // Month - prototype__proto.months = localeMonths; - prototype__proto._months = defaultLocaleMonths; - prototype__proto.monthsShort = localeMonthsShort; - prototype__proto._monthsShort = defaultLocaleMonthsShort; - prototype__proto.monthsParse = localeMonthsParse; - - // Week - prototype__proto.week = localeWeek; - prototype__proto._week = defaultLocaleWeek; - prototype__proto.firstDayOfYear = localeFirstDayOfYear; - prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; - - // Day of Week - prototype__proto.weekdays = localeWeekdays; - prototype__proto._weekdays = defaultLocaleWeekdays; - prototype__proto.weekdaysMin = localeWeekdaysMin; - prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; - prototype__proto.weekdaysShort = localeWeekdaysShort; - prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; - prototype__proto.weekdaysParse = localeWeekdaysParse; - - // Hours - prototype__proto.isPM = localeIsPM; - prototype__proto._meridiemParse = defaultLocaleMeridiemParse; - prototype__proto.meridiem = localeMeridiem; - - function lists__get (format, index, field, setter) { - var locale = locale_locales__getLocale(); - var utc = create_utc__createUTC().set(setter, index); - return locale[field](utc, format); - } - - function list (format, index, field, count, setter) { - if (typeof format === 'number') { - index = format; - format = undefined; - } - - format = format || ''; - - if (index != null) { - return lists__get(format, index, field, setter); - } - - var i; - var out = []; - for (i = 0; i < count; i++) { - out[i] = lists__get(format, i, field, setter); - } - return out; - } - - function lists__listMonths (format, index) { - return list(format, index, 'months', 12, 'month'); - } - - function lists__listMonthsShort (format, index) { - return list(format, index, 'monthsShort', 12, 'month'); - } - - function lists__listWeekdays (format, index) { - return list(format, index, 'weekdays', 7, 'day'); - } - - function lists__listWeekdaysShort (format, index) { - return list(format, index, 'weekdaysShort', 7, 'day'); - } - - function lists__listWeekdaysMin (format, index) { - return list(format, index, 'weekdaysMin', 7, 'day'); - } - - locale_locales__getSetGlobalLocale('en', { - ordinalParse: /\d{1,2}(th|st|nd|rd)/, - ordinal : function (number) { - var b = number % 10, - output = (toInt(number % 100 / 10) === 1) ? 'th' : - (b === 1) ? 'st' : - (b === 2) ? 'nd' : - (b === 3) ? 'rd' : 'th'; - return number + output; - } - }); - - // Side effect imports - utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); - utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - - var mathAbs = Math.abs; - - function duration_abs__abs () { - var data = this._data; - - this._milliseconds = mathAbs(this._milliseconds); - this._days = mathAbs(this._days); - this._months = mathAbs(this._months); - - data.milliseconds = mathAbs(data.milliseconds); - data.seconds = mathAbs(data.seconds); - data.minutes = mathAbs(data.minutes); - data.hours = mathAbs(data.hours); - data.months = mathAbs(data.months); - data.years = mathAbs(data.years); - - return this; - } - - function duration_add_subtract__addSubtract (duration, input, value, direction) { - var other = create__createDuration(input, value); - - duration._milliseconds += direction * other._milliseconds; - duration._days += direction * other._days; - duration._months += direction * other._months; - - return duration._bubble(); - } - - // supports only 2.0-style add(1, 's') or add(duration) - function duration_add_subtract__add (input, value) { - return duration_add_subtract__addSubtract(this, input, value, 1); - } - - // supports only 2.0-style subtract(1, 's') or subtract(duration) - function duration_add_subtract__subtract (input, value) { - return duration_add_subtract__addSubtract(this, input, value, -1); - } - - function bubble () { - var milliseconds = this._milliseconds; - var days = this._days; - var months = this._months; - var data = this._data; - var seconds, minutes, hours, years = 0; - - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; - - seconds = absFloor(milliseconds / 1000); - data.seconds = seconds % 60; - - minutes = absFloor(seconds / 60); - data.minutes = minutes % 60; - - hours = absFloor(minutes / 60); - data.hours = hours % 24; - - days += absFloor(hours / 24); - - // Accurately convert days to years, assume start from year 0. - years = absFloor(daysToYears(days)); - days -= absFloor(yearsToDays(years)); - - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absFloor(days / 30); - days %= 30; - - // 12 months -> 1 year - years += absFloor(months / 12); - months %= 12; - - data.days = days; - data.months = months; - data.years = years; - - return this; - } - - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } - - function yearsToDays (years) { - // years * 365 + absFloor(years / 4) - - // absFloor(years / 100) + absFloor(years / 400); - return years * 146097 / 400; - } - - function as (units) { - var days; - var months; - var milliseconds = this._milliseconds; - - units = normalizeUnits(units); - - if (units === 'month' || units === 'year') { - days = this._days + milliseconds / 864e5; - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - // handle milliseconds separately because of floating point math errors (issue #1867) - days = this._days + Math.round(yearsToDays(this._months / 12)); - switch (units) { - case 'week' : return days / 7 + milliseconds / 6048e5; - case 'day' : return days + milliseconds / 864e5; - case 'hour' : return days * 24 + milliseconds / 36e5; - case 'minute' : return days * 1440 + milliseconds / 6e4; - case 'second' : return days * 86400 + milliseconds / 1000; - // Math.floor prevents floating point math errors here - case 'millisecond': return Math.floor(days * 864e5) + milliseconds; - default: throw new Error('Unknown unit ' + units); - } - } - } - - // TODO: Use this.as('ms')? - function duration_as__valueOf () { - return ( - this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6 - ); - } - - function makeAs (alias) { - return function () { - return this.as(alias); - }; - } - - var asMilliseconds = makeAs('ms'); - var asSeconds = makeAs('s'); - var asMinutes = makeAs('m'); - var asHours = makeAs('h'); - var asDays = makeAs('d'); - var asWeeks = makeAs('w'); - var asMonths = makeAs('M'); - var asYears = makeAs('y'); - - function duration_get__get (units) { - units = normalizeUnits(units); - return this[units + 's'](); - } - - function makeGetter(name) { - return function () { - return this._data[name]; - }; - } - - var duration_get__milliseconds = makeGetter('milliseconds'); - var seconds = makeGetter('seconds'); - var minutes = makeGetter('minutes'); - var hours = makeGetter('hours'); - var days = makeGetter('days'); - var months = makeGetter('months'); - var years = makeGetter('years'); - - function weeks () { - return absFloor(this.days() / 7); - } - - var round = Math.round; - var thresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }; - - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); - } - - function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { - var duration = create__createDuration(posNegDuration).abs(); - var seconds = round(duration.as('s')); - var minutes = round(duration.as('m')); - var hours = round(duration.as('h')); - var days = round(duration.as('d')); - var months = round(duration.as('M')); - var years = round(duration.as('y')); - - var a = seconds < thresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < thresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < thresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < thresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < thresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; - - a[2] = withoutSuffix; - a[3] = +posNegDuration > 0; - a[4] = locale; - return substituteTimeAgo.apply(null, a); - } - - // This function allows you to set a threshold for relative time strings - function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { - if (thresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return thresholds[threshold]; - } - thresholds[threshold] = limit; - return true; - } - - function humanize (withSuffix) { - var locale = this.localeData(); - var output = duration_humanize__relativeTime(this, !withSuffix, locale); - - if (withSuffix) { - output = locale.pastFuture(+this, output); - } - - return locale.postformat(output); - } - - var iso_string__abs = Math.abs; - - function iso_string__toISOString() { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var Y = iso_string__abs(this.years()); - var M = iso_string__abs(this.months()); - var D = iso_string__abs(this.days()); - var h = iso_string__abs(this.hours()); - var m = iso_string__abs(this.minutes()); - var s = iso_string__abs(this.seconds() + this.milliseconds() / 1000); - var total = this.asSeconds(); - - if (!total) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - return (total < 0 ? '-' : '') + - 'P' + - (Y ? Y + 'Y' : '') + - (M ? M + 'M' : '') + - (D ? D + 'D' : '') + - ((h || m || s) ? 'T' : '') + - (h ? h + 'H' : '') + - (m ? m + 'M' : '') + - (s ? s + 'S' : ''); - } - - var duration_prototype__proto = Duration.prototype; - - duration_prototype__proto.abs = duration_abs__abs; - duration_prototype__proto.add = duration_add_subtract__add; - duration_prototype__proto.subtract = duration_add_subtract__subtract; - duration_prototype__proto.as = as; - duration_prototype__proto.asMilliseconds = asMilliseconds; - duration_prototype__proto.asSeconds = asSeconds; - duration_prototype__proto.asMinutes = asMinutes; - duration_prototype__proto.asHours = asHours; - duration_prototype__proto.asDays = asDays; - duration_prototype__proto.asWeeks = asWeeks; - duration_prototype__proto.asMonths = asMonths; - duration_prototype__proto.asYears = asYears; - duration_prototype__proto.valueOf = duration_as__valueOf; - duration_prototype__proto._bubble = bubble; - duration_prototype__proto.get = duration_get__get; - duration_prototype__proto.milliseconds = duration_get__milliseconds; - duration_prototype__proto.seconds = seconds; - duration_prototype__proto.minutes = minutes; - duration_prototype__proto.hours = hours; - duration_prototype__proto.days = days; - duration_prototype__proto.weeks = weeks; - duration_prototype__proto.months = months; - duration_prototype__proto.years = years; - duration_prototype__proto.humanize = humanize; - duration_prototype__proto.toISOString = iso_string__toISOString; - duration_prototype__proto.toString = iso_string__toISOString; - duration_prototype__proto.toJSON = iso_string__toISOString; - duration_prototype__proto.locale = locale; - duration_prototype__proto.localeData = localeData; - - // Deprecations - duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); - duration_prototype__proto.lang = lang; - - // Side effect imports - - addFormatToken('X', 0, 0, 'unix'); - addFormatToken('x', 0, 0, 'valueOf'); - - // PARSING - - addRegexToken('x', matchSigned); - addRegexToken('X', matchTimestamp); - addParseToken('X', function (input, array, config) { - config._d = new Date(parseFloat(input, 10) * 1000); - }); - addParseToken('x', function (input, array, config) { - config._d = new Date(toInt(input)); - }); - - // Side effect imports - - - utils_hooks__hooks.version = '2.10.3'; - - setHookCallback(local__createLocal); - - utils_hooks__hooks.fn = momentPrototype; - utils_hooks__hooks.min = min; - utils_hooks__hooks.max = max; - utils_hooks__hooks.utc = create_utc__createUTC; - utils_hooks__hooks.unix = moment__createUnix; - utils_hooks__hooks.months = lists__listMonths; - utils_hooks__hooks.isDate = isDate; - utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; - utils_hooks__hooks.invalid = valid__createInvalid; - utils_hooks__hooks.duration = create__createDuration; - utils_hooks__hooks.isMoment = isMoment; - utils_hooks__hooks.weekdays = lists__listWeekdays; - utils_hooks__hooks.parseZone = moment__createInZone; - utils_hooks__hooks.localeData = locale_locales__getLocale; - utils_hooks__hooks.isDuration = isDuration; - utils_hooks__hooks.monthsShort = lists__listMonthsShort; - utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; - utils_hooks__hooks.defineLocale = defineLocale; - utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; - utils_hooks__hooks.normalizeUnits = normalizeUnits; - utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; - - var _moment = utils_hooks__hooks; - - return _moment; - -})); \ No newline at end of file diff --git a/src/UI/JsLibraries/typeahead.js b/src/UI/JsLibraries/typeahead.js deleted file mode 100644 index 450a6ca43..000000000 --- a/src/UI/JsLibraries/typeahead.js +++ /dev/null @@ -1,1716 +0,0 @@ -/*! - * typeahead.js 0.10.2 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function($) { - var _ = { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - getUniqueId: function() { - var counter = 0; - return function() { - return counter++; - }; - }(), - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - noop: function() {} - }; - var VERSION = "0.10.2"; - var tokenizers = function(root) { - return { - nonword: nonword, - whitespace: whitespace, - obj: { - nonword: getObjTokenizer(nonword), - whitespace: getObjTokenizer(whitespace) - } - }; - function whitespace(s) { - return s.split(/\s+/); - } - function nonword(s) { - return s.split(/\W+/); - } - function getObjTokenizer(tokenizer) { - return function setKey(key) { - return function tokenize(o) { - return tokenizer(o[key]); - }; - }; - } - }(); - var LruCache = function() { - function LruCache(maxSize) { - this.maxSize = maxSize || 100; - this.size = 0; - this.hash = {}; - this.list = new List(); - } - _.mixin(LruCache.prototype, { - set: function set(key, val) { - var tailItem = this.list.tail, node; - if (this.size >= this.maxSize) { - this.list.remove(tailItem); - delete this.hash[tailItem.key]; - } - if (node = this.hash[key]) { - node.val = val; - this.list.moveToFront(node); - } else { - node = new Node(key, val); - this.list.add(node); - this.hash[key] = node; - this.size++; - } - }, - get: function get(key) { - var node = this.hash[key]; - if (node) { - this.list.moveToFront(node); - return node.val; - } - } - }); - function List() { - this.head = this.tail = null; - } - _.mixin(List.prototype, { - add: function add(node) { - if (this.head) { - node.next = this.head; - this.head.prev = node; - } - this.head = node; - this.tail = this.tail || node; - }, - remove: function remove(node) { - node.prev ? node.prev.next = node.next : this.head = node.next; - node.next ? node.next.prev = node.prev : this.tail = node.prev; - }, - moveToFront: function(node) { - this.remove(node); - this.add(node); - } - }); - function Node(key, val) { - this.key = key; - this.val = val; - this.prev = this.next = null; - } - return LruCache; - }(); - var PersistentStorage = function() { - var ls, methods; - try { - ls = window.localStorage; - ls.setItem("~~~", "!"); - ls.removeItem("~~~"); - } catch (err) { - ls = null; - } - function PersistentStorage(namespace) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + this.prefix); - } - if (ls && window.JSON) { - methods = { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (_.isNumber(ttl)) { - ls.setItem(this._ttlKey(key), encode(now() + ttl)); - } else { - ls.removeItem(this._ttlKey(key)); - } - return ls.setItem(this._prefix(key), encode(val)); - }, - remove: function(key) { - ls.removeItem(this._ttlKey(key)); - ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, key, keys = [], len = ls.length; - for (i = 0; i < len; i++) { - if ((key = ls.key(i)).match(this.keyMatcher)) { - keys.push(key.replace(this.keyMatcher, "")); - } - } - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(ls.getItem(this._ttlKey(key))); - return _.isNumber(ttl) && now() > ttl ? true : false; - } - }; - } else { - methods = { - get: _.noop, - set: _.noop, - remove: _.noop, - clear: _.noop, - isExpired: _.noop - }; - } - _.mixin(PersistentStorage.prototype, methods); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(_.isUndefined(val) ? null : val); - } - function decode(val) { - return JSON.parse(val); - } - }(); - var Transport = function() { - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, requestCache = new LruCache(10); - function Transport(o) { - o = o || {}; - this._send = o.transport ? callbackToDeferred(o.transport) : $.ajax; - this._get = o.rateLimiter ? o.rateLimiter(this._get) : this._get; - } - Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { - maxPendingRequests = num; - }; - Transport.resetCache = function clearCache() { - requestCache = new LruCache(10); - }; - _.mixin(Transport.prototype, { - _get: function(url, o, cb) { - var that = this, jqXhr; - if (jqXhr = pendingRequests[url]) { - jqXhr.done(done).fail(fail); - } else if (pendingRequestsCount < maxPendingRequests) { - pendingRequestsCount++; - pendingRequests[url] = this._send(url, o).done(done).fail(fail).always(always); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - cb && cb(null, resp); - requestCache.set(url, resp); - } - function fail() { - cb && cb(true); - } - function always() { - pendingRequestsCount--; - delete pendingRequests[url]; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(url, o, cb) { - var resp; - if (_.isFunction(o)) { - cb = o; - o = {}; - } - if (resp = requestCache.get(url)) { - _.defer(function() { - cb && cb(null, resp); - }); - } else { - this._get(url, o, cb); - } - return !!resp; - } - }); - return Transport; - function callbackToDeferred(fn) { - return function customSendWrapper(url, o) { - var deferred = $.Deferred(); - fn(url, o, onSuccess, onError); - return deferred; - function onSuccess(resp) { - _.defer(function() { - deferred.resolve(resp); - }); - } - function onError(err) { - _.defer(function() { - deferred.reject(err); - }); - } - }; - } - }(); - var SearchIndex = function() { - function SearchIndex(o) { - o = o || {}; - if (!o.datumTokenizer || !o.queryTokenizer) { - $.error("datumTokenizer and queryTokenizer are both required"); - } - this.datumTokenizer = o.datumTokenizer; - this.queryTokenizer = o.queryTokenizer; - this.reset(); - } - _.mixin(SearchIndex.prototype, { - bootstrap: function bootstrap(o) { - this.datums = o.datums; - this.trie = o.trie; - }, - add: function(data) { - var that = this; - data = _.isArray(data) ? data : [ data ]; - _.each(data, function(datum) { - var id, tokens; - id = that.datums.push(datum) - 1; - tokens = normalizeTokens(that.datumTokenizer(datum)); - _.each(tokens, function(token) { - var node, chars, ch; - node = that.trie; - chars = token.split(""); - while (ch = chars.shift()) { - node = node.children[ch] || (node.children[ch] = newNode()); - node.ids.push(id); - } - }); - }); - }, - get: function get(query) { - var that = this, tokens, matches; - tokens = normalizeTokens(this.queryTokenizer(query)); - _.each(tokens, function(token) { - var node, chars, ch, ids; - if (matches && matches.length === 0) { - return false; - } - node = that.trie; - chars = token.split(""); - while (node && (ch = chars.shift())) { - node = node.children[ch]; - } - if (node && chars.length === 0) { - ids = node.ids.slice(0); - matches = matches ? getIntersection(matches, ids) : ids; - } else { - matches = []; - return false; - } - }); - return matches ? _.map(unique(matches), function(id) { - return that.datums[id]; - }) : []; - }, - reset: function reset() { - this.datums = []; - this.trie = newNode(); - }, - serialize: function serialize() { - return { - datums: this.datums, - trie: this.trie - }; - } - }); - return SearchIndex; - function normalizeTokens(tokens) { - tokens = _.filter(tokens, function(token) { - return !!token; - }); - tokens = _.map(tokens, function(token) { - return token.toLowerCase(); - }); - return tokens; - } - function newNode() { - return { - ids: [], - children: {} - }; - } - function unique(array) { - var seen = {}, uniques = []; - for (var i = 0; i < array.length; i++) { - if (!seen[array[i]]) { - seen[array[i]] = true; - uniques.push(array[i]); - } - } - return uniques; - } - function getIntersection(arrayA, arrayB) { - var ai = 0, bi = 0, intersection = []; - arrayA = arrayA.sort(compare); - arrayB = arrayB.sort(compare); - while (ai < arrayA.length && bi < arrayB.length) { - if (arrayA[ai] < arrayB[bi]) { - ai++; - } else if (arrayA[ai] > arrayB[bi]) { - bi++; - } else { - intersection.push(arrayA[ai]); - ai++; - bi++; - } - } - return intersection; - function compare(a, b) { - return a - b; - } - } - }(); - var oParser = function() { - return { - local: getLocal, - prefetch: getPrefetch, - remote: getRemote - }; - function getLocal(o) { - return o.local || null; - } - function getPrefetch(o) { - var prefetch, defaults; - defaults = { - url: null, - thumbprint: "", - ttl: 24 * 60 * 60 * 1e3, - filter: null, - ajax: {} - }; - if (prefetch = o.prefetch || null) { - prefetch = _.isString(prefetch) ? { - url: prefetch - } : prefetch; - prefetch = _.mixin(defaults, prefetch); - prefetch.thumbprint = VERSION + prefetch.thumbprint; - prefetch.ajax.type = prefetch.ajax.type || "GET"; - prefetch.ajax.dataType = prefetch.ajax.dataType || "json"; - !prefetch.url && $.error("prefetch requires url to be set"); - } - return prefetch; - } - function getRemote(o) { - var remote, defaults; - defaults = { - url: null, - wildcard: "%QUERY", - replace: null, - rateLimitBy: "debounce", - rateLimitWait: 300, - send: null, - filter: null, - ajax: {} - }; - if (remote = o.remote || null) { - remote = _.isString(remote) ? { - url: remote - } : remote; - remote = _.mixin(defaults, remote); - remote.rateLimiter = /^throttle$/i.test(remote.rateLimitBy) ? byThrottle(remote.rateLimitWait) : byDebounce(remote.rateLimitWait); - remote.ajax.type = remote.ajax.type || "GET"; - remote.ajax.dataType = remote.ajax.dataType || "json"; - delete remote.rateLimitBy; - delete remote.rateLimitWait; - !remote.url && $.error("remote requires url to be set"); - } - return remote; - function byDebounce(wait) { - return function(fn) { - return _.debounce(fn, wait); - }; - } - function byThrottle(wait) { - return function(fn) { - return _.throttle(fn, wait); - }; - } - } - }(); - (function(root) { - var old, keys; - old = root.Bloodhound; - keys = { - data: "data", - protocol: "protocol", - thumbprint: "thumbprint" - }; - root.Bloodhound = Bloodhound; - function Bloodhound(o) { - if (!o || !o.local && !o.prefetch && !o.remote) { - $.error("one of local, prefetch, or remote is required"); - } - this.limit = o.limit || 5; - this.sorter = getSorter(o.sorter); - this.dupDetector = o.dupDetector || ignoreDuplicates; - this.local = oParser.local(o); - this.prefetch = oParser.prefetch(o); - this.remote = oParser.remote(o); - this.cacheKey = this.prefetch ? this.prefetch.cacheKey || this.prefetch.url : null; - this.index = new SearchIndex({ - datumTokenizer: o.datumTokenizer, - queryTokenizer: o.queryTokenizer - }); - this.storage = this.cacheKey ? new PersistentStorage(this.cacheKey) : null; - } - Bloodhound.noConflict = function noConflict() { - root.Bloodhound = old; - return Bloodhound; - }; - Bloodhound.tokenizers = tokenizers; - _.mixin(Bloodhound.prototype, { - _loadPrefetch: function loadPrefetch(o) { - var that = this, serialized, deferred; - if (serialized = this._readFromStorage(o.thumbprint)) { - this.index.bootstrap(serialized); - deferred = $.Deferred().resolve(); - } else { - deferred = $.ajax(o.url, o.ajax).done(handlePrefetchResponse); - } - return deferred; - function handlePrefetchResponse(resp) { - that.clear(); - that.add(o.filter ? o.filter(resp) : resp); - that._saveToStorage(that.index.serialize(), o.thumbprint, o.ttl); - } - }, - _getFromRemote: function getFromRemote(query, cb) { - var that = this, url, uriEncodedQuery; - query = query || ""; - uriEncodedQuery = encodeURIComponent(query); - url = this.remote.replace ? this.remote.replace(this.remote.url, query) : this.remote.url.replace(this.remote.wildcard, uriEncodedQuery); - return this.transport.get(url, this.remote.ajax, handleRemoteResponse); - function handleRemoteResponse(err, resp) { - err ? cb([]) : cb(that.remote.filter ? that.remote.filter(resp) : resp); - } - }, - _saveToStorage: function saveToStorage(data, thumbprint, ttl) { - if (this.storage) { - this.storage.set(keys.data, data, ttl); - this.storage.set(keys.protocol, location.protocol, ttl); - this.storage.set(keys.thumbprint, thumbprint, ttl); - } - }, - _readFromStorage: function readFromStorage(thumbprint) { - var stored = {}, isExpired; - if (this.storage) { - stored.data = this.storage.get(keys.data); - stored.protocol = this.storage.get(keys.protocol); - stored.thumbprint = this.storage.get(keys.thumbprint); - } - isExpired = stored.thumbprint !== thumbprint || stored.protocol !== location.protocol; - return stored.data && !isExpired ? stored.data : null; - }, - _initialize: function initialize() { - var that = this, local = this.local, deferred; - deferred = this.prefetch ? this._loadPrefetch(this.prefetch) : $.Deferred().resolve(); - local && deferred.done(addLocalToIndex); - this.transport = this.remote ? new Transport(this.remote) : null; - return this.initPromise = deferred.promise(); - function addLocalToIndex() { - that.add(_.isFunction(local) ? local() : local); - } - }, - initialize: function initialize(force) { - return !this.initPromise || force ? this._initialize() : this.initPromise; - }, - add: function add(data) { - this.index.add(data); - }, - get: function get(query, cb) { - var that = this, matches = [], cacheHit = false; - matches = this.index.get(query); - matches = this.sorter(matches).slice(0, this.limit); - if (matches.length < this.limit && this.transport) { - cacheHit = this._getFromRemote(query, returnRemoteMatches); - } - if (!cacheHit) { - (matches.length > 0 || !this.transport) && cb && cb(matches); - } - function returnRemoteMatches(remoteMatches) { - var matchesWithBackfill = matches.slice(0); - _.each(remoteMatches, function(remoteMatch) { - var isDuplicate; - isDuplicate = _.some(matchesWithBackfill, function(match) { - return that.dupDetector(remoteMatch, match); - }); - !isDuplicate && matchesWithBackfill.push(remoteMatch); - return matchesWithBackfill.length < that.limit; - }); - cb && cb(that.sorter(matchesWithBackfill)); - } - }, - clear: function clear() { - this.index.reset(); - }, - clearPrefetchCache: function clearPrefetchCache() { - this.storage && this.storage.clear(); - }, - clearRemoteCache: function clearRemoteCache() { - this.transport && Transport.resetCache(); - }, - ttAdapter: function ttAdapter() { - return _.bind(this.get, this); - } - }); - return Bloodhound; - function getSorter(sortFn) { - return _.isFunction(sortFn) ? sort : noSort; - function sort(array) { - return array.sort(sortFn); - } - function noSort(array) { - return array; - } - } - function ignoreDuplicates() { - return false; - } - })(this); - var html = { - wrapper: '<span class="twitter-typeahead"></span>', - dropdown: '<span class="tt-dropdown-menu"></span>', - dataset: '<div class="tt-dataset-%CLASS%"></div>', - suggestions: '<span class="tt-suggestions"></span>', - suggestion: '<div class="tt-suggestion"></div>' - }; - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - suggestions: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - if (_.isMsie() && _.isMsie() <= 7) { - _.mixin(css.input, { - marginTop: "-1px" - }); - } - var EventBus = function() { - var namespace = "typeahead:"; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - _.mixin(EventBus.prototype, { - trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); - } - }); - return EventBus; - }(); - var EventEmitter = function() { - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; - } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); - } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0; !cancelled && i < callbacks.length; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; - } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); - }; - } - }(); - var highlight = function(doc) { - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); - } - return !!match; - } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); - } - } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0; i < patterns.length; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o) { - var that = this, onBlur, onFocus, onKeydown, onInput; - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - if (!_.isMsie()) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); - }); - } - this.query = this.$input.val(); - this.$overflowHelper = buildOverflowHelper(this.$input); - } - Input.normalizeQuery = function(str) { - return (str || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); - }, - _onFocus: function onFocus() { - this.trigger("focused"); - }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } - }, - _onInput: function onInput() { - this._checkInputValue(); - }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault, hintValue, inputValue; - switch (keyName) { - case "tab": - hintValue = this.getHint(); - inputValue = this.getInputValue(); - preventDefault = hintValue && hintValue !== inputValue && !withModifier($e); - break; - - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkInputValue: function checkInputValue() { - var inputValue, areEquivalent, hasDifferentWhitespace; - inputValue = this.getInputValue(); - areEquivalent = areQueriesEquivalent(inputValue, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== inputValue.length : false; - if (!areEquivalent) { - this.trigger("queryChanged", this.query = inputValue); - } else if (hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - focus: function focus() { - this.$input.focus(); - }, - blur: function blur() { - this.$input.blur(); - }, - getQuery: function getQuery() { - return this.query; - }, - setQuery: function setQuery(query) { - this.query = query; - }, - getInputValue: function getInputValue() { - return this.$input.val(); - }, - setInputValue: function setInputValue(value, silent) { - this.$input.val(value); - silent ? this.clearHint() : this._checkInputValue(); - }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query, true); - }, - getHint: function getHint() { - return this.$hint.val(); - }, - setHint: function setHint(value) { - this.$hint.val(value); - }, - clearHint: function clearHint() { - this.setHint(""); - }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - getLanguageDirection: function getLanguageDirection() { - return (this.$input.css("direction") || "ltr").toLowerCase(); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; - }, - isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; - } - }); - return Input; - function buildOverflowHelper($input) { - return $('<pre aria-hidden="true"></pre>').css({ - position: "absolute", - visibility: "hidden", - whiteSpace: "pre", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; - } - }(); - var Dataset = function() { - var datasetKey = "ttDataset", valueKey = "ttValue", datumKey = "ttDatum"; - function Dataset(o) { - o = o || {}; - o.templates = o.templates || {}; - if (!o.source) { - $.error("missing source"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - this.query = null; - this.highlight = !!o.highlight; - this.name = o.name || _.getUniqueId(); - this.source = o.source; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.$el = $(html.dataset.replace("%CLASS%", this.name)); - } - Dataset.extractDatasetName = function extractDatasetName(el) { - return $(el).data(datasetKey); - }; - Dataset.extractValue = function extractDatum(el) { - return $(el).data(valueKey); - }; - Dataset.extractDatum = function extractDatum(el) { - return $(el).data(datumKey); - }; - _.mixin(Dataset.prototype, EventEmitter, { - _render: function render(query, suggestions) { - if (!this.$el) { - return; - } - var that = this, hasSuggestions; - this.$el.empty(); - hasSuggestions = suggestions && suggestions.length; - if (!hasSuggestions && this.templates.empty) { - this.$el.html(getEmptyHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } else if (hasSuggestions) { - this.$el.html(getSuggestionsHtml()).prepend(that.templates.header ? getHeaderHtml() : null).append(that.templates.footer ? getFooterHtml() : null); - } - this.trigger("rendered"); - function getEmptyHtml() { - return that.templates.empty({ - query: query, - isEmpty: true - }); - } - function getSuggestionsHtml() { - var $suggestions, nodes; - $suggestions = $(html.suggestions).css(css.suggestions); - nodes = _.map(suggestions, getSuggestionNode); - $suggestions.append.apply($suggestions, nodes); - that.highlight && highlight({ - node: $suggestions[0], - pattern: query - }); - return $suggestions; - function getSuggestionNode(suggestion) { - var $el; - $el = $(html.suggestion).append(that.templates.suggestion(suggestion)).data(datasetKey, that.name).data(valueKey, that.displayFn(suggestion)).data(datumKey, suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - return $el; - } - } - function getHeaderHtml() { - return that.templates.header({ - query: query, - isEmpty: !hasSuggestions - }); - } - function getFooterHtml() { - return that.templates.footer({ - query: query, - isEmpty: !hasSuggestions - }); - } - }, - getRoot: function getRoot() { - return this.$el; - }, - update: function update(query) { - var that = this; - this.query = query; - this.canceled = false; - this.source(query, render); - function render(suggestions) { - if (!that.canceled && query === that.query) { - that._render(query, suggestions); - } - } - }, - cancel: function cancel() { - this.canceled = true; - }, - clear: function clear() { - this.cancel(); - this.$el.empty(); - this.trigger("rendered"); - }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); - }, - destroy: function destroy() { - this.$el = null; - } - }); - return Dataset; - function getDisplayFn(display) { - display = display || "value"; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } - } - function getTemplates(templates, displayFn) { - return { - empty: templates.empty && _.templatify(templates.empty), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return "<p>" + displayFn(context) + "</p>"; - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Dropdown = function() { - function Dropdown(o) { - var that = this, onSuggestionClick, onSuggestionMouseEnter, onSuggestionMouseLeave; - o = o || {}; - if (!o.menu) { - $.error("menu is required"); - } - this.isOpen = false; - this.isEmpty = true; - this.datasets = _.map(o.datasets, initializeDataset); - onSuggestionClick = _.bind(this._onSuggestionClick, this); - onSuggestionMouseEnter = _.bind(this._onSuggestionMouseEnter, this); - onSuggestionMouseLeave = _.bind(this._onSuggestionMouseLeave, this); - this.$menu = $(o.menu).on("click.tt", ".tt-suggestion", onSuggestionClick).on("mouseenter.tt", ".tt-suggestion", onSuggestionMouseEnter).on("mouseleave.tt", ".tt-suggestion", onSuggestionMouseLeave); - _.each(this.datasets, function(dataset) { - that.$menu.append(dataset.getRoot()); - dataset.onSync("rendered", that._onRendered, that); - }); - } - _.mixin(Dropdown.prototype, EventEmitter, { - _onSuggestionClick: function onSuggestionClick($e) { - this.trigger("suggestionClicked", $($e.currentTarget)); - }, - _onSuggestionMouseEnter: function onSuggestionMouseEnter($e) { - this._removeCursor(); - this._setCursor($($e.currentTarget), true); - }, - _onSuggestionMouseLeave: function onSuggestionMouseLeave() { - this._removeCursor(); - }, - _onRendered: function onRendered() { - this.isEmpty = _.every(this.datasets, isDatasetEmpty); - this.isEmpty ? this._hide() : this.isOpen && this._show(); - this.trigger("datasetRendered"); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _hide: function() { - this.$menu.hide(); - }, - _show: function() { - this.$menu.css("display", "block"); - }, - _getSuggestions: function getSuggestions() { - return this.$menu.find(".tt-suggestion"); - }, - _getCursor: function getCursor() { - return this.$menu.find(".tt-cursor").first(); - }, - _setCursor: function setCursor($el, silent) { - $el.first().addClass("tt-cursor"); - !silent && this.trigger("cursorMoved"); - }, - _removeCursor: function removeCursor() { - this._getCursor().removeClass("tt-cursor"); - }, - _moveCursor: function moveCursor(increment) { - var $suggestions, $oldCursor, newCursorIndex, $newCursor; - if (!this.isOpen) { - return; - } - $oldCursor = this._getCursor(); - $suggestions = this._getSuggestions(); - this._removeCursor(); - newCursorIndex = $suggestions.index($oldCursor) + increment; - newCursorIndex = (newCursorIndex + 1) % ($suggestions.length + 1) - 1; - if (newCursorIndex === -1) { - this.trigger("cursorRemoved"); - return; - } else if (newCursorIndex < -1) { - newCursorIndex = $suggestions.length - 1; - } - this._setCursor($newCursor = $suggestions.eq(newCursorIndex)); - this._ensureVisible($newCursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, menuScrollTop, menuHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - menuScrollTop = this.$menu.scrollTop(); - menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10); - if (elTop < 0) { - this.$menu.scrollTop(menuScrollTop + elTop); - } else if (menuHeight < elBottom) { - this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); - } - }, - close: function close() { - if (this.isOpen) { - this.isOpen = false; - this._removeCursor(); - this._hide(); - this.trigger("closed"); - } - }, - open: function open() { - if (!this.isOpen) { - this.isOpen = true; - !this.isEmpty && this._show(); - this.trigger("opened"); - } - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$menu.css(dir === "ltr" ? css.ltr : css.rtl); - }, - moveCursorUp: function moveCursorUp() { - this._moveCursor(-1); - }, - moveCursorDown: function moveCursorDown() { - this._moveCursor(+1); - }, - getDatumForSuggestion: function getDatumForSuggestion($el) { - var datum = null; - if ($el.length) { - datum = { - raw: Dataset.extractDatum($el), - value: Dataset.extractValue($el), - datasetName: Dataset.extractDatasetName($el) - }; - } - return datum; - }, - getDatumForCursor: function getDatumForCursor() { - return this.getDatumForSuggestion(this._getCursor().first()); - }, - getDatumForTopSuggestion: function getDatumForTopSuggestion() { - return this.getDatumForSuggestion(this._getSuggestions().first()); - }, - update: function update(query) { - _.each(this.datasets, updateDataset); - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.isEmpty = true; - function clearDataset(dataset) { - dataset.clear(); - } - }, - isVisible: function isVisible() { - return this.isOpen && !this.isEmpty; - }, - destroy: function destroy() { - this.$menu.off(".tt"); - this.$menu = null; - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Dropdown; - function initializeDataset(oDataset) { - return new Dataset(oDataset); - } - }(); - var Typeahead = function() { - var attrsKey = "ttAttrs"; - function Typeahead(o) { - var $menu, $input, $hint; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - this.isActivated = false; - this.autoselect = !!o.autoselect; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.$node = buildDomStructure(o.input, o.withHint); - $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-input"); - $hint = this.$node.find(".tt-hint"); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - this.eventBus = o.eventBus || new EventBus({ - el: $input - }); - this.dropdown = new Dropdown({ - menu: $menu, - datasets: o.datasets - }).onSync("suggestionClicked", this._onSuggestionClicked, this).onSync("cursorMoved", this._onCursorMoved, this).onSync("cursorRemoved", this._onCursorRemoved, this).onSync("opened", this._onOpened, this).onSync("closed", this._onClosed, this).onAsync("datasetRendered", this._onDatasetRendered, this); - this.input = new Input({ - input: $input, - hint: $hint - }).onSync("focused", this._onFocused, this).onSync("blurred", this._onBlurred, this).onSync("enterKeyed", this._onEnterKeyed, this).onSync("tabKeyed", this._onTabKeyed, this).onSync("escKeyed", this._onEscKeyed, this).onSync("upKeyed", this._onUpKeyed, this).onSync("downKeyed", this._onDownKeyed, this).onSync("leftKeyed", this._onLeftKeyed, this).onSync("rightKeyed", this._onRightKeyed, this).onSync("queryChanged", this._onQueryChanged, this).onSync("whitespaceChanged", this._onWhitespaceChanged, this); - this._setLanguageDirection(); - } - _.mixin(Typeahead.prototype, { - _onSuggestionClicked: function onSuggestionClicked(type, $el) { - var datum; - if (datum = this.dropdown.getDatumForSuggestion($el)) { - this._select(datum); - } - }, - _onCursorMoved: function onCursorMoved() { - var datum = this.dropdown.getDatumForCursor(); - this.input.setInputValue(datum.value, true); - this.eventBus.trigger("cursorchanged", datum.raw, datum.datasetName); - }, - _onCursorRemoved: function onCursorRemoved() { - this.input.resetInputValue(); - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered() { - this._updateHint(); - }, - _onOpened: function onOpened() { - this._updateHint(); - this.eventBus.trigger("opened"); - }, - _onClosed: function onClosed() { - this.input.clearHint(); - this.eventBus.trigger("closed"); - }, - _onFocused: function onFocused() { - this.isActivated = true; - this.dropdown.open(); - }, - _onBlurred: function onBlurred() { - this.isActivated = false; - this.dropdown.empty(); - this.dropdown.close(); - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var cursorDatum, topSuggestionDatum; - cursorDatum = this.dropdown.getDatumForCursor(); - topSuggestionDatum = this.dropdown.getDatumForTopSuggestion(); - if (cursorDatum) { - this._select(cursorDatum); - $e.preventDefault(); - } else if (this.autoselect && topSuggestionDatum) { - this._select(topSuggestionDatum); - $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var datum; - if (datum = this.dropdown.getDatumForCursor()) { - this._select(datum); - $e.preventDefault(); - } else { - this._autocomplete(true); - } - }, - _onEscKeyed: function onEscKeyed() { - this.dropdown.close(); - this.input.resetInputValue(); - }, - _onUpKeyed: function onUpKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorUp(); - this.dropdown.open(); - }, - _onDownKeyed: function onDownKeyed() { - var query = this.input.getQuery(); - this.dropdown.isEmpty && query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.moveCursorDown(); - this.dropdown.open(); - }, - _onLeftKeyed: function onLeftKeyed() { - this.dir === "rtl" && this._autocomplete(); - }, - _onRightKeyed: function onRightKeyed() { - this.dir === "ltr" && this._autocomplete(); - }, - _onQueryChanged: function onQueryChanged(e, query) { - this.input.clearHintIfInvalid(); - query.length >= this.minLength ? this.dropdown.update(query) : this.dropdown.empty(); - this.dropdown.open(); - this._setLanguageDirection(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - this.dropdown.open(); - }, - _setLanguageDirection: function setLanguageDirection() { - var dir; - if (this.dir !== (dir = this.input.getLanguageDirection())) { - this.dir = dir; - this.$node.css("direction", dir); - this.dropdown.setLanguageDirection(dir); - } - }, - _updateHint: function updateHint() { - var datum, val, query, escapedQuery, frontMatchRegEx, match; - datum = this.dropdown.getDatumForTopSuggestion(); - if (datum && this.dropdown.isVisible() && !this.input.hasOverflow()) { - val = this.input.getInputValue(); - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(datum.value); - match ? this.input.setHint(val + match[1]) : this.input.clearHint(); - } else { - this.input.clearHint(); - } - }, - _autocomplete: function autocomplete(laxCursor) { - var hint, query, isCursorAtEnd, datum; - hint = this.input.getHint(); - query = this.input.getQuery(); - isCursorAtEnd = laxCursor || this.input.isCursorAtEnd(); - if (hint && query !== hint && isCursorAtEnd) { - datum = this.dropdown.getDatumForTopSuggestion(); - datum && this.input.setInputValue(datum.value); - this.eventBus.trigger("autocompleted", datum.raw, datum.datasetName); - } - }, - _select: function select(datum) { - this.input.setQuery(datum.value); - this.input.setInputValue(datum.value, true); - this._setLanguageDirection(); - this.eventBus.trigger("selected", datum.raw, datum.datasetName); - this.dropdown.close(); - _.defer(_.bind(this.dropdown.empty, this.dropdown)); - }, - open: function open() { - this.dropdown.open(); - }, - close: function close() { - this.dropdown.close(); - }, - setVal: function setVal(val) { - if (this.isActivated) { - this.input.setInputValue(val); - } else { - this.input.setQuery(val); - this.input.setInputValue(val, true); - } - this._setLanguageDirection(); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - destroy: function destroy() { - this.input.destroy(); - this.dropdown.destroy(); - destroyDomStructure(this.$node); - this.$node = null; - } - }); - return Typeahead; - function buildDomStructure(input, withHint) { - var $input, $wrapper, $dropdown, $hint; - $input = $(input); - $wrapper = $(html.wrapper).css(css.wrapper); - $dropdown = $(html.dropdown).css(css.dropdown); - $hint = $input.clone().css(css.hint).css(getBackgroundStyles($input)); - $hint.val("").removeData().addClass("tt-hint").removeAttr("id name placeholder").prop("disabled", true).attr({ - autocomplete: "off", - spellcheck: "false" - }); - $input.data(attrsKey, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass("tt-input").attr({ - autocomplete: "off", - spellcheck: false - }).css(withHint ? css.input : css.inputWithNoHint); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input.wrap($wrapper).parent().prepend(withHint ? $hint : null).append($dropdown); - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function destroyDomStructure($node) { - var $input = $node.find(".tt-input"); - _.each($input.data(attrsKey), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.detach().removeData(attrsKey).removeClass("tt-input").insertAfter($node); - $node.remove(); - } - }(); - (function() { - var old, typeaheadKey, methods; - old = $.fn.typeahead; - typeaheadKey = "ttTypeahead"; - methods = { - initialize: function initialize(o, datasets) { - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - return this.each(attach); - function attach() { - var $input = $(this), eventBus, typeahead; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - typeahead = new Typeahead({ - input: $input, - eventBus: eventBus = new EventBus({ - el: $input - }), - withHint: _.isUndefined(o.hint) ? true : !!o.hint, - minLength: o.minLength, - autoselect: o.autoselect, - datasets: datasets - }); - $input.data(typeaheadKey, typeahead); - } - }, - open: function open() { - return this.each(openTypeahead); - function openTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.open(); - } - } - }, - close: function close() { - return this.each(closeTypeahead); - function closeTypeahead() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.close(); - } - } - }, - val: function val(newVal) { - return !arguments.length ? getVal(this.first()) : this.each(setVal); - function setVal() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.setVal(newVal); - } - } - function getVal($input) { - var typeahead, query; - if (typeahead = $input.data(typeaheadKey)) { - query = typeahead.getVal(); - } - return query; - } - }, - destroy: function destroy() { - return this.each(unattach); - function unattach() { - var $input = $(this), typeahead; - if (typeahead = $input.data(typeaheadKey)) { - typeahead.destroy(); - $input.removeData(typeaheadKey); - } - } - } - }; - $.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - })(); -})(window.jQuery); \ No newline at end of file diff --git a/src/UI/JsLibraries/zero.clipboard.js b/src/UI/JsLibraries/zero.clipboard.js deleted file mode 100644 index dd44ac46a..000000000 --- a/src/UI/JsLibraries/zero.clipboard.js +++ /dev/null @@ -1,2581 +0,0 @@ -/*! - * ZeroClipboard - * The ZeroClipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie and a JavaScript interface. - * Copyright (c) 2009-2014 Jon Rohan, James M. Greene - * Licensed MIT - * http://zeroclipboard.org/ - * v2.2.0 - */ -(function(window, undefined) { - "use strict"; - /** - * Store references to critically important global functions that may be - * overridden on certain web pages. - */ - var _window = window, _document = _window.document, _navigator = _window.navigator, _setTimeout = _window.setTimeout, _clearTimeout = _window.clearTimeout, _setInterval = _window.setInterval, _clearInterval = _window.clearInterval, _getComputedStyle = _window.getComputedStyle, _encodeURIComponent = _window.encodeURIComponent, _ActiveXObject = _window.ActiveXObject, _Error = _window.Error, _parseInt = _window.Number.parseInt || _window.parseInt, _parseFloat = _window.Number.parseFloat || _window.parseFloat, _isNaN = _window.Number.isNaN || _window.isNaN, _now = _window.Date.now, _keys = _window.Object.keys, _defineProperty = _window.Object.defineProperty, _hasOwn = _window.Object.prototype.hasOwnProperty, _slice = _window.Array.prototype.slice, _unwrap = function() { - var unwrapper = function(el) { - return el; - }; - if (typeof _window.wrap === "function" && typeof _window.unwrap === "function") { - try { - var div = _document.createElement("div"); - var unwrappedDiv = _window.unwrap(div); - if (div.nodeType === 1 && unwrappedDiv && unwrappedDiv.nodeType === 1) { - unwrapper = _window.unwrap; - } - } catch (e) {} - } - return unwrapper; - }(); - /** - * Convert an `arguments` object into an Array. - * - * @returns The arguments as an Array - * @private - */ - var _args = function(argumentsObj) { - return _slice.call(argumentsObj, 0); - }; - /** - * Shallow-copy the owned, enumerable properties of one object over to another, similar to jQuery's `$.extend`. - * - * @returns The target object, augmented - * @private - */ - var _extend = function() { - var i, len, arg, prop, src, copy, args = _args(arguments), target = args[0] || {}; - for (i = 1, len = args.length; i < len; i++) { - if ((arg = args[i]) != null) { - for (prop in arg) { - if (_hasOwn.call(arg, prop)) { - src = target[prop]; - copy = arg[prop]; - if (target !== copy && copy !== undefined) { - target[prop] = copy; - } - } - } - } - } - return target; - }; - /** - * Return a deep copy of the source object or array. - * - * @returns Object or Array - * @private - */ - var _deepCopy = function(source) { - var copy, i, len, prop; - if (typeof source !== "object" || source == null || typeof source.nodeType === "number") { - copy = source; - } else if (typeof source.length === "number") { - copy = []; - for (i = 0, len = source.length; i < len; i++) { - if (_hasOwn.call(source, i)) { - copy[i] = _deepCopy(source[i]); - } - } - } else { - copy = {}; - for (prop in source) { - if (_hasOwn.call(source, prop)) { - copy[prop] = _deepCopy(source[prop]); - } - } - } - return copy; - }; - /** - * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to keep. - * The inverse of `_omit`, mostly. The big difference is that these properties do NOT need to be enumerable to - * be kept. - * - * @returns A new filtered object. - * @private - */ - var _pick = function(obj, keys) { - var newObj = {}; - for (var i = 0, len = keys.length; i < len; i++) { - if (keys[i] in obj) { - newObj[keys[i]] = obj[keys[i]]; - } - } - return newObj; - }; - /** - * Makes a shallow copy of `obj` (like `_extend`) but filters its properties based on a list of `keys` to omit. - * The inverse of `_pick`. - * - * @returns A new filtered object. - * @private - */ - var _omit = function(obj, keys) { - var newObj = {}; - for (var prop in obj) { - if (keys.indexOf(prop) === -1) { - newObj[prop] = obj[prop]; - } - } - return newObj; - }; - /** - * Remove all owned, enumerable properties from an object. - * - * @returns The original object without its owned, enumerable properties. - * @private - */ - var _deleteOwnProperties = function(obj) { - if (obj) { - for (var prop in obj) { - if (_hasOwn.call(obj, prop)) { - delete obj[prop]; - } - } - } - return obj; - }; - /** - * Determine if an element is contained within another element. - * - * @returns Boolean - * @private - */ - var _containedBy = function(el, ancestorEl) { - if (el && el.nodeType === 1 && el.ownerDocument && ancestorEl && (ancestorEl.nodeType === 1 && ancestorEl.ownerDocument && ancestorEl.ownerDocument === el.ownerDocument || ancestorEl.nodeType === 9 && !ancestorEl.ownerDocument && ancestorEl === el.ownerDocument)) { - do { - if (el === ancestorEl) { - return true; - } - el = el.parentNode; - } while (el); - } - return false; - }; - /** - * Get the URL path's parent directory. - * - * @returns String or `undefined` - * @private - */ - var _getDirPathOfUrl = function(url) { - var dir; - if (typeof url === "string" && url) { - dir = url.split("#")[0].split("?")[0]; - dir = url.slice(0, url.lastIndexOf("/") + 1); - } - return dir; - }; - /** - * Get the current script's URL by throwing an `Error` and analyzing it. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrlFromErrorStack = function(stack) { - var url, matches; - if (typeof stack === "string" && stack) { - matches = stack.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); - if (matches && matches[1]) { - url = matches[1]; - } else { - matches = stack.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/); - if (matches && matches[1]) { - url = matches[1]; - } - } - } - return url; - }; - /** - * Get the current script's URL by throwing an `Error` and analyzing it. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrlFromError = function() { - var url, err; - try { - throw new _Error(); - } catch (e) { - err = e; - } - if (err) { - url = err.sourceURL || err.fileName || _getCurrentScriptUrlFromErrorStack(err.stack); - } - return url; - }; - /** - * Get the current script's URL. - * - * @returns String or `undefined` - * @private - */ - var _getCurrentScriptUrl = function() { - var jsPath, scripts, i; - if (_document.currentScript && (jsPath = _document.currentScript.src)) { - return jsPath; - } - scripts = _document.getElementsByTagName("script"); - if (scripts.length === 1) { - return scripts[0].src || undefined; - } - if ("readyState" in scripts[0]) { - for (i = scripts.length; i--; ) { - if (scripts[i].readyState === "interactive" && (jsPath = scripts[i].src)) { - return jsPath; - } - } - } - if (_document.readyState === "loading" && (jsPath = scripts[scripts.length - 1].src)) { - return jsPath; - } - if (jsPath = _getCurrentScriptUrlFromError()) { - return jsPath; - } - return undefined; - }; - /** - * Get the unanimous parent directory of ALL script tags. - * If any script tags are either (a) inline or (b) from differing parent - * directories, this method must return `undefined`. - * - * @returns String or `undefined` - * @private - */ - var _getUnanimousScriptParentDir = function() { - var i, jsDir, jsPath, scripts = _document.getElementsByTagName("script"); - for (i = scripts.length; i--; ) { - if (!(jsPath = scripts[i].src)) { - jsDir = null; - break; - } - jsPath = _getDirPathOfUrl(jsPath); - if (jsDir == null) { - jsDir = jsPath; - } else if (jsDir !== jsPath) { - jsDir = null; - break; - } - } - return jsDir || undefined; - }; - /** - * Get the presumed location of the "ZeroClipboard.swf" file, based on the location - * of the executing JavaScript file (e.g. "ZeroClipboard.js", etc.). - * - * @returns String - * @private - */ - var _getDefaultSwfPath = function() { - var jsDir = _getDirPathOfUrl(_getCurrentScriptUrl()) || _getUnanimousScriptParentDir() || ""; - return jsDir + "ZeroClipboard.swf"; - }; - /** - * Keep track of if the page is framed (in an `iframe`). This can never change. - * @private - */ - var _pageIsFramed = function() { - return window.opener == null && (!!window.top && window != window.top || !!window.parent && window != window.parent); - }(); - /** - * Keep track of the state of the Flash object. - * @private - */ - var _flashState = { - bridge: null, - version: "0.0.0", - pluginType: "unknown", - disabled: null, - outdated: null, - sandboxed: null, - unavailable: null, - degraded: null, - deactivated: null, - overdue: null, - ready: null - }; - /** - * The minimum Flash Player version required to use ZeroClipboard completely. - * @readonly - * @private - */ - var _minimumFlashVersion = "11.0.0"; - /** - * The ZeroClipboard library version number, as reported by Flash, at the time the SWF was compiled. - */ - var _zcSwfVersion; - /** - * Keep track of all event listener registrations. - * @private - */ - var _handlers = {}; - /** - * Keep track of the currently activated element. - * @private - */ - var _currentElement; - /** - * Keep track of the element that was activated when a `copy` process started. - * @private - */ - var _copyTarget; - /** - * Keep track of data for the pending clipboard transaction. - * @private - */ - var _clipData = {}; - /** - * Keep track of data formats for the pending clipboard transaction. - * @private - */ - var _clipDataFormatMap = null; - /** - * Keep track of the Flash availability check timeout. - * @private - */ - var _flashCheckTimeout = 0; - /** - * Keep track of SWF network errors interval polling. - * @private - */ - var _swfFallbackCheckInterval = 0; - /** - * The `message` store for events - * @private - */ - var _eventMessages = { - ready: "Flash communication is established", - error: { - "flash-disabled": "Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.", - "flash-outdated": "Flash is too outdated to support ZeroClipboard", - "flash-sandboxed": "Attempting to run Flash in a sandboxed iframe, which is impossible", - "flash-unavailable": "Flash is unable to communicate bidirectionally with JavaScript", - "flash-degraded": "Flash is unable to preserve data fidelity when communicating with JavaScript", - "flash-deactivated": "Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.", - "flash-overdue": "Flash communication was established but NOT within the acceptable time limit", - "version-mismatch": "ZeroClipboard JS version number does not match ZeroClipboard SWF version number", - "clipboard-error": "At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard", - "config-mismatch": "ZeroClipboard configuration does not match Flash's reality", - "swf-not-found": "The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity" - } - }; - /** - * The `name`s of `error` events that can only occur is Flash has at least - * been able to load the SWF successfully. - * @private - */ - var _errorsThatOnlyOccurAfterFlashLoads = [ "flash-unavailable", "flash-degraded", "flash-overdue", "version-mismatch", "config-mismatch", "clipboard-error" ]; - /** - * The `name`s of `error` events that should likely result in the `_flashState` - * variable's property values being updated. - * @private - */ - var _flashStateErrorNames = [ "flash-disabled", "flash-outdated", "flash-sandboxed", "flash-unavailable", "flash-degraded", "flash-deactivated", "flash-overdue" ]; - /** - * A RegExp to match the `name` property of `error` events related to Flash. - * @private - */ - var _flashStateErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.map(function(errorName) { - return errorName.replace(/^flash-/, ""); - }).join("|") + ")$"); - /** - * A RegExp to match the `name` property of `error` events related to Flash, - * which is enabled. - * @private - */ - var _flashStateEnabledErrorNameMatchingRegex = new RegExp("^flash-(" + _flashStateErrorNames.slice(1).map(function(errorName) { - return errorName.replace(/^flash-/, ""); - }).join("|") + ")$"); - /** - * ZeroClipboard configuration defaults for the Core module. - * @private - */ - var _globalConfig = { - swfPath: _getDefaultSwfPath(), - trustedDomains: window.location.host ? [ window.location.host ] : [], - cacheBust: true, - forceEnhancedClipboard: false, - flashLoadTimeout: 3e4, - autoActivate: true, - bubbleEvents: true, - containerId: "global-zeroclipboard-html-bridge", - containerClass: "global-zeroclipboard-container", - swfObjectId: "global-zeroclipboard-flash-bridge", - hoverClass: "zeroclipboard-is-hover", - activeClass: "zeroclipboard-is-active", - forceHandCursor: false, - title: null, - zIndex: 999999999 - }; - /** - * The underlying implementation of `ZeroClipboard.config`. - * @private - */ - var _config = function(options) { - if (typeof options === "object" && options !== null) { - for (var prop in options) { - if (_hasOwn.call(options, prop)) { - if (/^(?:forceHandCursor|title|zIndex|bubbleEvents)$/.test(prop)) { - _globalConfig[prop] = options[prop]; - } else if (_flashState.bridge == null) { - if (prop === "containerId" || prop === "swfObjectId") { - if (_isValidHtml4Id(options[prop])) { - _globalConfig[prop] = options[prop]; - } else { - throw new Error("The specified `" + prop + "` value is not valid as an HTML4 Element ID"); - } - } else { - _globalConfig[prop] = options[prop]; - } - } - } - } - } - if (typeof options === "string" && options) { - if (_hasOwn.call(_globalConfig, options)) { - return _globalConfig[options]; - } - return; - } - return _deepCopy(_globalConfig); - }; - /** - * The underlying implementation of `ZeroClipboard.state`. - * @private - */ - var _state = function() { - _detectSandbox(); - return { - browser: _pick(_navigator, [ "userAgent", "platform", "appName" ]), - flash: _omit(_flashState, [ "bridge" ]), - zeroclipboard: { - version: ZeroClipboard.version, - config: ZeroClipboard.config() - } - }; - }; - /** - * The underlying implementation of `ZeroClipboard.isFlashUnusable`. - * @private - */ - var _isFlashUnusable = function() { - return !!(_flashState.disabled || _flashState.outdated || _flashState.sandboxed || _flashState.unavailable || _flashState.degraded || _flashState.deactivated); - }; - /** - * The underlying implementation of `ZeroClipboard.on`. - * @private - */ - var _on = function(eventType, listener) { - var i, len, events, added = {}; - if (typeof eventType === "string" && eventType) { - events = eventType.toLowerCase().split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - ZeroClipboard.on(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].replace(/^on/, ""); - added[eventType] = true; - if (!_handlers[eventType]) { - _handlers[eventType] = []; - } - _handlers[eventType].push(listener); - } - if (added.ready && _flashState.ready) { - ZeroClipboard.emit({ - type: "ready" - }); - } - if (added.error) { - for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { - if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")] === true) { - ZeroClipboard.emit({ - type: "error", - name: _flashStateErrorNames[i] - }); - break; - } - } - if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { - ZeroClipboard.emit({ - type: "error", - name: "version-mismatch", - jsVersion: ZeroClipboard.version, - swfVersion: _zcSwfVersion - }); - } - } - } - return ZeroClipboard; - }; - /** - * The underlying implementation of `ZeroClipboard.off`. - * @private - */ - var _off = function(eventType, listener) { - var i, len, foundIndex, events, perEventHandlers; - if (arguments.length === 0) { - events = _keys(_handlers); - } else if (typeof eventType === "string" && eventType) { - events = eventType.split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - ZeroClipboard.off(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].toLowerCase().replace(/^on/, ""); - perEventHandlers = _handlers[eventType]; - if (perEventHandlers && perEventHandlers.length) { - if (listener) { - foundIndex = perEventHandlers.indexOf(listener); - while (foundIndex !== -1) { - perEventHandlers.splice(foundIndex, 1); - foundIndex = perEventHandlers.indexOf(listener, foundIndex); - } - } else { - perEventHandlers.length = 0; - } - } - } - } - return ZeroClipboard; - }; - /** - * The underlying implementation of `ZeroClipboard.handlers`. - * @private - */ - var _listeners = function(eventType) { - var copy; - if (typeof eventType === "string" && eventType) { - copy = _deepCopy(_handlers[eventType]) || null; - } else { - copy = _deepCopy(_handlers); - } - return copy; - }; - /** - * The underlying implementation of `ZeroClipboard.emit`. - * @private - */ - var _emit = function(event) { - var eventCopy, returnVal, tmp; - event = _createEvent(event); - if (!event) { - return; - } - if (_preprocessEvent(event)) { - return; - } - if (event.type === "ready" && _flashState.overdue === true) { - return ZeroClipboard.emit({ - type: "error", - name: "flash-overdue" - }); - } - eventCopy = _extend({}, event); - _dispatchCallbacks.call(this, eventCopy); - if (event.type === "copy") { - tmp = _mapClipDataToFlash(_clipData); - returnVal = tmp.data; - _clipDataFormatMap = tmp.formatMap; - } - return returnVal; - }; - /** - * The underlying implementation of `ZeroClipboard.create`. - * @private - */ - var _create = function() { - var previousState = _flashState.sandboxed; - _detectSandbox(); - if (typeof _flashState.ready !== "boolean") { - _flashState.ready = false; - } - if (_flashState.sandboxed !== previousState && _flashState.sandboxed === true) { - _flashState.ready = false; - ZeroClipboard.emit({ - type: "error", - name: "flash-sandboxed" - }); - } else if (!ZeroClipboard.isFlashUnusable() && _flashState.bridge === null) { - var maxWait = _globalConfig.flashLoadTimeout; - if (typeof maxWait === "number" && maxWait >= 0) { - _flashCheckTimeout = _setTimeout(function() { - if (typeof _flashState.deactivated !== "boolean") { - _flashState.deactivated = true; - } - if (_flashState.deactivated === true) { - ZeroClipboard.emit({ - type: "error", - name: "flash-deactivated" - }); - } - }, maxWait); - } - _flashState.overdue = false; - _embedSwf(); - } - }; - /** - * The underlying implementation of `ZeroClipboard.destroy`. - * @private - */ - var _destroy = function() { - ZeroClipboard.clearData(); - ZeroClipboard.blur(); - ZeroClipboard.emit("destroy"); - _unembedSwf(); - ZeroClipboard.off(); - }; - /** - * The underlying implementation of `ZeroClipboard.setData`. - * @private - */ - var _setData = function(format, data) { - var dataObj; - if (typeof format === "object" && format && typeof data === "undefined") { - dataObj = format; - ZeroClipboard.clearData(); - } else if (typeof format === "string" && format) { - dataObj = {}; - dataObj[format] = data; - } else { - return; - } - for (var dataFormat in dataObj) { - if (typeof dataFormat === "string" && dataFormat && _hasOwn.call(dataObj, dataFormat) && typeof dataObj[dataFormat] === "string" && dataObj[dataFormat]) { - _clipData[dataFormat] = dataObj[dataFormat]; - } - } - }; - /** - * The underlying implementation of `ZeroClipboard.clearData`. - * @private - */ - var _clearData = function(format) { - if (typeof format === "undefined") { - _deleteOwnProperties(_clipData); - _clipDataFormatMap = null; - } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { - delete _clipData[format]; - } - }; - /** - * The underlying implementation of `ZeroClipboard.getData`. - * @private - */ - var _getData = function(format) { - if (typeof format === "undefined") { - return _deepCopy(_clipData); - } else if (typeof format === "string" && _hasOwn.call(_clipData, format)) { - return _clipData[format]; - } - }; - /** - * The underlying implementation of `ZeroClipboard.focus`/`ZeroClipboard.activate`. - * @private - */ - var _focus = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - if (_currentElement) { - _removeClass(_currentElement, _globalConfig.activeClass); - if (_currentElement !== element) { - _removeClass(_currentElement, _globalConfig.hoverClass); - } - } - _currentElement = element; - _addClass(element, _globalConfig.hoverClass); - var newTitle = element.getAttribute("title") || _globalConfig.title; - if (typeof newTitle === "string" && newTitle) { - var htmlBridge = _getHtmlBridge(_flashState.bridge); - if (htmlBridge) { - htmlBridge.setAttribute("title", newTitle); - } - } - var useHandCursor = _globalConfig.forceHandCursor === true || _getStyle(element, "cursor") === "pointer"; - _setHandCursor(useHandCursor); - _reposition(); - }; - /** - * The underlying implementation of `ZeroClipboard.blur`/`ZeroClipboard.deactivate`. - * @private - */ - var _blur = function() { - var htmlBridge = _getHtmlBridge(_flashState.bridge); - if (htmlBridge) { - htmlBridge.removeAttribute("title"); - htmlBridge.style.left = "0px"; - htmlBridge.style.top = "-9999px"; - htmlBridge.style.width = "1px"; - htmlBridge.style.height = "1px"; - } - if (_currentElement) { - _removeClass(_currentElement, _globalConfig.hoverClass); - _removeClass(_currentElement, _globalConfig.activeClass); - _currentElement = null; - } - }; - /** - * The underlying implementation of `ZeroClipboard.activeElement`. - * @private - */ - var _activeElement = function() { - return _currentElement || null; - }; - /** - * Check if a value is a valid HTML4 `ID` or `Name` token. - * @private - */ - var _isValidHtml4Id = function(id) { - return typeof id === "string" && id && /^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(id); - }; - /** - * Create or update an `event` object, based on the `eventType`. - * @private - */ - var _createEvent = function(event) { - var eventType; - if (typeof event === "string" && event) { - eventType = event; - event = {}; - } else if (typeof event === "object" && event && typeof event.type === "string" && event.type) { - eventType = event.type; - } - if (!eventType) { - return; - } - eventType = eventType.toLowerCase(); - if (!event.target && (/^(copy|aftercopy|_click)$/.test(eventType) || eventType === "error" && event.name === "clipboard-error")) { - event.target = _copyTarget; - } - _extend(event, { - type: eventType, - target: event.target || _currentElement || null, - relatedTarget: event.relatedTarget || null, - currentTarget: _flashState && _flashState.bridge || null, - timeStamp: event.timeStamp || _now() || null - }); - var msg = _eventMessages[event.type]; - if (event.type === "error" && event.name && msg) { - msg = msg[event.name]; - } - if (msg) { - event.message = msg; - } - if (event.type === "ready") { - _extend(event, { - target: null, - version: _flashState.version - }); - } - if (event.type === "error") { - if (_flashStateErrorNameMatchingRegex.test(event.name)) { - _extend(event, { - target: null, - minimumVersion: _minimumFlashVersion - }); - } - if (_flashStateEnabledErrorNameMatchingRegex.test(event.name)) { - _extend(event, { - version: _flashState.version - }); - } - } - if (event.type === "copy") { - event.clipboardData = { - setData: ZeroClipboard.setData, - clearData: ZeroClipboard.clearData - }; - } - if (event.type === "aftercopy") { - event = _mapClipResultsFromFlash(event, _clipDataFormatMap); - } - if (event.target && !event.relatedTarget) { - event.relatedTarget = _getRelatedTarget(event.target); - } - return _addMouseData(event); - }; - /** - * Get a relatedTarget from the target's `data-clipboard-target` attribute - * @private - */ - var _getRelatedTarget = function(targetEl) { - var relatedTargetId = targetEl && targetEl.getAttribute && targetEl.getAttribute("data-clipboard-target"); - return relatedTargetId ? _document.getElementById(relatedTargetId) : null; - }; - /** - * Add element and position data to `MouseEvent` instances - * @private - */ - var _addMouseData = function(event) { - if (event && /^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { - var srcElement = event.target; - var fromElement = event.type === "_mouseover" && event.relatedTarget ? event.relatedTarget : undefined; - var toElement = event.type === "_mouseout" && event.relatedTarget ? event.relatedTarget : undefined; - var pos = _getElementPosition(srcElement); - var screenLeft = _window.screenLeft || _window.screenX || 0; - var screenTop = _window.screenTop || _window.screenY || 0; - var scrollLeft = _document.body.scrollLeft + _document.documentElement.scrollLeft; - var scrollTop = _document.body.scrollTop + _document.documentElement.scrollTop; - var pageX = pos.left + (typeof event._stageX === "number" ? event._stageX : 0); - var pageY = pos.top + (typeof event._stageY === "number" ? event._stageY : 0); - var clientX = pageX - scrollLeft; - var clientY = pageY - scrollTop; - var screenX = screenLeft + clientX; - var screenY = screenTop + clientY; - var moveX = typeof event.movementX === "number" ? event.movementX : 0; - var moveY = typeof event.movementY === "number" ? event.movementY : 0; - delete event._stageX; - delete event._stageY; - _extend(event, { - srcElement: srcElement, - fromElement: fromElement, - toElement: toElement, - screenX: screenX, - screenY: screenY, - pageX: pageX, - pageY: pageY, - clientX: clientX, - clientY: clientY, - x: clientX, - y: clientY, - movementX: moveX, - movementY: moveY, - offsetX: 0, - offsetY: 0, - layerX: 0, - layerY: 0 - }); - } - return event; - }; - /** - * Determine if an event's registered handlers should be execute synchronously or asynchronously. - * - * @returns {boolean} - * @private - */ - var _shouldPerformAsync = function(event) { - var eventType = event && typeof event.type === "string" && event.type || ""; - return !/^(?:(?:before)?copy|destroy)$/.test(eventType); - }; - /** - * Control if a callback should be executed asynchronously or not. - * - * @returns `undefined` - * @private - */ - var _dispatchCallback = function(func, context, args, async) { - if (async) { - _setTimeout(function() { - func.apply(context, args); - }, 0); - } else { - func.apply(context, args); - } - }; - /** - * Handle the actual dispatching of events to client instances. - * - * @returns `undefined` - * @private - */ - var _dispatchCallbacks = function(event) { - if (!(typeof event === "object" && event && event.type)) { - return; - } - var async = _shouldPerformAsync(event); - var wildcardTypeHandlers = _handlers["*"] || []; - var specificTypeHandlers = _handlers[event.type] || []; - var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); - if (handlers && handlers.length) { - var i, len, func, context, eventCopy, originalContext = this; - for (i = 0, len = handlers.length; i < len; i++) { - func = handlers[i]; - context = originalContext; - if (typeof func === "string" && typeof _window[func] === "function") { - func = _window[func]; - } - if (typeof func === "object" && func && typeof func.handleEvent === "function") { - context = func; - func = func.handleEvent; - } - if (typeof func === "function") { - eventCopy = _extend({}, event); - _dispatchCallback(func, context, [ eventCopy ], async); - } - } - } - return this; - }; - /** - * Check an `error` event's `name` property to see if Flash has - * already loaded, which rules out possible `iframe` sandboxing. - * @private - */ - var _getSandboxStatusFromErrorEvent = function(event) { - var isSandboxed = null; - if (_pageIsFramed === false || event && event.type === "error" && event.name && _errorsThatOnlyOccurAfterFlashLoads.indexOf(event.name) !== -1) { - isSandboxed = false; - } - return isSandboxed; - }; - /** - * Preprocess any special behaviors, reactions, or state changes after receiving this event. - * Executes only once per event emitted, NOT once per client. - * @private - */ - var _preprocessEvent = function(event) { - var element = event.target || _currentElement || null; - var sourceIsSwf = event._source === "swf"; - delete event._source; - switch (event.type) { - case "error": - var isSandboxed = event.name === "flash-sandboxed" || _getSandboxStatusFromErrorEvent(event); - if (typeof isSandboxed === "boolean") { - _flashState.sandboxed = isSandboxed; - } - if (_flashStateErrorNames.indexOf(event.name) !== -1) { - _extend(_flashState, { - disabled: event.name === "flash-disabled", - outdated: event.name === "flash-outdated", - unavailable: event.name === "flash-unavailable", - degraded: event.name === "flash-degraded", - deactivated: event.name === "flash-deactivated", - overdue: event.name === "flash-overdue", - ready: false - }); - } else if (event.name === "version-mismatch") { - _zcSwfVersion = event.swfVersion; - _extend(_flashState, { - disabled: false, - outdated: false, - unavailable: false, - degraded: false, - deactivated: false, - overdue: false, - ready: false - }); - } - _clearTimeoutsAndPolling(); - break; - - case "ready": - _zcSwfVersion = event.swfVersion; - var wasDeactivated = _flashState.deactivated === true; - _extend(_flashState, { - disabled: false, - outdated: false, - sandboxed: false, - unavailable: false, - degraded: false, - deactivated: false, - overdue: wasDeactivated, - ready: !wasDeactivated - }); - _clearTimeoutsAndPolling(); - break; - - case "beforecopy": - _copyTarget = element; - break; - - case "copy": - var textContent, htmlContent, targetEl = event.relatedTarget; - if (!(_clipData["text/html"] || _clipData["text/plain"]) && targetEl && (htmlContent = targetEl.value || targetEl.outerHTML || targetEl.innerHTML) && (textContent = targetEl.value || targetEl.textContent || targetEl.innerText)) { - event.clipboardData.clearData(); - event.clipboardData.setData("text/plain", textContent); - if (htmlContent !== textContent) { - event.clipboardData.setData("text/html", htmlContent); - } - } else if (!_clipData["text/plain"] && event.target && (textContent = event.target.getAttribute("data-clipboard-text"))) { - event.clipboardData.clearData(); - event.clipboardData.setData("text/plain", textContent); - } - break; - - case "aftercopy": - _queueEmitClipboardErrors(event); - ZeroClipboard.clearData(); - if (element && element !== _safeActiveElement() && element.focus) { - element.focus(); - } - break; - - case "_mouseover": - ZeroClipboard.focus(element); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { - _fireMouseEvent(_extend({}, event, { - type: "mouseenter", - bubbles: false, - cancelable: false - })); - } - _fireMouseEvent(_extend({}, event, { - type: "mouseover" - })); - } - break; - - case "_mouseout": - ZeroClipboard.blur(); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - if (element && element !== event.relatedTarget && !_containedBy(event.relatedTarget, element)) { - _fireMouseEvent(_extend({}, event, { - type: "mouseleave", - bubbles: false, - cancelable: false - })); - } - _fireMouseEvent(_extend({}, event, { - type: "mouseout" - })); - } - break; - - case "_mousedown": - _addClass(element, _globalConfig.activeClass); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_mouseup": - _removeClass(element, _globalConfig.activeClass); - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_click": - _copyTarget = null; - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - - case "_mousemove": - if (_globalConfig.bubbleEvents === true && sourceIsSwf) { - _fireMouseEvent(_extend({}, event, { - type: event.type.slice(1) - })); - } - break; - } - if (/^_(?:click|mouse(?:over|out|down|up|move))$/.test(event.type)) { - return true; - } - }; - /** - * Check an "aftercopy" event for clipboard errors and emit a corresponding "error" event. - * @private - */ - var _queueEmitClipboardErrors = function(aftercopyEvent) { - if (aftercopyEvent.errors && aftercopyEvent.errors.length > 0) { - var errorEvent = _deepCopy(aftercopyEvent); - _extend(errorEvent, { - type: "error", - name: "clipboard-error" - }); - delete errorEvent.success; - _setTimeout(function() { - ZeroClipboard.emit(errorEvent); - }, 0); - } - }; - /** - * Dispatch a synthetic MouseEvent. - * - * @returns `undefined` - * @private - */ - var _fireMouseEvent = function(event) { - if (!(event && typeof event.type === "string" && event)) { - return; - } - var e, target = event.target || null, doc = target && target.ownerDocument || _document, defaults = { - view: doc.defaultView || _window, - canBubble: true, - cancelable: true, - detail: event.type === "click" ? 1 : 0, - button: typeof event.which === "number" ? event.which - 1 : typeof event.button === "number" ? event.button : doc.createEvent ? 0 : 1 - }, args = _extend(defaults, event); - if (!target) { - return; - } - if (doc.createEvent && target.dispatchEvent) { - args = [ args.type, args.canBubble, args.cancelable, args.view, args.detail, args.screenX, args.screenY, args.clientX, args.clientY, args.ctrlKey, args.altKey, args.shiftKey, args.metaKey, args.button, args.relatedTarget ]; - e = doc.createEvent("MouseEvents"); - if (e.initMouseEvent) { - e.initMouseEvent.apply(e, args); - e._source = "js"; - target.dispatchEvent(e); - } - } - }; - /** - * Continuously poll the DOM until either: - * (a) the fallback content becomes visible, or - * (b) we receive an event from SWF (handled elsewhere) - * - * IMPORTANT: - * This is NOT a necessary check but it can result in significantly faster - * detection of bad `swfPath` configuration and/or network/server issues [in - * supported browsers] than waiting for the entire `flashLoadTimeout` duration - * to elapse before detecting that the SWF cannot be loaded. The detection - * duration can be anywhere from 10-30 times faster [in supported browsers] by - * using this approach. - * - * @returns `undefined` - * @private - */ - var _watchForSwfFallbackContent = function() { - var maxWait = _globalConfig.flashLoadTimeout; - if (typeof maxWait === "number" && maxWait >= 0) { - var pollWait = Math.min(1e3, maxWait / 10); - var fallbackContentId = _globalConfig.swfObjectId + "_fallbackContent"; - _swfFallbackCheckInterval = _setInterval(function() { - var el = _document.getElementById(fallbackContentId); - if (_isElementVisible(el)) { - _clearTimeoutsAndPolling(); - _flashState.deactivated = null; - ZeroClipboard.emit({ - type: "error", - name: "swf-not-found" - }); - } - }, pollWait); - } - }; - /** - * Create the HTML bridge element to embed the Flash object into. - * @private - */ - var _createHtmlBridge = function() { - var container = _document.createElement("div"); - container.id = _globalConfig.containerId; - container.className = _globalConfig.containerClass; - container.style.position = "absolute"; - container.style.left = "0px"; - container.style.top = "-9999px"; - container.style.width = "1px"; - container.style.height = "1px"; - container.style.zIndex = "" + _getSafeZIndex(_globalConfig.zIndex); - return container; - }; - /** - * Get the HTML element container that wraps the Flash bridge object/element. - * @private - */ - var _getHtmlBridge = function(flashBridge) { - var htmlBridge = flashBridge && flashBridge.parentNode; - while (htmlBridge && htmlBridge.nodeName === "OBJECT" && htmlBridge.parentNode) { - htmlBridge = htmlBridge.parentNode; - } - return htmlBridge || null; - }; - /** - * Create the SWF object. - * - * @returns The SWF object reference. - * @private - */ - var _embedSwf = function() { - var len, flashBridge = _flashState.bridge, container = _getHtmlBridge(flashBridge); - if (!flashBridge) { - var allowScriptAccess = _determineScriptAccess(_window.location.host, _globalConfig); - var allowNetworking = allowScriptAccess === "never" ? "none" : "all"; - var flashvars = _vars(_extend({ - jsVersion: ZeroClipboard.version - }, _globalConfig)); - var swfUrl = _globalConfig.swfPath + _cacheBust(_globalConfig.swfPath, _globalConfig); - container = _createHtmlBridge(); - var divToBeReplaced = _document.createElement("div"); - container.appendChild(divToBeReplaced); - _document.body.appendChild(container); - var tmpDiv = _document.createElement("div"); - var usingActiveX = _flashState.pluginType === "activex"; - tmpDiv.innerHTML = '<object id="' + _globalConfig.swfObjectId + '" name="' + _globalConfig.swfObjectId + '" ' + 'width="100%" height="100%" ' + (usingActiveX ? 'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"' : 'type="application/x-shockwave-flash" data="' + swfUrl + '"') + ">" + (usingActiveX ? '<param name="movie" value="' + swfUrl + '"/>' : "") + '<param name="allowScriptAccess" value="' + allowScriptAccess + '"/>' + '<param name="allowNetworking" value="' + allowNetworking + '"/>' + '<param name="menu" value="false"/>' + '<param name="wmode" value="transparent"/>' + '<param name="flashvars" value="' + flashvars + '"/>' + '<div id="' + _globalConfig.swfObjectId + '_fallbackContent"> </div>' + "</object>"; - flashBridge = tmpDiv.firstChild; - tmpDiv = null; - _unwrap(flashBridge).ZeroClipboard = ZeroClipboard; - container.replaceChild(flashBridge, divToBeReplaced); - _watchForSwfFallbackContent(); - } - if (!flashBridge) { - flashBridge = _document[_globalConfig.swfObjectId]; - if (flashBridge && (len = flashBridge.length)) { - flashBridge = flashBridge[len - 1]; - } - if (!flashBridge && container) { - flashBridge = container.firstChild; - } - } - _flashState.bridge = flashBridge || null; - return flashBridge; - }; - /** - * Destroy the SWF object. - * @private - */ - var _unembedSwf = function() { - var flashBridge = _flashState.bridge; - if (flashBridge) { - var htmlBridge = _getHtmlBridge(flashBridge); - if (htmlBridge) { - if (_flashState.pluginType === "activex" && "readyState" in flashBridge) { - flashBridge.style.display = "none"; - (function removeSwfFromIE() { - if (flashBridge.readyState === 4) { - for (var prop in flashBridge) { - if (typeof flashBridge[prop] === "function") { - flashBridge[prop] = null; - } - } - if (flashBridge.parentNode) { - flashBridge.parentNode.removeChild(flashBridge); - } - if (htmlBridge.parentNode) { - htmlBridge.parentNode.removeChild(htmlBridge); - } - } else { - _setTimeout(removeSwfFromIE, 10); - } - })(); - } else { - if (flashBridge.parentNode) { - flashBridge.parentNode.removeChild(flashBridge); - } - if (htmlBridge.parentNode) { - htmlBridge.parentNode.removeChild(htmlBridge); - } - } - } - _clearTimeoutsAndPolling(); - _flashState.ready = null; - _flashState.bridge = null; - _flashState.deactivated = null; - _zcSwfVersion = undefined; - } - }; - /** - * Map the data format names of the "clipData" to Flash-friendly names. - * - * @returns A new transformed object. - * @private - */ - var _mapClipDataToFlash = function(clipData) { - var newClipData = {}, formatMap = {}; - if (!(typeof clipData === "object" && clipData)) { - return; - } - for (var dataFormat in clipData) { - if (dataFormat && _hasOwn.call(clipData, dataFormat) && typeof clipData[dataFormat] === "string" && clipData[dataFormat]) { - switch (dataFormat.toLowerCase()) { - case "text/plain": - case "text": - case "air:text": - case "flash:text": - newClipData.text = clipData[dataFormat]; - formatMap.text = dataFormat; - break; - - case "text/html": - case "html": - case "air:html": - case "flash:html": - newClipData.html = clipData[dataFormat]; - formatMap.html = dataFormat; - break; - - case "application/rtf": - case "text/rtf": - case "rtf": - case "richtext": - case "air:rtf": - case "flash:rtf": - newClipData.rtf = clipData[dataFormat]; - formatMap.rtf = dataFormat; - break; - - default: - break; - } - } - } - return { - data: newClipData, - formatMap: formatMap - }; - }; - /** - * Map the data format names from Flash-friendly names back to their original "clipData" names (via a format mapping). - * - * @returns A new transformed object. - * @private - */ - var _mapClipResultsFromFlash = function(clipResults, formatMap) { - if (!(typeof clipResults === "object" && clipResults && typeof formatMap === "object" && formatMap)) { - return clipResults; - } - var newResults = {}; - for (var prop in clipResults) { - if (_hasOwn.call(clipResults, prop)) { - if (prop === "errors") { - newResults[prop] = clipResults[prop] ? clipResults[prop].slice() : []; - for (var i = 0, len = newResults[prop].length; i < len; i++) { - newResults[prop][i].format = formatMap[newResults[prop][i].format]; - } - } else if (prop !== "success" && prop !== "data") { - newResults[prop] = clipResults[prop]; - } else { - newResults[prop] = {}; - var tmpHash = clipResults[prop]; - for (var dataFormat in tmpHash) { - if (dataFormat && _hasOwn.call(tmpHash, dataFormat) && _hasOwn.call(formatMap, dataFormat)) { - newResults[prop][formatMap[dataFormat]] = tmpHash[dataFormat]; - } - } - } - } - } - return newResults; - }; - /** - * Will look at a path, and will create a "?noCache={time}" or "&noCache={time}" - * query param string to return. Does NOT append that string to the original path. - * This is useful because ExternalInterface often breaks when a Flash SWF is cached. - * - * @returns The `noCache` query param with necessary "?"/"&" prefix. - * @private - */ - var _cacheBust = function(path, options) { - var cacheBust = options == null || options && options.cacheBust === true; - if (cacheBust) { - return (path.indexOf("?") === -1 ? "?" : "&") + "noCache=" + _now(); - } else { - return ""; - } - }; - /** - * Creates a query string for the FlashVars param. - * Does NOT include the cache-busting query param. - * - * @returns FlashVars query string - * @private - */ - var _vars = function(options) { - var i, len, domain, domains, str = "", trustedOriginsExpanded = []; - if (options.trustedDomains) { - if (typeof options.trustedDomains === "string") { - domains = [ options.trustedDomains ]; - } else if (typeof options.trustedDomains === "object" && "length" in options.trustedDomains) { - domains = options.trustedDomains; - } - } - if (domains && domains.length) { - for (i = 0, len = domains.length; i < len; i++) { - if (_hasOwn.call(domains, i) && domains[i] && typeof domains[i] === "string") { - domain = _extractDomain(domains[i]); - if (!domain) { - continue; - } - if (domain === "*") { - trustedOriginsExpanded.length = 0; - trustedOriginsExpanded.push(domain); - break; - } - trustedOriginsExpanded.push.apply(trustedOriginsExpanded, [ domain, "//" + domain, _window.location.protocol + "//" + domain ]); - } - } - } - if (trustedOriginsExpanded.length) { - str += "trustedOrigins=" + _encodeURIComponent(trustedOriginsExpanded.join(",")); - } - if (options.forceEnhancedClipboard === true) { - str += (str ? "&" : "") + "forceEnhancedClipboard=true"; - } - if (typeof options.swfObjectId === "string" && options.swfObjectId) { - str += (str ? "&" : "") + "swfObjectId=" + _encodeURIComponent(options.swfObjectId); - } - if (typeof options.jsVersion === "string" && options.jsVersion) { - str += (str ? "&" : "") + "jsVersion=" + _encodeURIComponent(options.jsVersion); - } - return str; - }; - /** - * Extract the domain (e.g. "github.com") from an origin (e.g. "https://github.com") or - * URL (e.g. "https://github.com/zeroclipboard/zeroclipboard/"). - * - * @returns the domain - * @private - */ - var _extractDomain = function(originOrUrl) { - if (originOrUrl == null || originOrUrl === "") { - return null; - } - originOrUrl = originOrUrl.replace(/^\s+|\s+$/g, ""); - if (originOrUrl === "") { - return null; - } - var protocolIndex = originOrUrl.indexOf("//"); - originOrUrl = protocolIndex === -1 ? originOrUrl : originOrUrl.slice(protocolIndex + 2); - var pathIndex = originOrUrl.indexOf("/"); - originOrUrl = pathIndex === -1 ? originOrUrl : protocolIndex === -1 || pathIndex === 0 ? null : originOrUrl.slice(0, pathIndex); - if (originOrUrl && originOrUrl.slice(-4).toLowerCase() === ".swf") { - return null; - } - return originOrUrl || null; - }; - /** - * Set `allowScriptAccess` based on `trustedDomains` and `window.location.host` vs. `swfPath`. - * - * @returns The appropriate script access level. - * @private - */ - var _determineScriptAccess = function() { - var _extractAllDomains = function(origins) { - var i, len, tmp, resultsArray = []; - if (typeof origins === "string") { - origins = [ origins ]; - } - if (!(typeof origins === "object" && origins && typeof origins.length === "number")) { - return resultsArray; - } - for (i = 0, len = origins.length; i < len; i++) { - if (_hasOwn.call(origins, i) && (tmp = _extractDomain(origins[i]))) { - if (tmp === "*") { - resultsArray.length = 0; - resultsArray.push("*"); - break; - } - if (resultsArray.indexOf(tmp) === -1) { - resultsArray.push(tmp); - } - } - } - return resultsArray; - }; - return function(currentDomain, configOptions) { - var swfDomain = _extractDomain(configOptions.swfPath); - if (swfDomain === null) { - swfDomain = currentDomain; - } - var trustedDomains = _extractAllDomains(configOptions.trustedDomains); - var len = trustedDomains.length; - if (len > 0) { - if (len === 1 && trustedDomains[0] === "*") { - return "always"; - } - if (trustedDomains.indexOf(currentDomain) !== -1) { - if (len === 1 && currentDomain === swfDomain) { - return "sameDomain"; - } - return "always"; - } - } - return "never"; - }; - }(); - /** - * Get the currently active/focused DOM element. - * - * @returns the currently active/focused element, or `null` - * @private - */ - var _safeActiveElement = function() { - try { - return _document.activeElement; - } catch (err) { - return null; - } - }; - /** - * Add a class to an element, if it doesn't already have it. - * - * @returns The element, with its new class added. - * @private - */ - var _addClass = function(element, value) { - var c, cl, className, classNames = []; - if (typeof value === "string" && value) { - classNames = value.split(/\s+/); - } - if (element && element.nodeType === 1 && classNames.length > 0) { - if (element.classList) { - for (c = 0, cl = classNames.length; c < cl; c++) { - element.classList.add(classNames[c]); - } - } else if (element.hasOwnProperty("className")) { - className = " " + element.className + " "; - for (c = 0, cl = classNames.length; c < cl; c++) { - if (className.indexOf(" " + classNames[c] + " ") === -1) { - className += classNames[c] + " "; - } - } - element.className = className.replace(/^\s+|\s+$/g, ""); - } - } - return element; - }; - /** - * Remove a class from an element, if it has it. - * - * @returns The element, with its class removed. - * @private - */ - var _removeClass = function(element, value) { - var c, cl, className, classNames = []; - if (typeof value === "string" && value) { - classNames = value.split(/\s+/); - } - if (element && element.nodeType === 1 && classNames.length > 0) { - if (element.classList && element.classList.length > 0) { - for (c = 0, cl = classNames.length; c < cl; c++) { - element.classList.remove(classNames[c]); - } - } else if (element.className) { - className = (" " + element.className + " ").replace(/[\r\n\t]/g, " "); - for (c = 0, cl = classNames.length; c < cl; c++) { - className = className.replace(" " + classNames[c] + " ", " "); - } - element.className = className.replace(/^\s+|\s+$/g, ""); - } - } - return element; - }; - /** - * Attempt to interpret the element's CSS styling. If `prop` is `"cursor"`, - * then we assume that it should be a hand ("pointer") cursor if the element - * is an anchor element ("a" tag). - * - * @returns The computed style property. - * @private - */ - var _getStyle = function(el, prop) { - var value = _getComputedStyle(el, null).getPropertyValue(prop); - if (prop === "cursor") { - if (!value || value === "auto") { - if (el.nodeName === "A") { - return "pointer"; - } - } - } - return value; - }; - /** - * Get the absolutely positioned coordinates of a DOM element. - * - * @returns Object containing the element's position, width, and height. - * @private - */ - var _getElementPosition = function(el) { - var pos = { - left: 0, - top: 0, - width: 0, - height: 0 - }; - if (el.getBoundingClientRect) { - var elRect = el.getBoundingClientRect(); - var pageXOffset = _window.pageXOffset; - var pageYOffset = _window.pageYOffset; - var leftBorderWidth = _document.documentElement.clientLeft || 0; - var topBorderWidth = _document.documentElement.clientTop || 0; - var leftBodyOffset = 0; - var topBodyOffset = 0; - if (_getStyle(_document.body, "position") === "relative") { - var bodyRect = _document.body.getBoundingClientRect(); - var htmlRect = _document.documentElement.getBoundingClientRect(); - leftBodyOffset = bodyRect.left - htmlRect.left || 0; - topBodyOffset = bodyRect.top - htmlRect.top || 0; - } - pos.left = elRect.left + pageXOffset - leftBorderWidth - leftBodyOffset; - pos.top = elRect.top + pageYOffset - topBorderWidth - topBodyOffset; - pos.width = "width" in elRect ? elRect.width : elRect.right - elRect.left; - pos.height = "height" in elRect ? elRect.height : elRect.bottom - elRect.top; - } - return pos; - }; - /** - * Determine is an element is visible somewhere within the document (page). - * - * @returns Boolean - * @private - */ - var _isElementVisible = function(el) { - if (!el) { - return false; - } - var styles = _getComputedStyle(el, null); - var hasCssHeight = _parseFloat(styles.height) > 0; - var hasCssWidth = _parseFloat(styles.width) > 0; - var hasCssTop = _parseFloat(styles.top) >= 0; - var hasCssLeft = _parseFloat(styles.left) >= 0; - var cssKnows = hasCssHeight && hasCssWidth && hasCssTop && hasCssLeft; - var rect = cssKnows ? null : _getElementPosition(el); - var isVisible = styles.display !== "none" && styles.visibility !== "collapse" && (cssKnows || !!rect && (hasCssHeight || rect.height > 0) && (hasCssWidth || rect.width > 0) && (hasCssTop || rect.top >= 0) && (hasCssLeft || rect.left >= 0)); - return isVisible; - }; - /** - * Clear all existing timeouts and interval polling delegates. - * - * @returns `undefined` - * @private - */ - var _clearTimeoutsAndPolling = function() { - _clearTimeout(_flashCheckTimeout); - _flashCheckTimeout = 0; - _clearInterval(_swfFallbackCheckInterval); - _swfFallbackCheckInterval = 0; - }; - /** - * Reposition the Flash object to cover the currently activated element. - * - * @returns `undefined` - * @private - */ - var _reposition = function() { - var htmlBridge; - if (_currentElement && (htmlBridge = _getHtmlBridge(_flashState.bridge))) { - var pos = _getElementPosition(_currentElement); - _extend(htmlBridge.style, { - width: pos.width + "px", - height: pos.height + "px", - top: pos.top + "px", - left: pos.left + "px", - zIndex: "" + _getSafeZIndex(_globalConfig.zIndex) - }); - } - }; - /** - * Sends a signal to the Flash object to display the hand cursor if `true`. - * - * @returns `undefined` - * @private - */ - var _setHandCursor = function(enabled) { - if (_flashState.ready === true) { - if (_flashState.bridge && typeof _flashState.bridge.setHandCursor === "function") { - _flashState.bridge.setHandCursor(enabled); - } else { - _flashState.ready = false; - } - } - }; - /** - * Get a safe value for `zIndex` - * - * @returns an integer, or "auto" - * @private - */ - var _getSafeZIndex = function(val) { - if (/^(?:auto|inherit)$/.test(val)) { - return val; - } - var zIndex; - if (typeof val === "number" && !_isNaN(val)) { - zIndex = val; - } else if (typeof val === "string") { - zIndex = _getSafeZIndex(_parseInt(val, 10)); - } - return typeof zIndex === "number" ? zIndex : "auto"; - }; - /** - * Attempt to detect if ZeroClipboard is executing inside of a sandboxed iframe. - * If it is, Flash Player cannot be used, so ZeroClipboard is dead in the water. - * - * @see {@link http://lists.w3.org/Archives/Public/public-whatwg-archive/2014Dec/0002.html} - * @see {@link https://github.com/zeroclipboard/zeroclipboard/issues/511} - * @see {@link http://zeroclipboard.org/test-iframes.html} - * - * @returns `true` (is sandboxed), `false` (is not sandboxed), or `null` (uncertain) - * @private - */ - var _detectSandbox = function(doNotReassessFlashSupport) { - var effectiveScriptOrigin, frame, frameError, previousState = _flashState.sandboxed, isSandboxed = null; - doNotReassessFlashSupport = doNotReassessFlashSupport === true; - if (_pageIsFramed === false) { - isSandboxed = false; - } else { - try { - frame = window.frameElement || null; - } catch (e) { - frameError = { - name: e.name, - message: e.message - }; - } - if (frame && frame.nodeType === 1 && frame.nodeName === "IFRAME") { - try { - isSandboxed = frame.hasAttribute("sandbox"); - } catch (e) { - isSandboxed = null; - } - } else { - try { - effectiveScriptOrigin = document.domain || null; - } catch (e) { - effectiveScriptOrigin = null; - } - if (effectiveScriptOrigin === null || frameError && frameError.name === "SecurityError" && /(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(frameError.message.toLowerCase())) { - isSandboxed = true; - } - } - } - _flashState.sandboxed = isSandboxed; - if (previousState !== isSandboxed && !doNotReassessFlashSupport) { - _detectFlashSupport(_ActiveXObject); - } - return isSandboxed; - }; - /** - * Detect the Flash Player status, version, and plugin type. - * - * @see {@link https://code.google.com/p/doctype-mirror/wiki/ArticleDetectFlash#The_code} - * @see {@link http://stackoverflow.com/questions/12866060/detecting-pepper-ppapi-flash-with-javascript} - * - * @returns `undefined` - * @private - */ - var _detectFlashSupport = function(ActiveXObject) { - var plugin, ax, mimeType, hasFlash = false, isActiveX = false, isPPAPI = false, flashVersion = ""; - /** - * Derived from Apple's suggested sniffer. - * @param {String} desc e.g. "Shockwave Flash 7.0 r61" - * @returns {String} "7.0.61" - * @private - */ - function parseFlashVersion(desc) { - var matches = desc.match(/[\d]+/g); - matches.length = 3; - return matches.join("."); - } - function isPepperFlash(flashPlayerFileName) { - return !!flashPlayerFileName && (flashPlayerFileName = flashPlayerFileName.toLowerCase()) && (/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(flashPlayerFileName) || flashPlayerFileName.slice(-13) === "chrome.plugin"); - } - function inspectPlugin(plugin) { - if (plugin) { - hasFlash = true; - if (plugin.version) { - flashVersion = parseFlashVersion(plugin.version); - } - if (!flashVersion && plugin.description) { - flashVersion = parseFlashVersion(plugin.description); - } - if (plugin.filename) { - isPPAPI = isPepperFlash(plugin.filename); - } - } - } - if (_navigator.plugins && _navigator.plugins.length) { - plugin = _navigator.plugins["Shockwave Flash"]; - inspectPlugin(plugin); - if (_navigator.plugins["Shockwave Flash 2.0"]) { - hasFlash = true; - flashVersion = "2.0.0.11"; - } - } else if (_navigator.mimeTypes && _navigator.mimeTypes.length) { - mimeType = _navigator.mimeTypes["application/x-shockwave-flash"]; - plugin = mimeType && mimeType.enabledPlugin; - inspectPlugin(plugin); - } else if (typeof ActiveXObject !== "undefined") { - isActiveX = true; - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7"); - hasFlash = true; - flashVersion = parseFlashVersion(ax.GetVariable("$version")); - } catch (e1) { - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash.6"); - hasFlash = true; - flashVersion = "6.0.21"; - } catch (e2) { - try { - ax = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"); - hasFlash = true; - flashVersion = parseFlashVersion(ax.GetVariable("$version")); - } catch (e3) { - isActiveX = false; - } - } - } - } - _flashState.disabled = hasFlash !== true; - _flashState.outdated = flashVersion && _parseFloat(flashVersion) < _parseFloat(_minimumFlashVersion); - _flashState.version = flashVersion || "0.0.0"; - _flashState.pluginType = isPPAPI ? "pepper" : isActiveX ? "activex" : hasFlash ? "netscape" : "unknown"; - }; - /** - * Invoke the Flash detection algorithms immediately upon inclusion so we're not waiting later. - */ - _detectFlashSupport(_ActiveXObject); - /** - * Always assess the `sandboxed` state of the page at important Flash-related moments. - */ - _detectSandbox(true); - /** - * A shell constructor for `ZeroClipboard` client instances. - * - * @constructor - */ - var ZeroClipboard = function() { - if (!(this instanceof ZeroClipboard)) { - return new ZeroClipboard(); - } - if (typeof ZeroClipboard._createClient === "function") { - ZeroClipboard._createClient.apply(this, _args(arguments)); - } - }; - /** - * The ZeroClipboard library's version number. - * - * @static - * @readonly - * @property {string} - */ - _defineProperty(ZeroClipboard, "version", { - value: "2.2.0", - writable: false, - configurable: true, - enumerable: true - }); - /** - * Update or get a copy of the ZeroClipboard global configuration. - * Returns a copy of the current/updated configuration. - * - * @returns Object - * @static - */ - ZeroClipboard.config = function() { - return _config.apply(this, _args(arguments)); - }; - /** - * Diagnostic method that describes the state of the browser, Flash Player, and ZeroClipboard. - * - * @returns Object - * @static - */ - ZeroClipboard.state = function() { - return _state.apply(this, _args(arguments)); - }; - /** - * Check if Flash is unusable for any reason: disabled, outdated, deactivated, etc. - * - * @returns Boolean - * @static - */ - ZeroClipboard.isFlashUnusable = function() { - return _isFlashUnusable.apply(this, _args(arguments)); - }; - /** - * Register an event listener. - * - * @returns `ZeroClipboard` - * @static - */ - ZeroClipboard.on = function() { - return _on.apply(this, _args(arguments)); - }; - /** - * Unregister an event listener. - * If no `listener` function/object is provided, it will unregister all listeners for the provided `eventType`. - * If no `eventType` is provided, it will unregister all listeners for every event type. - * - * @returns `ZeroClipboard` - * @static - */ - ZeroClipboard.off = function() { - return _off.apply(this, _args(arguments)); - }; - /** - * Retrieve event listeners for an `eventType`. - * If no `eventType` is provided, it will retrieve all listeners for every event type. - * - * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` - */ - ZeroClipboard.handlers = function() { - return _listeners.apply(this, _args(arguments)); - }; - /** - * Event emission receiver from the Flash object, forwarding to any registered JavaScript event listeners. - * - * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. - * @static - */ - ZeroClipboard.emit = function() { - return _emit.apply(this, _args(arguments)); - }; - /** - * Create and embed the Flash object. - * - * @returns The Flash object - * @static - */ - ZeroClipboard.create = function() { - return _create.apply(this, _args(arguments)); - }; - /** - * Self-destruct and clean up everything, including the embedded Flash object. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.destroy = function() { - return _destroy.apply(this, _args(arguments)); - }; - /** - * Set the pending data for clipboard injection. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.setData = function() { - return _setData.apply(this, _args(arguments)); - }; - /** - * Clear the pending data for clipboard injection. - * If no `format` is provided, all pending data formats will be cleared. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.clearData = function() { - return _clearData.apply(this, _args(arguments)); - }; - /** - * Get a copy of the pending data for clipboard injection. - * If no `format` is provided, a copy of ALL pending data formats will be returned. - * - * @returns `String` or `Object` - * @static - */ - ZeroClipboard.getData = function() { - return _getData.apply(this, _args(arguments)); - }; - /** - * Sets the current HTML object that the Flash object should overlay. This will put the global - * Flash object on top of the current element; depending on the setup, this may also set the - * pending clipboard text data as well as the Flash object's wrapping element's title attribute - * based on the underlying HTML element and ZeroClipboard configuration. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.focus = ZeroClipboard.activate = function() { - return _focus.apply(this, _args(arguments)); - }; - /** - * Un-overlays the Flash object. This will put the global Flash object off-screen; depending on - * the setup, this may also unset the Flash object's wrapping element's title attribute based on - * the underlying HTML element and ZeroClipboard configuration. - * - * @returns `undefined` - * @static - */ - ZeroClipboard.blur = ZeroClipboard.deactivate = function() { - return _blur.apply(this, _args(arguments)); - }; - /** - * Returns the currently focused/"activated" HTML element that the Flash object is wrapping. - * - * @returns `HTMLElement` or `null` - * @static - */ - ZeroClipboard.activeElement = function() { - return _activeElement.apply(this, _args(arguments)); - }; - /** - * Keep track of the ZeroClipboard client instance counter. - */ - var _clientIdCounter = 0; - /** - * Keep track of the state of the client instances. - * - * Entry structure: - * _clientMeta[client.id] = { - * instance: client, - * elements: [], - * handlers: {} - * }; - */ - var _clientMeta = {}; - /** - * Keep track of the ZeroClipboard clipped elements counter. - */ - var _elementIdCounter = 0; - /** - * Keep track of the state of the clipped element relationships to clients. - * - * Entry structure: - * _elementMeta[element.zcClippingId] = [client1.id, client2.id]; - */ - var _elementMeta = {}; - /** - * Keep track of the state of the mouse event handlers for clipped elements. - * - * Entry structure: - * _mouseHandlers[element.zcClippingId] = { - * mouseover: function(event) {}, - * mouseout: function(event) {}, - * mouseenter: function(event) {}, - * mouseleave: function(event) {}, - * mousemove: function(event) {} - * }; - */ - var _mouseHandlers = {}; - /** - * Extending the ZeroClipboard configuration defaults for the Client module. - */ - _extend(_globalConfig, { - autoActivate: true - }); - /** - * The real constructor for `ZeroClipboard` client instances. - * @private - */ - var _clientConstructor = function(elements) { - var client = this; - client.id = "" + _clientIdCounter++; - _clientMeta[client.id] = { - instance: client, - elements: [], - handlers: {} - }; - if (elements) { - client.clip(elements); - } - ZeroClipboard.on("*", function(event) { - return client.emit(event); - }); - ZeroClipboard.on("destroy", function() { - client.destroy(); - }); - ZeroClipboard.create(); - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.on`. - * @private - */ - var _clientOn = function(eventType, listener) { - var i, len, events, added = {}, meta = _clientMeta[this.id], handlers = meta && meta.handlers; - if (!meta) { - throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance"); - } - if (typeof eventType === "string" && eventType) { - events = eventType.toLowerCase().split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - this.on(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].replace(/^on/, ""); - added[eventType] = true; - if (!handlers[eventType]) { - handlers[eventType] = []; - } - handlers[eventType].push(listener); - } - if (added.ready && _flashState.ready) { - this.emit({ - type: "ready", - client: this - }); - } - if (added.error) { - for (i = 0, len = _flashStateErrorNames.length; i < len; i++) { - if (_flashState[_flashStateErrorNames[i].replace(/^flash-/, "")]) { - this.emit({ - type: "error", - name: _flashStateErrorNames[i], - client: this - }); - break; - } - } - if (_zcSwfVersion !== undefined && ZeroClipboard.version !== _zcSwfVersion) { - this.emit({ - type: "error", - name: "version-mismatch", - jsVersion: ZeroClipboard.version, - swfVersion: _zcSwfVersion - }); - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.off`. - * @private - */ - var _clientOff = function(eventType, listener) { - var i, len, foundIndex, events, perEventHandlers, meta = _clientMeta[this.id], handlers = meta && meta.handlers; - if (!handlers) { - return this; - } - if (arguments.length === 0) { - events = _keys(handlers); - } else if (typeof eventType === "string" && eventType) { - events = eventType.split(/\s+/); - } else if (typeof eventType === "object" && eventType && typeof listener === "undefined") { - for (i in eventType) { - if (_hasOwn.call(eventType, i) && typeof i === "string" && i && typeof eventType[i] === "function") { - this.off(i, eventType[i]); - } - } - } - if (events && events.length) { - for (i = 0, len = events.length; i < len; i++) { - eventType = events[i].toLowerCase().replace(/^on/, ""); - perEventHandlers = handlers[eventType]; - if (perEventHandlers && perEventHandlers.length) { - if (listener) { - foundIndex = perEventHandlers.indexOf(listener); - while (foundIndex !== -1) { - perEventHandlers.splice(foundIndex, 1); - foundIndex = perEventHandlers.indexOf(listener, foundIndex); - } - } else { - perEventHandlers.length = 0; - } - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.handlers`. - * @private - */ - var _clientListeners = function(eventType) { - var copy = null, handlers = _clientMeta[this.id] && _clientMeta[this.id].handlers; - if (handlers) { - if (typeof eventType === "string" && eventType) { - copy = handlers[eventType] ? handlers[eventType].slice(0) : []; - } else { - copy = _deepCopy(handlers); - } - } - return copy; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.emit`. - * @private - */ - var _clientEmit = function(event) { - if (_clientShouldEmit.call(this, event)) { - if (typeof event === "object" && event && typeof event.type === "string" && event.type) { - event = _extend({}, event); - } - var eventCopy = _extend({}, _createEvent(event), { - client: this - }); - _clientDispatchCallbacks.call(this, eventCopy); - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.clip`. - * @private - */ - var _clientClip = function(elements) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance"); - } - elements = _prepClip(elements); - for (var i = 0; i < elements.length; i++) { - if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { - if (!elements[i].zcClippingId) { - elements[i].zcClippingId = "zcClippingId_" + _elementIdCounter++; - _elementMeta[elements[i].zcClippingId] = [ this.id ]; - if (_globalConfig.autoActivate === true) { - _addMouseHandlers(elements[i]); - } - } else if (_elementMeta[elements[i].zcClippingId].indexOf(this.id) === -1) { - _elementMeta[elements[i].zcClippingId].push(this.id); - } - var clippedElements = _clientMeta[this.id] && _clientMeta[this.id].elements; - if (clippedElements.indexOf(elements[i]) === -1) { - clippedElements.push(elements[i]); - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.unclip`. - * @private - */ - var _clientUnclip = function(elements) { - var meta = _clientMeta[this.id]; - if (!meta) { - return this; - } - var clippedElements = meta.elements; - var arrayIndex; - if (typeof elements === "undefined") { - elements = clippedElements.slice(0); - } else { - elements = _prepClip(elements); - } - for (var i = elements.length; i--; ) { - if (_hasOwn.call(elements, i) && elements[i] && elements[i].nodeType === 1) { - arrayIndex = 0; - while ((arrayIndex = clippedElements.indexOf(elements[i], arrayIndex)) !== -1) { - clippedElements.splice(arrayIndex, 1); - } - var clientIds = _elementMeta[elements[i].zcClippingId]; - if (clientIds) { - arrayIndex = 0; - while ((arrayIndex = clientIds.indexOf(this.id, arrayIndex)) !== -1) { - clientIds.splice(arrayIndex, 1); - } - if (clientIds.length === 0) { - if (_globalConfig.autoActivate === true) { - _removeMouseHandlers(elements[i]); - } - delete elements[i].zcClippingId; - } - } - } - } - return this; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.elements`. - * @private - */ - var _clientElements = function() { - var meta = _clientMeta[this.id]; - return meta && meta.elements ? meta.elements.slice(0) : []; - }; - /** - * The underlying implementation of `ZeroClipboard.Client.prototype.destroy`. - * @private - */ - var _clientDestroy = function() { - if (!_clientMeta[this.id]) { - return; - } - this.unclip(); - this.off(); - delete _clientMeta[this.id]; - }; - /** - * Inspect an Event to see if the Client (`this`) should honor it for emission. - * @private - */ - var _clientShouldEmit = function(event) { - if (!(event && event.type)) { - return false; - } - if (event.client && event.client !== this) { - return false; - } - var meta = _clientMeta[this.id]; - var clippedEls = meta && meta.elements; - var hasClippedEls = !!clippedEls && clippedEls.length > 0; - var goodTarget = !event.target || hasClippedEls && clippedEls.indexOf(event.target) !== -1; - var goodRelTarget = event.relatedTarget && hasClippedEls && clippedEls.indexOf(event.relatedTarget) !== -1; - var goodClient = event.client && event.client === this; - if (!meta || !(goodTarget || goodRelTarget || goodClient)) { - return false; - } - return true; - }; - /** - * Handle the actual dispatching of events to a client instance. - * - * @returns `undefined` - * @private - */ - var _clientDispatchCallbacks = function(event) { - var meta = _clientMeta[this.id]; - if (!(typeof event === "object" && event && event.type && meta)) { - return; - } - var async = _shouldPerformAsync(event); - var wildcardTypeHandlers = meta && meta.handlers["*"] || []; - var specificTypeHandlers = meta && meta.handlers[event.type] || []; - var handlers = wildcardTypeHandlers.concat(specificTypeHandlers); - if (handlers && handlers.length) { - var i, len, func, context, eventCopy, originalContext = this; - for (i = 0, len = handlers.length; i < len; i++) { - func = handlers[i]; - context = originalContext; - if (typeof func === "string" && typeof _window[func] === "function") { - func = _window[func]; - } - if (typeof func === "object" && func && typeof func.handleEvent === "function") { - context = func; - func = func.handleEvent; - } - if (typeof func === "function") { - eventCopy = _extend({}, event); - _dispatchCallback(func, context, [ eventCopy ], async); - } - } - } - }; - /** - * Prepares the elements for clipping/unclipping. - * - * @returns An Array of elements. - * @private - */ - var _prepClip = function(elements) { - if (typeof elements === "string") { - elements = []; - } - return typeof elements.length !== "number" ? [ elements ] : elements; - }; - /** - * Add a `mouseover` handler function for a clipped element. - * - * @returns `undefined` - * @private - */ - var _addMouseHandlers = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - var _suppressMouseEvents = function(event) { - if (!(event || (event = _window.event))) { - return; - } - if (event._source !== "js") { - event.stopImmediatePropagation(); - event.preventDefault(); - } - delete event._source; - }; - var _elementMouseOver = function(event) { - if (!(event || (event = _window.event))) { - return; - } - _suppressMouseEvents(event); - ZeroClipboard.focus(element); - }; - element.addEventListener("mouseover", _elementMouseOver, false); - element.addEventListener("mouseout", _suppressMouseEvents, false); - element.addEventListener("mouseenter", _suppressMouseEvents, false); - element.addEventListener("mouseleave", _suppressMouseEvents, false); - element.addEventListener("mousemove", _suppressMouseEvents, false); - _mouseHandlers[element.zcClippingId] = { - mouseover: _elementMouseOver, - mouseout: _suppressMouseEvents, - mouseenter: _suppressMouseEvents, - mouseleave: _suppressMouseEvents, - mousemove: _suppressMouseEvents - }; - }; - /** - * Remove a `mouseover` handler function for a clipped element. - * - * @returns `undefined` - * @private - */ - var _removeMouseHandlers = function(element) { - if (!(element && element.nodeType === 1)) { - return; - } - var mouseHandlers = _mouseHandlers[element.zcClippingId]; - if (!(typeof mouseHandlers === "object" && mouseHandlers)) { - return; - } - var key, val, mouseEvents = [ "move", "leave", "enter", "out", "over" ]; - for (var i = 0, len = mouseEvents.length; i < len; i++) { - key = "mouse" + mouseEvents[i]; - val = mouseHandlers[key]; - if (typeof val === "function") { - element.removeEventListener(key, val, false); - } - } - delete _mouseHandlers[element.zcClippingId]; - }; - /** - * Creates a new ZeroClipboard client instance. - * Optionally, auto-`clip` an element or collection of elements. - * - * @constructor - */ - ZeroClipboard._createClient = function() { - _clientConstructor.apply(this, _args(arguments)); - }; - /** - * Register an event listener to the client. - * - * @returns `this` - */ - ZeroClipboard.prototype.on = function() { - return _clientOn.apply(this, _args(arguments)); - }; - /** - * Unregister an event handler from the client. - * If no `listener` function/object is provided, it will unregister all handlers for the provided `eventType`. - * If no `eventType` is provided, it will unregister all handlers for every event type. - * - * @returns `this` - */ - ZeroClipboard.prototype.off = function() { - return _clientOff.apply(this, _args(arguments)); - }; - /** - * Retrieve event listeners for an `eventType` from the client. - * If no `eventType` is provided, it will retrieve all listeners for every event type. - * - * @returns array of listeners for the `eventType`; if no `eventType`, then a map/hash object of listeners for all event types; or `null` - */ - ZeroClipboard.prototype.handlers = function() { - return _clientListeners.apply(this, _args(arguments)); - }; - /** - * Event emission receiver from the Flash object for this client's registered JavaScript event listeners. - * - * @returns For the "copy" event, returns the Flash-friendly "clipData" object; otherwise `undefined`. - */ - ZeroClipboard.prototype.emit = function() { - return _clientEmit.apply(this, _args(arguments)); - }; - /** - * Register clipboard actions for new element(s) to the client. - * - * @returns `this` - */ - ZeroClipboard.prototype.clip = function() { - return _clientClip.apply(this, _args(arguments)); - }; - /** - * Unregister the clipboard actions of previously registered element(s) on the page. - * If no elements are provided, ALL registered elements will be unregistered. - * - * @returns `this` - */ - ZeroClipboard.prototype.unclip = function() { - return _clientUnclip.apply(this, _args(arguments)); - }; - /** - * Get all of the elements to which this client is clipped. - * - * @returns array of clipped elements - */ - ZeroClipboard.prototype.elements = function() { - return _clientElements.apply(this, _args(arguments)); - }; - /** - * Self-destruct and clean up everything for a single client. - * This will NOT destroy the embedded Flash object. - * - * @returns `undefined` - */ - ZeroClipboard.prototype.destroy = function() { - return _clientDestroy.apply(this, _args(arguments)); - }; - /** - * Stores the pending plain text to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setText = function(text) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("text/plain", text); - return this; - }; - /** - * Stores the pending HTML text to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setHtml = function(html) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("text/html", html); - return this; - }; - /** - * Stores the pending rich text (RTF) to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setRichText = function(richText) { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData("application/rtf", richText); - return this; - }; - /** - * Stores the pending data to inject into the clipboard. - * - * @returns `this` - */ - ZeroClipboard.prototype.setData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.setData.apply(this, _args(arguments)); - return this; - }; - /** - * Clears the pending data to inject into the clipboard. - * If no `format` is provided, all pending data formats will be cleared. - * - * @returns `this` - */ - ZeroClipboard.prototype.clearData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance"); - } - ZeroClipboard.clearData.apply(this, _args(arguments)); - return this; - }; - /** - * Gets a copy of the pending data to inject into the clipboard. - * If no `format` is provided, a copy of ALL pending data formats will be returned. - * - * @returns `String` or `Object` - */ - ZeroClipboard.prototype.getData = function() { - if (!_clientMeta[this.id]) { - throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance"); - } - return ZeroClipboard.getData.apply(this, _args(arguments)); - }; - if (typeof define === "function" && define.amd) { - define(function() { - return ZeroClipboard; - }); - } else if (typeof module === "object" && module && typeof module.exports === "object" && module.exports) { - module.exports = ZeroClipboard; - } else { - window.ZeroClipboard = ZeroClipboard; - } -})(function() { - return this || window; -}()); \ No newline at end of file diff --git a/src/UI/LifeCycle.js b/src/UI/LifeCycle.js deleted file mode 100644 index 59a237340..000000000 --- a/src/UI/LifeCycle.js +++ /dev/null @@ -1,3 +0,0 @@ -window.onbeforeunload = function() { - window.NzbDrone.unloading = true; -}; \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/EpisodesCell.js b/src/UI/ManualImport/Cells/EpisodesCell.js deleted file mode 100644 index 68c4b5166..000000000 --- a/src/UI/ManualImport/Cells/EpisodesCell.js +++ /dev/null @@ -1,46 +0,0 @@ -var _ = require('underscore'); -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectEpisodeLayout = require('../Episode/SelectEpisodeLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'episodes-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - var episodes = this.model.get('episodes'); - - if (episodes) - { - var episodeNumbers = _.map(episodes, 'episodeNumber'); - - this.$el.html(episodeNumbers.join(', ')); - } - - return this; - }, - - _onClick : function () { - var series = this.model.get('series'); - var seasonNumber = this.model.get('seasonNumber'); - - if (series === undefined || seasonNumber === undefined) { - return; - } - - var view = new SelectEpisodeLayout({ series: series, seasonNumber: seasonNumber }); - - this.listenTo(view, 'manualimport:selected:episodes', this._setEpisodes); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setEpisodes : function (e) { - this.model.set('episodes', e.episodes); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/PathCell.js b/src/UI/ManualImport/Cells/PathCell.js deleted file mode 100644 index 7397d1623..000000000 --- a/src/UI/ManualImport/Cells/PathCell.js +++ /dev/null @@ -1,16 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'path-cell', - - render : function() { - this.$el.empty(); - - var relativePath = this.model.get('relativePath'); - var path = this.model.get('path'); - - this.$el.html('<div title="{0}">{1}</div>'.format(path, relativePath)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/QualityCell.js b/src/UI/ManualImport/Cells/QualityCell.js deleted file mode 100644 index 181ebf254..000000000 --- a/src/UI/ManualImport/Cells/QualityCell.js +++ /dev/null @@ -1,23 +0,0 @@ -var vent = require('../../vent'); -var QualityCell = require('../../Cells/QualityCell'); -var SelectQualityLayout = require('../Quality/SelectQualityLayout'); - -module.exports = QualityCell.extend({ - className : 'quality-cell editable', - - events : { - 'click' : '_onClick' - }, - - _onClick : function () { - var view = new SelectQualityLayout(); - - this.listenTo(view, 'manualimport:selected:quality', this._setQuality); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setQuality : function (e) { - this.model.set('quality', e.quality); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeasonCell.js b/src/UI/ManualImport/Cells/SeasonCell.js deleted file mode 100644 index 6120055ea..000000000 --- a/src/UI/ManualImport/Cells/SeasonCell.js +++ /dev/null @@ -1,47 +0,0 @@ -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectSeasonLayout = require('../Season/SelectSeasonLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'season-cell', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - if (this.model.has('seasonNumber')) { - this.$el.html(this.model.get('seasonNumber')); - } - - this.delegateEvents(); - return this; - }, - - _onClick : function () { - var series = this.model.get('series'); - - if (!series) { - return; - } - - var view = new SelectSeasonLayout({ seasons: series.seasons }); - - this.listenTo(view, 'manualimport:selected:season', this._setSeason); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setSeason : function (e) { - if (this.model.has('seasonNumber') && e.seasonNumber === this.model.get('seasonNumber')) { - return; - } - - this.model.set({ - seasonNumber : e.seasonNumber, - episodes : [] - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Cells/SeriesCell.js b/src/UI/ManualImport/Cells/SeriesCell.js deleted file mode 100644 index cb66f6826..000000000 --- a/src/UI/ManualImport/Cells/SeriesCell.js +++ /dev/null @@ -1,45 +0,0 @@ -var vent = require('../../vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SelectSeriesLayout = require('../Series/SelectSeriesLayout'); - -module.exports = NzbDroneCell.extend({ - className : 'series-title-cell editable', - - events : { - 'click' : '_onClick' - }, - - render : function() { - this.$el.empty(); - - var series = this.model.get('series'); - - if (series) - { - this.$el.html(series.title); - } - - this.delegateEvents(); - return this; - }, - - _onClick : function () { - var view = new SelectSeriesLayout(); - - this.listenTo(view, 'manualimport:selected:series', this._setSeries); - - vent.trigger(vent.Commands.OpenModal2Command, view); - }, - - _setSeries : function (e) { - if (this.model.has('series') && e.model.id === this.model.get('series').id) { - return; - } - - this.model.set({ - series : e.model.toJSON(), - seasonNumber : undefined, - episodes : [] - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyView.js b/src/UI/ManualImport/EmptyView.js deleted file mode 100644 index 2b4394d3f..000000000 --- a/src/UI/ManualImport/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'ManualImport/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/ManualImport/EmptyViewTemplate.hbs b/src/UI/ManualImport/EmptyViewTemplate.hbs deleted file mode 100644 index fe59eb600..000000000 --- a/src/UI/ManualImport/EmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -No video files were found in the selected folder. \ No newline at end of file diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayout.js b/src/UI/ManualImport/Episode/SelectEpisodeLayout.js deleted file mode 100644 index 04617a0bc..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeLayout.js +++ /dev/null @@ -1,81 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EpisodeCollection = require('../../Series/EpisodeCollection'); -var LoadingView = require('../../Shared/LoadingView'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var EpisodeNumberCell = require('../../Series/Details/EpisodeNumberCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var SelectEpisodeRow = require('./SelectEpisodeRow'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Episode/SelectEpisodeLayoutTemplate', - - regions : { - episodes : '.x-episodes' - }, - - events : { - 'click .x-select' : '_selectEpisodes' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'episodeNumber', - label : '#', - cell : EpisodeNumberCell - }, - { - name : 'title', - label : 'Title', - hideSeriesLink : true, - cell : 'string', - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - } - ], - - initialize : function(options) { - this.series = options.series; - this.seasonNumber = options.seasonNumber; - }, - - onRender : function() { - this.episodes.show(new LoadingView()); - - this.episodeCollection = new EpisodeCollection({ seriesId : this.series.id }); - this.episodeCollection.fetch(); - - this.listenToOnce(this.episodeCollection, 'sync', function () { - - this.episodeView = new Backgrid.Grid({ - columns : this.columns, - collection : this.episodeCollection.bySeason(this.seasonNumber), - className : 'table table-hover season-grid', - row : SelectEpisodeRow - }); - - this.episodes.show(this.episodeView); - }); - }, - - _selectEpisodes : function () { - var episodes = _.map(this.episodeView.getSelectedModels(), function (episode) { - return episode.toJSON(); - }); - - this.trigger('manualimport:selected:episodes', { episodes: episodes }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); diff --git a/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs b/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs deleted file mode 100644 index 68a9af81a..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeLayoutTemplate.hbs +++ /dev/null @@ -1,21 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Episode(s) - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="col-md-12 x-episodes"></div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-select" data-dismiss="modal">Select Episodes</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Episode/SelectEpisodeRow.js b/src/UI/ManualImport/Episode/SelectEpisodeRow.js deleted file mode 100644 index 6dc90fc99..000000000 --- a/src/UI/ManualImport/Episode/SelectEpisodeRow.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'select-episode-row', - - events : { - 'click' : '_toggle' - }, - - _toggle : function(e) { - - if (e.target.type === 'checkbox') { - return; - } - - var checked = this.$el.find('.select-row-cell :checkbox').prop('checked'); - - this.model.trigger('backgrid:select', this.model, !checked); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Folder/SelectFolderView.js b/src/UI/ManualImport/Folder/SelectFolderView.js deleted file mode 100644 index 0a2c066c2..000000000 --- a/src/UI/ManualImport/Folder/SelectFolderView.js +++ /dev/null @@ -1,84 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Config = require('../../Config'); -var Marionette = require('marionette'); -var moment = require('moment'); -require('../../Mixins/FileBrowser'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Folder/SelectFolderViewTemplate', - - ui : { - path : '.x-path', - buttons : '.x-button' - }, - - events: { - 'click .x-manual-import' : '_manualImport', - 'click .x-automatic-import' : '_automaticImport', - 'change .x-path' : '_updateButtons', - 'keyup .x-path' : '_updateButtons', - 'click .x-recent-folder' : '_selectRecentFolder' - }, - - initialize : function () { - this.templateHelpers = { - recentFolders: Config.getValueJson('manualimport.recentfolders', []) - }; - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this._updateButtons(); - }, - - path : function() { - return this.ui.path.val(); - }, - - _manualImport : function () { - var path = this.ui.path.val(); - - if (path) { - this._setRecentFolders(path); - this.trigger('manualImport', { folder: path }); - } - }, - - _automaticImport : function () { - var path = this.ui.path.val(); - - if (path) { - this._setRecentFolders(path); - this.trigger('automaticImport', { folder: path }); - } - }, - - _updateButtons : function () { - if (this.ui.path.val()) { - this.ui.buttons.removeAttr('disabled'); - } - - else { - this.ui.buttons.attr('disabled', 'disabled'); - } - }, - - _selectRecentFolder : function (e) { - var path = $(e.target).closest('tr').data('path'); - this.ui.path.val(path); - this.ui.path.trigger('change'); - }, - - _setRecentFolders : function (path) { - var recentFolders = Config.getValueJson('manualimport.recentfolders', []); - - recentFolders = _.filter(recentFolders, function (folder) { - return folder.path.toLowerCase() !== path.toLowerCase(); - }); - - recentFolders.unshift({ path: path, lastUsed: moment.utc().toISOString() }); - - Config.setValueJson('manualimport.recentfolders', _.take(recentFolders, 5)); - } -}); diff --git a/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs b/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs deleted file mode 100644 index 4f681aecb..000000000 --- a/src/UI/ManualImport/Folder/SelectFolderViewTemplate.hbs +++ /dev/null @@ -1,43 +0,0 @@ -<div class="select-folder"> - <div class="row"> - <div class="form-group"> - <div class="col-md-12"> - <input type="text" class="form-control x-path" placeholder="Select a folder to import" name="path"> - </div> - </div> - </div> - <div class="recent-folders"> - {{#if recentFolders}} - <h4>Recent Folders</h4> - - <table class="table table-hover"> - <thead> - <tr> - <th>Path</th> - <th>Last Used</th> - </tr> - </thead> - <tbody> - {{#each recentFolders}} - <tr class="recent-folder x-recent-folder" data-path="{{path}}"> - <td>{{path}}</td> - <td>{{RelativeDate lastUsed}}</td> - </tr> - {{/each}} - </tbody> - </table> - {{/if}} - </div> - <div class="buttons"> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <button class="btn btn-primary btn-lg btn-block x-automatic-import x-button"><i class="icon-lidarr-search-automatic"></i> Import File(s) Automatically</button> - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <button class="btn btn-primary btn-lg btn-block x-manual-import x-button"><i class="icon-lidarr-search-manual"></i> Manual Import</button> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportCollection.js b/src/UI/ManualImport/ManualImportCollection.js deleted file mode 100644 index c7cff70f7..000000000 --- a/src/UI/ManualImport/ManualImportCollection.js +++ /dev/null @@ -1,74 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var ManualImportModel = require('./ManualImportModel'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); - -var Collection = PageableCollection.extend({ - model : ManualImportModel, - url : window.NzbDrone.ApiRoot + '/manualimport', - - state : { - sortKey : 'quality', - order : 1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : PageableCollection.prototype.fetch, - - initialize : function (options) { - options = options || {}; - - if (!options.folder && !options.downloadId) { - throw 'folder or downloadId is required'; - } - - this.folder = options.folder; - this.downloadId = options.downloadId; - }, - - fetch : function(options) { - options = options || {}; - - options.data = { folder : this.folder, downloadId : this.downloadId }; - - return this.originalFetch.call(this, options); - }, - - sortMappings : { - series : { - sortValue : function(model, attr, order) { - var series = model.get(attr); - - if (series) { - return series.sortTitle; - } - - return ''; - } - }, - - quality : { - sortKey : 'qualityWeight' - } - }, - - comparator : function(model1, model2) { - var quality1 = model1.get('quality'); - var quality2 = model2.get('quality'); - - if (quality1 < quality2) { - return 1; - } - - if (quality1 > quality2) { - return -1; - } - - return 0; - } -}); - -Collection = AsSortedCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayout.js b/src/UI/ManualImport/ManualImportLayout.js deleted file mode 100644 index 9d32c2769..000000000 --- a/src/UI/ManualImport/ManualImportLayout.js +++ /dev/null @@ -1,259 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var CommandController = require('../Commands/CommandController'); -var EmptyView = require('./EmptyView'); -var SelectFolderView = require('./Folder/SelectFolderView'); -var LoadingView = require('../Shared/LoadingView'); -var ManualImportRow = require('./ManualImportRow'); -var SelectAllCell = require('../Cells/SelectAllCell'); -var PathCell = require('./Cells/PathCell'); -var SeriesCell = require('./Cells/SeriesCell'); -var SeasonCell = require('./Cells/SeasonCell'); -var EpisodesCell = require('./Cells/EpisodesCell'); -var QualityCell = require('./Cells/QualityCell'); -var FileSizeCell = require('../Cells/FileSizeCell'); -var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); -var ManualImportCollection = require('./ManualImportCollection'); -var Messenger = require('../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'ManualImport/ManualImportLayoutTemplate', - - regions : { - workspace : '.x-workspace' - }, - - ui : { - importButton : '.x-import', - importMode : '.x-importmode' - }, - - events : { - 'click .x-import' : '_import' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'relativePath', - label : 'Relative Path', - cell : PathCell, - sortable : true - }, - { - name : 'series', - label : 'Series', - cell : SeriesCell, - sortable : true - }, - { - name : 'seasonNumber', - label : 'Season', - cell : SeasonCell, - sortable : true - }, - { - name : 'episodes', - label : 'Episode(s)', - cell : EpisodesCell, - sortable : false - }, - { - name : 'quality', - label : 'Quality', - cell : QualityCell, - sortable : true - - }, - { - name : 'size', - label : 'Size', - cell : FileSizeCell, - sortable : true - }, - { - name : 'rejections', - label : '<i class="icon-lidarr-header-rejections" />', - tooltip : 'Rejections', - cell : ApprovalStatusCell, - sortable : false, - sortType : 'fixed', - direction : 'ascending', - title : 'Import Rejected' - } - ], - - initialize : function(options) { - this.folder = options.folder; - this.downloadId = options.downloadId; - this.title = options.title; - this.importMode = options.importMode || 'Move'; - - this.templateHelpers = { - title : this.title || this.folder - }; - }, - - onRender : function() { - - if (this.folder || this.downloadId) { - this._showLoading(); - this._loadCollection(); - this.ui.importMode.val(this.importMode); - } - - else { - this._showSelectFolder(); - this.ui.importButton.hide(); - this.ui.importMode.hide(); - } - }, - - _showLoading : function () { - this.workspace.show(new LoadingView()); - }, - - _loadCollection : function () { - this.manualImportCollection = new ManualImportCollection({ folder: this.folder, downloadId: this.downloadId }); - this.manualImportCollection.fetch(); - - this.listenTo(this.manualImportCollection, 'sync', this._showTable); - this.listenTo(this.manualImportCollection, 'backgrid:selected', this._updateButtons); - }, - - _showTable : function () { - if (this.manualImportCollection.length === 0) { - this.workspace.show(new EmptyView()); - return; - } - - this.fileView = new Backgrid.Grid({ - columns : this.columns, - collection : this.manualImportCollection, - className : 'table table-hover', - row : ManualImportRow - }); - - this.workspace.show(this.fileView); - this._updateButtons(); - }, - - _showSelectFolder : function () { - this.selectFolderView = new SelectFolderView(); - this.workspace.show(this.selectFolderView); - - this.listenTo(this.selectFolderView, 'manualImport', this._manualImport); - this.listenTo(this.selectFolderView, 'automaticImport', this._automaticImport); - }, - - _manualImport : function (e) { - this.folder = e.folder; - this.templateHelpers.title = this.folder; - this.render(); - }, - - _automaticImport : function (e) { - CommandController.Execute('downloadedAlbumsScan', { - name : 'downloadedAlbumsScan', - path : e.folder - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _import : function () { - var selected = this.fileView.getSelectedModels(); - - if (selected.length === 0) { - return; - } - - if (_.any(selected, function (model) { - return !model.has('series'); - })) { - - this._showErrorMessage('Series must be chosen for each selected file'); - return; - } - - if (_.any(selected, function (model) { - return !model.has('seasonNumber'); - })) { - - this._showErrorMessage('Season must be chosen for each selected file'); - return; - } - - if (_.any(selected, function (model) { - return !model.has('episodes') || model.get('episodes').length === 0; - })) { - - this._showErrorMessage('One or more episodes must be chosen for each selected file'); - return; - } - - var importMode = this.ui.importMode.val(); - - CommandController.Execute('manualImport', { - name : 'manualImport', - files : _.map(selected, function (file) { - return { - path : file.get('path'), - seriesId : file.get('series').id, - episodeIds : _.map(file.get('episodes'), 'id'), - quality : file.get('quality'), - downloadId : file.get('downloadId') - }; - }), - importMode : importMode - }); - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _updateButtons : function (model, selected) { - if (!this.fileView) { - this.ui.importButton.attr('disabled', 'disabled'); - return; - } - - if (!model) { - return; - } - - var selectedModels = this.fileView.getSelectedModels(); - var selectedCount = 0; - - if (selected) { - selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length : selectedModels.length + 1; - } - - else { - selectedCount = _.any(selectedModels, { id : model.id }) ? selectedModels.length - 1 : selectedModels.length; - } - - if (selectedCount === 0) { - this.ui.importButton.attr('disabled', 'disabled'); - } - - else { - this.ui.importButton.removeAttr('disabled'); - } - }, - - _showErrorMessage : function (message) { - Messenger.show({ - message : message, - type : 'error', - hideAfter : 5 - }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportLayoutTemplate.hbs b/src/UI/ManualImport/ManualImportLayoutTemplate.hbs deleted file mode 100644 index 194e094e9..000000000 --- a/src/UI/ManualImport/ManualImportLayoutTemplate.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - {{#if title}}{{title}}{{else}}Select Folder{{/if}} - </h3> - - </div> - <div class="modal-body"> - <div class="x-workspace"></div> - <div class="x-footer"></div> - </div> - <div class="modal-footer"> - <div class="col-md-2 pull-left"> - <select class="form-control x-importmode"> - <option value="Move">Move Files</option> - <option value="Copy">Copy Files</option> - </select> - </div> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-import" disabled="disabled">Import</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/ManualImportModel.js b/src/UI/ManualImport/ManualImportModel.js deleted file mode 100644 index dfd34cead..000000000 --- a/src/UI/ManualImport/ManualImportModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ -}); \ No newline at end of file diff --git a/src/UI/ManualImport/ManualImportRow.js b/src/UI/ManualImport/ManualImportRow.js deleted file mode 100644 index 5699e83c3..000000000 --- a/src/UI/ManualImport/ManualImportRow.js +++ /dev/null @@ -1,41 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'manual-import-row', - - _originalInit : Backgrid.Row.prototype.initialize, - _originalRender : Backgrid.Row.prototype.render, - - initialize : function () { - this._originalInit.apply(this, arguments); - - this.listenTo(this.model, 'change', this._setError); - this.listenTo(this.model, 'change', this._setClasses); - }, - - render : function () { - this._originalRender.apply(this, arguments); - this._setError(); - this._setClasses(); - - return this; - }, - - _setError : function () { - if (this.model.has('series') && - this.model.has('seasonNumber') && - (this.model.has('episodes') && this.model.get('episodes').length > 0)&& - this.model.has('quality')) { - this.$el.removeClass('manual-import-error'); - } - - else { - this.$el.addClass('manual-import-error'); - } - }, - - _setClasses : function () { - this.$el.toggleClass('has-series', this.model.has('series')); - this.$el.toggleClass('has-season', this.model.has('seasonNumber')); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayout.js b/src/UI/ManualImport/Quality/SelectQualityLayout.js deleted file mode 100644 index beba005e9..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityLayout.js +++ /dev/null @@ -1,43 +0,0 @@ -var _ = require('underscore'); -var vent = require('../../vent'); -var Marionette = require('marionette'); -var LoadingView = require('../../Shared/LoadingView'); -var ProfileSchemaCollection = require('../../Settings/Profile/ProfileSchemaCollection'); -var SelectQualityView = require('./SelectQualityView'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Quality/SelectQualityLayoutTemplate', - - regions : { - quality : '.x-quality' - }, - - events : { - 'click .x-select' : '_selectQuality' - }, - - initialize : function() { - this.profileSchemaCollection = new ProfileSchemaCollection(); - this.profileSchemaCollection.fetch(); - - this.listenTo(this.profileSchemaCollection, 'sync', this._showQuality); - }, - - onRender : function() { - this.quality.show(new LoadingView()); - }, - - _showQuality : function () { - var qualities = _.map(this.profileSchemaCollection.first().get('items'), function (quality) { - return quality.quality; - }); - - this.selectQualityView = new SelectQualityView({ qualities: qualities }); - this.quality.show(this.selectQualityView); - }, - - _selectQuality : function () { - this.trigger('manualimport:selected:quality', { quality: this.selectQualityView.selectedQuality() }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs deleted file mode 100644 index d5d2098e6..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityLayoutTemplate.hbs +++ /dev/null @@ -1,19 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Quality - </h3> - - </div> - <div class="modal-body"> - <div class="x-quality"></div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - <button class="btn btn-success x-select" data-dismiss="modal">Select Quality</button> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Quality/SelectQualityView.js b/src/UI/ManualImport/Quality/SelectQualityView.js deleted file mode 100644 index 8a39fab82..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityView.js +++ /dev/null @@ -1,37 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Quality/SelectQualityViewTemplate', - - ui : { - select : '.x-select-quality', - proper : 'x-proper' - }, - - initialize : function(options) { - this.qualities = options.qualities; - - this.templateHelpers = { - qualities: this.qualities - }; - }, - - selectedQuality : function () { - var selected = parseInt(this.ui.select.val(), 10); - var proper = this.ui.proper.prop('checked'); - - var quality = _.find(this.qualities, function(q) { - return q.id === selected; - }); - - - return { - quality : quality, - revision : { - version : proper ? 2 : 1, - real : 0 - } - }; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs b/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs deleted file mode 100644 index a04342280..000000000 --- a/src/UI/ManualImport/Quality/SelectQualityViewTemplate.hbs +++ /dev/null @@ -1,33 +0,0 @@ -<div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-4 control-label">Quality</label> - - <div class="col-sm-4"> - <select class="form-control x-select-quality"> - <option value="-1">Select Quality</option> - {{#each qualities}} - <option value="{{id}}">{{name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Proper</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-proper"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> -</div> diff --git a/src/UI/ManualImport/Season/SelectSeasonLayout.js b/src/UI/ManualImport/Season/SelectSeasonLayout.js deleted file mode 100644 index 6f46f9cd9..000000000 --- a/src/UI/ManualImport/Season/SelectSeasonLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Season/SelectSeasonLayoutTemplate', - - events : { - 'change .x-select-season' : '_selectSeason' - }, - - initialize : function(options) { - - this.templateHelpers = { - seasons : options.seasons - }; - }, - - _selectSeason : function (e) { - var seasonNumber = parseInt(e.target.value, 10); - - if (seasonNumber === -1) { - return; - } - - this.trigger('manualimport:selected:season', { seasonNumber: seasonNumber }); - vent.trigger(vent.Commands.CloseModal2Command); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs b/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs deleted file mode 100644 index b459c6bf5..000000000 --- a/src/UI/ManualImport/Season/SelectSeasonLayoutTemplate.hbs +++ /dev/null @@ -1,29 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Season - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="form-group col-md-4 col-md-offset-4"> - <select class="form-control x-select-season"> - <option value="-1">Select Season</option> - {{#each seasons}} - <option value="{{seasonNumber}}">Season {{seasonNumber}}</option> - {{/each}} - </select> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - </div> - </div> -</div> - - diff --git a/src/UI/ManualImport/Series/SelectSeriesLayout.js b/src/UI/ManualImport/Series/SelectSeriesLayout.js deleted file mode 100644 index 2d0ea1487..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesLayout.js +++ /dev/null @@ -1,101 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var SeriesCollection = require('../../Series/SeriesCollection'); -var SelectRow = require('./SelectSeriesRow'); - -module.exports = Marionette.Layout.extend({ - template : 'ManualImport/Series/SelectSeriesLayoutTemplate', - - regions : { - series : '.x-series' - }, - - ui : { - filter : '.x-filter' - }, - - columns : [ - { - name : 'title', - label : 'Title', - cell : 'String', - sortValue : 'sortTitle' - } - ], - - initialize : function() { - this.seriesCollection = SeriesCollection.clone(); - this._setModelCollection(); - - this.listenTo(this.seriesCollection, 'row:selected', this._onSelected); - this.listenTo(this, 'modal:afterShow', this._setFocus); - }, - - onRender : function() { - this.seriesView = new Backgrid.Grid({ - columns : this.columns, - collection : this.seriesCollection, - className : 'table table-hover season-grid', - row : SelectRow - }); - - this.series.show(this.seriesView); - this._setupFilter(); - }, - - _setupFilter : function () { - var self = this; - - //TODO: This should be a mixin (same as Add Series searching) - this.ui.filter.keyup(function(e) { - if (_.contains([ - 9, - 16, - 17, - 18, - 19, - 20, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 91, - 92, - 93 - ], e.keyCode)) { - return; - } - - self._filter(self.ui.filter.val()); - }); - }, - - _filter : function (term) { - this.seriesCollection.setFilter(['title', term, 'contains']); - this._setModelCollection(); - }, - - _onSelected : function (e) { - this.trigger('manualimport:selected:series', { model: e.model }); - - vent.trigger(vent.Commands.CloseModal2Command); - }, - - _setFocus : function () { - this.ui.filter.focus(); - }, - - _setModelCollection: function () { - var self = this; - - _.each(this.seriesCollection.models, function (model) { - model.collection = self.seriesCollection; - }); - } -}); diff --git a/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs b/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs deleted file mode 100644 index 0db951d99..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesLayoutTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="modal-content"> - <div class="manual-import-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3> - Manual Import - Select Series - </h3> - - </div> - <div class="modal-body"> - <div class="row"> - <div class="col-md-12"> - <div class="form-group"> - <input type="text" class="form-control x-filter" placeholder="Filter series" /> - </div> - </div> - </div> - - <div class="row"> - <div class="col-md-12 x-series"></div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-default" data-dismiss="modal">Cancel</button> - </div> - </div> -</div> - - diff --git a/src/UI/ManualImport/Series/SelectSeriesRow.js b/src/UI/ManualImport/Series/SelectSeriesRow.js deleted file mode 100644 index 38a2d5ca6..000000000 --- a/src/UI/ManualImport/Series/SelectSeriesRow.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'select-row select-series-row', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - this.model.collection.trigger('row:selected', { model: this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryView.js b/src/UI/ManualImport/Summary/ManualImportSummaryView.js deleted file mode 100644 index a4ab847c2..000000000 --- a/src/UI/ManualImport/Summary/ManualImportSummaryView.js +++ /dev/null @@ -1,20 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'ManualImport/Summary/ManualImportSummaryViewTemplate', - - initialize : function (options) { - var episodes = _.map(options.episodes, function (episode) { - return episode.toJSON(); - }); - - this.templateHelpers = { - file : options.file, - series : options.series, - season : options.season, - episodes : episodes, - quality : options.quality - }; - } -}); \ No newline at end of file diff --git a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs b/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs deleted file mode 100644 index d65ff52f1..000000000 --- a/src/UI/ManualImport/Summary/ManualImportSummaryViewTemplate.hbs +++ /dev/null @@ -1,19 +0,0 @@ -<dl class="dl-horizontal"> - - <dt>Path:</dt> - <dd>{{file}}</dd> - - <dt>Series:</dt> - <dd>{{series.title}}</dd> - - <dt>Season:</dt> - <dd>{{season.seasonNumber}}</dd> - - {{#each episodes}} - <dt>Episode:</dt> - <dd>{{episodeNumber}} - {{title}}</dd> - {{/each}} - - <dt>Quality:</dt> - <dd>{{quality.name}}</dd> -</dl> diff --git a/src/UI/ManualImport/manualimport.less b/src/UI/ManualImport/manualimport.less deleted file mode 100644 index c1d7af5a2..000000000 --- a/src/UI/ManualImport/manualimport.less +++ /dev/null @@ -1,63 +0,0 @@ -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/Bootstrap/variables"; - -.manual-import-modal { - .path-cell { - word-break : break-all; - } - - .file-size-cell { - min-width : 80px; - } - - .has-series { - .season-cell { - .clickable(); - } - } - - .has-season { - .episodes-cell { - .clickable(); - } - } - - .editable { - .clickable(); - - .badge { - .clickable(); - } - } - - .select-row { - .clickable(); - } - - .select-folder { - .buttons { - margin-top: 20px; - - .row { - margin-top: 10px; - } - } - - .recent-folders { - margin-top: 20px; - } - - .recent-folder { - .clickable(); - } - } - - .manual-import-error { - background-color : #fdefef; - } - - .recent-folder { - .clickable(); - } -} diff --git a/src/UI/Mixins/AsChangeTrackingModel.js b/src/UI/Mixins/AsChangeTrackingModel.js deleted file mode 100644 index b3524b244..000000000 --- a/src/UI/Mixins/AsChangeTrackingModel.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = function() { - var originalInit = this.prototype.initialize; - - this.prototype.initialize = function() { - - this.isSaved = true; - - this.on('change', function() { - this.isSaved = false; - }, this); - - this.on('sync', function() { - this.isSaved = true; - }, this); - - if (originalInit) { - originalInit.call(this); - } - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsEditModalView.js b/src/UI/Mixins/AsEditModalView.js deleted file mode 100644 index 383e0315b..000000000 --- a/src/UI/Mixins/AsEditModalView.js +++ /dev/null @@ -1,114 +0,0 @@ -var AppLayout = require('../AppLayout'); - -module.exports = function() { - var originalInitialize = this.prototype.initialize; - var originalOnBeforeClose = this.prototype.onBeforeClose; - - var saveInternal = function() { - var self = this; - - if (this.saving) { - return this.savePromise; - } - - this.saving = true; - this.ui.indicator.show(); - - if (this._onBeforeSave) { - this._onBeforeSave.call(this); - } - - this.savePromise = this.model.save(); - - this.savePromise.always(function() { - self.saving = false; - - if (!self.isClosed) { - self.ui.indicator.hide(); - } - }); - - this.savePromise.done(function() { - self.originalModelData = JSON.stringify(self.model.toJSON()); - }); - - return this.savePromise; - }; - - this.prototype.initialize = function(options) { - if (!this.model) { - throw 'View has no model'; - } - - this.testing = false; - this.saving = false; - - this.originalModelData = JSON.stringify(this.model.toJSON()); - - this.events = this.events || {}; - this.events['click .x-save'] = '_save'; - this.events['click .x-save-and-add'] = '_saveAndAdd'; - this.events['click .x-test'] = '_test'; - this.events['click .x-delete'] = '_delete'; - - this.ui = this.ui || {}; - this.ui.indicator = '.x-indicator'; - - if (originalInitialize) { - originalInitialize.call(this, options); - } - }; - - this.prototype._save = function() { - var self = this; - var promise = saveInternal.call(this); - - promise.done(function() { - if (self._onAfterSave) { - self._onAfterSave.call(self); - } - }); - }; - - this.prototype._saveAndAdd = function() { - var self = this; - var promise = saveInternal.call(this); - - promise.done(function() { - if (self._onAfterSaveAndAdd) { - self._onAfterSaveAndAdd.call(self); - } - }); - }; - - this.prototype._test = function() { - var self = this; - - if (this.testing) { - return; - } - - this.testing = true; - this.ui.indicator.show(); - - this.model.test().always(function() { - self.testing = false; - self.ui.indicator.hide(); - }); - }; - - this.prototype._delete = function() { - var view = new this._deleteView({ model : this.model }); - AppLayout.modalRegion.show(view); - }; - - this.prototype.onBeforeClose = function() { - this.model.set(JSON.parse(this.originalModelData)); - - if (originalOnBeforeClose) { - originalOnBeforeClose.call(this); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsFilteredCollection.js b/src/UI/Mixins/AsFilteredCollection.js deleted file mode 100644 index 4b3fd3272..000000000 --- a/src/UI/Mixins/AsFilteredCollection.js +++ /dev/null @@ -1,79 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = function() { - - this.prototype.setFilter = function(filter, options) { - options = _.extend({ reset : true }, options || {}); - - this.state.filterKey = filter[0]; - this.state.filterValue = filter[1]; - this.state.filterType = filter[2] || 'equal'; - - if (options.reset) { - if (this.mode !== 'server') { - this.fullCollection.resetFiltered(); - } else { - return this.fetch(); - } - } - }; - - this.prototype.setFilterMode = function(mode, options) { - return this.setFilter(this.filterModes[mode], options); - }; - - var originalMakeFullCollection = this.prototype._makeFullCollection; - - this.prototype._makeFullCollection = function(models, options) { - var self = this; - - self.shadowCollection = originalMakeFullCollection.call(this, models, options); - - var filterModel = function(model) { - if (_.isFunction(self.state.filterType)) { - return self.state.filterType(model); - } - - if (!self.state.filterKey) { - return true; - } - else if (self.state.filterType === 'contains') { - return model.get(self.state.filterKey).toLowerCase().indexOf(self.state.filterValue.toLowerCase()) > -1; - } - else { - return model.get(self.state.filterKey) === self.state.filterValue; - } - }; - - self.shadowCollection.filtered = function() { - return this.filter(filterModel); - }; - - var filteredModels = self.shadowCollection.filtered(); - var fullCollection = originalMakeFullCollection.call(this, filteredModels, options); - - fullCollection.resetFiltered = function(options) { - Backbone.Collection.prototype.reset.call(this, self.shadowCollection.filtered(), options); - }; - - fullCollection.reset = function(models, options) { - self.shadowCollection.reset(models, options); - self.fullCollection.resetFiltered(); - }; - - return fullCollection; - }; - - _.extend(this.prototype.state, { - filterKey : null, - filterValue : null - }); - - _.extend(this.prototype.queryParams, { - filterKey : 'filterKey', - filterValue : 'filterValue' - }); - - return this; -}; diff --git a/src/UI/Mixins/AsModelBoundView.js b/src/UI/Mixins/AsModelBoundView.js deleted file mode 100644 index 12d3fcca3..000000000 --- a/src/UI/Mixins/AsModelBoundView.js +++ /dev/null @@ -1,46 +0,0 @@ -var ModelBinder = require('backbone.modelbinder'); - -module.exports = function() { - - var originalOnRender = this.prototype.onRender; - var originalBeforeClose = this.prototype.onBeforeClose; - - this.prototype.onRender = function() { - - if (!this.model) { - throw 'View has no model for binding'; - } - - if (!this._modelBinder) { - this._modelBinder = new ModelBinder(); - } - - var options = { - changeTriggers : { - '' : 'change typeahead:selected typeahead:autocompleted', - '[contenteditable]' : 'blur', - '[data-onkeyup]' : 'keyup' - } - }; - - this._modelBinder.bind(this.model, this.el, null, options); - - if (originalOnRender) { - originalOnRender.call(this); - } - }; - - this.prototype.onBeforeClose = function() { - - if (this._modelBinder) { - this._modelBinder.unbind(); - delete this._modelBinder; - } - - if (originalBeforeClose) { - originalBeforeClose.call(this); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsNamedView.js b/src/UI/Mixins/AsNamedView.js deleted file mode 100644 index 8bdd4b604..000000000 --- a/src/UI/Mixins/AsNamedView.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = function() { - - window.NzbDrone.NameViews = window.NzbDrone.NameViews || !window.NzbDrone.Production; - - var regex = new RegExp('/', 'g'); - - var _getViewName = function(template) { - if (template) { - return template.toLocaleLowerCase().replace('template', '').replace(regex, '-'); - } - - return undefined; - }; - - var originalOnRender = this.onRender; - - this.onRender = function() { - - if (window.NzbDrone.NameViews) { - this.$el.addClass('iv-' + _getViewName(this.template)); - } - - if (originalOnRender) { - return originalOnRender.call(this); - } - - return undefined; - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsPageableCollection.js b/src/UI/Mixins/AsPageableCollection.js deleted file mode 100644 index 60145e569..000000000 --- a/src/UI/Mixins/AsPageableCollection.js +++ /dev/null @@ -1,45 +0,0 @@ -var _ = require('underscore'); - -module.exports = function() { - var originalMakeCollectionEventHandler = this.prototype._makeCollectionEventHandler; - - this.prototype._makeCollectionEventHandler = function (pageCollection, fullCollection) { - var self = this; - this.pageCollection = pageCollection; - this.fullCollection = fullCollection; - var eventHandler = originalMakeCollectionEventHandler.apply(this, arguments); - - return _.wrap(eventHandler, _.bind(self._resetEventHandler, self)); - }; - - this.prototype._resetEventHandler = function (originalEventHandler, event, model, collection, options) { - if (event === 'reset') { - var currentPage = this.state.currentPage; - var pageSize = this.state.pageSize; - - originalEventHandler.apply(this, [].slice.call(arguments, 1)); - - var totalPages = Math.max(1,Math.ceil(this.state.totalRecords / pageSize)); - var newPage = Math.min(currentPage, totalPages); - - if (newPage !== this.state.currentPage) { - this.state.currentPage = newPage; - - // If backbone pageable fixes their reset bug - // (they reset the page number, but not the range), - // we'll want to do this for all resets where the page number changed - if (currentPage !== newPage) { - var pageStart = (newPage - 1) * pageSize; - var pageEnd = pageStart + pageSize; - - this.pageCollection.reset(this.fullCollection.models.slice(pageStart, pageEnd), - _.extend({}, options, { parse : false })); - } - } - } else { - originalEventHandler.call(this, [].slice.call(arguments, 1)); - } - }; - - return this; -}; diff --git a/src/UI/Mixins/AsPersistedStateCollection.js b/src/UI/Mixins/AsPersistedStateCollection.js deleted file mode 100644 index cecdeb2d8..000000000 --- a/src/UI/Mixins/AsPersistedStateCollection.js +++ /dev/null @@ -1,72 +0,0 @@ -var _ = require('underscore'); -var Config = require('../Config'); - -module.exports = function() { - - var originalInit = this.prototype.initialize; - this.prototype.initialize = function(options) { - - options = options || {}; - - if (options.tableName) { - this.tableName = options.tableName; - } - - if (!this.tableName && !options.tableName) { - throw 'tableName is required'; - } - - _setInitialState.call(this); - - this.on('backgrid:sort', _storeStateFromBackgrid, this); - this.on('drone:sort', _storeState, this); - - if (originalInit) { - originalInit.call(this, options); - } - }; - - if (!this.prototype._getSortMapping) { - this.prototype._getSortMapping = function(key) { - return { - name : key, - sortKey : key - }; - }; - } - - var _setInitialState = function() { - var key = Config.getValue('{0}.sortKey'.format(this.tableName), this.state.sortKey); - var direction = Config.getValue('{0}.sortDirection'.format(this.tableName), this.state.order); - var order = parseInt(direction, 10); - - this.state.sortKey = this._getSortMapping(key).sortKey; - this.state.order = order; - }; - - var _storeStateFromBackgrid = function(column, sortDirection) { - var order = _convertDirectionToInt(sortDirection); - var sortKey = this._getSortMapping(column.get('name')).sortKey; - - Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); - Config.setValue('{0}.sortDirection'.format(this.tableName), order); - }; - - var _storeState = function(sortModel, sortDirection) { - var order = _convertDirectionToInt(sortDirection); - var sortKey = this._getSortMapping(sortModel.get('name')).sortKey; - - Config.setValue('{0}.sortKey'.format(this.tableName), sortKey); - Config.setValue('{0}.sortDirection'.format(this.tableName), order); - }; - - var _convertDirectionToInt = function(dir) { - if (dir === 'ascending') { - return '-1'; - } - - return '1'; - }; - - return this; -}; diff --git a/src/UI/Mixins/AsSortedCollection.js b/src/UI/Mixins/AsSortedCollection.js deleted file mode 100644 index 78a31edb6..000000000 --- a/src/UI/Mixins/AsSortedCollection.js +++ /dev/null @@ -1,130 +0,0 @@ -var _ = require('underscore'); -var Config = require('../Config'); - -module.exports = function() { - - var originalSetSorting = this.prototype.setSorting; - - this.prototype.setSorting = function(sortKey, order, options) { - var sortMapping = this._getSortMapping(sortKey); - - options = _.defaults({ sortValue : sortMapping.sortValue }, options || {}); - - return originalSetSorting.call(this, sortMapping.sortKey, order, options); - }; - - this.prototype._getSortMappings = function() { - var result = {}; - - if (this.sortMappings) { - _.each(this.sortMappings, function(values, key) { - var item = { - name : key, - sortKey : values.sortKey || key, - sortValue : values.sortValue - }; - result[key] = item; - result[item.sortKey] = item; - }); - } - - return result; - }; - - this.prototype._getSortMapping = function(key) { - var sortMappings = this._getSortMappings(); - - return sortMappings[key] || { - name : key, - sortKey : key - }; - }; - - this.prototype._getSecondarySorting = function() { - var sortKey = this.state.secondarySortKey; - var sortOrder = this.state.secondarySortOrder || -1; - - if (!sortKey || sortKey === this.state.sortKey) { - return null; - } - - var sortMapping = this._getSortMapping(sortKey); - - if (!sortMapping.sortValue) { - sortMapping.sortValue = function(model, attr) { - return model.get(attr); - }; - } - - return { - key : sortKey, - order : sortOrder, - sortValue : sortMapping.sortValue - }; - }; - - this.prototype._makeComparator = function(sortKey, order, sortValue) { - var state = this.state; - var secondarySorting = this._getSecondarySorting(); - - sortKey = sortKey || state.sortKey; - order = order || state.order; - - if (!sortKey || !order) { - return; - } - - if (!sortValue) { - sortValue = function(model, attr) { - return model.get(attr); - }; - } - - return function(left, right) { - var l = sortValue(left, sortKey, order); - var r = sortValue(right, sortKey, order); - var t; - - if (order === 1) { - t = l; - l = r; - r = t; - } - - if (l === r) { - - if (secondarySorting) { - var ls = secondarySorting.sortValue(left, secondarySorting.key, order); - var rs = secondarySorting.sortValue(right, secondarySorting.key, order); - var ts; - - if (secondarySorting.order === 1) { - ts = ls; - ls = rs; - rs = ts; - } - - if (ls === rs) { - return 0; - } - - if (ls < rs) { - return -1; - } - - return 1; - } - - return 0; - } - - else if (l < r) { - return -1; - } - - return 1; - }; - }; - - return this; -}; diff --git a/src/UI/Mixins/AsSortedCollectionView.js b/src/UI/Mixins/AsSortedCollectionView.js deleted file mode 100644 index e68b833a7..000000000 --- a/src/UI/Mixins/AsSortedCollectionView.js +++ /dev/null @@ -1,24 +0,0 @@ -module.exports = function() { - this.prototype.appendHtml = function(collectionView, itemView, index) { - var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el; - var collection = collectionView.collection; - - // If the index of the model is at the end of the collection append, else insert at proper index - if (index >= collection.size() - 1) { - childrenContainer.append(itemView.el); - } else { - var previousModel = collection.at(index + 1); - var previousView = this.children.findByModel(previousModel); - - if (previousView) { - previousView.$el.before(itemView.$el); - } - - else { - childrenContainer.append(itemView.el); - } - } - }; - - return this; -}; \ No newline at end of file diff --git a/src/UI/Mixins/AsValidatedView.js b/src/UI/Mixins/AsValidatedView.js deleted file mode 100644 index 22e3c0844..000000000 --- a/src/UI/Mixins/AsValidatedView.js +++ /dev/null @@ -1,93 +0,0 @@ -var Validation = require('backbone.validation'); -var _ = require('underscore'); - -module.exports = (function() { - 'use strict'; - return function() { - - var originalInitialize = this.prototype.initialize; - var originalOnRender = this.prototype.onRender; - var originalBeforeClose = this.prototype.onBeforeClose; - - var errorHandler = function(response) { - if (this.model) { - this.model.trigger('validation:failed', response); - } else { - this.trigger('validation:failed', response); - } - }; - - var validatedSync = function(method, model, options) { - model.trigger('validation:sync'); - - arguments[2].isValidatedCall = true; - return model._originalSync.apply(this, arguments).fail(errorHandler.bind(this)); - }; - - var bindToModel = function(model) { - if (!model._originalSync) { - model._originalSync = model.sync; - model.sync = validatedSync.bind(this); - } - }; - - var validationFailed = function(response) { - if (response.status === 400) { - var view = this; - var validationErrors = JSON.parse(response.responseText); - _.each(validationErrors, function(error) { - view.$el.processServerError(error); - }); - } - }; - - this.prototype.initialize = function(options) { - if (this.model) { - this.listenTo(this.model, 'validation:sync', function() { - this.$el.removeAllErrors(); - }); - - this.listenTo(this.model, 'validation:failed', validationFailed); - } else { - this.listenTo(this, 'validation:sync', function() { - this.$el.removeAllErrors(); - }); - - this.listenTo(this, 'validation:failed', validationFailed); - } - - if (originalInitialize) { - originalInitialize.call(this, options); - } - }; - - this.prototype.onRender = function() { - Validation.bind(this); - this.bindToModelValidation = bindToModel.bind(this); - - if (this.model) { - this.bindToModelValidation(this.model); - } - - if (originalOnRender) { - originalOnRender.call(this); - } - }; - - this.prototype.onBeforeClose = function() { - if (this.model) { - Validation.unbind(this); - - //If we don't do this the next time the model is used the sync is bound to an old view - this.model.sync = this.model._originalSync; - this.model._originalSync = undefined; - } - - if (originalBeforeClose) { - originalBeforeClose.call(this); - } - }; - - return this; - }; -}).call(this); \ No newline at end of file diff --git a/src/UI/Mixins/AutoComplete.js b/src/UI/Mixins/AutoComplete.js deleted file mode 100644 index f0499d373..000000000 --- a/src/UI/Mixins/AutoComplete.js +++ /dev/null @@ -1,51 +0,0 @@ -var $ = require('jquery'); -require('typeahead'); - -$.fn.autoComplete = function(options) { - if (!options) { - throw 'options are required'; - } - - if (!options.resource) { - throw 'resource is required'; - } - - if (!options.query) { - throw 'query is required'; - } - - $(this).typeahead({ - hint : true, - highlight : true, - minLength : 3, - items : 20 - }, { - name : options.resource.replace('/'), - displayKey : '', - source : function(filter, callback) { - var data = options.data || {}; - data[options.query] = filter; - $.ajax({ - url : window.NzbDrone.ApiRoot + options.resource, - dataType : 'json', - type : 'GET', - data : data, - success : function(response) { - if (options.filter) { - options.filter.call(this, filter, response, callback); - } else { - var matches = []; - - $.each(response, function(i, d) { - if (d[options.query] && d[options.property].startsWith(filter)) { - matches.push({ value : d[options.property] }); - } - }); - - callback(matches); - } - } - }); - } - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/CopyToClipboard.js b/src/UI/Mixins/CopyToClipboard.js deleted file mode 100644 index 77db6e39a..000000000 --- a/src/UI/Mixins/CopyToClipboard.js +++ /dev/null @@ -1,22 +0,0 @@ -var $ = require('jquery'); -var StatusModel = require('../System/StatusModel'); -var ZeroClipboard = require('zero.clipboard'); -var Messenger = require('../Shared/Messenger'); - -$.fn.copyToClipboard = function(input) { - - ZeroClipboard.config({ - swfPath : StatusModel.get('urlBase') + '/Content/zero.clipboard.swf' - }); - - var client = new ZeroClipboard(this); - - client.on('ready', function(e) { - client.on('copy', function(e) { - e.clipboardData.setData("text/plain", input.val()); - }); - client.on('aftercopy', function() { - Messenger.show({ message : 'Copied text to clipboard' }); - }); - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/DirectoryAutoComplete.js b/src/UI/Mixins/DirectoryAutoComplete.js deleted file mode 100644 index f18ed35de..000000000 --- a/src/UI/Mixins/DirectoryAutoComplete.js +++ /dev/null @@ -1,29 +0,0 @@ -var $ = require('jquery'); -require('./AutoComplete'); - -$.fn.directoryAutoComplete = function(options) { - options = options || {}; - - var query = 'path'; - var data = { - includeFiles: options.includeFiles || false - }; - - $(this).autoComplete({ - resource : '/filesystem', - query : query, - data : data, - filter : function(filter, response, callback) { - var matches = []; - var results = response.directories.concat(response.files); - - $.each(results, function(i, d) { - if (d[query] && d[query].startsWith(filter)) { - matches.push({ value : d[query] }); - } - }); - - callback(matches); - } - }); -}; \ No newline at end of file diff --git a/src/UI/Mixins/FileBrowser.js b/src/UI/Mixins/FileBrowser.js deleted file mode 100644 index a19318fb4..000000000 --- a/src/UI/Mixins/FileBrowser.js +++ /dev/null @@ -1,32 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); -require('../Shared/FileBrowser/FileBrowserLayout'); -require('./DirectoryAutoComplete'); - -$.fn.fileBrowser = function(options) { - var inputs = $(this); - - inputs.each(function() { - var input = $(this); - var inputOptions = $.extend({ input : input, showFiles: input.hasClass('x-filepath') }, options); - var inputGroup = $('<div class="input-group"></div>'); - var inputGroupButton = $('<span class="input-group-btn"></span>'); - - var button = $('<button class="btn btn-primary x-file-browser" title="Browse"><i class="icon-lidarr-folder-open"/></button>'); - - if (input.parent('.input-group').length > 0) { - input.parent('.input-group').find('.input-group-btn').prepend(button); - } else { - inputGroupButton.append(button); - input.wrap(inputGroup); - input.after(inputGroupButton); - } - - button.on('click', function() { - vent.trigger(vent.Commands.ShowFileBrowser, inputOptions); - }); - - input.directoryAutoComplete({ includeFiles: inputOptions.showFiles }); - }); - -}; diff --git a/src/UI/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js deleted file mode 100644 index 0f6a542b4..000000000 --- a/src/UI/Mixins/TagInput.js +++ /dev/null @@ -1,156 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var TagCollection = require('../Tags/TagCollection'); -var TagModel = require('../Tags/TagModel'); -require('bootstrap.tagsinput'); - -var substringMatcher = function(tagCollection) { - return function findMatches (q, cb) { - q = q.replace(/[^-_a-z0-9]/gi, '').toLowerCase(); - var matches = _.select(tagCollection.toJSON(), function(tag) { - return tag.label.toLowerCase().indexOf(q) > -1; - }); - cb(matches); - }; -}; -var getExistingTags = function(tagValues) { - return _.select(TagCollection.toJSON(), function(tag) { - return _.contains(tagValues, tag.id); - }); -}; - -var testTag = function(item) { - var tagLimitations = new RegExp('[^-_a-z0-9]', 'i'); - try { - return !tagLimitations.test(item); - } - catch (e) { - return false; - } -}; - -var originalAdd = $.fn.tagsinput.Constructor.prototype.add; -var originalRemove = $.fn.tagsinput.Constructor.prototype.remove; -var originalBuild = $.fn.tagsinput.Constructor.prototype.build; - -$.fn.tagsinput.Constructor.prototype.add = function(item, dontPushVal) { - var tagCollection = this.options.tagCollection; - - if (!tagCollection) { - originalAdd.call(this, item, dontPushVal); - return; - } - var self = this; - - if (typeof item === 'string') { - var existing = _.find(tagCollection.toJSON(), { label : item }); - - if (existing) { - originalAdd.call(this, existing, dontPushVal); - } else if (this.options.allowNew) { - if (item === null || item === '' || !testTag(item)) { - return; - } - - var newTag = new TagModel(); - newTag.set({ label : item.toLowerCase() }); - tagCollection.add(newTag); - - newTag.save().done(function() { - item = newTag.toJSON(); - originalAdd.call(self, item, dontPushVal); - }); - } - } else { - originalAdd.call(self, item, dontPushVal); - } - - self.$input.typeahead('val', ''); -}; - -$.fn.tagsinput.Constructor.prototype.remove = function(item, dontPushVal) { - if (item === null) { - return; - } - - originalRemove.call(this, item, dontPushVal); -}; - -$.fn.tagsinput.Constructor.prototype.build = function(options) { - var self = this; - var defaults = { - confirmKeys : [ - 9, - 13, - 32, - 44, - 59 - ] //tab, enter, space, comma, semi-colon - }; - - options = $.extend({}, defaults, options); - - self.$input.on('keydown', function(event) { - if (event.which === 9) { - var e = $.Event('keypress'); - e.which = 9; - self.$input.trigger(e); - event.preventDefault(); - } - }); - - self.$input.on('focusout', function() { - self.add(self.$input.val()); - self.$input.val(''); - }); - - originalBuild.call(this, options); -}; - -$.fn.tagInput = function(options) { - options = $.extend({}, { allowNew : true }, options); - - var input = this; - var model = options.model; - var property = options.property; - - var tagInput = $(this).tagsinput({ - tagCollection : TagCollection, - freeInput : true, - allowNew : options.allowNew, - itemValue : 'id', - itemText : 'label', - trimValue : true, - typeaheadjs : { - name : 'tags', - displayKey : 'label', - source : substringMatcher(TagCollection) - } - }); - - //Override the free input being set to false because we're using objects - $(tagInput)[0].options.freeInput = true; - - if (model) { - var tags = getExistingTags(model.get(property)); - - //Remove any existing tags and re-add them - $(this).tagsinput('removeAll'); - _.each(tags, function(tag) { - $(input).tagsinput('add', tag); - }); - $(this).tagsinput('refresh'); - $(this).on('itemAdded', function(event) { - var tags = model.get(property); - tags.push(event.item.id); - model.set(property, tags); - }); - $(this).on('itemRemoved', function(event) { - if (!event.item) { - return; - } - var tags = _.without(model.get(property), event.item.id); - model.set(property, tags); - }); - } -}; \ No newline at end of file diff --git a/src/UI/Mixins/backbone.signalr.mixin.js b/src/UI/Mixins/backbone.signalr.mixin.js deleted file mode 100644 index 8aad9b71c..000000000 --- a/src/UI/Mixins/backbone.signalr.mixin.js +++ /dev/null @@ -1,46 +0,0 @@ -var vent = require('vent'); -var _ = require('underscore'); -var Backbone = require('backbone'); - -require('signalR'); - -module.exports = _.extend(Backbone.Collection.prototype, { - bindSignalR : function(bindOptions) { - - var collection = this; - bindOptions = bindOptions || {}; - - var processMessage = function(options) { - if (options.action === 'sync') { - console.log('sync received, re-fetching collection'); - collection.fetch(); - - return; - } - - if (options.action === 'deleted') { - collection.remove(new collection.model(options.resource, { parse : true })); - - return; - } - - var model = new collection.model(options.resource, { parse : true }); - - //updateOnly will prevent the collection from adding a new item - if (bindOptions.updateOnly && !collection.get(model.get('id'))) { - return; - } - - collection.add(model, { - merge : true, - changeSource : 'signalr' - }); - - console.log(options.action + ': {0}}'.format(options.resource)); - }; - - collection.listenTo(vent, 'server:' + collection.url.split('/api/')[1], processMessage); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Navbar/NavbarLayout.js b/src/UI/Navbar/NavbarLayout.js deleted file mode 100644 index c364a0923..000000000 --- a/src/UI/Navbar/NavbarLayout.js +++ /dev/null @@ -1,63 +0,0 @@ -var Marionette = require('marionette'); -var $ = require('jquery'); -var HealthView = require('../Health/HealthView'); -var QueueView = require('../Activity/Queue/QueueView'); -require('./Search'); - -module.exports = Marionette.Layout.extend({ - template : 'Navbar/NavbarLayoutTemplate', - - regions : { - health : '#x-health', - queue : '#x-queue-count' - }, - - ui : { - search : '.x-series-search', - collapse : '.x-navbar-collapse' - }, - - events : { - 'click a' : 'onClick' - }, - - onRender : function() { - this.ui.search.bindSearch(); - this.health.show(new HealthView()); - this.queue.show(new QueueView()); - }, - - onClick : function(event) { - var target = $(event.target); - - //look down for <a/> - var href = event.target.getAttribute('href'); - - if (href && href.startsWith("http")) { - return; - } - - event.preventDefault(); - - //if couldn't find it look up' - if (!href && target.closest('a') && target.closest('a')[0]) { - - var linkElement = target.closest('a')[0]; - - href = linkElement.getAttribute('href'); - this.setActive(linkElement); - } else { - this.setActive(event.target); - } - - if ($(window).width() < 768) { - this.ui.collapse.collapse('hide'); - } - }, - - setActive : function(element) { - //Todo: Set active on first load - this.$('a').removeClass('active'); - $(element).addClass('active'); - } -}); \ No newline at end of file diff --git a/src/UI/Navbar/NavbarLayoutTemplate.hbs b/src/UI/Navbar/NavbarLayoutTemplate.hbs deleted file mode 100644 index 4fb80fabd..000000000 --- a/src/UI/Navbar/NavbarLayoutTemplate.hbs +++ /dev/null @@ -1,44 +0,0 @@ -<!-- Static navbar --> -<div class="navbar navbar-nzbdrone" role="navigation"> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle navbar-inverse" data-toggle="collapse" data-target=".navbar-collapse"> - <span class="sr-only">Toggle navigation</span> - <span class="icon-lidarr-navbar-collapsed fa-lg"></span> - </button> - <a class="navbar-brand" href="{{UrlBase}}/"> - <!--<img src="{{UrlBase}}/Content/Images/logo.png?v=2" alt="Lidarr">--> - <img src="{{UrlBase}}/Content/Images/logos/128.png" class="visible-lg"/> - <img src="{{UrlBase}}/Content/Images/logos/64.png" class="visible-md visible-sm"/> - <span class="visible-xs"> - <img src="{{UrlBase}}/Content/Images/logos/32.png"/> - <span class="logo-text">lidarr</span> - </span> - - </a> - </div> - <div class="navbar-collapse collapse x-navbar-collapse"> - <ul class="nav navbar-nav"> - <li><a href="{{UrlBase}}/" class="x-series-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-artist"></i> Artists</a></li> - <li><a href="{{UrlBase}}/calendar" class="x-calendar-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-calendar"></i> Calendar</a></li> - <li><a href="{{UrlBase}}/activity" class="x-activity-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-activity"></i> Activity<span id="x-queue-count" class="navbar-info"></span></a></li> - <li><a href="{{UrlBase}}/wanted" class="x-wanted-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-wanted"></i> Wanted</a></li> - <li><a href="{{UrlBase}}/settings" class="x-settings-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-settings"></i> Settings</a></li> - <li><a href="{{UrlBase}}/system" class="x-system-nav"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-system"></i> System<span id="x-health" class="navbar-info"></span></a></li> - <li><a href="https://paypal.me/Lidarr" target="_blank"><i class="icon-lidarr-navbar-icon icon-lidarr-navbar-donate"></i> Donate</a></li> - </ul> - <ul class="nav navbar-nav navbar-right"> - <li class="active screen-size"></li> - </ul> - </div><!--/.nav-collapse --> - </div><!--/.container-fluid --> - - <div class="col-md-12 search"> - <div class="col-md-6 col-md-offset-3"> - <div class="input-group"> - <span class="input-group-addon"><i class="fa fa-search"></i></span> - <input type="text" class="col-md-6 form-control x-series-search" placeholder="Search the artist in your library"> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Navbar/Search.js b/src/UI/Navbar/Search.js deleted file mode 100644 index c6ff2c400..000000000 --- a/src/UI/Navbar/Search.js +++ /dev/null @@ -1,42 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Backbone = require('backbone'); -var jdu = require('jdu'); -var ArtistCollection = require('../Artist/ArtistCollection'); -require('typeahead'); - -vent.on(vent.Hotkeys.NavbarSearch, function() { - $('.x-artist-search').focus(); -}); - -var stringCleaner = function(text) { - return jdu.replace(text.toLowerCase()); -}; - -var substringMatcher = function() { - - return function findMatches (q, cb) { - var matches = _.select(ArtistCollection.toJSON(), function(artist) { - return stringCleaner(artist.name).indexOf(stringCleaner(q)) > -1; - }); - cb(matches); - }; -}; - -$.fn.bindSearch = function() { - $(this).typeahead({ - hint : true, - minLength : 1 - }, { - name : 'artist', - displayKey : 'name', - source : substringMatcher() - }); - - $(this).on('typeahead:selected typeahead:autocompleted', function(e, artist) { - this.blur(); - $(this).val(''); - Backbone.history.navigate('/artist/{0}'.format(artist.nameSlug), { trigger : true }); - }); -}; \ No newline at end of file diff --git a/src/UI/Profile/ProfileCollection.js b/src/UI/Profile/ProfileCollection.js deleted file mode 100644 index 838ac3c9c..000000000 --- a/src/UI/Profile/ProfileCollection.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); -var ProfileModel = require('./ProfileModel'); - -var ProfileCollection = Backbone.Collection.extend({ - model : ProfileModel, - url : window.NzbDrone.ApiRoot + '/profile' -}); - -var profiles = new ProfileCollection(); - -profiles.fetch(); - -module.exports = profiles; diff --git a/src/UI/Profile/ProfileModel.js b/src/UI/Profile/ProfileModel.js deleted file mode 100644 index 259e4be5f..000000000 --- a/src/UI/Profile/ProfileModel.js +++ /dev/null @@ -1,9 +0,0 @@ -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({ - defaults : { - id : null, - name : '', - cutoff : null - } -}); \ No newline at end of file diff --git a/src/UI/Profile/ProfileSelectionPartial.hbs b/src/UI/Profile/ProfileSelectionPartial.hbs deleted file mode 100644 index 5526b361d..000000000 --- a/src/UI/Profile/ProfileSelectionPartial.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<select class="col-md-2 form-control x-profile"> - {{#each this}} - <option value="{{id}}">{{name}}</option> - {{/each}} -</select> \ No newline at end of file diff --git a/src/UI/Quality/QualityDefinitionCollection.js b/src/UI/Quality/QualityDefinitionCollection.js deleted file mode 100644 index 7f111f2fd..000000000 --- a/src/UI/Quality/QualityDefinitionCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var QualityDefinitionModel = require('./QualityDefinitionModel'); - -module.exports = Backbone.Collection.extend({ - model : QualityDefinitionModel, - url : window.NzbDrone.ApiRoot + '/qualitydefinition' -}); \ No newline at end of file diff --git a/src/UI/Quality/QualityDefinitionModel.js b/src/UI/Quality/QualityDefinitionModel.js deleted file mode 100644 index e5a901b6d..000000000 --- a/src/UI/Quality/QualityDefinitionModel.js +++ /dev/null @@ -1,14 +0,0 @@ -var ModelBase = require('../Settings/SettingsModelBase'); - -module.exports = ModelBase.extend({ - baseInitialize : ModelBase.prototype.initialize, - - initialize : function() { - var name = this.get('quality').name; - - this.successMessage = 'Saved ' + name + ' quality settings'; - this.errorMessage = 'Couldn\'t save ' + name + ' quality settings'; - - this.baseInitialize.call(this); - } -}); \ No newline at end of file diff --git a/src/UI/Release/AgeCell.js b/src/UI/Release/AgeCell.js deleted file mode 100644 index f5a4bc7de..000000000 --- a/src/UI/Release/AgeCell.js +++ /dev/null @@ -1,33 +0,0 @@ -var moment = require('moment'); -var Backgrid = require('backgrid'); -var UiSettings = require('../Shared/UiSettingsModel'); -var FormatHelpers = require('../Shared/FormatHelpers'); - -module.exports = Backgrid.Cell.extend({ - className : 'age-cell', - - render : function() { - var age = this.model.get('age'); - var ageHours = this.model.get('ageHours'); - var ageMinutes = this.model.get('ageMinutes'); - var published = moment(this.model.get('publishDate')); - var publishedFormatted = published.format('{0} {1}'.format(UiSettings.get('shortDateFormat'), UiSettings.time(true, true))); - var formatted = age; - var suffix = FormatHelpers.plural(age, 'day'); - - if (age < 2) { - formatted = ageHours.toFixed(1); - suffix = FormatHelpers.plural(Math.round(ageHours), 'hour'); - } - - if (ageHours < 2) { - formatted = ageMinutes.toFixed(1); - suffix = FormatHelpers.plural(Math.round(ageMinutes), 'minute'); - } - - this.$el.html('<div title="{2}">{0} {1}</div>'.format(formatted, suffix, publishedFormatted)); - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/DownloadReportCell.js b/src/UI/Release/DownloadReportCell.js deleted file mode 100644 index 3c54d55ba..000000000 --- a/src/UI/Release/DownloadReportCell.js +++ /dev/null @@ -1,49 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'download-report-cell', - - events : { - 'click' : '_onClick' - }, - - _onClick : function() { - if (!this.model.get('downloadAllowed')) { - return; - } - - var self = this; - - this.$el.html('<i class="icon-lidarr-spinner fa-spin" title="Adding to download queue" />'); - - //Using success callback instead of promise so it - //gets called before the sync event is triggered - var promise = this.model.save(null, { - success : function() { - self.model.set('queued', true); - } - }); - - promise.fail(function (xhr) { - if (xhr.responseJSON && xhr.responseJSON.message) { - self.$el.html('<i class="icon-lidarr-download-failed" title="{0}" />'.format(xhr.responseJSON.message)); - } else { - self.$el.html('<i class="icon-lidarr-download-failed" title="Failed to add to download queue" />'); - } - }); - }, - - render : function() { - this.$el.empty(); - - if (this.model.get('queued')) { - this.$el.html('<i class="icon-lidarr-downloading" title="Added to downloaded queue" />'); - } else if (this.model.get('downloadAllowed')) { - this.$el.html('<i class="icon-lidarr-download" title="Add to download queue" />'); - } else { - this.className = 'no-download-report-cell'; - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/PeersCell.js b/src/UI/Release/PeersCell.js deleted file mode 100644 index 033c69115..000000000 --- a/src/UI/Release/PeersCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'peers-cell', - - render : function() { - if (this.model.get('protocol') === 'torrent') { - var seeders = this.model.get('seeders') || 0; - var leechers = this.model.get('leechers') || 0; - - var level = 'danger'; - - if (seeders > 0) { - level = 'warning'; - } else if (seeders > 10) { - level = 'info'; - } else if (seeders > 50) { - level = 'primary'; - } - - this.$el.html('<div class="label label-{2}" title="{0} seeders, {1} leechers">{0} / {1}</div>'.format(seeders, leechers, level)); - } - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/ProtocolCell.js b/src/UI/Release/ProtocolCell.js deleted file mode 100644 index ede35f9b3..000000000 --- a/src/UI/Release/ProtocolCell.js +++ /dev/null @@ -1,24 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'protocol-cell', - - render : function() { - var protocol = this.model.get('protocol') || 'Unknown'; - var label = '??'; - - if (protocol) { - if (protocol === 'torrent') { - label = 'torrent'; - } else if (protocol === 'usenet') { - label = 'nzb'; - } - - this.$el.html('<div class="label label-default protocol-{0}" title="{0}">{1}</div>'.format(protocol, label)); - } - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Release/ReleaseCollection.js b/src/UI/Release/ReleaseCollection.js deleted file mode 100644 index a2dbb13ed..000000000 --- a/src/UI/Release/ReleaseCollection.js +++ /dev/null @@ -1,56 +0,0 @@ -var PagableCollection = require('backbone.pageable'); -var ReleaseModel = require('./ReleaseModel'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/release', - model : ReleaseModel, - - state : { - pageSize : 2000, - sortKey : 'download', - order : -1 - }, - - mode : 'client', - - sortMappings : { - 'quality' : { - sortKey : 'qualityWeight' - }, - 'rejections' : { - sortValue : function(model) { - var rejections = model.get('rejections'); - var releaseWeight = model.get('releaseWeight'); - - if (rejections.length !== 0) { - return releaseWeight + 1000000; - } - - return releaseWeight; - } - }, - 'download' : { - sortKey : 'releaseWeight' - }, - 'seeders' : { - sortValue : function(model) { - var seeders = model.get('seeders') || 0; - var leechers = model.get('leechers') || 0; - - return seeders * 1000000 + leechers; - } - }, - 'age' : { - sortKey : 'ageMinutes' - } - }, - - fetchAlbumReleases : function(albumId) { - return this.fetch({ data : { albumId : albumId } }); - } -}); - -Collection = AsSortedCollection.call(Collection); - -module.exports = Collection; \ No newline at end of file diff --git a/src/UI/Release/ReleaseLayout.js b/src/UI/Release/ReleaseLayout.js deleted file mode 100644 index 07f4a1af6..000000000 --- a/src/UI/Release/ReleaseLayout.js +++ /dev/null @@ -1,78 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ReleaseCollection = require('./ReleaseCollection'); -var IndexerCell = require('../Cells/IndexerCell'); -var EpisodeNumberCell = require('../Cells/EpisodeNumberCell'); -var FileSizeCell = require('../Cells/FileSizeCell'); -var QualityCell = require('../Cells/QualityCell'); -var ApprovalStatusCell = require('../Cells/ApprovalStatusCell'); -var LoadingView = require('../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'Release/ReleaseLayoutTemplate', - - regions : { - grid : '#x-grid', - toolbar : '#x-toolbar' - }, - - columns : [ - { - name : 'indexer', - label : 'Indexer', - sortable : true, - cell : IndexerCell - }, - { - name : 'title', - label : 'Title', - sortable : true, - cell : Backgrid.StringCell - }, - { - name : 'episodeNumbers', - episodes : 'episodeNumbers', - label : 'season', - cell : EpisodeNumberCell - }, - { - name : 'size', - label : 'Size', - sortable : true, - cell : FileSizeCell - }, - { - name : 'quality', - label : 'Quality', - sortable : true, - cell : QualityCell - }, - { - name : 'rejections', - label : '', - cell : ApprovalStatusCell, - title : 'Release Rejected' - } - ], - - initialize : function() { - this.collection = new ReleaseCollection(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - this.collection.fetch(); - }, - - _showTable : function() { - if (!this.isClosed) { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } - } -}); \ No newline at end of file diff --git a/src/UI/Release/ReleaseLayoutTemplate.hbs b/src/UI/Release/ReleaseLayoutTemplate.hbs deleted file mode 100644 index 429260d74..000000000 --- a/src/UI/Release/ReleaseLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid"/> - </div> -</div> - diff --git a/src/UI/Release/ReleaseModel.js b/src/UI/Release/ReleaseModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Release/ReleaseModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewCollection.js b/src/UI/Rename/RenamePreviewCollection.js deleted file mode 100644 index 3d5325933..000000000 --- a/src/UI/Rename/RenamePreviewCollection.js +++ /dev/null @@ -1,34 +0,0 @@ -var Backbone = require('backbone'); -var RenamePreviewModel = require('./RenamePreviewModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/rename', - model : RenamePreviewModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - if (!options.artistId) { - throw 'artistId is required'; - } - - this.artistId = options.artistId; - this.albumId = options.albumId; - }, - - fetch : function(options) { - if (!this.artistId) { - throw 'artistId is required'; - } - - options = options || {}; - options.data = {}; - options.data.artistId = this.artistId; - - if (this.albumId !== undefined) { - options.data.albumId = this.albumId; - } - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewCollectionView.js b/src/UI/Rename/RenamePreviewCollectionView.js deleted file mode 100644 index 7751b666d..000000000 --- a/src/UI/Rename/RenamePreviewCollectionView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); -var RenamePreviewItemView = require('./RenamePreviewItemView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : RenamePreviewItemView -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewEmptyCollectionView.js b/src/UI/Rename/RenamePreviewEmptyCollectionView.js deleted file mode 100644 index f7b7a5166..000000000 --- a/src/UI/Rename/RenamePreviewEmptyCollectionView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewEmptyCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs b/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs deleted file mode 100644 index eeb0e3d0c..000000000 --- a/src/UI/Rename/RenamePreviewEmptyCollectionViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="alert alert-success"> - Success! My work is done, no files to rename. -</div> \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewFormatView.js b/src/UI/Rename/RenamePreviewFormatView.js deleted file mode 100644 index 8baab7d0e..000000000 --- a/src/UI/Rename/RenamePreviewFormatView.js +++ /dev/null @@ -1,22 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var NamingModel = require('../Settings/MediaManagement/Naming/NamingModel'); - -module.exports = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewFormatViewTemplate', - - templateHelpers : function() { - var type = this.model.get('seriesType'); - - return { - rename : this.naming.get('renameTracks'), - format : this.naming.get('standardTrackFormat') - }; - }, - - initialize : function() { - this.naming = new NamingModel(); - this.naming.fetch(); - this.listenTo(this.naming, 'sync', this.render); - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs b/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs deleted file mode 100644 index 77297f56b..000000000 --- a/src/UI/Rename/RenamePreviewFormatViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if rename}} -Naming pattern: {{format}} -{{/if}} diff --git a/src/UI/Rename/RenamePreviewItemView.js b/src/UI/Rename/RenamePreviewItemView.js deleted file mode 100644 index 394cd6045..000000000 --- a/src/UI/Rename/RenamePreviewItemView.js +++ /dev/null @@ -1,39 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Rename/RenamePreviewItemViewTemplate', - - ui : { - itemDiv : '.rename-preview-item', - checkboxIcon : '.rename-checkbox i' - }, - - onRender : function() { - this._setItemState(); - this.listenTo(this.model, 'change', this._setItemState); - this.listenTo(this.model, 'rename:select', this._onRenameAll); - }, - - _setItemState : function() { - var checked = this.model.get('rename'); - this.model.trigger('rename:select', this.model, checked); - - if (checked) { - this.ui.itemDiv.removeClass('do-not-rename'); - this.ui.checkboxIcon.addClass('icon-lidarr-checked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-unchecked'); - } else { - this.ui.itemDiv.addClass('do-not-rename'); - this.ui.checkboxIcon.addClass('icon-lidarr-unchecked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-checked'); - } - }, - - _onRenameAll : function(model, checked) { - this.model.set('rename', checked); - } -}); - -module.exports = AsModelBoundView.apply(view); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewItemViewTemplate.hbs b/src/UI/Rename/RenamePreviewItemViewTemplate.hbs deleted file mode 100644 index 57ef7200f..000000000 --- a/src/UI/Rename/RenamePreviewItemViewTemplate.hbs +++ /dev/null @@ -1,20 +0,0 @@ -<div class="rename-preview-item"> - <div class="row"> - <div class="rename-checkbox col-md-1"> - <label class="checkbox-button" title="Rename File"> - <input type="checkbox" name="rename"/> - <div class="btn"> - <i></i> - </div> - </label> - </div> - <div class="col-md-11"> - <div class="row"> - <div class="col-md-12 file-path"><i class="icon-lidarr-existing" title="Existing path" /> {{existingPath}}</div> - </div> - <div class="row"> - <div class="col-md-12 file-path"><i class="icon-lidarr-suggested" title="Suggested path" /> {{newPath}}</div> - </div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewLayout.js b/src/UI/Rename/RenamePreviewLayout.js deleted file mode 100644 index 9ea4f6c05..000000000 --- a/src/UI/Rename/RenamePreviewLayout.js +++ /dev/null @@ -1,124 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var RenamePreviewCollection = require('./RenamePreviewCollection'); -var RenamePreviewCollectionView = require('./RenamePreviewCollectionView'); -var EmptyCollectionView = require('./RenamePreviewEmptyCollectionView'); -var RenamePreviewFormatView = require('./RenamePreviewFormatView'); -var LoadingView = require('../Shared/LoadingView'); -var CommandController = require('../Commands/CommandController'); - -module.exports = Marionette.Layout.extend({ - className : 'modal-lg', - template : 'Rename/RenamePreviewLayoutTemplate', - - regions : { - renamePreviews : '#rename-previews', - formatRegion : '.x-format-region' - }, - - ui : { - pathInfo : '.x-path-info', - renameAll : '.x-rename-all', - checkboxIcon : '.x-rename-all-button i' - }, - - events : { - 'click .x-organize' : '_organizeFiles', - 'change .x-rename-all' : '_toggleAll' - }, - - initialize : function(options) { - this.model = options.artist; - this.albumId = options.albumId; - - var viewOptions = {}; - viewOptions.artistId = this.model.id; - viewOptions.albumId = this.albumId; - - this.collection = new RenamePreviewCollection(viewOptions); - this.listenTo(this.collection, 'sync', this._showPreviews); - this.listenTo(this.collection, 'rename:select', this._itemRenameChanged); - - this.collection.fetch(); - }, - - onRender : function() { - this.renamePreviews.show(new LoadingView()); - this.formatRegion.show(new RenamePreviewFormatView({ model : this.model })); - }, - - _showPreviews : function() { - if (this.collection.length === 0) { - this.ui.pathInfo.hide(); - this.renamePreviews.show(new EmptyCollectionView()); - return; - } - - this.ui.pathInfo.show(); - this.collection.invoke('set', { rename : true }); - this.renamePreviews.show(new RenamePreviewCollectionView({ collection : this.collection })); - }, - - _organizeFiles : function() { - if (this.collection.length === 0) { - vent.trigger(vent.Commands.CloseModalCommand); - } - - var files = _.map(this.collection.where({ rename : true }), function(model) { - return model.get('trackFileId'); - }); - - if (files.length === 0) { - vent.trigger(vent.Commands.CloseModalCommand); - return; - } - - if (this.albumId) { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - artistId : this.model.id, - albumId : this.albumId, - files : files - }); - } else { - CommandController.Execute('renameFiles', { - name : 'renameFiles', - artistId : this.model.id, - albumId : -1, - files : files - }); - } - - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _setCheckedState : function(checked) { - if (checked) { - this.ui.checkboxIcon.addClass('icon-lidarr-checked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-unchecked'); - } else { - this.ui.checkboxIcon.addClass('icon-lidarr-unchecked'); - this.ui.checkboxIcon.removeClass('icon-lidarr-checked'); - } - }, - - _toggleAll : function() { - var checked = this.ui.renameAll.prop('checked'); - this._setCheckedState(checked); - - this.collection.each(function(model) { - model.trigger('rename:select', model, checked); - }); - }, - - _itemRenameChanged : function(model, checked) { - var allChecked = this.collection.all(function(m) { - return m.get('rename'); - }); - - if (!checked || allChecked) { - this._setCheckedState(checked); - } - } -}); \ No newline at end of file diff --git a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs b/src/UI/Rename/RenamePreviewLayoutTemplate.hbs deleted file mode 100644 index ccb5d9b3d..000000000 --- a/src/UI/Rename/RenamePreviewLayoutTemplate.hbs +++ /dev/null @@ -1,34 +0,0 @@ -<div class="modal-content"> - <div class="rename-preview-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3> - <i class="icon-lidarr-rename"></i> Organize & Rename - </h3> - - </div> - <div class="modal-body"> - <div class="alert alert-info"> - <div class="path-info x-path-info">All paths are relative to: <strong>{{path}}</strong></div> - <div class="x-format-region"></div> - </div> - - <div id="rename-previews"></div> - - </div> - <div class="modal-footer"> - - <span class="rename-all-button x-rename-all-button pull-left"> - <label class="checkbox-button" title="Toggle all"> - <input type="checkbox" checked="checked" class="x-rename-all"/> - <div class="btn btn-icon-only"> - <i class="icon-lidarr-checked"></i> - </div> - </label> - </span> - - <button class="btn" data-dismiss="modal">Close</button> - <button class="btn btn-primary x-organize">Organize</button> - </div> - </div> -</div> diff --git a/src/UI/Rename/RenamePreviewModel.js b/src/UI/Rename/RenamePreviewModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Rename/RenamePreviewModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Rename/rename.less b/src/UI/Rename/rename.less deleted file mode 100644 index 07e694016..000000000 --- a/src/UI/Rename/rename.less +++ /dev/null @@ -1,42 +0,0 @@ -@import "../Content/FontAwesome/font-awesome"; -@import "../Content/Bootstrap/variables"; -@import "../Content/variables"; - -.rename-preview-item { - margin-bottom: 5px; - padding: 5px; - border-bottom: 1px solid #e5e5e5; - - &.do-not-rename { - background-color: #aaaaaa; - opacity: 0.7; - } - - .rename-checkbox { - width: 40px; - padding-top: 5px; - margin-right: 10px; - - .checkbox-button { - .btn { - text-align: left; - width: 38px; - } - } - } -} - -.path-info { - display: none; -} - -.rename-all-button { - display: inline-block; - - .checkbox-button { - .btn { - text-align: left; - width: 38px; - } - } -} diff --git a/src/UI/Router.js b/src/UI/Router.js deleted file mode 100644 index e72f0af93..000000000 --- a/src/UI/Router.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var Controller = require('./Controller'); - -module.exports = Marionette.AppRouter.extend({ - controller : new Controller(), - appRoutes : { - 'addartist' : 'addArtist', - 'addartist/:action(/:query)' : 'addArtist', - 'calendar' : 'calendar', - 'settings' : 'settings', - 'settings/:action(/:query)' : 'settings', - 'wanted' : 'wanted', - 'wanted/:action' : 'wanted', - 'history' : 'activity', - 'history/:action' : 'activity', - 'activity' : 'activity', - 'activity/:action' : 'activity', - 'rss' : 'rss', - 'system' : 'system', - 'system/:action' : 'system', - 'albumstudio' : 'albumStudio', - 'artisteditor' : 'artistEditor', - ':whatever' : 'showNotFound' - } -}); \ No newline at end of file diff --git a/src/UI/Series/Delete/DeleteSeriesTemplate.hbs b/src/UI/Series/Delete/DeleteSeriesTemplate.hbs deleted file mode 100644 index caccec733..000000000 --- a/src/UI/Series/Delete/DeleteSeriesTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete {{title}}</h3> - </div> - <div class="modal-body delete-series-modal"> - - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - <h3 class="path">{{path}}</h3> - - <div class="form-group"> - <label class="col-sm-4 control-label">Delete all files</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-delete-files"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn slide-button btn-danger"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Do you want to delete all files from disk?"/> - <i class="icon-lidarr-form-warning" title="This option is irreversible, use with extreme caution"/> - </span> - </div> - </div> - </div> - <div class="col-md-offset-1 col-md-5 delete-files-info x-delete-files-info"> - {{episodeFileCount}} episode files will be deleted - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Series/Delete/DeleteSeriesView.js b/src/UI/Series/Delete/DeleteSeriesView.js deleted file mode 100644 index de6640b5e..000000000 --- a/src/UI/Series/Delete/DeleteSeriesView.js +++ /dev/null @@ -1,41 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Delete/DeleteSeriesTemplate', - - events : { - 'click .x-confirm-delete' : 'removeSeries', - 'change .x-delete-files' : 'changeDeletedFiles' - }, - - ui : { - deleteFiles : '.x-delete-files', - deleteFilesInfo : '.x-delete-files-info', - indicator : '.x-indicator' - }, - - removeSeries : function() { - var self = this; - var deleteFiles = this.ui.deleteFiles.prop('checked'); - this.ui.indicator.show(); - - this.model.destroy({ - data : { 'deleteFiles' : deleteFiles }, - wait : true - }).done(function() { - vent.trigger(vent.Events.SeriesDeleted, { series : self.model }); - vent.trigger(vent.Commands.CloseModalCommand); - }); - }, - - changeDeletedFiles : function() { - var deleteFiles = this.ui.deleteFiles.prop('checked'); - - if (deleteFiles) { - this.ui.deleteFilesInfo.show(); - } else { - this.ui.deleteFilesInfo.hide(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeNumberCell.js b/src/UI/Series/Details/EpisodeNumberCell.js deleted file mode 100644 index 9a84e644e..000000000 --- a/src/UI/Series/Details/EpisodeNumberCell.js +++ /dev/null @@ -1,47 +0,0 @@ -var Marionette = require('marionette'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var reqres = require('../../reqres'); -var SeriesCollection = require('../SeriesCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-number-cell', - template : 'Series/Details/EpisodeNumberCellTemplate', - - render : function() { - this.$el.empty(); - this.$el.html(this.model.get('episodeNumber')); - - var series = SeriesCollection.get(this.model.get('seriesId')); - - if (series.get('seriesType') === 'anime' && this.model.has('absoluteEpisodeNumber')) { - this.$el.html('{0} ({1})'.format(this.model.get('episodeNumber'), this.model.get('absoluteEpisodeNumber'))); - } - - var alternateTitles = []; - - if (reqres.hasHandler(reqres.Requests.GetAlternateNameBySeasonNumber)) { - alternateTitles = reqres.request(reqres.Requests.GetAlternateNameBySeasonNumber, this.model.get('seriesId'), this.model.get('seasonNumber'), this.model.get('sceneSeasonNumber')); - } - - if (this.model.get('sceneSeasonNumber') > 0 || this.model.get('sceneEpisodeNumber') > 0 || this.model.has('sceneAbsoluteEpisodeNumber') || alternateTitles.length > 0) { - this.templateFunction = Marionette.TemplateCache.get(this.template); - - var json = this.model.toJSON(); - json.alternateTitles = alternateTitles; - - var html = this.templateFunction(json); - - this.$el.popover({ - content : html, - html : true, - trigger : 'hover', - title : 'Scene Information', - placement : 'right', - container : this.$el - }); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs b/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs deleted file mode 100644 index a9028a423..000000000 --- a/src/UI/Series/Details/EpisodeNumberCellTemplate.hbs +++ /dev/null @@ -1,39 +0,0 @@ -<div class="scene-info"> - {{#if sceneSeasonNumber}} - <div class="row"> - <div class="key">Season</div> - <div class="value">{{sceneSeasonNumber}}</div> - </div> - {{/if}} - - {{#if sceneEpisodeNumber}} - <div class="row"> - <div class="key">Episode</div> - <div class="value">{{sceneEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if sceneAbsoluteEpisodeNumber}} - <div class="row"> - <div class="key">Absolute</div> - <div class="value">{{sceneAbsoluteEpisodeNumber}}</div> - </div> - {{/if}} - - {{#if alternateTitles}} - <div class="row"> - {{#if_gt alternateTitles.length compare="1"}} - <div class="key">Titles</div> - {{else}} - <div class="key">Title</div> - {{/if_gt}} - <div class="value"> - <ul> - {{#each alternateTitles}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - </div> - {{/if}} -</div> \ No newline at end of file diff --git a/src/UI/Series/Details/EpisodeWarningCell.js b/src/UI/Series/Details/EpisodeWarningCell.js deleted file mode 100644 index 1178ac4ab..000000000 --- a/src/UI/Series/Details/EpisodeWarningCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var SeriesCollection = require('../SeriesCollection'); - -module.exports = NzbDroneCell.extend({ - className : 'episode-warning-cell', - - render : function() { - this.$el.empty(); - - if (this.model.get('unverifiedSceneNumbering')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Scene number hasn\'t been verified yet."></i>'); - } - - else if (SeriesCollection.get(this.model.get('seriesId')).get('seriesType') === 'anime' && this.model.get('seasonNumber') > 0 && !this.model.has('absoluteEpisodeNumber')) { - this.$el.html('<i class="icon-lidarr-form-warning" title="Episode does not have an absolute episode number"></i>'); - } - - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/InfoView.js b/src/UI/Series/Details/InfoView.js deleted file mode 100644 index c7fab9fc4..000000000 --- a/src/UI/Series/Details/InfoView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Details/InfoViewTemplate', - - initialize : function(options) { - this.episodeFileCollection = options.episodeFileCollection; - - this.listenTo(this.model, 'change', this.render); - this.listenTo(this.episodeFileCollection, 'sync', this.render); - }, - - templateHelpers : function() { - return { - fileCount : this.episodeFileCollection.length - }; - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/InfoViewTemplate.hbs b/src/UI/Series/Details/InfoViewTemplate.hbs deleted file mode 100644 index b52130246..000000000 --- a/src/UI/Series/Details/InfoViewTemplate.hbs +++ /dev/null @@ -1,73 +0,0 @@ -<div class="row"> - <div class="col-md-9"> - {{profile profileId}} - - {{#if network}} - <span class="label label-info">{{network}}</span> - {{/if}} - - <span class="label label-info">{{runtime}} minutes</span> - <span class="label label-info">{{path}}</span> - - {{#if ratings}} - <span class="label label-info" title="{{ratings.votes}} vote{{#if_gt ratings.votes compare="1"}}s{{/if_gt}}">{{ratings.value}}</span> - {{/if}} - - <span class="label label-info">{{Bytes sizeOnDisk}}</span> - - {{#if_eq fileCount compare="1"}} - <span class="label label-info"> 1 file</span> - {{else}} - <span class="label label-info"> {{fileCount}} files</span> - {{/if_eq}} - - {{#if_eq status compare="continuing"}} - <span class="label label-info">Continuing</span> - {{else}} - <span class="label label-default">Ended</span> - {{/if_eq}} - </div> - <div class="col-md-3"> - <span class="series-info-links"> - <a href="{{traktUrl}}" class="label label-info">Trakt</a> - - <a href="{{tvdbUrl}}" class="label label-info">The TVDB</a> - - {{#if imdbId}} - <a href="{{imdbUrl}}" class="label label-info">IMDB</a> - {{/if}} - - {{#if tvRageId}} - <a href="{{tvRageUrl}}" class="label label-info">TV Rage</a> - {{/if}} - - {{#if tvMazeId}} - <a href="{{tvMazeUrl}}" class="label label-info">TV Maze</a> - {{/if}} - </span> - </div> -</div> - -{{#if alternateTitles}} -<div class="row"> - <div class="col-md-12"> - {{#each alternateTitles}} - {{#if_eq seasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - - {{#if_eq sceneSeasonNumber compare="-1"}} - <span class="label label-default">{{title}}</span> - {{/if_eq}} - {{/each}} - </div> -</div> -{{/if}} - -{{#if tags}} -<div class="row"> - <div class="col-md-12"> - {{tagDisplay tags}} - </div> -</div> -{{/if}} diff --git a/src/UI/Series/Details/SeasonCollectionView.js b/src/UI/Series/Details/SeasonCollectionView.js deleted file mode 100644 index 24da6171c..000000000 --- a/src/UI/Series/Details/SeasonCollectionView.js +++ /dev/null @@ -1,44 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var SeasonLayout = require('./SeasonLayout'); -var AsSortedCollectionView = require('../../Mixins/AsSortedCollectionView'); - -var view = Marionette.CollectionView.extend({ - - itemView : SeasonLayout, - - initialize : function(options) { - if (!options.episodeCollection) { - throw 'episodeCollection is needed'; - } - - this.episodeCollection = options.episodeCollection; - this.series = options.series; - }, - - itemViewOptions : function() { - return { - episodeCollection : this.episodeCollection, - series : this.series - }; - }, - - onEpisodeGrabbed : function(message) { - if (message.episode.series.id !== this.episodeCollection.seriesId) { - return; - } - - var self = this; - - _.each(message.episode.episodes, function(episode) { - var ep = self.episodeCollection.get(episode.id); - ep.set('downloading', true); - }); - - this.render(); - } -}); - -AsSortedCollectionView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayout.js b/src/UI/Series/Details/SeasonLayout.js deleted file mode 100644 index f87553e79..000000000 --- a/src/UI/Series/Details/SeasonLayout.js +++ /dev/null @@ -1,301 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var ToggleCell = require('../../Cells/EpisodeMonitoredCell'); -var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var EpisodeActionsCell = require('../../Cells/EpisodeActionsCell'); -var EpisodeNumberCell = require('./EpisodeNumberCell'); -var EpisodeWarningCell = require('./EpisodeWarningCell'); -var CommandController = require('../../Commands/CommandController'); -var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -var moment = require('moment'); -var _ = require('underscore'); -var Messenger = require('../../Shared/Messenger'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Details/SeasonLayoutTemplate', - - ui : { - seasonSearch : '.x-season-search', - seasonMonitored : '.x-season-monitored', - seasonRename : '.x-season-rename' - }, - - events : { - 'click .x-season-episode-file-editor' : '_openEpisodeFileEditor', - 'click .x-season-monitored' : '_seasonMonitored', - 'click .x-season-search' : '_seasonSearch', - 'click .x-season-rename' : '_seasonRename', - 'click .x-show-hide-episodes' : '_showHideEpisodes', - 'dblclick .series-season h2' : '_showHideEpisodes' - }, - - regions : { - episodeGrid : '.x-episode-grid' - }, - - columns : [ - { - name : 'monitored', - label : '', - cell : ToggleCell, - trueClass : 'icon-lidarr-monitored', - falseClass : 'icon-lidarr-unmonitored', - tooltip : 'Toggle monitored status', - sortable : false - }, - { - name : 'episodeNumber', - label : '#', - cell : EpisodeNumberCell - }, - { - name : 'this', - label : '', - cell : EpisodeWarningCell, - sortable : false, - className : 'episode-warning-cell' - }, - { - name : 'this', - label : 'Title', - hideSeriesLink : true, - cell : EpisodeTitleCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Air Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - }, - { - name : 'this', - label : '', - cell : EpisodeActionsCell, - sortable : false - } - ], - - templateHelpers : function() { - var episodeCount = this.episodeCollection.filter(function(episode) { - return episode.get('hasFile') || episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()); - }).length; - - var episodeFileCount = this.episodeCollection.where({ hasFile : true }).length; - var percentOfEpisodes = 100; - - if (episodeCount > 0) { - percentOfEpisodes = episodeFileCount / episodeCount * 100; - } - - return { - showingEpisodes : this.showingEpisodes, - episodeCount : episodeCount, - episodeFileCount : episodeFileCount, - percentOfEpisodes : percentOfEpisodes - }; - }, - - initialize : function(options) { - if (!options.episodeCollection) { - throw 'episodeCollection is required'; - } - - this.series = options.series; - this.fullEpisodeCollection = options.episodeCollection; - this.episodeCollection = this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')); - this._updateEpisodeCollection(); - - this.showingEpisodes = this._shouldShowEpisodes(); - - this.listenTo(this.model, 'sync', this._afterSeasonMonitored); - this.listenTo(this.episodeCollection, 'sync', this.render); - - this.listenTo(this.fullEpisodeCollection, 'sync', this._refreshEpisodes); - }, - - onRender : function() { - if (this.showingEpisodes) { - this._showEpisodes(); - } - - this._setSeasonMonitoredState(); - - CommandController.bindToCommand({ - element : this.ui.seasonSearch, - command : { - name : 'seasonSearch', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - } - }); - - CommandController.bindToCommand({ - element : this.ui.seasonRename, - command : { - name : 'renameFiles', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - } - }); - }, - - _seasonSearch : function() { - CommandController.Execute('seasonSearch', { - name : 'seasonSearch', - seriesId : this.series.id, - seasonNumber : this.model.get('seasonNumber') - }); - }, - - _seasonRename : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { - series : this.series, - seasonNumber : this.model.get('seasonNumber') - }); - }, - - _seasonMonitored : function() { - if (!this.series.get('monitored')) { - - Messenger.show({ - message : 'Unable to change monitored state when series is not monitored', - type : 'error' - }); - - return; - } - - var name = 'monitored'; - this.model.set(name, !this.model.get(name)); - this.series.setSeasonMonitored(this.model.get('seasonNumber')); - - var savePromise = this.series.save().always(this._afterSeasonMonitored.bind(this)); - - this.ui.seasonMonitored.spinForPromise(savePromise); - }, - - _afterSeasonMonitored : function() { - var self = this; - - _.each(this.episodeCollection.models, function(episode) { - episode.set({ monitored : self.model.get('monitored') }); - }); - - this.render(); - }, - - _setSeasonMonitoredState : function() { - this.ui.seasonMonitored.removeClass('icon-lidarr-spinner fa-spin'); - - if (this.model.get('monitored')) { - this.ui.seasonMonitored.addClass('icon-lidarr-monitored'); - this.ui.seasonMonitored.removeClass('icon-lidarr-unmonitored'); - } else { - this.ui.seasonMonitored.addClass('icon-lidarr-unmonitored'); - this.ui.seasonMonitored.removeClass('icon-lidarr-monitored'); - } - }, - - _showEpisodes : function() { - this.episodeGrid.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.episodeCollection, - className : 'table table-hover season-grid' - })); - }, - - _shouldShowEpisodes : function() { - var startDate = moment().add('month', -1); - var endDate = moment().add('year', 1); - - return this.episodeCollection.some(function(episode) { - var airDate = episode.get('airDateUtc'); - - if (airDate) { - var airDateMoment = moment(airDate); - - if (airDateMoment.isAfter(startDate) && airDateMoment.isBefore(endDate)) { - return true; - } - } - - return false; - }); - }, - - _showHideEpisodes : function() { - if (this.showingEpisodes) { - this.showingEpisodes = false; - this.episodeGrid.close(); - } else { - this.showingEpisodes = true; - this._showEpisodes(); - } - - this.templateHelpers.showingEpisodes = this.showingEpisodes; - this.render(); - }, - - _episodeMonitoredToggled : function(options) { - var model = options.model; - var shiftKey = options.shiftKey; - - if (!this.episodeCollection.get(model.get('id'))) { - return; - } - - if (!shiftKey) { - return; - } - - var lastToggled = this.episodeCollection.lastToggled; - - if (!lastToggled) { - return; - } - - var currentIndex = this.episodeCollection.indexOf(model); - var lastIndex = this.episodeCollection.indexOf(lastToggled); - - var low = Math.min(currentIndex, lastIndex); - var high = Math.max(currentIndex, lastIndex); - var range = _.range(low + 1, high); - - this.episodeCollection.lastToggled = model; - }, - - _updateEpisodeCollection : function() { - var self = this; - - this.episodeCollection.add(this.fullEpisodeCollection.bySeason(this.model.get('seasonNumber')).models, { merge : true }); - - this.episodeCollection.each(function(model) { - model.episodeCollection = self.episodeCollection; - }); - }, - - _refreshEpisodes : function() { - this._updateEpisodeCollection(); - this.episodeCollection.fullCollection.sort(); - this.render(); - }, - - _openEpisodeFileEditor : function() { - var view = new EpisodeFileEditorLayout({ - model : this.model, - series : this.series, - episodeCollection : this.episodeCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/SeasonLayoutTemplate.hbs b/src/UI/Series/Details/SeasonLayoutTemplate.hbs deleted file mode 100644 index f16e6439d..000000000 --- a/src/UI/Series/Details/SeasonLayoutTemplate.hbs +++ /dev/null @@ -1,50 +0,0 @@ -<div class="series-season" id="season-{{seasonNumber}}"> - <h2> - <i class="x-season-monitored season-monitored clickable" title="Toggle season monitored status"/> - - {{#if seasonNumber}} - Season {{seasonNumber}} - {{else}} - Specials - {{/if}} - - - {{#if_eq episodeCount compare=0}} - {{#if monitored}} - <span class="badge badge-primary season-status" title="No aired episodes"> </span> - {{else}} - <span class="badge badge-warning season-status" title="Season is not monitored"> </span> - {{/if}} - {{else}} - {{#if_eq percentOfEpisodes compare=100}} - <span class="badge badge-success season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> - {{else}} - <span class="badge badge-danger season-status" title="{{episodeFileCount}}/{{episodeCount}} episodes downloaded">{{episodeFileCount}} / {{episodeCount}}</span> - {{/if_eq}} - {{/if_eq}} - - <span class="season-actions pull-right"> - <div class="x-season-episode-file-editor"> - <i class="icon-lidarr-episode-file" title="Modify episode files for season"/> - </div> - <div class="x-season-rename"> - <i class="icon-lidarr-rename" title="Preview rename for season {{seasonNumber}}"/> - </div> - <div class="x-season-search"> - <i class="icon-lidarr-search" title="Search for monitored episodes in season {{seasonNumber}}"/> - </div> - </span> - </h2> - <div class="show-hide-episodes x-show-hide-episodes"> - <h4> - {{#if showingEpisodes}} - <i class="icon-lidarr-panel-hide"/> - Hide Episodes - {{else}} - <i class="icon-lidarr-panel-show"/> - Show Episodes - {{/if}} - </h4> - </div> - <div class="x-episode-grid table-responsive"></div> -</div> diff --git a/src/UI/Series/Details/SeriesDetailsLayout.js b/src/UI/Series/Details/SeriesDetailsLayout.js deleted file mode 100644 index 4b3d34297..000000000 --- a/src/UI/Series/Details/SeriesDetailsLayout.js +++ /dev/null @@ -1,263 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var reqres = require('../../reqres'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var EpisodeCollection = require('../EpisodeCollection'); -var EpisodeFileCollection = require('../EpisodeFileCollection'); -var SeasonCollection = require('../SeasonCollection'); -var SeasonCollectionView = require('./SeasonCollectionView'); -var InfoView = require('./InfoView'); -var CommandController = require('../../Commands/CommandController'); -var LoadingView = require('../../Shared/LoadingView'); -var EpisodeFileEditorLayout = require('../../EpisodeFile/Editor/EpisodeFileEditorLayout'); -require('backstrech'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - itemViewContainer : '.x-series-seasons', - template : 'Series/Details/SeriesDetailsTemplate', - - regions : { - seasons : '#seasons', - info : '#info' - }, - - ui : { - header : '.x-header', - monitored : '.x-monitored', - edit : '.x-edit', - refresh : '.x-refresh', - rename : '.x-rename', - search : '.x-search', - poster : '.x-series-poster' - }, - - events : { - 'click .x-episode-file-editor' : '_openEpisodeFileEditor', - 'click .x-monitored' : '_toggleMonitored', - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshSeries', - 'click .x-rename' : '_renameSeries', - 'click .x-search' : '_seriesSearch' - }, - - initialize : function() { - this.seriesCollection = ArtistCollection.clone(); - this.seriesCollection.shadowCollection.bindSignalR(); - - - this.listenTo(this.model, 'change:monitored', this._setMonitoredState); - this.listenTo(this.model, 'remove', this._seriesRemoved); - //this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - - this.listenTo(this.model, 'change', function(model, options) { - if (options && options.changeSource === 'signalr') { - this._refresh(); - } - }); - - this.listenTo(this.model, 'change:images', this._updateImages); - - }, - - onShow : function() { - this._showBackdrop(); - this._showSeasons(); - this._setMonitoredState(); - this._showInfo(); - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshSeries' - } - }); - CommandController.bindToCommand({ - element : this.ui.search, - command : { - name : 'seriesSearch' - } - }); - console.log(this.model); - - /*CommandController.bindToCommand({ - element : this.ui.rename, - command : { - name : 'renameFiles', - seriesId : this.model.spotifyId, - seasonNumber : -1 - } - });*/ - }, - - onClose : function() { - if (this._backstrech) { - this._backstrech.destroy(); - delete this._backstrech; - } - - $('body').removeClass('backdrop'); - reqres.removeHandler(reqres.Requests.GetEpisodeFileById); - }, - - _getImage : function(type) { - var image = _.where(this.model.get('images'), { coverType : type }); - - if (image && image[0]) { - return image[0].url; - } - - return undefined; - }, - - _toggleMonitored : function() { - var savePromise = this.model.save('monitored', !this.model.get('monitored'), { wait : true }); - - this.ui.monitored.spinForPromise(savePromise); - }, - - _setMonitoredState : function() { - var monitored = this.model.get('monitored'); - - this.ui.monitored.removeAttr('data-idle-icon'); - this.ui.monitored.removeClass('fa-spin icon-lidarr-spinner'); - - if (monitored) { - this.ui.monitored.addClass('icon-lidarr-monitored'); - this.ui.monitored.removeClass('icon-lidarr-unmonitored'); - this.$el.removeClass('series-not-monitored'); - } else { - this.ui.monitored.addClass('icon-lidarr-unmonitored'); - this.ui.monitored.removeClass('icon-lidarr-monitored'); - this.$el.addClass('series-not-monitored'); - } - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshSeries : function() { - CommandController.Execute('refreshSeries', { - name : 'refreshSeries', - seriesId : this.model.id - }); - }, - - _seriesRemoved : function() { - Backbone.history.navigate('/', { trigger : true }); - }, - - _renameSeries : function() { - vent.trigger(vent.Commands.ShowRenamePreview, { series : this.model }); - }, - - _seriesSearch : function() { - console.log('_seriesSearch:', this.model); - CommandController.Execute('seriesSearch', { - name : 'seriesSearch', - seriesId : this.model.id - }); - }, - - _showSeasons : function() { - var self = this; - return; - - this.seasons.show(new LoadingView()); - - this.seasonCollection = new SeasonCollection(this.model.get('seasons')); - this.episodeCollection = new EpisodeCollection({ seriesId : this.model.id }).bindSignalR(); - this.episodeFileCollection = new EpisodeFileCollection({ seriesId : this.model.id }).bindSignalR(); - - reqres.setHandler(reqres.Requests.GetEpisodeFileById, function(episodeFileId) { - return self.episodeFileCollection.get(episodeFileId); - }); - - reqres.setHandler(reqres.Requests.GetAlternateNameBySeasonNumber, function(seriesId, seasonNumber, sceneSeasonNumber) { - if (self.model.get('id') !== seriesId) { - return []; - } - - if (sceneSeasonNumber === undefined) { - sceneSeasonNumber = seasonNumber; - } - - return _.where(self.model.get('alternateTitles'), - function(alt) { - return alt.sceneSeasonNumber === sceneSeasonNumber || alt.seasonNumber === seasonNumber; - }); - }); - - $.when(this.episodeCollection.fetch(), this.episodeFileCollection.fetch()).done(function() { - var seasonCollectionView = new SeasonCollectionView({ - collection : self.seasonCollection, - episodeCollection : self.episodeCollection, - series : self.model - }); - - if (!self.isClosed) { - self.seasons.show(seasonCollectionView); - } - }); - }, - - _showInfo : function() { - this.info.show(new InfoView({ - model : this.model, - episodeFileCollection : this.episodeFileCollection - })); - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'renamefiles') { - if (options.command.get('seriesId') === this.model.get('id')) { - this._refresh(); - } - } - }, - - _refresh : function() { - this.seasonCollection.add(this.model.get('seasons'), { merge : true }); - this.episodeCollection.fetch(); - this.episodeFileCollection.fetch(); - - this._setMonitoredState(); - this._showInfo(); - }, - - _openEpisodeFileEditor : function() { - var view = new EpisodeFileEditorLayout({ - series : this.model, - episodeCollection : this.episodeCollection - }); - - vent.trigger(vent.Commands.OpenModalCommand, view); - }, - - _updateImages : function () { - var poster = this._getImage('poster'); - - if (poster) { - this.ui.poster.attr('src', poster); - } - - this._showBackdrop(); - }, - - _showBackdrop : function () { - $('body').addClass('backdrop'); - var fanArt = this._getImage('fanart'); - - if (fanArt) { - this._backstrech = $.backstretch(fanArt); - } else { - $('body').removeClass('backdrop'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/Details/SeriesDetailsTemplate.hbs b/src/UI/Series/Details/SeriesDetailsTemplate.hbs deleted file mode 100644 index 605ead424..000000000 --- a/src/UI/Series/Details/SeriesDetailsTemplate.hbs +++ /dev/null @@ -1,35 +0,0 @@ -<div class="row series-page-header"> - <div class="visible-lg col-lg-2 poster"> - {{poster}} - </div> - <div class="col-md-12 col-lg-10"> - <div> - <h1 class="header-text"> - <i class="x-monitored" title="Toggle monitored state for entire series"/> - {{title}} - <div class="series-actions pull-right"> - <div class="x-episode-file-editor"> - <i class="icon-lidarr-episode-file" title="Modify episode files for series"/> - </div> - <div class="x-refresh"> - <i class="icon-lidarr-refresh icon-can-spin" title="Update series info and scan disk"/> - </div> - <div class="x-rename"> - <i class="icon-lidarr-rename" title="Preview rename for all episodes"/> - </div> - <div class="x-search"> - <i class="icon-lidarr-search" title="Search for monitored episodes in this series"/> - </div> - <div class="x-edit"> - <i class="icon-lidarr-edit" title="Edit series"/> - </div> - </div> - </h1> - </div> - <div class="series-detail-overview"> - {{overview}} - </div> - <div id="info" class="series-info"></div> - </div> -</div> -<div id="seasons"></div> diff --git a/src/UI/Series/Edit/EditSeriesView.js b/src/UI/Series/Edit/EditSeriesView.js deleted file mode 100644 index 3f8c789e8..000000000 --- a/src/UI/Series/Edit/EditSeriesView.js +++ /dev/null @@ -1,54 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Profiles = require('../../Profile/ProfileCollection'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); -require('../../Mixins/TagInput'); -require('../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Series/Edit/EditSeriesViewTemplate', - - ui : { - profile : '.x-profile', - path : '.x-path', - tags : '.x-tags' - }, - - events : { - 'click .x-remove' : '_removeSeries' - }, - - initialize : function() { - this.model.set('profiles', Profiles); - }, - - onRender : function() { - this.ui.path.fileBrowser(); - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onBeforeSave : function() { - var profileId = this.ui.profile.val(); - this.model.set({ profileId : profileId }); - }, - - _onAfterSave : function() { - this.trigger('saved'); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _removeSeries : function() { - vent.trigger(vent.Commands.DeleteSeriesCommand, { series : this.model }); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs b/src/UI/Series/Edit/EditSeriesViewTemplate.hbs deleted file mode 100644 index a85058ed3..000000000 --- a/src/UI/Series/Edit/EditSeriesViewTemplate.hbs +++ /dev/null @@ -1,104 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>{{title}}</h3> - </div> - <div class="modal-body edit-series-modal"> - <div class="row"> - <div class="col-sm-3 hidden-xs"> - {{poster}} - </div> - <div class="col-sm-9"> - <div class="form-horizontal"> - - <div class="form-group"> - <label class="col-sm-4 control-label">Monitored</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="monitored"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr download episodes for this series?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Use Season Folder</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="seasonFolder"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should downloaded episodes be stored in season folders?"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Profile</label> - - <div class="col-sm-4"> - <select class="form-control x-profile" id="inputProfile" name="profileId"> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Series Type</label> - <div class="col-sm-4"> - {{> SeriesTypeSelectionPartial}} - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Path</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-path" placeholder="Path" name="path"> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-4 control-label">Tags</label> - - <div class="col-sm-6"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-danger pull-left x-remove">Delete</button> - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Series/Editor/Organize/OrganizeFilesView.js b/src/UI/Series/Editor/Organize/OrganizeFilesView.js deleted file mode 100644 index 25534fb21..000000000 --- a/src/UI/Series/Editor/Organize/OrganizeFilesView.js +++ /dev/null @@ -1,33 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Editor/Organize/OrganizeFilesViewTemplate', - - events : { - 'click .x-confirm-organize' : '_organize' - }, - - initialize : function(options) { - this.series = options.series; - this.templateHelpers = { - numberOfSeries : this.series.length, - series : new Backbone.Collection(this.series).toJSON() - }; - }, - - _organize : function() { - var seriesIds = _.pluck(this.series, 'id'); - - CommandController.Execute('renameSeries', { - name : 'renameSeries', - seriesIds : seriesIds - }); - - this.trigger('organizingFiles'); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs b/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs deleted file mode 100644 index 356db72db..000000000 --- a/src/UI/Series/Editor/Organize/OrganizeFilesViewTemplate.hbs +++ /dev/null @@ -1,25 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Organize of Selected Series</h3> - </div> - <div class="modal-body update-files-series-modal"> - <div class="alert alert-info"> - <button type="button" class="close" data-dismiss="alert">×</button> - Tip: To preview a rename... select "Cancel" then any series title and use the <i data-original-title="" class="icon-lidarr-rename" title=""></i> - </div> - - Are you sure you want to update all files in the {{numberOfSeries}} selected series? - - {{debug}} - <ul class="selected-series"> - {{#each series}} - <li>{{title}}</li> - {{/each}} - </ul> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-organize">Organize</button> - </div> -</div> diff --git a/src/UI/Series/Editor/SeriesEditorFooterView.js b/src/UI/Series/Editor/SeriesEditorFooterView.js deleted file mode 100644 index e2783bbeb..000000000 --- a/src/UI/Series/Editor/SeriesEditorFooterView.js +++ /dev/null @@ -1,126 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var vent = require('vent'); -var Profiles = require('../../Profile/ProfileCollection'); -var RootFolders = require('../../AddArtist/RootFolders/RootFolderCollection'); -var RootFolderLayout = require('../../AddArtist/RootFolders/RootFolderLayout'); -var UpdateFilesSeriesView = require('./Organize/OrganizeFilesView'); -var Config = require('../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Series/Editor/SeriesEditorFooterViewTemplate', - - ui : { - monitored : '.x-monitored', - profile : '.x-profiles', - seasonFolder : '.x-season-folder', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count', - container : '.series-editor-footer', - actions : '.x-action' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged', - 'click .x-organize-files' : '_organizeFiles' - }, - - templateHelpers : function() { - return { - profiles : Profiles, - rootFolders : RootFolders.toJSON() - }; - }, - - initialize : function(options) { - this.seriesCollection = options.collection; - - RootFolders.fetch().done(function() { - RootFolders.synced = true; - }); - - this.editorGrid = options.editorGrid; - this.listenTo(this.seriesCollection, 'backgrid:selected', this._updateInfo); - this.listenTo(RootFolders, 'all', this.render); - }, - - onRender : function() { - this._updateInfo(); - }, - - _updateAndSave : function() { - var selected = this.editorGrid.getSelectedModels(); - - var monitored = this.ui.monitored.val(); - var profile = this.ui.profile.val(); - var seasonFolder = this.ui.seasonFolder.val(); - var rootFolder = this.ui.rootFolder.val(); - - _.each(selected, function(model) { - if (monitored === 'true') { - model.set('monitored', true); - } else if (monitored === 'false') { - model.set('monitored', false); - } - - if (profile !== 'noChange') { - model.set('profileId', parseInt(profile, 10)); - } - - if (seasonFolder === 'true') { - model.set('seasonFolder', true); - } else if (seasonFolder === 'false') { - model.set('seasonFolder', false); - } - - if (rootFolder !== 'noChange') { - var rootFolderPath = RootFolders.get(parseInt(rootFolder, 10)); - - model.set('rootFolderPath', rootFolderPath.get('path')); - } - - model.edited = true; - }); - - this.seriesCollection.save(); - }, - - _updateInfo : function() { - var selected = this.editorGrid.getSelectedModels(); - var selectedCount = selected.length; - - this.ui.selectedCount.html('{0} series selected'.format(selectedCount)); - - if (selectedCount === 0) { - this.ui.actions.attr('disabled', 'disabled'); - } else { - this.ui.actions.removeAttr('disabled'); - } - }, - - _rootFolderChanged : function() { - var rootFolderValue = this.ui.rootFolder.val(); - if (rootFolderValue === 'addNew') { - var rootFolderLayout = new RootFolderLayout(); - this.listenToOnce(rootFolderLayout, 'folderSelected', this._setRootFolder); - vent.trigger(vent.Commands.OpenModalCommand, rootFolderLayout); - } else { - Config.setValue(Config.Keys.DefaultRootFolderId, rootFolderValue); - } - }, - - _setRootFolder : function(options) { - vent.trigger(vent.Commands.CloseModalCommand); - this.ui.rootFolder.val(options.model.id); - this._rootFolderChanged(); - }, - - _organizeFiles : function() { - var selected = this.editorGrid.getSelectedModels(); - var updateFilesSeriesView = new UpdateFilesSeriesView({ series : selected }); - this.listenToOnce(updateFilesSeriesView, 'updatingFiles', this._afterSave); - - vent.trigger(vent.Commands.OpenModalCommand, updateFilesSeriesView); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs b/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs deleted file mode 100644 index c47b3c50a..000000000 --- a/src/UI/Series/Editor/SeriesEditorFooterViewTemplate.hbs +++ /dev/null @@ -1,54 +0,0 @@ -<div class="series-editor-footer"> - <div class="row"> - <div class="form-group col-md-2"> - <label>Monitored</label> - - <select class="form-control x-action x-monitored"> - <option value="noChange">No change</option> - <option value="true">Monitored</option> - <option value="false">Unmonitored</option> - </select> - </div> - - <div class="form-group col-md-2"> - <label>Profile</label> - - <select class="form-control x-action x-profiles"> - <option value="noChange">No change</option> - {{#each profiles.models}} - <option value="{{id}}">{{attributes.name}}</option> - {{/each}} - </select> - </div> - - <div class="form-group col-md-2"> - <label>Season Folder</label> - - <select class="form-control x-action x-season-folder"> - <option value="noChange">No change</option> - <option value="true">Yes</option> - <option value="false">No</option> - </select> - </div> - - <div class="form-group col-md-3"> - <label>Root Folder</label> - - <select class="form-control x-action x-root-folder" validation-name="RootFolderPath"> - <option value="noChange">No change</option> - {{#each rootFolders}} - <option value="{{id}}">{{path}}</option> - {{/each}} - <option value="addNew">Add a different path</option> - </select> - </div> - - <div class="form-group col-md-3 actions"> - <label class="x-selected-count">0 series selected</label> - <div> - <button class="btn btn-primary x-action x-save">Save</button> - <button class="btn btn-danger x-action x-organize-files" title="Organize and rename episode files">Organize</button> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Editor/SeriesEditorLayout.js b/src/UI/Series/Editor/SeriesEditorLayout.js deleted file mode 100644 index 6927699d5..000000000 --- a/src/UI/Series/Editor/SeriesEditorLayout.js +++ /dev/null @@ -1,184 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var EmptyView = require('../Index/EmptyView'); -var SeriesCollection = require('../SeriesCollection'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var SeriesStatusCell = require('../../Cells/SeriesStatusCell'); -var SeasonFolderCell = require('../../Cells/SeasonFolderCell'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var FooterView = require('./SeriesEditorFooterView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Editor/SeriesEditorLayoutTemplate', - - regions : { - seriesRegion : '#x-series-editor', - toolbar : '#x-toolbar' - }, - - ui : { - monitored : '.x-monitored', - profiles : '.x-profiles', - rootFolder : '.x-root-folder', - selectedCount : '.x-selected-count' - }, - - events : { - 'click .x-save' : '_updateAndSave', - 'change .x-root-folder' : '_rootFolderChanged' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue : 'this' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'seasonFolder', - label : 'Season Folder', - cell : SeasonFolderCell - }, - { - name : 'path', - label : 'Path', - cell : 'string' - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - items : [ - { - title : 'Season Pass', - icon : 'icon-lidarr-monitored', - route : 'seasonpass' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshseries', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.seriesCollection = SeriesCollection.clone(); - this.seriesCollection.shadowCollection.bindSignalR(); - this.listenTo(this.seriesCollection, 'save', this.render); - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'serieseditor.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-series-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-series-ended', - callback : this._setFilter - } - ] - }; - }, - - onRender : function() { - this._showToolbar(); - this._showTable(); - }, - - onClose : function() { - vent.trigger(vent.Commands.CloseControlPanelCommand); - }, - - _showTable : function() { - if (this.seriesCollection.shadowCollection.length === 0) { - this.seriesRegion.show(new EmptyView()); - this.toolbar.close(); - return; - } - - this.columns[0].sortedCollection = this.seriesCollection; - - this.editorGrid = new Backgrid.Grid({ - collection : this.seriesCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this.seriesRegion.show(this.editorGrid); - this._showFooter(); - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [ - this.leftSideButtons - ], - right : [ - this.filteringOptions - ], - context : this - })); - }, - - _showFooter : function() { - vent.trigger(vent.Commands.OpenControlPanelCommand, new FooterView({ - editorGrid : this.editorGrid, - collection : this.seriesCollection - })); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.seriesCollection.setFilterMode(mode); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs b/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs deleted file mode 100644 index 1d0519894..000000000 --- a/src/UI/Series/Editor/SeriesEditorLayoutTemplate.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<div id="x-toolbar"></div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-series-editor" class="table-responsive"></div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Series/EpisodeCollection.js b/src/UI/Series/EpisodeCollection.js deleted file mode 100644 index a6794394b..000000000 --- a/src/UI/Series/EpisodeCollection.js +++ /dev/null @@ -1,62 +0,0 @@ -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var EpisodeModel = require('./EpisodeModel'); -require('./EpisodeCollection'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/episode', - model : EpisodeModel, - - state : { - sortKey : 'episodeNumber', - order : 1, - pageSize : 100000 - }, - - mode : 'client', - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.seriesId = options.seriesId; - }, - - bySeason : function(season) { - var filtered = this.filter(function(episode) { - return episode.get('seasonNumber') === season; - }); - - var EpisodeCollection = require('./EpisodeCollection'); - - return new EpisodeCollection(filtered); - }, - - comparator : function(model1, model2) { - var episode1 = model1.get('episodeNumber'); - var episode2 = model2.get('episodeNumber'); - - if (episode1 < episode2) { - return 1; - } - - if (episode1 > episode2) { - return -1; - } - - return 0; - }, - - fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { seriesId : this.seriesId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeFileCollection.js b/src/UI/Series/EpisodeFileCollection.js deleted file mode 100644 index dff988512..000000000 --- a/src/UI/Series/EpisodeFileCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var Backbone = require('backbone'); -var EpisodeFileModel = require('./EpisodeFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/episodefile', - model : EpisodeFileModel, - - originalFetch : Backbone.Collection.prototype.fetch, - - initialize : function(options) { - this.seriesId = options.seriesId; - this.models = []; - }, - - fetch : function(options) { - if (!this.seriesId) { - throw 'seriesId is required'; - } - - if (!options) { - options = {}; - } - - options.data = { seriesId : this.seriesId }; - - return this.originalFetch.call(this, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeFileModel.js b/src/UI/Series/EpisodeFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Series/EpisodeFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Series/EpisodeModel.js b/src/UI/Series/EpisodeModel.js deleted file mode 100644 index ebb72cf29..000000000 --- a/src/UI/Series/EpisodeModel.js +++ /dev/null @@ -1,20 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - seasonNumber : 0, - status : 0 - }, - - methodUrls : { - 'update' : window.NzbDrone.ApiRoot + '/episode' - }, - - sync : function(method, model, options) { - if (model.methodUrls && model.methodUrls[method.toLowerCase()]) { - options = options || {}; - options.url = model.methodUrls[method.toLowerCase()]; - } - return Backbone.sync(method, model, options); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/EmptyTemplate.hbs b/src/UI/Series/Index/EmptyTemplate.hbs deleted file mode 100644 index 23b8b513c..000000000 --- a/src/UI/Series/Index/EmptyTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<div class="no-series"> - <div class="row"> - <div class="well col-md-12"> - <i class="icon-lidarr-comment"/> - You must be new around here. You should add some music. - </div> - </div> - <div class="row"> - <div class="col-md-4 col-md-offset-4"> - <a href="/addartist" class='btn btn-lg btn-block btn-success x-add-artist'> - <i class='icon-lidarr-add'></i> - Add Music - </a> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/EmptyView.js b/src/UI/Series/Index/EmptyView.js deleted file mode 100644 index 01dcc07a4..000000000 --- a/src/UI/Series/Index/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Series/Index/EmptyTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/EpisodeProgressPartial.hbs b/src/UI/Series/Index/EpisodeProgressPartial.hbs deleted file mode 100644 index db5c49a2b..000000000 --- a/src/UI/Series/Index/EpisodeProgressPartial.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div class="progress episode-progress"> - <span class="progressbar-back-text">{{episodeFileCount}} / {{episodeCount}}</span> - <div class="progress-bar {{EpisodeProgressClass}} episode-progress" style="width:{{percentOfEpisodes}}%"><span class="progressbar-front-text">{{episodeFileCount}} / {{episodeCount}}</span></div> -</div> \ No newline at end of file diff --git a/src/UI/Series/Index/FooterModel.js b/src/UI/Series/Index/FooterModel.js deleted file mode 100644 index 235552061..000000000 --- a/src/UI/Series/Index/FooterModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Series/Index/FooterView.js b/src/UI/Series/Index/FooterView.js deleted file mode 100644 index 1d31cc404..000000000 --- a/src/UI/Series/Index/FooterView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Series/Index/FooterViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/FooterViewTemplate.hbs b/src/UI/Series/Index/FooterViewTemplate.hbs deleted file mode 100644 index 1b45fa747..000000000 --- a/src/UI/Series/Index/FooterViewTemplate.hbs +++ /dev/null @@ -1,46 +0,0 @@ -<div class="row"> - <div class="series-legend legend col-xs-6 col-sm-4"> - <ul class='legend-labels'> - <li><span class="progress-bar"></span>Continuing (All episodes downloaded)</li> - <li><span class="progress-bar-success"></span>Ended (All episodes downloaded)</li> - <li><span class="progress-bar-danger"></span>Missing Episodes (Series monitored)</li> - <li><span class="progress-bar-warning"></span>Missing Episodes (Series not monitored)</li> - </ul> - </div> - <div class="col-xs-5 col-sm-7"> - <div class="row"> - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Series</dt> - <dd>{{series}}</dd> - - <dt>Ended</dt> - <dd>{{ended}}</dd> - - <dt>Continuing</dt> - <dd>{{continuing}}</dd> - </dl> - </div> - - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Monitored</dt> - <dd>{{monitored}}</dd> - - <dt>Unmonitored</dt> - <dd>{{unmonitored}}</dd> - </dl> - </div> - - <div class="series-stats col-sm-4"> - <dl class="dl-horizontal"> - <dt>Episodes</dt> - <dd>{{episodes}}</dd> - - <dt>Files</dt> - <dd>{{episodeFiles}}</dd> - </dl> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js b/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js deleted file mode 100644 index 7db4b76f0..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var ListItemView = require('./SeriesOverviewItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ListItemView, - itemViewContainer : '#x-series-list', - template : 'Series/Index/Overview/SeriesOverviewCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs deleted file mode 100644 index 046bb3348..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div id="x-series-list"/> diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js b/src/UI/Series/Index/Overview/SeriesOverviewItemView.js deleted file mode 100644 index bb780480b..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemView.js +++ /dev/null @@ -1,7 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var SeriesIndexItemView = require('../SeriesIndexItemView'); - -module.exports = SeriesIndexItemView.extend({ - template : 'Series/Index/Overview/SeriesOverviewItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs b/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs deleted file mode 100644 index 6fb80ec50..000000000 --- a/src/UI/Series/Index/Overview/SeriesOverviewItemViewTemplate.hbs +++ /dev/null @@ -1,59 +0,0 @@ -<div class="series-item"> - <div class="row"> - <div class="col-md-2 col-xs-3"> - <a href="{{route}}"> - {{poster}} - </a> - </div> - <div class="col-md-10 col-xs-9"> - <div class="row"> - <div class="col-md-10 col-xs-10"> - <a href="artist/{{artistSlug}}" target="_blank"> - <h2>{{artistName}}</h2> - </a> - </div> - <div class="col-md-2 col-xs-2"> - <div class="pull-right series-overview-list-actions"> - <i class="icon-lidarr-refresh x-refresh" title="Update artist info and scan disk"/> - <i class="icon-lidarr-edit x-edit" title="Edit Artist"/> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-12"> - <div> - {{truncate overview 600}} - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> -   - </div> - </div> - <div class="row"> - <div class="col-md-10 col-xs-8"> - <!--{{#if_eq status compare="ended"}} - <span class="label label-danger">Ended</span> - {{/if_eq}}--> - - <!-- - NOTE: We can show next drop date of album in future - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}}--> - - {{albumCountHelper}} - - {{profile profileId}} - </div> - <div class="col-md-2 col-xs-4"> - {{> EpisodeProgressPartial }} - </div> - <div class="col-md-8 col-xs-10"> - Path {{path}} - </div> - </div> - </div> - </div> -</div> diff --git a/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js b/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js deleted file mode 100644 index 0d6094f1c..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var PosterItemView = require('./SeriesPostersItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : PosterItemView, - itemViewContainer : '#x-series-posters', - template : 'Series/Index/Posters/SeriesPostersCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs b/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs deleted file mode 100644 index 11b8e8ac7..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersCollectionViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<ul id="x-series-posters" class="series-posters"></ul> \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemView.js b/src/UI/Series/Index/Posters/SeriesPostersItemView.js deleted file mode 100644 index 9a42b4655..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersItemView.js +++ /dev/null @@ -1,19 +0,0 @@ -var SeriesIndexItemView = require('../SeriesIndexItemView'); - -module.exports = SeriesIndexItemView.extend({ - tagName : 'li', - template : 'Series/Index/Posters/SeriesPostersItemViewTemplate', - - initialize : function() { - this.events['mouseenter .x-series-poster-container'] = 'posterHoverAction'; - this.events['mouseleave .x-series-poster-container'] = 'posterHoverAction'; - - this.ui.controls = '.x-series-controls'; - this.ui.title = '.x-title'; - }, - - posterHoverAction : function() { - this.ui.controls.slideToggle(); - this.ui.title.slideToggle(); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs b/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs deleted file mode 100644 index 7b217b779..000000000 --- a/src/UI/Series/Index/Posters/SeriesPostersItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="series-posters-item"> - <div class="center"> - <div class="series-poster-container x-series-poster-container"> - <div class="series-controls x-series-controls"> - <i class="icon-lidarr-refresh x-refresh" title="Refresh Series"/> - <i class="icon-lidarr-edit x-edit" title="Edit Series"/> - </div> - {{#unless_eq status compare="continuing"}} - <div class="ended-banner">Ended</div> - {{/unless_eq}} - <a href="{{route}}"> - {{poster}} - <div class="center title">{{title}}</div> - </a> - <div class="hidden-title x-title"> - {{title}} - </div> - </div> - </div> - - <div class="center"> - <div class="labels"> - {{> EpisodeProgressPartial }} - - {{#if nextAiring}} - <span class="label label-default">{{RelativeDate nextAiring}}</span> - {{/if}} - </div> - </div> -</div> diff --git a/src/UI/Series/Index/SeriesIndexItemView.js b/src/UI/Series/Index/SeriesIndexItemView.js deleted file mode 100644 index 91d3b329b..000000000 --- a/src/UI/Series/Index/SeriesIndexItemView.js +++ /dev/null @@ -1,35 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - ui : { - refresh : '.x-refresh' - }, - - events : { - 'click .x-edit' : '_editSeries', - 'click .x-refresh' : '_refreshArtist' - }, - - onRender : function() { - CommandController.bindToCommand({ - element : this.ui.refresh, - command : { - name : 'refreshArtist', - seriesId : this.model.get('id') - } - }); - }, - - _editSeries : function() { - vent.trigger(vent.Commands.EditSeriesCommand, { series : this.model }); - }, - - _refreshArtist : function() { - CommandController.Execute('refreshArtist', { - name : 'refreshArtist', - seriesId : this.model.id - }); - } -}); \ No newline at end of file diff --git a/src/UI/Series/Index/SeriesIndexLayout.js b/src/UI/Series/Index/SeriesIndexLayout.js deleted file mode 100644 index 09be2418c..000000000 --- a/src/UI/Series/Index/SeriesIndexLayout.js +++ /dev/null @@ -1,357 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var PosterCollectionView = require('./Posters/SeriesPostersCollectionView'); -var ListCollectionView = require('./Overview/SeriesOverviewCollectionView'); -var EmptyView = require('./EmptyView'); -var ArtistCollection = require('../../Artist/ArtistCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var TemplatedCell = require('../../Cells/TemplatedCell'); -var ProfileCell = require('../../Cells/ProfileCell'); -var EpisodeProgressCell = require('../../Cells/EpisodeProgressCell'); -var SeriesActionsCell = require('../../Cells/SeriesActionsCell'); -var SeriesStatusCell = require('../../Cells/SeriesStatusCell'); -var FooterView = require('./FooterView'); -var FooterModel = require('./FooterModel'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Series/Index/SeriesIndexLayoutTemplate', - - - regions : { - seriesRegion : '#x-series', - toolbar : '#x-toolbar', - toolbar2 : '#x-toolbar2', - footer : '#x-series-footer' - }, - - columns : [ - { - name : 'statusWeight', - label : '', - cell : SeriesStatusCell - }, - { - name : 'title', - label : 'Title', - cell : SeriesTitleCell, - cellValue : 'this', - sortValue : 'sortTitle' - }, - { - name : 'seasonCount', - label : 'Seasons', - cell : 'integer' - }, - { - name : 'profileId', - label : 'Profile', - cell : ProfileCell - }, - { - name : 'network', - label : 'Network', - cell : 'string' - }, - { - name : 'nextAiring', - label : 'Next Airing', - cell : RelativeDateCell - }, - { - name : 'percentOfEpisodes', - label : 'Episodes', - cell : EpisodeProgressCell, - className : 'episode-progress-cell' - }, - { - name : 'this', - label : '', - sortable : false, - cell : SeriesActionsCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Add Artist', - icon : 'icon-lidarr-add', - route : 'addartist' - }, - { - title : 'Season Pass', - icon : 'icon-lidarr-monitored', - route : 'seasonpass' - }, - { - title : 'Series Editor', - icon : 'icon-lidarr-edit', - route : 'serieseditor' - }, - { - title : 'RSS Sync', - icon : 'icon-lidarr-rss', - command : 'rsssync', - errorMessage : 'RSS Sync Failed!' - }, - { - title : 'Update Library', - icon : 'icon-lidarr-refresh', - command : 'refreshseries', - successMessage : 'Library was updated!', - errorMessage : 'Library update failed!' - } - ] - }, - - initialize : function() { - this.artistCollection = ArtistCollection.clone(); - this.artistCollection.shadowCollection.bindSignalR(); - - this.listenTo(this.artistCollection, 'sync', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'add', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.listenTo(this.artistCollection, 'remove', function(model, collection, options) { - this.artistCollection.fullCollection.resetFiltered(); - this._renderView(); - }); - - this.sortingOptions = { - type : 'sorting', - storeState : false, - viewCollection : this.artistCollection, - items : [ - { - title : 'Title', - name : 'title' - }, - { - title : 'Seasons', - name : 'seasonCount' - }, - { - title : 'Quality', - name : 'profileId' - }, - { - title : 'Network', - name : 'network' - }, - { - title : 'Next Airing', - name : 'nextAiring' - }, - { - title : 'Episodes', - name : 'percentOfEpisodes' - } - ] - }; - - this.filteringOptions = { - type : 'radio', - storeState : true, - menuKey : 'series.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'continuing', - title : '', - tooltip : 'Continuing Only', - icon : 'icon-lidarr-series-continuing', - callback : this._setFilter - }, - { - key : 'ended', - title : '', - tooltip : 'Ended Only', - icon : 'icon-lidarr-series-ended', - callback : this._setFilter - }, - { - key : 'missing', - title : '', - tooltip : 'Missing', - icon : 'icon-lidarr-missing', - callback : this._setFilter - } - ] - }; - - this.viewButtons = { - type : 'radio', - storeState : true, - menuKey : 'seriesViewMode', - defaultAction : 'listView', - items : [ - { - key : 'posterView', - title : '', - tooltip : 'Posters', - icon : 'icon-lidarr-view-poster', - callback : this._showPosters - }, - { - key : 'listView', - title : '', - tooltip : 'Overview List', - icon : 'icon-lidarr-view-list', - callback : this._showList - }, - { - key : 'tableView', - title : '', - tooltip : 'Table', - icon : 'icon-lidarr-view-table', - callback : this._showTable - } - ] - }; - }, - - onShow : function() { - this._showToolbar(); - this._fetchCollection(); - }, - - _showTable : function() { - this.currentView = new Backgrid.Grid({ - collection : this.artistCollection, - columns : this.columns, - className : 'table table-hover' - }); - - this._renderView(); - }, - - _showList : function() { - this.currentView = new ListCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _showPosters : function() { - this.currentView = new PosterCollectionView({ - collection : this.artistCollection - }); - - this._renderView(); - }, - - _renderView : function() { - // Problem is this is calling before artistCollection has updated. Where are the promises with backbone? - if (this.artistCollection.length === 0) { - this.seriesRegion.show(new EmptyView()); - - this.toolbar.close(); - this.toolbar2.close(); - } else { - this.seriesRegion.show(this.currentView); - - this._showToolbar(); - this._showFooter(); - } - }, - - _fetchCollection : function() { - this.artistCollection.fetch(); - console.log('index page, collection: ', this.artistCollection); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.artistCollection.setFilterMode(mode); - }, - - _showToolbar : function() { - if (this.toolbar.currentView) { - return; - } - - this.toolbar2.show(new ToolbarLayout({ - right : [ - this.filteringOptions - ], - context : this - })); - - this.toolbar.show(new ToolbarLayout({ - right : [ - this.sortingOptions, - this.viewButtons - ], - left : [ - this.leftSideButtons - ], - context : this - })); - }, - - _showFooter : function() { - var footerModel = new FooterModel(); - var series = this.artistCollection.models.length; - var episodes = 0; - var episodeFiles = 0; - var ended = 0; - var continuing = 0; - var monitored = 0; - - _.each(this.artistCollection.models, function(model) { - episodes += model.get('episodeCount'); // TODO: Refactor to Seasons and Tracks - episodeFiles += model.get('episodeFileCount'); - - /*if (model.get('status').toLowerCase() === 'ended') { - ended++; - } else { - continuing++; - }*/ - - if (model.get('monitored')) { - monitored++; - } - }); - - footerModel.set({ - series : series, - ended : ended, - continuing : continuing, - monitored : monitored, - unmonitored : series - monitored, - episodes : episodes, - episodeFiles : episodeFiles - }); - - this.footer.show(new FooterView({ model : footerModel })); - } -}); diff --git a/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs b/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs deleted file mode 100644 index d9e6b3263..000000000 --- a/src/UI/Series/Index/SeriesIndexLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div class="toolbars"> - <div id="x-toolbar"></div> - <div id="x-toolbar2"></div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-series" class="table-responsive"></div> - </div> -</div> - -<div id="x-series-footer"></div> \ No newline at end of file diff --git a/src/UI/Series/SeasonCollection.js b/src/UI/Series/SeasonCollection.js deleted file mode 100644 index ed661af2b..000000000 --- a/src/UI/Series/SeasonCollection.js +++ /dev/null @@ -1,10 +0,0 @@ -var Backbone = require('backbone'); -var SeasonModel = require('./SeasonModel'); - -module.exports = Backbone.Collection.extend({ - model : SeasonModel, - - comparator : function(season) { - return -season.get('seasonNumber'); - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeasonModel.js b/src/UI/Series/SeasonModel.js deleted file mode 100644 index 1ba049eb6..000000000 --- a/src/UI/Series/SeasonModel.js +++ /dev/null @@ -1,11 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - seasonNumber : 0 - }, - - initialize : function() { - this.set('id', this.get('seasonNumber')); - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeriesCollection.js b/src/UI/Series/SeriesCollection.js deleted file mode 100644 index bef8fe338..000000000 --- a/src/UI/Series/SeriesCollection.js +++ /dev/null @@ -1,120 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); -var PageableCollection = require('backbone.pageable'); -var SeriesModel = require('./SeriesModel'); -var ApiData = require('../Shared/ApiData'); -var AsFilteredCollection = require('../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../Mixins/AsPersistedStateCollection'); -var moment = require('moment'); -require('../Mixins/backbone.signalr.mixin'); - -var Collection = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/series', - model : SeriesModel, - tableName : 'series', - - state : { - sortKey : 'sortTitle', - order : -1, - pageSize : 100000, - secondarySortKey : 'sortTitle', - secondarySortOrder : -1 - }, - - mode : 'client', - - save : function() { - var self = this; - - var proxy = _.extend(new Backbone.Model(), { - id : '', - - url : self.url + '/editor', - - toJSON : function() { - return self.filter(function(model) { - return model.edited; - }); - } - }); - - this.listenTo(proxy, 'sync', function(proxyModel, models) { - this.add(models, { merge : true }); - this.trigger('save', this); - }); - - return proxy.save(); - }, - - filterModes : { - 'all' : [ - null, - null - ], - 'continuing' : [ - 'status', - 'continuing' - ], - 'ended' : [ - 'status', - 'ended' - ], - 'monitored' : [ - 'monitored', - true - ], - 'missing' : [ - null, - null, - function(model) { return model.get('episodeCount') !== model.get('episodeFileCount'); } - ] - }, - - sortMappings : { - title : { - sortKey : 'sortTitle' - }, - - nextAiring : { - sortValue : function(model, attr, order) { - var nextAiring = model.get(attr); - - if (nextAiring) { - return moment(nextAiring).unix(); - } - - if (order === 1) { - return 0; - } - - return Number.MAX_VALUE; - } - }, - - percentOfEpisodes : { - sortValue : function(model, attr) { - var percentOfEpisodes = model.get(attr); - var episodeCount = model.get('episodeCount'); - - return percentOfEpisodes + episodeCount / 1000000; - } - }, - - path : { - sortValue : function(model) { - var path = model.get('path'); - - return path.toLowerCase(); - } - } - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); -Collection = AsPersistedStateCollection.call(Collection); - -var data = ApiData.get('series'); - -module.exports = new Collection(data, { full : true }).bindSignalR(); diff --git a/src/UI/Series/SeriesController.js b/src/UI/Series/SeriesController.js deleted file mode 100644 index 3216e64c3..000000000 --- a/src/UI/Series/SeriesController.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneController = require('../Shared/NzbDroneController'); -var AppLayout = require('../AppLayout'); -var SeriesCollection = require('./SeriesCollection'); -var SeriesIndexLayout = require('./Index/SeriesIndexLayout'); -var SeriesDetailsLayout = require('./Details/SeriesDetailsLayout'); - -module.exports = NzbDroneController.extend({ - _originalInit : NzbDroneController.prototype.initialize, - - initialize : function() { - this.route('', this.series); - this.route('series', this.series); - this.route('series/:query', this.seriesDetails); - - this._originalInit.apply(this, arguments); - }, - - series : function() { - this.setTitle('Lidarr'); - this.showMainRegion(new SeriesIndexLayout()); - }, - - seriesDetails : function(query) { - var series = SeriesCollection.where({ titleSlug : query }); - - if (series.length !== 0) { - var targetSeries = series[0]; - this.setTitle(targetSeries.get('title')); - this.showMainRegion(new SeriesDetailsLayout({ model : targetSeries })); - } else { - this.showNotFound(); - } - } -}); \ No newline at end of file diff --git a/src/UI/Series/SeriesModel.js b/src/UI/Series/SeriesModel.js deleted file mode 100644 index 9d154fa7d..000000000 --- a/src/UI/Series/SeriesModel.js +++ /dev/null @@ -1,31 +0,0 @@ -var Backbone = require('backbone'); -var _ = require('underscore'); - -module.exports = Backbone.Model.extend({ - urlRoot : window.NzbDrone.ApiRoot + '/series', - - defaults : { - episodeFileCount : 0, - episodeCount : 0, - isExisting : false, - status : 0 - }, - - setSeasonMonitored : function(seasonNumber) { - _.each(this.get('seasons'), function(season) { - if (season.seasonNumber === seasonNumber) { - season.monitored = !season.monitored; - } - }); - }, - - setSeasonPass : function(seasonNumber) { - _.each(this.get('seasons'), function(season) { - if (season.seasonNumber >= seasonNumber) { - season.monitored = true; - } else { - season.monitored = false; - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Series/series.less b/src/UI/Series/series.less deleted file mode 100644 index f17229f6f..000000000 --- a/src/UI/Series/series.less +++ /dev/null @@ -1,477 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/card.less"; -@import "../Shared/Styles/clickable.less"; -@import "../Content/prefixer"; - -.series-poster { - min-width: 56px; - max-width: 100%; -} - -.truncate { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.edit-series-modal, .delete-series-modal { - overflow : visible; - - .series-poster { - padding-left : 20px; - width : 168px; - } - - .form-horizontal { - margin-top : 10px; - } - - .twitter-typeahead { - .form-control[disabled] { - background-color: #ffffff; - } - } -} - -.delete-series-modal { - .path { - margin-left : 30px; - } - - .delete-files-info { - margin-top : 10px; - display : none; - } -} - -.series-item { - padding-bottom : 30px; - - :hover { - text-decoration : none; - } - - h2 { - margin-top : 0px; - } - - a { - color : #000000; - } -} - -.series-page-header { - .card(black); - .opacity(0.9); - background : #000000; - color : #ffffff; - padding : 30px 15px; - margin : 50px 10px; - - .poster { - margin-top : 4px; - } - - .header-text { - margin-top : 0px; - } -} - -.series-season { - .card; - .opacity(0.9); - margin : 30px 10px; - padding : 10px 25px; - - .show-hide-episodes { - .clickable(); - text-align : center; - - i { - .clickable(); - } - } -} - -.series-posters { - list-style-type: none; - - @media (max-width: @screen-xs-max) { - padding : 0px; - } - - li { - display : inline-block; - vertical-align : top; - } - - .series-posters-item { - - .card; - .clickable; - margin-bottom : 20px; - height : 315px; - - .center { - display : block; - margin-left : auto; - margin-right : auto; - text-align : center; - - .progress { - text-align : left; - margin-top : 5px; - left : 0px; - width : 170px; - - .progressbar-front-text, .progressbar-back-text { - width : 170px; - } - } - } - - .labels { - display : inline-block; - .opacity(0.75); - width : 170px; - - :hover { - cursor : default; - } - - .label { - margin-top : 3px; - display : block; - } - - .tooltip { - .opacity(1); - } - } - - @media (max-width: @screen-xs-max) { - height : 235px; - margin : 5px; - padding : 6px 5px; - - .center { - .progress { - width : 125px; - - .progressbar-front-text, .progressbar-back-text { - width : 125px - } - } - } - - .labels { - width: 125px; - } - } - } - - .series-poster-container { - position : relative; - overflow : hidden; - display : inline-block; - - .placeholder-image ~ .title { - opacity: 1.0; - } - - .title { - position : absolute; - top : 25px; - color : #f5f5f5; - width : 100%; - font-size : 22px; - line-height: 24px; - opacity : 0.0; - font-weight: 100; - } - - .ended-banner { - color : #eeeeee; - background-color : #b94a48; - .box-shadow(2px 2px 20px #888888); - -moz-transform-origin : 50% 50%; - -webkit-transform-origin : 50% 50%; - position : absolute; - width : 320px; - top : 200px; - left : -122px; - text-align : center; - .opacity(0.9); - - .transform(rotate(45deg)); - } - - .series-controls { - position : absolute;; - top : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : right; - padding-right : 10px; - display : none; - .opacity(0.8); - - i { - .clickable(); - } - } - - .hidden-title { - position : absolute;; - bottom : 0px; - overflow : hidden; - background-color : #eeeeee; - width : 100%; - text-align : center; - .opacity(0.8); - display : none; - } - - .series-poster { - width : 168px; - height : 247px; - display : block; - font-size : 34px; - line-height : 34px; - } - - @media (max-width: @screen-xs-max) { - .series-poster { - width : 120px; - height : 176px; - } - - .ended-banner { - top : 145px; - left : -137px; - } - } - } -} - -.series-detail-overview { - margin-bottom : 50px; -} - -.series-season { - - .episode-number-cell { - width : 40px; - white-space: nowrap; - } - .episode-air-date-cell { - width : 150px; - } - - .episode-status-cell { - width : 100px; - } - - .episode-title-cell { - cursor : pointer; - } -} - -.episode-detail-modal { - - .episode-info { - margin-bottom : 10px; - } - - .episode-overview { - font-style : italic; - } - - .episode-file-info { - margin-top : 30px; - font-size : 12px; - } - - .episode-history-details-cell .popover { - max-width: 800px; - } - - .hidden-series-title { - display : none; - } -} - -.season-grid { - .toggle-cell { - width : 28px; - text-align : center; - padding-left : 0px; - padding-right : 0px; - } - - .toggle-cell { - i { - .clickable; - } - } -} - -.season-actions { - width: 100px; -} - -.season-actions, .series-actions { - - div { - display : inline-block - } - - text-transform : none; - - i { - .clickable(); - font-size : 24px; - margin-left : 5px; - } -} - -.series-stats { - font-size : 11px; -} - -.series-legend { - padding-top : 5px; -} - -.seasonpass-series { - .card; - margin : 20px 0px; - - .title { - font-weight : 300; - font-size : 24px; - line-height : 30px; - margin-left : 5px; - } - - .season-select { - margin-bottom : 0px; - } - - .expander { - .clickable; - line-height : 30px; - margin-left : 8px; - width : 16px; - } - - .season-grid { - margin-top : 10px; - } - - .season-pass-button { - display : inline-block; - } - - .series-monitor-toggle { - font-size : 24px; - margin-top : 3px; - } - - .help-inline { - margin-top : 7px; - display : inline-block; - } -} - -.season-status { - font-size : 11px; - vertical-align : middle !important; -} - -//Overview List -.series-overview-list-actions { - min-width: 56px; - max-width: 56px; - - i { - .clickable(); - } -} - -//Editor - -.series-editor-footer { - max-width: 1160px; - color: #f5f5f5; - margin-left: auto; - margin-right: auto; - - .form-group { - padding-top: 0px; - } -} - -.update-files-series-modal { - .selected-series { - margin-top: 15px; - } -} - -//Series Details - -.series-not-monitored { - .season-monitored, .episode-monitored { - color: #888888; - cursor: not-allowed; - - i { - cursor: not-allowed; - } - } -} - -.series-info { - .row { - margin-bottom : 3px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - max-width : 100%; - white-space : normal; - word-wrap : break-word; - } - } - - .series-info-links { - @media (max-width: @screen-sm-max) { - display : inline-block; - margin-top : 5px; - } - } -} - -.scene-info { - .key, .value { - display : inline-block; - } - - .key { - width : 80px; - margin-left : 10px; - vertical-align : top; - } - - .value { - margin-right : 10px; - max-width : 170px; - } - - ul { - padding-left : 0px; - list-style-type : none; - } -} diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js deleted file mode 100644 index 9efced249..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); -var AddItemView = require('./DownloadClientAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), - itemViewContainer : '.add-download-client .items', - template : 'Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs deleted file mode 100644 index f3a823f4a..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Download Client</h3> - </div> - <div class="modal-body"> - <div class="add-download-client add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js deleted file mode 100644 index 75a39e2b5..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemView.js +++ /dev/null @@ -1,58 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/DownloadClientEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this.model.set({ - id : undefined, - enable : true - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this.model.set({ - id : undefined, - enable : true - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js b/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js deleted file mode 100644 index 603a4dfdc..000000000 --- a/src/UI/Settings/DownloadClient/Add/DownloadClientSchemaModal.js +++ /dev/null @@ -1,39 +0,0 @@ -var _ = require('underscore'); -var AppLayout = require('../../../AppLayout'); -var Backbone = require('backbone'); -var SchemaCollection = require('../DownloadClientCollection'); -var AddCollectionView = require('./DownloadClientAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var groupedSchemaCollection = new Backbone.Collection(); - - schemaCollection.on('sync', function() { - - var groups = schemaCollection.groupBy(function(model, iterator) { - return model.get('protocol'); - }); - var modelCollection = _.map(groups, function(values, key, list) { - return { - 'header' : key, - collection : values - }; - }); - - groupedSchemaCollection.reset(modelCollection); - }); - - var view = new AddCollectionView({ - collection : groupedSchemaCollection, - targetCollection : collection - }); - - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js deleted file mode 100644 index e2b9e8556..000000000 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs b/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs deleted file mode 100644 index f31729279..000000000 --- a/src/UI/Settings/DownloadClient/Delete/DownloadClientDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Download Client</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollection.js b/src/UI/Settings/DownloadClient/DownloadClientCollection.js deleted file mode 100644 index 6e0a37083..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollection.js +++ /dev/null @@ -1,25 +0,0 @@ -var Backbone = require('backbone'); -var DownloadClientModel = require('./DownloadClientModel'); - -module.exports = Backbone.Collection.extend({ - model : DownloadClientModel, - url : window.NzbDrone.ApiRoot + '/downloadclient', - - comparator : function(left, right, collection) { - var result = 0; - - if (left.get('protocol')) { - result = -left.get('protocol').localeCompare(right.get('protocol')); - } - - if (result === 0 && left.get('name')) { - result = left.get('name').localeCompare(right.get('name')); - } - - if (result === 0) { - result = left.get('implementation').localeCompare(right.get('implementation')); - } - - return result; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js b/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js deleted file mode 100644 index 457c7afcb..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./DownloadClientItemView'); -var SchemaModal = require('./Add/DownloadClientSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.download-client-list', - template : 'Settings/DownloadClient/DownloadClientCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs deleted file mode 100644 index dbf558b5d..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Download Clients</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="download-client-list thingies"> - <li> - <div class="download-client-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemView.js b/src/UI/Settings/DownloadClient/DownloadClientItemView.js deleted file mode 100644 index fc8a65b4f..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/DownloadClientEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DownloadClientItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs deleted file mode 100644 index ca9fb65f9..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientItemViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="download-client-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if enable}} - <span class="label label-success">Enabled</span> - {{else}} - <span class="label label-default">Not Enabled</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayout.js b/src/UI/Settings/DownloadClient/DownloadClientLayout.js deleted file mode 100644 index fdd6e1b80..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientLayout.js +++ /dev/null @@ -1,32 +0,0 @@ -var Marionette = require('marionette'); -var DownloadClientCollection = require('./DownloadClientCollection'); -var DownloadClientCollectionView = require('./DownloadClientCollectionView'); -var DownloadHandlingView = require('./DownloadHandling/DownloadHandlingView'); -var DroneFactoryView = require('./DroneFactory/DroneFactoryView'); -var RemotePathMappingCollection = require('./RemotePathMapping/RemotePathMappingCollection'); -var RemotePathMappingCollectionView = require('./RemotePathMapping/RemotePathMappingCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/DownloadClient/DownloadClientLayoutTemplate', - - regions : { - downloadClients : '#x-download-clients-region', - downloadHandling : '#x-download-handling-region', - droneFactory : '#x-dronefactory-region', - remotePathMappings : '#x-remotepath-mapping-region' - }, - - initialize : function() { - this.downloadClientsCollection = new DownloadClientCollection(); - this.downloadClientsCollection.fetch(); - this.remotePathMappingCollection = new RemotePathMappingCollection(); - this.remotePathMappingCollection.fetch(); - }, - - onShow : function() { - this.downloadClients.show(new DownloadClientCollectionView({ collection : this.downloadClientsCollection })); - this.downloadHandling.show(new DownloadHandlingView({ model : this.model })); - this.droneFactory.show(new DroneFactoryView({ model : this.model })); - this.remotePathMappings.show(new RemotePathMappingCollectionView({ collection : this.remotePathMappingCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs deleted file mode 100644 index ab039d682..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<div id="x-download-clients-region"></div> -<div class="form-horizontal"> - <div id="x-download-handling-region"></div> - <div id="x-dronefactory-region"></div> - <div id="x-remotepath-mapping-region"></div> -</div> diff --git a/src/UI/Settings/DownloadClient/DownloadClientModel.js b/src/UI/Settings/DownloadClient/DownloadClientModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js b/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js deleted file mode 100644 index eef6d7557..000000000 --- a/src/UI/Settings/DownloadClient/DownloadClientSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/downloadclient', - successMessage : 'Download client settings saved', - errorMessage : 'Failed to save download client settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js deleted file mode 100644 index f3411025c..000000000 --- a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingView.js +++ /dev/null @@ -1,50 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate', - - ui : { - completedDownloadHandlingCheckbox : '.x-completed-download-handling', - completedDownloadOptions : '.x-completed-download-options', - failedAutoRedownladCheckbox : '.x-failed-auto-redownload', - failedDownloadOptions : '.x-failed-download-options' - }, - - events : { - 'change .x-completed-download-handling' : '_setCompletedDownloadOptionsVisibility', - 'change .x-failed-auto-redownload' : '_setFailedDownloadOptionsVisibility' - }, - - onRender : function() { - if (!this.ui.completedDownloadHandlingCheckbox.prop('checked')) { - this.ui.completedDownloadOptions.hide(); - } - if (!this.ui.failedAutoRedownladCheckbox.prop('checked')) { - this.ui.failedDownloadOptions.hide(); - } - }, - - _setCompletedDownloadOptionsVisibility : function() { - var checked = this.ui.completedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.completedDownloadOptions.slideDown(); - } else { - this.ui.completedDownloadOptions.slideUp(); - } - }, - - _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.failedAutoRedownladCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } else { - this.ui.failedDownloadOptions.slideUp(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs b/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs deleted file mode 100644 index bf0c70b94..000000000 --- a/src/UI/Settings/DownloadClient/DownloadHandling/DownloadHandlingViewTemplate.hbs +++ /dev/null @@ -1,93 +0,0 @@ -<fieldset> - <legend>Completed Download Handling</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableCompletedDownloadHandling" class="x-completed-download-handling"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically import completed downloads from download client"/> - </span> - </div> - </div> - </div> - - <div class="x-completed-download-options advanced-setting"> - <div class="form-group"> - <label class="col-sm-3 control-label">Remove</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeCompletedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Remove imported downloads from download client history"/> - </span> - </div> - </div> - </div> - </div> -</fieldset> - -<fieldset> - <legend>Failed Download Handling</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Redownload</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoRedownloadFailed" class="x-failed-auto-redownload"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically search for and attempt to download a different release"/> - </span> - </div> - </div> - </div> - <div class="x-failed-download-options advanced-setting"> - <div class="form-group "> - <label class="col-sm-3 control-label">Remove</label> - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="removeFailedDownloads"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Remove failed downloads from download client history"/> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js deleted file mode 100644 index 154be0a4b..000000000 --- a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryView.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -require('../../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate', - - ui : { - droneFactory : '.x-path' - }, - - onShow : function() { - this.ui.droneFactory.fileBrowser(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs b/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs deleted file mode 100644 index c0385cd43..000000000 --- a/src/UI/Settings/DownloadClient/DroneFactory/DroneFactoryViewTemplate.hbs +++ /dev/null @@ -1,29 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Drone Factory Options</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Drone Factory</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Optional folder to periodically scan for possible imports"/> - <i class="icon-lidarr-form-warning" title="Do not use the folder that contains some or all of your sorted and named Music Artists - doing so could cause data loss"></i> - <i class="icon-lidarr-form-warning" title="Download client history items that are stored in the drone factory will be ignored."/> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <input type="text" name="downloadedAlbumsFolder" class="form-control x-path" /> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Drone Factory Interval</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Interval in minutes to scan the Drone Factory. Set to zero to disable."/> - <i class="icon-lidarr-form-warning" title="Setting a high interval or disabling scanning will prevent albums from being imported."></i> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" name="downloadedAlbumsScanInterval" class="form-control" /> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js deleted file mode 100644 index 1ae48d999..000000000 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditView.js +++ /dev/null @@ -1,56 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/DownloadClientDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/FileBrowser'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/Edit/DownloadClientEditViewTemplate', - - ui : { - path : '.x-path', - modalBody : '.modal-body' - }, - - events : { - 'click .x-back' : '_back' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/DownloadClientSchemaModal').open(this.targetCollection); - }, - _back : function() { - require('../Add/DownloadClientSchemaModal').open(this.targetCollection); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs deleted file mode 100644 index 245af60f8..000000000 --- a/src/UI/Settings/DownloadClient/Edit/DownloadClientEditViewTemplate.hbs +++ /dev/null @@ -1,68 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementation}}</h3> - {{else}} - <h3>Add - {{implementation}}</h3> - {{/if}} - </div> - <div class="modal-body download-client-modal"> - {{formMessage message}} - - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js deleted file mode 100644 index 2906e2254..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var RemotePathMappingModel = require('./RemotePathMappingModel'); - -module.exports = Backbone.Collection.extend({ - model : RemotePathMappingModel, - url : window.NzbDrone.ApiRoot + '/remotePathMapping' -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js deleted file mode 100644 index 9a24a95d3..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionView.js +++ /dev/null @@ -1,28 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var RemotePathMappingItemView = require('./RemotePathMappingItemView'); -var EditView = require('./RemotePathMappingEditView'); -var RemotePathMappingModel = require('./RemotePathMappingModel'); -require('bootstrap'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate', - itemViewContainer : '.x-rows', - itemView : RemotePathMappingItemView, - - events : { - 'click .x-add' : '_addMapping' - }, - - _addMapping : function() { - var model = new RemotePathMappingModel(); - model.collection = this.collection; - - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs deleted file mode 100644 index fcaa91061..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingCollectionViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Remote Path Mappings</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-2">Host</span> - <span class="col-sm-5">Remote Path</span> - <span class="col-sm-4">Local Path</span> - </div> - </div> - <div class="rows x-rows"> - </div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new mapping" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js deleted file mode 100644 index 1ddf5f94b..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs deleted file mode 100644 index 10d94278a..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Mapping</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete the mapping for '{{localPath}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js deleted file mode 100644 index 642901162..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditView.js +++ /dev/null @@ -1,45 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('./RemotePathMappingDeleteView'); -var CommandController = require('../../../Commands/CommandController'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Mixins/FileBrowser'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate', - - ui : { - path : '.x-path', - modalBody : '.modal-body' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs deleted file mode 100644 index b28850d15..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingEditViewTemplate.hbs +++ /dev/null @@ -1,63 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit Mapping</h3> - {{else}} - <h3>Add Mapping</h3> - {{/if}} - </div> - <div class="modal-body remotepath-mapping-modal"> - <div class="form-horizontal"> - <div> - <p>Use this feature if you have a remotely running Download Client. Lidarr will use the information provided to translate the paths provided by the Download Client API to something Lidarr can access and import.</p> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label">Host</label> - - <div class="col-sm-1 col-sm-push-3 help-inline"> - <i class="icon-lidarr-form-info" title="Host you specified for the remote Download Client." /> - </div> - - <div class="col-sm-3 col-sm-pull-1"> - <input type="text" name="host" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Remote Path</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Root path to the directory that the Download Client accesses." /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="remotePath" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Local Path</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Path that Lidarr should use to access the same directory remotely." /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="localPath" class="form-control x-path"/> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js deleted file mode 100644 index d81690e57..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemView.js +++ /dev/null @@ -1,25 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./RemotePathMappingEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate', - className : 'row', - - events : { - 'click .x-edit' : '_editMapping' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _editMapping : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs deleted file mode 100644 index b17a77b38..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ - <div class="col-sm-2"> - {{host}} - </div> - <div class="col-sm-5"> - {{remotePath}} - </div> - <div class="col-sm-4"> - {{localPath}} - </div> - <div class="col-sm-1"> - <div class="pull-right"><i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit Mapping"></i></div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js deleted file mode 100644 index e8ea08465..000000000 --- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var $ = require('jquery'); -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/DownloadClient/downloadclient.less b/src/UI/Settings/DownloadClient/downloadclient.less deleted file mode 100644 index cc2f8f77e..000000000 --- a/src/UI/Settings/DownloadClient/downloadclient.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -.download-client-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.download-client-item { - - .clickable; - - width: 290px; - height: 90px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: -3px; - } - } -} - -.modal-overflow { - overflow-y: visible; -} - -.add-download-client { - li.add-thingy-item { - width: 33%; - } -} diff --git a/src/UI/Settings/General/GeneralSettingsModel.js b/src/UI/Settings/General/GeneralSettingsModel.js deleted file mode 100644 index b8ef7de49..000000000 --- a/src/UI/Settings/General/GeneralSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/host', - successMessage : 'General settings saved', - errorMessage : 'Failed to save general settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/General/GeneralView.js b/src/UI/Settings/General/GeneralView.js deleted file mode 100644 index 81f638f34..000000000 --- a/src/UI/Settings/General/GeneralView.js +++ /dev/null @@ -1,136 +0,0 @@ -var vent = require('../../vent'); -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); - -require('../../Mixins/CopyToClipboard'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/General/GeneralViewTemplate', - - events : { - 'change .x-auth' : '_setAuthOptionsVisibility', - 'change .x-proxy' : '_setProxyOptionsVisibility', - 'change .x-ssl' : '_setSslOptionsVisibility', - 'click .x-reset-api-key' : '_resetApiKey', - 'change .x-update-mechanism' : '_setScriptGroupVisibility' - }, - - ui : { - authToggle : '.x-auth', - authOptions : '.x-auth-options', - sslToggle : '.x-ssl', - sslOptions : '.x-ssl-options', - resetApiKey : '.x-reset-api-key', - copyApiKey : '.x-copy-api-key', - apiKeyInput : '.x-api-key', - updateMechanism : '.x-update-mechanism', - scriptGroup : '.x-script-group', - proxyToggle : '.x-proxy', - proxyOptions : '.x-proxy-settings' - }, - - initialize : function() { - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - if (this.ui.authToggle.val() === 'none') { - this.ui.authOptions.hide(); - } - - if (!this.ui.proxyToggle.prop('checked')) { - this.ui.proxyOptions.hide(); - } - - if (!this.ui.sslToggle.prop('checked')) { - this.ui.sslOptions.hide(); - } - - if (!this._showScriptGroup()) { - this.ui.scriptGroup.hide(); - } - - CommandController.bindToCommand({ - element : this.ui.resetApiKey, - command : { - name : 'resetApiKey' - } - }); - }, - - onShow : function() { - this.ui.copyApiKey.copyToClipboard(this.ui.apiKeyInput); - }, - - _setAuthOptionsVisibility : function() { - - var showAuthOptions = this.ui.authToggle.val() !== 'none'; - - if (showAuthOptions) { - this.ui.authOptions.slideDown(); - } - - else { - this.ui.authOptions.slideUp(); - } - }, - - _setProxyOptionsVisibility : function() { - if (this.ui.proxyToggle.prop('checked')) { - this.ui.proxyOptions.slideDown(); - } - else { - this.ui.proxyOptions.slideUp(); - } - }, - - _setSslOptionsVisibility : function() { - - var showSslOptions = this.ui.sslToggle.prop('checked'); - - if (showSslOptions) { - this.ui.sslOptions.slideDown(); - } - - else { - this.ui.sslOptions.slideUp(); - } - }, - - _resetApiKey : function() { - if (window.confirm('Reset API Key?')) { - CommandController.Execute('resetApiKey', { - name : 'resetApiKey' - }); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'resetapikey') { - this.model.fetch(); - } - }, - - _setScriptGroupVisibility : function() { - - if (this._showScriptGroup()) { - this.ui.scriptGroup.slideDown(); - } - - else { - this.ui.scriptGroup.slideUp(); - } - }, - - _showScriptGroup : function() { - return this.ui.updateMechanism.val() === 'script'; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; - diff --git a/src/UI/Settings/General/GeneralViewTemplate.hbs b/src/UI/Settings/General/GeneralViewTemplate.hbs deleted file mode 100644 index 225ef9996..000000000 --- a/src/UI/Settings/General/GeneralViewTemplate.hbs +++ /dev/null @@ -1,382 +0,0 @@ -<div class="form-horizontal"> - <fieldset> - <legend>Start-Up</legend> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Bind Address</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect" /> - <i class="icon-lidarr-form-info" title="Valid IP4 address or '*' for all interfaces"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="bindAddress" class="form-control" /> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Port Number</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="number" placeholder="8686" name="port" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">URL Base</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - <i class="icon-lidarr-form-info" title="For reverse proxy support, default is empty"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="urlBase" class="form-control"/> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Enable SSL</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableSsl" class="x-ssl"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="Requires restart running as administrator to take effect"/> - </span> - </div> - </div> - </div> - - <div class="x-ssl-options"> - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">SSL Port Number</label> - - <div class="col-sm-4"> - <input type="number" placeholder="8989" name="sslPort" class="form-control"/> - </div> - </div> - - {{#if_windows}} - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">SSL Cert Hash</label> - - <div class="col-sm-4"> - <input type="text" name="sslCertHash" class="form-control"/> - </div> - </div> - {{/if_windows}} - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Open browser on start</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="launchBrowser" class="form-control"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Open a web browser and navigate to Lidarr homepage on app start. Has no effect if installed as a windows service"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Security</legend> - <div class="form-group"> - <label class="col-sm-3 control-label">Authentication</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - <i class="icon-lidarr-form-info" title="Require Username and Password to access Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="authenticationMethod" class="form-control x-auth"> - <option value="none">None</option> - <option value="basic">Basic (Browser popup)</option> - <option value="forms">Forms (Login page)</option> - </select> - </div> - </div> - - <div class="x-auth-options"> - <div class="form-group"> - <label class="col-sm-3 control-label">Username</label> - - <div class="col-sm-4"> - <input type="text" placeholder="Username" name="username" spellcheck="false" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Password</label> - - <div class="col-sm-4"> - <input type="password" name="password" autocomplete="new-password" class="form-control"/> - </div> - </div> - </div> - - <div class="form-group api-key"> - <label class="col-sm-3 control-label">API Key</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <div class="input-group"> - <input type="text" name="apiKey" readonly="readonly" class="form-control x-api-key"/> - <div class="input-group-btn"> - <button class="btn btn-icon-only x-copy-api-key hidden-xs"><i class="icon-lidarr-copy"></i></button> - <button class="btn btn-danger btn-icon-only x-reset-api-key" title="Reset API Key"><i class="icon-lidarr-refresh"></i></button> - </div> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Proxy Settings</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Use Proxy</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="proxyEnabled" class="form-control x-proxy"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <div class="x-proxy-settings"> - <div class="form-group"> - <label class="col-sm-3 control-label">Proxy Type</label> - - <div class="col-sm-4"> - <select name="proxyType" class="form-control"> - <option value="http" selected="selected">HTTP(S)</option> - <option value="socks4">Socks4</option> - <option value="socks5">Socks5 (This option supports Tor)</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Hostname</label> - - <div class="col-sm-4"> - <input type="text" placeholder="localhost" name="proxyHostname" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Port</label> - - <div class="col-sm-4"> - <input type="number" placeholder="8080" name="proxyPort" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Username</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="You only need to enter a username and password if one is required. Leave them blank otherwise."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="proxyUsername" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Password</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="You only need to enter a username and password if one is required. Leave them blank otherwise."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="password" name="proxyPassword" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Addresses for the proxy to ignore</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Use ',' as a separator, and '*.' as a wildcard for subdomains"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="proxyBypassFilter" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Bypass Proxy for Local Addresses</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="proxyBypassLocalAddresses" class="form-control"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Logging</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Log Level</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-warning" title="Trace logging should only be enabled temporarily"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <select name="logLevel" class="form-control"> - <option value="Trace">Trace</option> - <option value="Debug">Debug</option> - <option value="Info">Info</option> - </select> - </div> - </div> - </fieldset> - <fieldset> - <legend>Analytics</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="analyticsEnabled" class="form-control"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Send anonymous usage and error information to Lidarr's servers. This includes information on your browser, which Lidarr WebUI pages you use, error reporting as well as OS and runtime version. We will use this information to prioritize features and bug fixes."/> - <i class="icon-lidarr-form-warning" title="Requires restart to take effect"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset class="advanced-setting"> - <legend>Updates</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Branch</label> - - <div class="col-sm-4"> - <input type="text" placeholder="master" name="branch" class="form-control"/> - </div> - </div> - - {{#if_mono}} - <div class="alert alert-warning">Please see: <a href="https://github.com/NzbDrone/NzbDrone/wiki/Updating">the wiki</a> for more information</div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Automatic</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="updateAutomatically"/> - <p> - <span>On</span> - <span>Off</span> - </p> - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Automatically download and install updates. You will still be able to install from System: Updates"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Mechanism</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Use built-in updater or external script"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="updateMechanism" class="form-control x-update-mechanism"> - <option value="builtIn">Built-in</option> - <option value="script">Script</option> - </select> - </div> - </div> - - <div class="form-group x-script-group"> - <label class="col-sm-3 control-label">Script Path</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Path to a custom script that take an extracted update package and handle the remainder of the update process"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="updateScriptPath" class="form-control"/> - </div> - </div> - {{/if_mono}} - </fieldset> -</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js b/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js deleted file mode 100644 index 5a4102cf2..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var ThingyHeaderGroupView = require('../../ThingyHeaderGroupView'); -var AddItemView = require('./IndexerAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : ThingyHeaderGroupView.extend({ itemView : AddItemView }), - itemViewContainer : '.add-indexer .items', - template : 'Settings/Indexers/Add/IndexerAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs deleted file mode 100644 index f6e1c9339..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,18 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Indexer</h3> - </div> - <div class="modal-body"> - <div class="alert alert-info"> - Lidarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.<br/> - For more information on the individual indexers, click on the info buttons. - </div> - <div class="add-indexer add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js b/src/UI/Settings/Indexers/Add/IndexerAddItemView.js deleted file mode 100644 index 3a8b0493a..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemView.js +++ /dev/null @@ -1,52 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/IndexerEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Add/IndexerAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this._openEdit(); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this._openEdit(); - }, - - _openEdit : function() { - this.model.set({ - id : undefined, - enableRss : this.model.get('supportsRss'), - enableSearch : this.model.get('supportsSearch') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs b/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js b/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js deleted file mode 100644 index 52b430e89..000000000 --- a/src/UI/Settings/Indexers/Add/IndexerSchemaModal.js +++ /dev/null @@ -1,39 +0,0 @@ -var _ = require('underscore'); -var AppLayout = require('../../../AppLayout'); -var Backbone = require('backbone'); -var SchemaCollection = require('../IndexerCollection'); -var AddCollectionView = require('./IndexerAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - - var groupedSchemaCollection = new Backbone.Collection(); - - schemaCollection.on('sync', function() { - - var groups = schemaCollection.groupBy(function(model, iterator) { - return model.get('protocol'); - }); - var modelCollection = _.map(groups, function(values, key, list) { - return { - "header" : key, - collection : values - }; - }); - - groupedSchemaCollection.reset(modelCollection); - }); - - var view = new AddCollectionView({ - collection : groupedSchemaCollection, - targetCollection : collection - }); - - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js b/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js deleted file mode 100644 index 58e7e3eb5..000000000 --- a/src/UI/Settings/Indexers/Delete/IndexerDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Delete/IndexerDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs b/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs deleted file mode 100644 index c5c7ad7db..000000000 --- a/src/UI/Settings/Indexers/Delete/IndexerDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Indexer</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditView.js b/src/UI/Settings/Indexers/Edit/IndexerEditView.js deleted file mode 100644 index 616c863a7..000000000 --- a/src/UI/Settings/Indexers/Edit/IndexerEditView.js +++ /dev/null @@ -1,122 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/IndexerDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/AutoComplete'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Edit/IndexerEditViewTemplate', - - events : { - 'click .x-back' : '_back', - 'click .x-captcha-refresh' : '_onRefreshCaptcha' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/IndexerSchemaModal').open(this.targetCollection); - }, - - _back : function() { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('../Add/IndexerSchemaModal').open(this.targetCollection); - }, - - _onRefreshCaptcha : function(event) { - var self = this; - - var target = $(event.target).parents('.input-group'); - - this.ui.indicator.show(); - - this.model.requestAction("checkCaptcha") - .then(function(result) { - if (!result.captchaRequest) { - self.model.setFieldValue('CaptchaToken', ''); - - return result; - } - - return self._showCaptcha(target, result.captchaRequest); - }) - .always(function() { - self.ui.indicator.hide(); - }); - }, - - _showCaptcha : function(target, captchaRequest) { - var self = this; - - var widget = $('<div class="g-recaptcha"></div>').insertAfter(target); - - return this._loadRecaptchaWidget(widget[0], captchaRequest.siteKey, captchaRequest.secretToken) - .then(function(captchaResponse) { - target.parents('.form-group').removeAllErrors(); - widget.remove(); - - var queryParams = { - responseUrl : captchaRequest.responseUrl, - ray : captchaRequest.ray, - captchaResponse: captchaResponse - }; - - return self.model.requestAction("getCaptchaCookie", queryParams); - }) - .then(function(response) { - self.model.setFieldValue('CaptchaToken', response.captchaToken); - }); - }, - - _loadRecaptchaWidget : function(widget, sitekey, stoken) { - var promise = $.Deferred(); - - var renderWidget = function() { - window.grecaptcha.render(widget, { - 'sitekey' : sitekey, - 'stoken' : stoken, - 'callback' : promise.resolve - }); - }; - - if (window.grecaptcha) { - renderWidget(); - } else { - window.grecaptchaLoadCallback = function() { - delete window.grecaptchaLoadCallback; - renderWidget(); - }; - - $.getScript('https://www.google.com/recaptcha/api.js?onload=grecaptchaLoadCallback&render=explicit') - .fail(function() { promise.reject(); }); - } - - return promise; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs b/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs deleted file mode 100644 index 81ac7c8e3..000000000 --- a/src/UI/Settings/Indexers/Edit/IndexerEditViewTemplate.hbs +++ /dev/null @@ -1,92 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit - {{implementationName}}</h3> - {{else}} - <h3>Add - {{implementationName}}</h3> - {{/if}} - </div> - <div class="modal-body indexer-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable RSS Sync</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableRss" {{#unless supportsRss}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - {{#unless supportsRss}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="" data-original-title="RSS is not supported with this indexer"></i> - </span> - {{/unless}} - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable Search</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - - <input type="checkbox" name="enableSearch" {{#unless supportsSearch}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - {{#unless supportsSearch}} - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="" data-original-title="Search is not supported with this indexer"></i> - </span> - {{/unless}} - </div> - </div> - </div> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerCollection.js b/src/UI/Settings/Indexers/IndexerCollection.js deleted file mode 100644 index 3eb447392..000000000 --- a/src/UI/Settings/Indexers/IndexerCollection.js +++ /dev/null @@ -1,25 +0,0 @@ -var Backbone = require('backbone'); -var IndexerModel = require('./IndexerModel'); - -module.exports = Backbone.Collection.extend({ - model : IndexerModel, - url : window.NzbDrone.ApiRoot + '/indexer', - - comparator : function(left, right, collection) { - var result = 0; - - if (left.get('protocol')) { - result = -left.get('protocol').localeCompare(right.get('protocol')); - } - - if (result === 0 && left.get('name')) { - result = left.get('name').localeCompare(right.get('name')); - } - - if (result === 0) { - result = left.get('implementation').localeCompare(right.get('implementation')); - } - - return result; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerCollectionView.js b/src/UI/Settings/Indexers/IndexerCollectionView.js deleted file mode 100644 index df6ae9596..000000000 --- a/src/UI/Settings/Indexers/IndexerCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./IndexerItemView'); -var SchemaModal = require('./Add/IndexerSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.indexer-list', - template : 'Settings/Indexers/IndexerCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs deleted file mode 100644 index 571a31b3a..000000000 --- a/src/UI/Settings/Indexers/IndexerCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Indexers</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="indexer-list thingies"> - <li> - <div class="indexer-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerItemView.js b/src/UI/Settings/Indexers/IndexerItemView.js deleted file mode 100644 index 29cf3d7c5..000000000 --- a/src/UI/Settings/Indexers/IndexerItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/IndexerEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/IndexerItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs b/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs deleted file mode 100644 index abef39886..000000000 --- a/src/UI/Settings/Indexers/IndexerItemViewTemplate.hbs +++ /dev/null @@ -1,27 +0,0 @@ -<div class="indexer-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if supportsRss}} - {{#if enableRss}} - <span class="label label-success">RSS</span> - {{else}} - <span class="label label-default">RSS</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">RSS</span> - {{/if}} - - {{#if supportsSearch}} - {{#if enableSearch}} - <span class="label label-success">Search</span> - {{else}} - <span class="label label-default">Search</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">Search</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerLayout.js b/src/UI/Settings/Indexers/IndexerLayout.js deleted file mode 100644 index f6cbd1ab6..000000000 --- a/src/UI/Settings/Indexers/IndexerLayout.js +++ /dev/null @@ -1,30 +0,0 @@ -var Marionette = require('marionette'); -var IndexerCollection = require('./IndexerCollection'); -var CollectionView = require('./IndexerCollectionView'); -var OptionsView = require('./Options/IndexerOptionsView'); -var RestrictionCollection = require('./Restriction/RestrictionCollection'); -var RestrictionCollectionView = require('./Restriction/RestrictionCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Indexers/IndexerLayoutTemplate', - - regions : { - indexers : '#x-indexers-region', - indexerOptions : '#x-indexer-options-region', - restriction : '#x-restriction-region' - }, - - initialize : function() { - this.indexersCollection = new IndexerCollection(); - this.indexersCollection.fetch(); - - this.restrictionCollection = new RestrictionCollection(); - this.restrictionCollection.fetch(); - }, - - onShow : function() { - this.indexers.show(new CollectionView({ collection : this.indexersCollection })); - this.indexerOptions.show(new OptionsView({ model : this.model })); - this.restriction.show(new RestrictionCollectionView({ collection : this.restrictionCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs b/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs deleted file mode 100644 index b82535642..000000000 --- a/src/UI/Settings/Indexers/IndexerLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div id="x-indexers-region"></div> -<div class="form-horizontal"> - <div id="x-indexer-options-region"></div> - <div id="x-restriction-region"></div> -</div> diff --git a/src/UI/Settings/Indexers/IndexerModel.js b/src/UI/Settings/Indexers/IndexerModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Indexers/IndexerModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/IndexerSettingsModel.js b/src/UI/Settings/Indexers/IndexerSettingsModel.js deleted file mode 100644 index 14b9db863..000000000 --- a/src/UI/Settings/Indexers/IndexerSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/indexer', - successMessage : 'Indexer settings saved', - errorMessage : 'Failed to save indexer settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js b/src/UI/Settings/Indexers/Options/IndexerOptionsView.js deleted file mode 100644 index 5d4386faa..000000000 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsView.js +++ /dev/null @@ -1,12 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Options/IndexerOptionsViewTemplate' -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs deleted file mode 100644 index d4c3940aa..000000000 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs +++ /dev/null @@ -1,40 +0,0 @@ -<fieldset> - <legend>Options</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Minimum Age</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" min="0" name="minimumAge" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Retention</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Usenet only: Set to zero to set to unlimited"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" min="0" name="retention" class="form-control"/> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">RSS Sync Interval</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-warning" title="This will apply to all indexers, please follow the rules set forth by them"/> - <i class="icon-lidarr-form-info" title="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"/> - </div> - - <div class="col-sm-2 col-sm-pull-1"> - <input type="number" name="rssSyncInterval" class="form-control" min="0" max="120"/> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js b/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js deleted file mode 100644 index 369250343..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var RestrictionModel = require('./RestrictionModel'); - -module.exports = Backbone.Collection.extend({ - model : RestrictionModel, - url : window.NzbDrone.ApiRoot + '/Restriction' -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js b/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js deleted file mode 100644 index 58b3a6bfa..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionView.js +++ /dev/null @@ -1,26 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var RestrictionItemView = require('./RestrictionItemView'); -var EditView = require('./RestrictionEditView'); -require('../../../Tags/TagHelpers'); -require('bootstrap'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionCollectionViewTemplate', - itemViewContainer : '.x-rows', - itemView : RestrictionItemView, - - events : { - 'click .x-add' : '_addMapping' - }, - - _addMapping : function() { - var model = this.collection.create({ tags : [] }); - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs deleted file mode 100644 index fa7f55dee..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionCollectionViewTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Restrictions</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-4">Must Contain</span> - <span class="col-sm-4">Must Not Contain</span> - <span class="col-sm-3">Tags</span> - </div> - </div> - <div class="rows x-rows"> - </div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new restriction" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js b/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js deleted file mode 100644 index d2166c5ed..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs deleted file mode 100644 index 215631e5b..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Restriction</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete this restriction?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js b/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js deleted file mode 100644 index e8540d1a5..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionEditView.js +++ /dev/null @@ -1,55 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('./RestrictionDeleteView'); -var CommandController = require('../../../Commands/CommandController'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Mixins/TagInput'); -require('bootstrap'); -require('bootstrap.tagsinput'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionEditViewTemplate', - - ui : { - required : '.x-required', - ignored : '.x-ignored', - tags : '.x-tags' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - this.ui.required.tagsinput({ - trimValue : true, - tagClass : 'label label-success' - }); - - this.ui.ignored.tagsinput({ - trimValue : true, - tagClass : 'label label-danger' - }); - - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs deleted file mode 100644 index 0133bcf0d..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionEditViewTemplate.hbs +++ /dev/null @@ -1,60 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit Restriction</h3> - {{else}} - <h3>Add Restriction</h3> - {{/if}} - </div> - <div class="modal-body remotepath-mapping-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Must contain</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="The release must contain at least one of these terms (case insensitive)" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="required" class="form-control x-required"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Must not contain</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="The release will be rejected if it contains one or more of terms (case insensitive)" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="ignored" class="form-control x-ignored"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Restrictions will apply to series with one or more matching tags. Leave blank to apply to all series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - </div> - </div> -</div> diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js b/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js deleted file mode 100644 index 729d8ef7d..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionItemView.js +++ /dev/null @@ -1,28 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./RestrictionEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Indexers/Restriction/RestrictionItemViewTemplate', - className : 'row', - - ui : { - tags : '.x-tags' - }, - - events : { - 'click .x-edit' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs deleted file mode 100644 index d7e583d38..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ - <div class="col-sm-4"> - {{genericTagDisplay required 'label label-success'}} - </div> - <div class="col-sm-4"> - {{genericTagDisplay ignored 'label label-danger'}} - </div> - <div class="col-sm-3"> - {{tagDisplay tags}} - </div> - <div class="col-sm-1"> - <div class="pull-right"><i class="icon-lidarr-edit x-edit" title="" data-original-title="Edit"></i></div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionModel.js b/src/UI/Settings/Indexers/Restriction/RestrictionModel.js deleted file mode 100644 index e8ea08465..000000000 --- a/src/UI/Settings/Indexers/Restriction/RestrictionModel.js +++ /dev/null @@ -1,4 +0,0 @@ -var $ = require('jquery'); -var DeepModel = require('backbone.deepmodel'); - -module.exports = DeepModel.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Indexers/indexers.less b/src/UI/Settings/Indexers/indexers.less deleted file mode 100644 index 3fed3ef5f..000000000 --- a/src/UI/Settings/Indexers/indexers.less +++ /dev/null @@ -1,33 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -.indexer-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.indexer-item { - - .clickable; - - width: 290px; - height: 90px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: -3px; - } - } -} - -.modal-overflow { - overflow-y: visible; -} - -.add-indexer { - li.add-thingy-item { - width: 33%; - } -} \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js b/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js deleted file mode 100644 index 49c2cad37..000000000 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementView.js +++ /dev/null @@ -1,23 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -require('../../../Mixins/DirectoryAutoComplete'); -require('../../../Mixins/FileBrowser'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/FileManagement/FileManagementViewTemplate', - - ui : { - recyclingBin : '.x-path' - }, - - onShow : function() { - this.ui.recyclingBin.fileBrowser(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs b/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs deleted file mode 100644 index a20331425..000000000 --- a/src/UI/Settings/MediaManagement/FileManagement/FileManagementViewTemplate.hbs +++ /dev/null @@ -1,97 +0,0 @@ -<fieldset> - <legend>File Management</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Ignore Deleted Tracks</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoUnmonitorPreviouslyDownloadedTracks"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Tracks deleted from disk are automatically unmonitored in Lidarr"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Download Propers</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="autoDownloadPropers"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should Lidarr automatically upgrade to propers when available?"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Analyse Audio Files</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableMediaInfo"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Extract audio information such as bitrate, runtime and codec information from files. This requires Lidarr to read parts of the file which may cause high disk or network activity during scans."/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Change File Date</label> - - <div class="col-sm-1 col-sm-push-2 help-inline"> - <i class="icon-lidarr-form-info" title="Change file date on import/rescan"/> - </div> - - <div class="col-sm-3 col-sm-pull-1"> - <select class="form-control" name="fileDate"> - <option value="none">None</option> - <option value="albumReleaseDate">Album Release Date</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Recycling Bin</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="Track files will go here when deleted instead of being permanently deleted"/> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <input type="text" name="recycleBin" class="form-control x-path"/> - </div> - - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayout.js b/src/UI/Settings/MediaManagement/MediaManagementLayout.js deleted file mode 100644 index da6ea2954..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var NamingView = require('./Naming/NamingView'); -var SortingView = require('./Sorting/SortingView'); -var FileManagementView = require('./FileManagement/FileManagementView'); -var PermissionsView = require('./Permissions/PermissionsView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/MediaManagement/MediaManagementLayoutTemplate', - - regions : { - episodeNaming : '#episode-naming', - sorting : '#sorting', - fileManagement : '#file-management', - permissions : '#permissions' - }, - - initialize : function(options) { - this.settings = options.settings; - this.namingSettings = options.namingSettings; - }, - - onShow : function() { - this.episodeNaming.show(new NamingView({ model : this.namingSettings })); - this.sorting.show(new SortingView({ model : this.settings })); - this.fileManagement.show(new FileManagementView({ model : this.settings })); - this.permissions.show(new PermissionsView({ model : this.settings })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs b/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs deleted file mode 100644 index 44fb14ac3..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<div class="form-horizontal"> - <div id="episode-naming"></div> - <div id="sorting"></div> - <div id="file-management"></div> - {{#if_mono}}<div id="permissions"></div>{{/if_mono}} -</div> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js b/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js deleted file mode 100644 index f80d74800..000000000 --- a/src/UI/Settings/MediaManagement/MediaManagementSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/mediamanagement', - successMessage : 'Media management settings saved', - errorMessage : 'Failed to save media management settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js deleted file mode 100644 index 279febf54..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingView.js +++ /dev/null @@ -1,112 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Config = require('../../../../Config'); -var NamingSampleModel = require('../NamingSampleModel'); -var BasicNamingModel = require('./BasicNamingModel'); -var AsModelBoundView = require('../../../../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate', - - ui : { - namingOptions : '.x-naming-options', - singleTrackExample : '.x-single-track-example' - }, - - initialize : function(options) { - this.namingModel = options.model; - this.model = new BasicNamingModel(); - - this._parseNamingModel(); - - this.listenTo(this.model, 'change', this._buildFormat); - this.listenTo(this.namingModel, 'sync', this._parseNamingModel); - }, - - _parseNamingModel : function() { - var standardFormat = this.namingModel.get('standardTrackFormat'); - - var includeArtistName = standardFormat.match(/\{Artist[-_. ]Name\}/i); - var includeAlbumTitle = standardFormat.match(/\{Album[-_. ]Title\}/i); - var includeQuality = standardFormat.match(/\{Quality[-_. ]Title\}/i); - var numberStyle = standardFormat.match(/\{track(?:\:0+)?\}/i); - var replaceSpaces = standardFormat.indexOf(' ') === -1; - var separator = standardFormat.match(/\}( - |\.-\.|\.| )|( - |\.-\.|\.| )\{/i); - - if (separator === null || separator[1] === '.-.') { - separator = ' - '; - } else { - separator = separator[1]; - } - - if (numberStyle === null) { - numberStyle = '{track:00}'; - } else { - numberStyle = numberStyle[0]; - } - - this.model.set({ - includeArtistName : includeArtistName !== null, - includeAlbumTitle : includeAlbumTitle !== null, - includeQuality : includeQuality !== null, - numberStyle : numberStyle, - replaceSpaces : replaceSpaces, - separator : separator - }, { silent : true }); - }, - - _buildFormat : function() { - if (Config.getValueBoolean(Config.Keys.AdvancedSettings)) { - return; - } - - var standardTrackFormat = ''; - - if (this.model.get('includeArtistName')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Artist.Name}'; - } else { - standardTrackFormat += '{Artist Name}'; - } - - standardTrackFormat += this.model.get('separator'); - } - - if (this.model.get('includeAlbumTitle')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Album.Title}'; - } else { - standardTrackFormat += '{Album Title}'; - } - - standardTrackFormat += this.model.get('separator'); - } - - standardTrackFormat += this.model.get('numberStyle'); - - standardTrackFormat += this.model.get('separator'); - - if (this.model.get('replaceSpaces')) { - standardTrackFormat += '{Track.Title}'; - } else { - standardTrackFormat += '{Track Title}'; - } - - - if (this.model.get('includeQuality')) { - if (this.model.get('replaceSpaces')) { - standardTrackFormat += ' {Quality.Title}'; - } else { - standardTrackFormat += ' {Quality Title}'; - } - } - - if (this.model.get('replaceSpaces')) { - standardTrackFormat = standardTrackFormat.replace(/\s/g, '.'); - } - - this.namingModel.set('standardTrackFormat', standardTrackFormat); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs deleted file mode 100644 index 512245138..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Basic/BasicNamingViewTemplate.hbs +++ /dev/null @@ -1,98 +0,0 @@ -<div class="form-group"> - <label class="col-sm-3 control-label">Include Artist Name</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeArtistName"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Include Album Title</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeAlbumTitle"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Include Quality</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="includeQuality"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Replace Spaces</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceSpaces"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Separator</label> - - <div class="col-sm-9"> - <select class="form-control" name="separator"> - <option value=" - ">Dash</option> - <option value=" ">Space</option> - <option value=".">Period</option> - </select> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Numbering Style</label> - - <div class="col-sm-9"> - <select class="form-control" name="numberStyle"> - <option value="{track}">1</option> - <option value="{track:00}">01</option> - </select> - </div> -</div> diff --git a/src/UI/Settings/MediaManagement/Naming/NamingModel.js b/src/UI/Settings/MediaManagement/Naming/NamingModel.js deleted file mode 100644 index 5ff713850..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var ModelBase = require('../../SettingsModelBase'); - -module.exports = ModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/naming', - successMessage : 'MediaManagement settings saved', - errorMessage : 'Couldn\'t save naming settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js b/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js deleted file mode 100644 index 375d74a6f..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingSampleModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ url : window.NzbDrone.ApiRoot + '/config/naming/samples' }); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingView.js b/src/UI/Settings/MediaManagement/Naming/NamingView.js deleted file mode 100644 index 55dbb860f..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingView.js +++ /dev/null @@ -1,72 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var NamingSampleModel = require('./NamingSampleModel'); -var BasicNamingView = require('./Basic/BasicNamingView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -module.exports = (function() { - var view = Marionette.Layout.extend({ - template : 'Settings/MediaManagement/Naming/NamingViewTemplate', - ui : { - namingOptions : '.x-naming-options', - renameTracksCheckbox : '.x-rename-tracks', - singleTrackExample : '.x-single-track-example', - namingTokenHelper : '.x-naming-token-helper', - artistFolderExample : '.x-artist-folder-example', - albumFolderExample : '.x-album-folder-example' - }, - events : { - "change .x-rename-tracks" : '_setFailedDownloadOptionsVisibility', - "click .x-show-wizard" : '_showWizard', - "click .x-naming-token-helper a" : '_addToken' - }, - regions : { basicNamingRegion : '.x-basic-naming' }, - onRender : function() { - if (!this.model.get('renameTracks')) { - this.ui.namingOptions.hide(); - } - var basicNamingView = new BasicNamingView({ model : this.model }); - this.basicNamingRegion.show(basicNamingView); - this.namingSampleModel = new NamingSampleModel(); - this.listenTo(this.model, 'change', this._updateSamples); - this.listenTo(this.namingSampleModel, 'sync', this._showSamples); - this._updateSamples(); - }, - _setFailedDownloadOptionsVisibility : function() { - var checked = this.ui.renameTracksCheckbox.prop('checked'); - if (checked) { - this.ui.namingOptions.slideDown(); - } else { - this.ui.namingOptions.slideUp(); - } - }, - _updateSamples : function() { - this.namingSampleModel.fetch({ data : this.model.toJSON() }); - }, - _showSamples : function() { - this.ui.singleTrackExample.html(this.namingSampleModel.get('singleTrackExample')); - this.ui.artistFolderExample.html(this.namingSampleModel.get('artistFolderExample')); - this.ui.albumFolderExample.html(this.namingSampleModel.get('albumFolderExample')); - }, - _addToken : function(e) { - e.preventDefault(); - e.stopPropagation(); - var target = e.target; - var token = ''; - var input = this.$(target).closest('.x-helper-input').children('input'); - if (this.$(target).attr('data-token')) { - token = '{{0}}'.format(this.$(target).attr('data-token')); - } else { - token = this.$(target).attr('data-separator'); - } - input.val(input.val() + token); - input.change(); - this.ui.namingTokenHelper.removeClass('open'); - input.focus(); - }, - }); - AsModelBoundView.call(view); - AsValidatedView.call(view); - return view; -}).call(this); \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs deleted file mode 100644 index 6b46ace90..000000000 --- a/src/UI/Settings/MediaManagement/Naming/NamingViewTemplate.hbs +++ /dev/null @@ -1,153 +0,0 @@ -<fieldset> - <legend>Track Naming</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Rename Tracks</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="renameTracks" class="x-rename-tracks"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-warning" title="Lidarr will use the existing file name if set to no"/> - </span> - </div> - </div> - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Replace Illegal Characters</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="replaceIllegalCharacters" /> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Replace or Remove illegal characters"/> - </span> - </div> - </div> - </div> - - <div class="x-naming-options"> - <div class="basic-setting x-basic-naming"></div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Standard Track Format</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used"></i> - <a href="https://github.com/NzbDrone/NzbDrone/wiki/Sorting-and-Renaming" class="help-link" title="More information"><i class="icon-lidarr-form-info-link"/></a> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="standardTrackFormat" data-onkeyup="true" /> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - {{> AlbumTitleNamingPartial}} - {{> ReleaseYearNamingPartial}} - {{> TrackNumNamingPartial}} - {{> TrackTitleNamingPartial}} - {{> QualityNamingPartial}} - {{> MediaInfoNamingPartial}} - {{> ReleaseGroupNamingPartial}} - {{> OriginalTitleNamingPartial}} - {{> SeparatorNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - </div> - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Artist Folder Format</label> - - <div class="col-sm-1 col-sm-push-8 help-inline"> - <i class="icon-lidarr-form-info" title="" data-original-title="All caps or all lower-case can also be used. Only used when adding a new artist."></i> - </div> - - <div class="col-sm-8 col-sm-pull-1"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="artistFolderFormat" data-onkeyup="true"/> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Album Folder Format</label> - - <div class="col-sm-8"> - <div class="input-group x-helper-input"> - <input type="text" class="form-control naming-format" name="albumFolderFormat" data-onkeyup="true"/> - <div class="input-group-btn btn-group x-naming-token-helper"> - <button class="btn btn-icon-only dropdown-toggle" data-toggle="dropdown"> - <i class="icon-lidarr-add"></i> - </button> - <ul class="dropdown-menu"> - {{> ArtistNameNamingPartial}} - {{> AlbumTitleNamingPartial}} - {{> ReleaseYearNamingPartial}} - {{> SeparatorNamingPartial}} - </ul> - </div> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Single Track Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-single-track-example naming-example"></p> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Artist Folder Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-artist-folder-example naming-example"></p> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Album Folder Example</label> - - <div class="col-sm-8"> - <p class="form-control-static x-album-folder-example naming-example"></p> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs deleted file mode 100644 index ba31a196e..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AbsoluteEpisodeNamingPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="absolute">Absolute</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="absolute">1</a></li> - <li><a href="#" data-token="absolute:00">01</a></li> - <li><a href="#" data-token="absolute:000">001</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs deleted file mode 100644 index ed845e2c0..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AirDateNamingPartial.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Air-Date">Air-Date</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Air-Date">Air-Date</a></li> - <li><a href="#" data-token="Air Date">Air Date</a></li> - <li><a href="#" data-token="Air.Date">Air.Date</a></li> - <li><a href="#" data-token="Air_Date">Air_Date</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs deleted file mode 100644 index 9e3da7a54..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/AlbumTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Album Title">Album Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Album Title">Album Title</a></li> - <li><a href="#" data-token="Album.Title">Album.Title</a></li> - <li><a href="#" data-token="Album_Title">Album_Title</a></li> - <li><a href="#" data-token="Album CleanTitle">Album CleanTitle</a></li> - <li><a href="#" data-token="Album.CleanTitle">Album.CleanTitle</a></li> - <li><a href="#" data-token="Album_CleanTitle">Album_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs deleted file mode 100644 index c951bd123..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ArtistNameNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Artist Name">Artist Name</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Artist Name">Artist Name</a></li> - <li><a href="#" data-token="Artist.Name">Artist.Name</a></li> - <li><a href="#" data-token="Artist_Name">Artist_Name</a></li> - <li><a href="#" data-token="Artist CleanName">Artist CleanName</a></li> - <li><a href="#" data-token="Artist.CleanName">Artist.CleanName</a></li> - <li><a href="#" data-token="Artist_CleanName">Artist_CleanName</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs deleted file mode 100644 index 4c20f4ffa..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="episode">Episode</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="episode">1</a></li> - <li><a href="#" data-token="episode:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs deleted file mode 100644 index 10f2ec67e..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/EpisodeTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Episode Title">Episode Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Episode Title">Episode Title</a></li> - <li><a href="#" data-token="Episode.Title">Episode.Title</a></li> - <li><a href="#" data-token="Episode_Title">Episode_Title</a></li> - <li><a href="#" data-token="Episode CleanTitle">Episode CleanTitle</a></li> - <li><a href="#" data-token="Episode.CleanTitle">Episode.CleanTitle</a></li> - <li><a href="#" data-token="Episode_CleanTitle">Episode_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs deleted file mode 100644 index 49203cafc..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/MediaInfoNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="MediaInfo.Simple">MediaInfo</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="MediaInfo Simple">MediaInfo Simple</a></li> - <li><a href="#" data-token="MediaInfo.Simple">MediaInfo.Simple</a></li> - <li><a href="#" data-token="MediaInfo_Simple">MediaInfo_Simple</a></li> - <li><a href="#" data-token="MediaInfo Full">MediaInfo Full</a></li> - <li><a href="#" data-token="MediaInfo.Full">MediaInfo.Full</a></li> - <li><a href="#" data-token="MediaInfo_Full">MediaInfo_Full</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs deleted file mode 100644 index cef96b894..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/OriginalTitleNamingPartial.hbs +++ /dev/null @@ -1 +0,0 @@ -<li><a href="#" data-token="Original Title">Original Title</a></li> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs deleted file mode 100644 index b3da5f0af..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/QualityNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Quality Full">Quality</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Quality Full">Quality Full</a></li> - <li><a href="#" data-token="Quality.Full">Quality.Full</a></li> - <li><a href="#" data-token="Quality_Full">Quality_Full</a></li> - <li><a href="#" data-token="Quality Title">Quality Title</a></li> - <li><a href="#" data-token="Quality.Title">Quality.Title</a></li> - <li><a href="#" data-token="Quality_Title">Quality_Title</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs deleted file mode 100644 index bf9ea50a4..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseGroupNamingPartial.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Release Group">Release Group</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Release Group">Release Group</a></li> - <li><a href="#" data-token="Release.Group">Release.Group</a></li> - <li><a href="#" data-token="Release_Group">Release_Group</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs deleted file mode 100644 index 0a4153d66..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/ReleaseYearNamingPartial.hbs +++ /dev/null @@ -1 +0,0 @@ -<li><a href="#" data-token="Release Year">Release Year</a></li> \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs deleted file mode 100644 index 2c56024da..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeasonNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="season">Season</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="season">1</a></li> - <li><a href="#" data-token="season:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs deleted file mode 100644 index 2b19d32b5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeparatorNamingPartial.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-separator=" - ">Separator</a> - <ul class="dropdown-menu"> - <li><a href="#" data-separator=" - ">Space-Dash-Space</a></li> - <li><a href="#" data-separator="-">Dash</a></li> - <li><a href="#" data-separator=" ">Space</a></li> - <li><a href="#" data-separator=".">Period</a></li> - <li><a href="#" data-separator="_">Underscore</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs deleted file mode 100644 index cc76c95b5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/SeriesTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Series Title">Series Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Series Title">Series Title</a></li> - <li><a href="#" data-token="Series.Title">Series.Title</a></li> - <li><a href="#" data-token="Series_Title">Series_Title</a></li> - <li><a href="#" data-token="Series CleanTitle">Series CleanTitle</a></li> - <li><a href="#" data-token="Series.CleanTitle">Series.CleanTitle</a></li> - <li><a href="#" data-token="Series_CleanTitle">Series_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs deleted file mode 100644 index 68ddc8ff5..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/TrackNumNamingPartial.hbs +++ /dev/null @@ -1,7 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="track">Track</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="track">1</a></li> - <li><a href="#" data-token="track:00">01</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs b/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs deleted file mode 100644 index 591c57098..000000000 --- a/src/UI/Settings/MediaManagement/Naming/Partials/TrackTitleNamingPartial.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<li class="dropdown-submenu"> - <a href="#" tabindex="-1" data-token="Track Title">Track Title</a> - <ul class="dropdown-menu"> - <li><a href="#" data-token="Track Title">Track Title</a></li> - <li><a href="#" data-token="Track.Title">Track.Title</a></li> - <li><a href="#" data-token="Track_Title">Track_Title</a></li> - <li><a href="#" data-token="Track CleanTitle">Track CleanTitle</a></li> - <li><a href="#" data-token="Track.CleanTitle">Track.CleanTitle</a></li> - <li><a href="#" data-token="Track_CleanTitle">Track_CleanTitle</a></li> - </ul> -</li> diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js deleted file mode 100644 index 6bf74221b..000000000 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js +++ /dev/null @@ -1,11 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Permissions/PermissionsViewTemplate' -}); -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs b/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs deleted file mode 100644 index 75f8ddf48..000000000 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsViewTemplate.hbs +++ /dev/null @@ -1,74 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Permissions</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Set Permissions</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="setPermissionsLinux"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Should chmod/chown be run when files are imported/renamed?"/> - <i class="icon-lidarr-form-warning" title="If you're unsure what these settings do, do not alter them."/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">File chmod mask</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Octal, applied to media files when imported/renamed by Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="fileChmod" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Folder chmod mask</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Octal, applied to artist/album folders created by Lidarr"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="folderChmod" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">chown User</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Username or uid. Use uid for remote file systems."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="chownUser" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">chown Group</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Group name or gid. Use gid for remote file systems."/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <input type="text" name="chownGroup" class="form-control"/> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingView.js b/src/UI/Settings/MediaManagement/Sorting/SortingView.js deleted file mode 100644 index 9194f895e..000000000 --- a/src/UI/Settings/MediaManagement/Sorting/SortingView.js +++ /dev/null @@ -1,39 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/MediaManagement/Sorting/SortingViewTemplate', - - events : { - 'change .x-import-extra-files' : '_setExtraFileExtensionVisibility' - }, - - ui : { - importExtraFiles : '.x-import-extra-files', - extraFileExtensions : '.x-extra-file-extensions' - }, - - onRender : function() { - if (!this.ui.importExtraFiles.prop('checked')) { - this.ui.extraFileExtensions.hide(); - } - }, - - _setExtraFileExtensionVisibility : function() { - var showExtraFileExtensions = this.ui.importExtraFiles.prop('checked'); - - if (showExtraFileExtensions) { - this.ui.extraFileExtensions.slideDown(); - } - - else { - this.ui.extraFileExtensions.slideUp(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; diff --git a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs b/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs deleted file mode 100644 index dcc6fa291..000000000 --- a/src/UI/Settings/MediaManagement/Sorting/SortingViewTemplate.hbs +++ /dev/null @@ -1,115 +0,0 @@ -<fieldset class="advanced-setting"> - <legend>Folders</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Create Empty Artist Folders</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="createEmptyArtistFolders"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Create missing artist folders during disk scan"/> - </span> - </div> - </div> - </div> -</fieldset> - -<fieldset> - <legend>Importing</legend> - -{{#if_mono}} - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Skip Free Space Check</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="skipFreeSpaceCheckWhenImporting"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Use when drone is unable to detect free space from your artist root folder"/> - </span> - </div> - </div> - </div> -{{/if_mono}} - - <div class="form-group advanced-setting"> - <label class="col-sm-3 control-label">Use Hardlinks instead of Copy</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="copyUsingHardlinks"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Use Hardlinks when trying to copy files from torrents that are still being seeded"/> - <i class="icon-lidarr-form-warning" title="Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Lidarr's rename function as a work around."/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Import Extra Files</label> - - <div class="col-sm-9"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="importExtraFiles" class="x-import-extra-files"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Import matching extra files (subtitles, nfo, etc) after importing an album or track file"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-extra-file-extensions"> - <label class="col-sm-3 control-label">Extra File Extensions</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Comma separated list of extra files to import, ie sub,nfo (.nfo will be imported as .nfo-orig)"/> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" name="extraFileExtensions" class="form-control"/> - </div> - </div> - -</fieldset> diff --git a/src/UI/Settings/Metadata/MetadataCollection.js b/src/UI/Settings/Metadata/MetadataCollection.js deleted file mode 100644 index f37d80961..000000000 --- a/src/UI/Settings/Metadata/MetadataCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var MetadataModel = require('./MetadataModel'); - -module.exports = Backbone.Collection.extend({ - model : MetadataModel, - url : window.NzbDrone.ApiRoot + '/metadata' -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataCollectionView.js b/src/UI/Settings/Metadata/MetadataCollectionView.js deleted file mode 100644 index 1f60d8fe0..000000000 --- a/src/UI/Settings/Metadata/MetadataCollectionView.js +++ /dev/null @@ -1,9 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var MetadataItemView = require('./MetadataItemView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : MetadataItemView, - itemViewContainer : '#x-metadata', - template : 'Settings/Metadata/MetadataCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs deleted file mode 100644 index a5c034668..000000000 --- a/src/UI/Settings/Metadata/MetadataCollectionViewTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<fieldset> - <legend>Metadata</legend> - <div class="row"> - <div class="col-md-12"> - <ul id="x-metadata" class="metadata-list"></ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataEditView.js b/src/UI/Settings/Metadata/MetadataEditView.js deleted file mode 100644 index ed364824f..000000000 --- a/src/UI/Settings/Metadata/MetadataEditView.js +++ /dev/null @@ -1,19 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../Mixins/AsEditModalView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Metadata/MetadataEditViewTemplate', - - _onAfterSave : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs deleted file mode 100644 index 86aa18ca2..000000000 --- a/src/UI/Settings/Metadata/MetadataEditViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Edit</h3> - </div> - <div class="modal-body"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5 controls"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enable"/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - </div> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Metadata/MetadataItemView.js b/src/UI/Settings/Metadata/MetadataItemView.js deleted file mode 100644 index c72066d6c..000000000 --- a/src/UI/Settings/Metadata/MetadataItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./MetadataEditView'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Metadata/MetadataItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ model : this.model }); - AppLayout.modalRegion.show(view); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs b/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs deleted file mode 100644 index af9adc982..000000000 --- a/src/UI/Settings/Metadata/MetadataItemViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="metadata-item"> - <div> - <h3>{{implementationName}}</h3> - </div> - - <div class="settings"> - {{#if enable}} - <span class="label label-success">Enabled</span> - {{else}} - <span class="label label-default">Not Enabled</span> - {{/if}} - <hr> - {{#each fields}} - {{#if_eq type compare="checkbox"}} - {{#if value}} - <span class="label label-success">{{label}}</span> - {{else}} - <span class="label">{{label}}</span> - {{/if}} - {{/if_eq}} - {{/each}} - </div> -</div> diff --git a/src/UI/Settings/Metadata/MetadataLayout.js b/src/UI/Settings/Metadata/MetadataLayout.js deleted file mode 100644 index 66b5f5901..000000000 --- a/src/UI/Settings/Metadata/MetadataLayout.js +++ /dev/null @@ -1,20 +0,0 @@ -var Marionette = require('marionette'); -var MetadataCollection = require('./MetadataCollection'); -var MetadataCollectionView = require('./MetadataCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Metadata/MetadataLayoutTemplate', - - regions : { - metadata : '#x-metadata-providers' - }, - - initialize : function(options) { - this.settings = options.settings; - this.metadataCollection = new MetadataCollection(); - this.metadataCollection.fetch(); - }, - onShow : function() { - this.metadata.show(new MetadataCollectionView({ collection : this.metadataCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs b/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs deleted file mode 100644 index a32fe464e..000000000 --- a/src/UI/Settings/Metadata/MetadataLayoutTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="x-metadata-providers"/> -</div> diff --git a/src/UI/Settings/Metadata/MetadataModel.js b/src/UI/Settings/Metadata/MetadataModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Metadata/MetadataModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Metadata/metadata.less b/src/UI/Settings/Metadata/metadata.less deleted file mode 100644 index 566114a39..000000000 --- a/src/UI/Settings/Metadata/metadata.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../../Shared/Styles/card"; - -.metadata-list { - li { - display: inline-block; - vertical-align: top; - } -} - -.metadata-item { - - .card; - .clickable; - - width: 200px; - height: 230px; - padding: 10px 15px; - - h3 { - margin-top: 0px; - display: inline-block; - width: 180px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .btn-group { - margin-top: 8px; - } - - .label { - margin-top : 3px; - display : block; - text-align : center; - } -} \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js b/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js deleted file mode 100644 index 68a304fd9..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var ThingyAddCollectionView = require('../../ThingyAddCollectionView'); -var AddItemView = require('./NotificationAddItemView'); - -module.exports = ThingyAddCollectionView.extend({ - itemView : AddItemView, - itemViewContainer : '.add-notifications .items', - template : 'Settings/Notifications/Add/NotificationAddCollectionViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs b/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs deleted file mode 100644 index 0075fa504..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddCollectionViewTemplate.hbs +++ /dev/null @@ -1,14 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Add Notification</h3> - </div> - <div class="modal-body"> - <div class="add-notifications add-thingies"> - <ul class="items"></ul> - </div> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> -</div> diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js b/src/UI/Settings/Notifications/Add/NotificationAddItemView.js deleted file mode 100644 index 04b7c8944..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemView.js +++ /dev/null @@ -1,64 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('../Edit/NotificationEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Add/NotificationAddItemViewTemplate', - tagName : 'li', - className : 'add-thingy-item', - - events : { - 'click .x-preset' : '_addPreset', - 'click' : '_add' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - _addPreset : function(e) { - var presetName = $(e.target).closest('.x-preset').attr('data-id'); - - var presetData = _.where(this.model.get('presets'), { name : presetName })[0]; - - this.model.set(presetData); - - this.model.set({ - id : undefined, - onGrab : this.model.get('supportsOnGrab'), - onDownload : this.model.get('supportsOnDownload'), - onUpgrade : this.model.get('supportsOnUpgrade'), - onRename : this.model.get('supportsOnRename') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - }, - - _add : function(e) { - if ($(e.target).closest('.btn,.btn-group').length !== 0 && $(e.target).closest('.x-custom').length === 0) { - return; - } - - this.model.set({ - id : undefined, - onGrab : this.model.get('supportsOnGrab'), - onDownload : this.model.get('supportsOnDownload'), - onUpgrade : this.model.get('supportsOnUpgrade'), - onRename : this.model.get('supportsOnRename') - }); - - var editView = new EditView({ - model : this.model, - targetCollection : this.targetCollection - }); - - AppLayout.modalRegion.show(editView); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs b/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs deleted file mode 100644 index 1cdb3dffc..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationAddItemViewTemplate.hbs +++ /dev/null @@ -1,30 +0,0 @@ -<div class="add-thingy"> - <div> - {{implementationName}} - </div> - <div class="pull-right"> - {{#if_gt presets.length compare=0}} - <button class="btn btn-xs btn-default x-custom"> - Custom - </button> - <div class="btn-group"> - <button class="btn btn-xs btn-default dropdown-toggle" data-toggle="dropdown"> - Presets - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {{#each presets}} - <li class="x-preset" data-id="{{name}}"> - <a>{{name}}</a> - </li> - {{/each}} - </ul> - </div> - {{/if_gt}} - {{#if infoLink}} - <a class="btn btn-xs btn-default x-info" href="{{infoLink}}"> - <i class="icon-lidarr-form-info"/> - </a> - {{/if}} - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js b/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js deleted file mode 100644 index 54b60973b..000000000 --- a/src/UI/Settings/Notifications/Add/NotificationSchemaModal.js +++ /dev/null @@ -1,18 +0,0 @@ -var AppLayout = require('../../../AppLayout'); -var SchemaCollection = require('../NotificationCollection'); -var AddCollectionView = require('./NotificationAddCollectionView'); - -module.exports = { - open : function(collection) { - var schemaCollection = new SchemaCollection(); - var originalUrl = schemaCollection.url; - schemaCollection.url = schemaCollection.url + '/schema'; - schemaCollection.fetch(); - schemaCollection.url = originalUrl; - var view = new AddCollectionView({ - collection : schemaCollection, - targetCollection : collection - }); - AppLayout.modalRegion.show(view); - } -}; \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js b/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js deleted file mode 100644 index f80ab92a7..000000000 --- a/src/UI/Settings/Notifications/Delete/NotificationDeleteView.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Delete/NotificationDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - _delete : function() { - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs b/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs deleted file mode 100644 index 1e6a52b73..000000000 --- a/src/UI/Settings/Notifications/Delete/NotificationDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Notification</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditView.js b/src/UI/Settings/Notifications/Edit/NotificationEditView.js deleted file mode 100644 index f305d4c0e..000000000 --- a/src/UI/Settings/Notifications/Edit/NotificationEditView.js +++ /dev/null @@ -1,141 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var vent = require('vent'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/NotificationDeleteView'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); -require('../../../Form/FormBuilder'); -require('../../../Mixins/TagInput'); -require('../../../Mixins/FileBrowser'); -require('bootstrap.tagsinput'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Notifications/Edit/NotificationEditViewTemplate', - - ui : { - onDownloadToggle : '.x-on-download', - onUpgradeSection : '.x-on-upgrade', - tags : '.x-tags', - modalBody : '.x-modal-body', - formTag : '.x-form-tag', - path : '.x-path', - authorizedNotificationButton : '.AuthorizeNotification' - }, - - events : { - 'click .x-back' : '_back', - 'change .x-on-download' : '_onDownloadChanged', - 'click .AuthorizeNotification' : '_onAuthorizeNotification' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - this._onDownloadChanged(); - - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - - this.ui.formTag.tagsinput({ - trimValue : true, - tagClass : 'label label-default' - }); - }, - - onShow : function() { - if (this.ui.path.length > 0) { - this.ui.modalBody.addClass('modal-overflow'); - } - - this.ui.path.fileBrowser(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _onAfterSaveAndAdd : function() { - this.targetCollection.add(this.model, { merge : true }); - - require('../Add/NotificationSchemaModal').open(this.targetCollection); - }, - - _back : function() { - if (this.model.isNew()) { - this.model.destroy(); - } - - require('../Add/NotificationSchemaModal').open(this.targetCollection); - }, - - _onDownloadChanged : function() { - var checked = this.ui.onDownloadToggle.prop('checked'); - - if (checked) { - this.ui.onUpgradeSection.show(); - } else { - this.ui.onUpgradeSection.hide(); - } - }, - - _onAuthorizeNotification : function() { - this.ui.indicator.show(); - - var self = this; - var callbackUrl = window.location.origin + window.NzbDrone.UrlBase + '/oauth.html'; - - var promise = this.model.requestAction('startOAuth', { callbackUrl: callbackUrl }) - .then(function(response) { - return self._showOAuthWindow(response.oauthUrl); - }) - .then(function(responseQueryParams) { - return self.model.requestAction('getOAuthToken', responseQueryParams); - }) - .then(function(response) { - self.model.setFieldValue('AccessToken', response.accessToken); - self.model.setFieldValue('AccessTokenSecret', response.accessTokenSecret); - }); - - promise.always(function() { - self.ui.indicator.hide(); - }); - }, - - _showOAuthWindow : function(oauthUrl) { - var promise = $.Deferred(); - - window.open(oauthUrl); - var selfWindow = window; - selfWindow.onCompleteOauth = function(query, callback) { - delete selfWindow.onCompleteOauth; - - var queryParams = {}; - var splitQuery = query.substring(1).split('&'); - _.each(splitQuery, function (param) { - var paramSplit = param.split('='); - queryParams[paramSplit[0]] = paramSplit[1]; - }); - - callback(); - - promise.resolve(queryParams); - }; - - return promise; - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs b/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs deleted file mode 100644 index 148d78118..000000000 --- a/src/UI/Settings/Notifications/Edit/NotificationEditViewTemplate.hbs +++ /dev/null @@ -1,148 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - {{#if id}} - <h3>Edit - {{implementationName}}</h3> - {{else}} - <h3>Add - {{implementationName}}</h3> - {{/if}} - </div> - <div class="modal-body notification-modal x-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"/> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">On Grab</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onGrab" {{#unless supportsOnGrab}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are available for download and has been sent to a download client"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">On Download</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onDownload" class="x-on-download" {{#unless supportsOnDownload}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are successfully downloaded"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-on-upgrade"> - <label class="col-sm-3 control-label">On Upgrade</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onUpgrade" {{#unless supportsOnUpgrade}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are upgraded to a better quality"/> - </span> - </div> - </div> - </div> - - <div class="form-group x-on-upgrade"> - <label class="col-sm-3 control-label">On Rename</label> - - <div class="col-sm-5"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="onRename" {{#unless supportsOnRename}}disabled="disabled"{{/unless}}/> - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Be notified when albums are renamed"/> - </span> - </div> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Filter Series Tags</label> - - <div class="col-sm-5"> - <input type="text" class="form-control x-tags"> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Only send notifications for artist with matching tags"/> - </div> - </div> - - <hr> - - {{formBuilder}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{else}} - <button class="btn pull-left x-back">Back</button> - {{/if}} - - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn x-test">test <i class="x-test-icon icon-lidarr-test"/></button> - <button class="btn" data-dismiss="modal">Cancel</button> - - <div class="btn-group"> - <button class="btn btn-primary x-save">Save</button> - <button class="btn btn-icon-only btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - <li class="save-and-add x-save-and-add"> - save and add - </li> - </ul> - </div> - </div> -</div> diff --git a/src/UI/Settings/Notifications/NotificationCollection.js b/src/UI/Settings/Notifications/NotificationCollection.js deleted file mode 100644 index 25160c33a..000000000 --- a/src/UI/Settings/Notifications/NotificationCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var NotificationModel = require('./NotificationModel'); - -module.exports = Backbone.Collection.extend({ - model : NotificationModel, - url : window.NzbDrone.ApiRoot + '/notification' -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationCollectionView.js b/src/UI/Settings/Notifications/NotificationCollectionView.js deleted file mode 100644 index c25433e5c..000000000 --- a/src/UI/Settings/Notifications/NotificationCollectionView.js +++ /dev/null @@ -1,25 +0,0 @@ -var Marionette = require('marionette'); -var ItemView = require('./NotificationItemView'); -var SchemaModal = require('./Add/NotificationSchemaModal'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ItemView, - itemViewContainer : '.notification-list', - template : 'Settings/Notifications/NotificationCollectionViewTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_openSchemaModal' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _openSchemaModal : function() { - SchemaModal.open(this.collection); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs b/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs deleted file mode 100644 index 47e34c168..000000000 --- a/src/UI/Settings/Notifications/NotificationCollectionViewTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Connections</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="notification-list thingies"> - <li> - <div class="notification-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationItemView.js b/src/UI/Settings/Notifications/NotificationItemView.js deleted file mode 100644 index 6f3665b2f..000000000 --- a/src/UI/Settings/Notifications/NotificationItemView.js +++ /dev/null @@ -1,24 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/NotificationEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Notifications/NotificationItemViewTemplate', - tagName : 'li', - - events : { - 'click' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs b/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs deleted file mode 100644 index bbfa0a3af..000000000 --- a/src/UI/Settings/Notifications/NotificationItemViewTemplate.hbs +++ /dev/null @@ -1,47 +0,0 @@ -<div class="notification-item thingy"> - <div> - <h3>{{name}}</h3> - </div> - - <div class="settings"> - {{#if supportsOnGrab}} - {{#if onGrab}} - <span class="label label-success">On Grab</span> - {{else}} - <span class="label label-default">On Grab</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Grab</span> - {{/if}} - - {{#if supportsOnDownload}} - {{#if onDownload}} - <span class="label label-success">On Download</span> - {{else}} - <span class="label label-default">On Download</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Download</span> - {{/if}} - - {{#if supportsOnUpgrade}} - {{#if onUpgrade}} - <span class="label label-success">On Upgrade</span> - {{else}} - <span class="label label-default">On Upgrade</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Upgrade</span> - {{/if}} - - {{#if supportsOnRename}} - {{#if onRename}} - <span class="label label-success">On Rename</span> - {{else}} - <span class="label label-default">On Rename</span> - {{/if}} - {{else}} - <span class="label label-default label-disabled">On Rename</span> - {{/if}} - </div> -</div> diff --git a/src/UI/Settings/Notifications/NotificationModel.js b/src/UI/Settings/Notifications/NotificationModel.js deleted file mode 100644 index 288e45362..000000000 --- a/src/UI/Settings/Notifications/NotificationModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var ProviderSettingsModelBase = require('../ProviderSettingsModelBase'); - -module.exports = ProviderSettingsModelBase.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Notifications/notifications.less b/src/UI/Settings/Notifications/notifications.less deleted file mode 100644 index fb3a0e4a0..000000000 --- a/src/UI/Settings/Notifications/notifications.less +++ /dev/null @@ -1,37 +0,0 @@ -@import "../../Shared/Styles/clickable.less"; - -//.notifications { -// width: -webkit-fit-content; -// width: -moz-fit-content; -// width: fit-content; -//} - -.notification-item { - .clickable; - - width: 290px; - height: 115px; - padding: 20px 20px; - - .settings { - margin-top: 5px; - - .label { - display : inline-block; - margin-bottom : 2px; - padding : 4px 6px 3px 6px; - } - } - - &.add-card { - .center { - margin-top: -4px; - } - } -} - -.add-notifications { - li.add-thingy-item { - width: 40%; - } -} \ No newline at end of file diff --git a/src/UI/Settings/Profile/AllowedLabeler.js b/src/UI/Settings/Profile/AllowedLabeler.js deleted file mode 100644 index c5da373c3..000000000 --- a/src/UI/Settings/Profile/AllowedLabeler.js +++ /dev/null @@ -1,19 +0,0 @@ -var Handlebars = require('handlebars'); -var _ = require('underscore'); - -Handlebars.registerHelper('allowedLabeler', function() { - var ret = ''; - var cutoff = this.cutoff; - - _.each(this.items, function(item) { - if (item.allowed) { - if (item.quality.id === cutoff.id) { - ret += '<li><span class="label label-info" title="Cutoff">' + item.quality.name + '</span></li>'; - } else { - ret += '<li><span class="label label-default">' + item.quality.name + '</span></li>'; - } - } - }); - - return new Handlebars.SafeString(ret); -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileCollection.js b/src/UI/Settings/Profile/Delay/DelayProfileCollection.js deleted file mode 100644 index fcb240a5b..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var DelayProfileModel = require('./DelayProfileModel'); - -module.exports = Backbone.Collection.extend({ - model : DelayProfileModel, - url : window.NzbDrone.ApiRoot + '/delayprofile' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js b/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js deleted file mode 100644 index 87cc93d2d..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js +++ /dev/null @@ -1,13 +0,0 @@ -var BackboneSortableCollectionView = require('backbone.collectionview'); -var DelayProfileItemView = require('./DelayProfileItemView'); - -module.exports = BackboneSortableCollectionView.extend({ - className : 'delay-profiles', - modelView : DelayProfileItemView, - - events : { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'keydown' : '_onKeydown' - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileItemView.js b/src/UI/Settings/Profile/Delay/DelayProfileItemView.js deleted file mode 100644 index b8d89364b..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileItemView.js +++ /dev/null @@ -1,25 +0,0 @@ -var $ = require('jquery'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var EditView = require('./Edit/DelayProfileEditView'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/DelayProfileItemViewTemplate', - className : 'row', - - events : { - 'click .x-edit' : '_edit' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _edit : function() { - var view = new EditView({ - model : this.model, - targetCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs b/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs deleted file mode 100644 index 5f7450195..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileItemViewTemplate.hbs +++ /dev/null @@ -1,57 +0,0 @@ - <div class="col-sm-2"> - {{#if enableUsenet}} - {{#if enableTorrent}} - {{#if_eq preferredProtocol compare="usenet"}} - Prefer Usenet - {{else}} - Prefer Torrent - {{/if_eq}} - {{else}} - Only Usenet - {{/if}} - {{else}} - Only Torrent - {{/if}} - </div> - <div class="col-sm-2"> - {{#if enableUsenet}} - {{#if_eq usenetDelay compare="0"}} - No delay - {{else}} - {{#if_eq usenetDelay compare="1"}} - 1 minute - {{else}} - {{usenetDelay}} minutes - {{/if_eq}} - {{/if_eq}} - {{else}} - - - {{/if}} - </div> - <div class="col-sm-2"> - {{#if enableTorrent}} - {{#if_eq torrentDelay compare="0"}} - No delay - {{else}} - {{#if_eq torrentDelay compare="1"}} - 1 minute - {{else}} - {{torrentDelay}} minutes - {{/if_eq}} - {{/if_eq}} - {{else}} - - - {{/if}} - </div> - <div class="col-sm-5"> - {{tagDisplay tags}} - </div> - <div class="col-sm-1"> - <div class="pull-right"> - {{#unless_eq id compare="1"}} - <i class="drag-handle icon-lidarr-reorder x-drag-handle" title="Reorder"/> - {{/unless_eq}} - - <i class="icon-lidarr-edit x-edit" title="Edit"></i> - </div> - </div> \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileLayout.js b/src/UI/Settings/Profile/Delay/DelayProfileLayout.js deleted file mode 100644 index 024be5a99..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileLayout.js +++ /dev/null @@ -1,101 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var DelayProfileCollectionView = require('./DelayProfileCollectionView'); -var EditView = require('./Edit/DelayProfileEditView'); -var Model = require('./DelayProfileModel'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Profile/Delay/DelayProfileLayoutTemplate', - - regions : { - delayProfiles : '.x-rows' - }, - - events : { - 'click .x-add' : '_add' - }, - - initialize : function(options) { - this.collection = options.collection; - - this._updateOrderedCollection(); - - this.listenTo(this.collection, 'sync', this._updateOrderedCollection); - this.listenTo(this.collection, 'add', this._updateOrderedCollection); - this.listenTo(this.collection, 'remove', function() { - this.collection.fetch(); - }); - }, - - onRender : function() { - - this.sortableListView = new DelayProfileCollectionView({ - sortable : true, - collection : this.orderedCollection, - - sortableOptions : { - handle : '.x-drag-handle' - }, - - sortableModelsFilter : function(model) { - return model.get('id') !== 1; - } - }); - - this.delayProfiles.show(this.sortableListView); - - this.listenTo(this.sortableListView, 'sortStop', this._updateOrder); - }, - - _updateOrder : function() { - var self = this; - - this.collection.forEach(function(model) { - if (model.get('id') === 1) { - return; - } - - var orderedModel = self.orderedCollection.get(model); - var order = self.orderedCollection.indexOf(orderedModel) + 1; - - if (model.get('order') !== order) { - model.set('order', order); - model.save(); - } - }); - }, - - _add : function() { - var model = new Model({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'usenet', - usenetDelay : 0, - torrentDelay : 0, - order : this.collection.length, - tags : [] - }); - - model.collection = this.collection; - var view = new EditView({ - model : model, - targetCollection : this.collection - }); - - AppLayout.modalRegion.show(view); - }, - - _updateOrderedCollection : function() { - if (!this.orderedCollection) { - this.orderedCollection = new Backbone.Collection(); - } - - this.orderedCollection.reset(_.sortBy(this.collection.models, function(model) { - return model.get('order'); - })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs deleted file mode 100644 index 7787c1456..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs +++ /dev/null @@ -1,24 +0,0 @@ -<fieldset> - <legend>Delay Profiles</legend> - - <div class="col-md-12"> - <div class="rule-setting-list"> - <div class="rule-setting-header x-header hidden-xs"> - <div class="row"> - <span class="col-sm-2">Protocol</span> - <span class="col-sm-2">Usenet Delay</span> - <span class="col-sm-2">Torrent Delay</span> - <span class="col-sm-5">Tags</span> - </div> - </div> - <div class="rows x-rows"></div> - <div class="rule-setting-footer"> - <div class="pull-right"> - <span class="add-rule-setting-mapping"> - <i class="icon-lidarr-add x-add" title="Add new delay profile" /> - </span> - </div> - </div> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Profile/Delay/DelayProfileModel.js b/src/UI/Settings/Profile/Delay/DelayProfileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/Profile/Delay/DelayProfileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js b/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js deleted file mode 100644 index 6b948d782..000000000 --- a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteView.js +++ /dev/null @@ -1,21 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate', - - events : { - 'click .x-confirm-delete' : '_delete' - }, - - _delete : function() { - var collection = this.model.collection; - - this.model.destroy({ - wait : true, - success : function() { - vent.trigger(vent.Commands.CloseModalCommand); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs b/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs deleted file mode 100644 index dc6b5125f..000000000 --- a/src/UI/Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete Delay Profile</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete this delay profile?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js b/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js deleted file mode 100644 index 277527f79..000000000 --- a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js +++ /dev/null @@ -1,122 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../../../AppLayout'); -var Marionette = require('marionette'); -var DeleteView = require('../Delete/DelayProfileDeleteView'); -var AsModelBoundView = require('../../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../../Mixins/AsValidatedView'); -var AsEditModalView = require('../../../../Mixins/AsEditModalView'); -require('../../../../Mixins/TagInput'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate', - - _deleteView : DeleteView, - - ui : { - tags : '.x-tags', - usenetDelay : '.x-usenet-delay', - torrentDelay : '.x-torrent-delay', - protocol : '.x-protocol' - }, - - events : { - 'change .x-protocol' : '_updateModel' - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - }, - - onRender : function() { - if (this.model.id !== 1) { - this.ui.tags.tagInput({ - model : this.model, - property : 'tags' - }); - } - - this._toggleControls(); - }, - - _onAfterSave : function() { - this.targetCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _updateModel : function() { - var protocol = this.ui.protocol.val(); - - if (protocol === 'preferUsenet') { - this.model.set({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'usenet' - }); - } - - if (protocol === 'preferTorrent') { - this.model.set({ - enableUsenet : true, - enableTorrent : true, - preferredProtocol : 'torrent' - }); - } - - if (protocol === 'onlyUsenet') { - this.model.set({ - enableUsenet : true, - enableTorrent : false, - preferredProtocol : 'usenet' - }); - } - - if (protocol === 'onlyTorrent') { - this.model.set({ - enableUsenet : false, - enableTorrent : true, - preferredProtocol : 'torrent' - }); - } - - this._toggleControls(); - }, - - _toggleControls : function() { - var enableUsenet = this.model.get('enableUsenet'); - var enableTorrent = this.model.get('enableTorrent'); - var preferred = this.model.get('preferredProtocol'); - - if (preferred === 'usenet') { - this.ui.protocol.val('preferUsenet'); - } - - else { - this.ui.protocol.val('preferTorrent'); - } - - if (enableUsenet) { - this.ui.usenetDelay.show(); - } - - else { - this.ui.protocol.val('onlyTorrent'); - this.ui.usenetDelay.hide(); - } - - if (enableTorrent) { - this.ui.torrentDelay.show(); - } - - else { - this.ui.protocol.val('onlyUsenet'); - this.ui.torrentDelay.hide(); - } - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); -AsEditModalView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs b/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs deleted file mode 100644 index dedef6482..000000000 --- a/src/UI/Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate.hbs +++ /dev/null @@ -1,80 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit - Delay Profile</h3> - {{else}} - <h3>Add - Delay Profile</h3> - {{/if}} - </div> - <div class="modal-body indexer-modal"> - <div class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-3 control-label">Protocol</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <select class="form-control x-protocol"> - <option value="preferUsenet">Prefer Usenet</option> - <option value="preferTorrent">Prefer Torrent</option> - <option value="onlyUsenet">Only Usenet</option> - <option value="onlyTorrent">Only Torrent</option> - </select> - </div> - </div> - - <div class="form-group x-usenet-delay"> - <label class="col-sm-3 control-label">Usenet Delay</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Delay in minutes to wait before grabbing a release from Usenet" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="number" class="form-control" name="usenetDelay"/> - </div> - </div> - - <div class="form-group x-torrent-delay"> - <label class="col-sm-3 control-label">Torrent Delay</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="Delay in minutes to wait before grabbing a torrent" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="number" class="form-control" name="torrentDelay"/> - </div> - </div> - - {{#if_eq id compare="1"}} - <div class="alert alert-info" role="alert">This is the default profile. It applies to all series that don't have an explicit profile.</div> - {{else}} - <div class="form-group"> - <label class="col-sm-3 control-label">Tags</label> - - <div class="col-sm-1 col-sm-push-5 help-inline"> - <i class="icon-lidarr-form-info" title="One or more tags to apply these rules to matching series" /> - </div> - - <div class="col-sm-5 col-sm-pull-1"> - <input type="text" class="form-control x-tags"> - </div> - </div> - {{/if_eq}} - </div> - </div> - <div class="modal-footer"> - {{#if id}} - {{#if_gt id compare="1"}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if_gt}} - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/DeleteProfileView.js b/src/UI/Settings/Profile/DeleteProfileView.js deleted file mode 100644 index 4b91e0e07..000000000 --- a/src/UI/Settings/Profile/DeleteProfileView.js +++ /dev/null @@ -1,16 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/DeleteProfileViewTemplate', - - events : { - 'click .x-confirm-delete' : '_removeProfile' - }, - - _removeProfile : function() { - this.model.destroy({ wait : true }).done(function() { - vent.trigger(vent.Commands.CloseModalCommand); - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs b/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs deleted file mode 100644 index c9a826855..000000000 --- a/src/UI/Settings/Profile/DeleteProfileViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3>Delete: {{name}}</h3> - </div> - <div class="modal-body"> - <p>Are you sure you want to delete '{{name}}'?</p> - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-danger x-confirm-delete">Delete</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/EditProfileItemView.js b/src/UI/Settings/Profile/Edit/EditProfileItemView.js deleted file mode 100644 index 535fff211..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileItemView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileItemViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs deleted file mode 100644 index 7feea8fac..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileItemViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<i class="select-handle pull-left x-select" /> -<span class="quality-label">{{quality.name}}</span> -<i class="drag-handle pull-right icon-lidarr-reorder advanced-setting x-drag-handle" /> diff --git a/src/UI/Settings/Profile/Edit/EditProfileLayout.js b/src/UI/Settings/Profile/Edit/EditProfileLayout.js deleted file mode 100644 index 721b04479..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileLayout.js +++ /dev/null @@ -1,117 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var AppLayout = require('../../../AppLayout'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var EditProfileItemView = require('./EditProfileItemView'); -var QualitySortableCollectionView = require('./QualitySortableCollectionView'); -var EditProfileView = require('./EditProfileView'); -var DeleteView = require('../DeleteProfileView'); -var SeriesCollection = require('../../../Artist/ArtistCollection'); -var Config = require('../../../Config'); -var AsEditModalView = require('../../../Mixins/AsEditModalView'); - -var view = Marionette.Layout.extend({ - template : 'Settings/Profile/Edit/EditProfileLayoutTemplate', - - regions : { - fields : '#x-fields', - qualities : '#x-qualities' - }, - - ui : { - deleteButton : '.x-delete' - }, - - _deleteView : DeleteView, - - initialize : function(options) { - this.profileCollection = options.profileCollection; - this.itemsCollection = new Backbone.Collection(_.toArray(this.model.get('items')).reverse()); - this.listenTo(SeriesCollection, 'all', this._updateDisableStatus); - }, - - onRender : function() { - this._updateDisableStatus(); - }, - - onShow : function() { - this.fieldsView = new EditProfileView({ model : this.model }); - this._showFieldsView(); - var advancedShown = Config.getValueBoolean(Config.Keys.AdvancedSettings, false); - - this.sortableListView = new QualitySortableCollectionView({ - selectable : true, - selectMultiple : true, - clickToSelect : true, - clickToToggle : true, - sortable : advancedShown, - - sortableOptions : { - handle : '.x-drag-handle' - }, - - visibleModelsFilter : function(model) { - return model.get('quality').id !== 0 || advancedShown; - }, - - collection : this.itemsCollection, - model : this.model - }); - - this.sortableListView.setSelectedModels(this.itemsCollection.filter(function(item) { - return item.get('allowed') === true; - })); - this.qualities.show(this.sortableListView); - - this.listenTo(this.sortableListView, 'selectionChanged', this._selectionChanged); - this.listenTo(this.sortableListView, 'sortStop', this._updateModel); - }, - - _onBeforeSave : function() { - var cutoff = this.fieldsView.getCutoff(); - this.model.set('cutoff', cutoff); - }, - - _onAfterSave : function() { - this.profileCollection.add(this.model, { merge : true }); - vent.trigger(vent.Commands.CloseModalCommand); - }, - - _selectionChanged : function(newSelectedModels, oldSelectedModels) { - var addedModels = _.difference(newSelectedModels, oldSelectedModels); - var removeModels = _.difference(oldSelectedModels, newSelectedModels); - - _.each(removeModels, function(item) { - item.set('allowed', false); - }); - _.each(addedModels, function(item) { - item.set('allowed', true); - }); - this._updateModel(); - }, - - _updateModel : function() { - this.model.set('items', this.itemsCollection.toJSON().reverse()); - - this._showFieldsView(); - }, - - _showFieldsView : function() { - this.fields.show(this.fieldsView); - }, - - _updateDisableStatus : function() { - if (this._isQualityInUse()) { - this.ui.deleteButton.addClass('disabled'); - this.ui.deleteButton.attr('title', 'Can\'t delete a profile that is attached to a artist.'); - } else { - this.ui.deleteButton.removeClass('disabled'); - } - }, - - _isQualityInUse : function() { - return SeriesCollection.where({ 'profileId' : this.model.id }).length !== 0; - } -}); -module.exports = AsEditModalView.call(view); diff --git a/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs deleted file mode 100644 index d68c46fb6..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileLayoutTemplate.hbs +++ /dev/null @@ -1,36 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - {{#if id}} - <h3>Edit</h3> - {{else}} - <h3>Add</h3> - {{/if}} -</div> - <div class="modal-body"> - <div class="form-horizontal"> - <div id="x-fields"></div> - <div class="form-group"> - <label class="col-sm-3 control-label">Qualities</label> - - <div class="col-sm-5"> - <div class="controls qualities-controls"> - <span id="x-qualities"></span> - </div> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Qualities higher in the list are more preferred. Only checked qualities will be wanted."/> - </div> - </div> - </div> - </div> - <div class="modal-footer"> - {{#if id}} - <button class="btn btn-danger pull-left x-delete">Delete</button> - {{/if}} - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Cancel</button> - <button class="btn btn-primary x-save">Save</button> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/EditProfileView.js b/src/UI/Settings/Profile/Edit/EditProfileView.js deleted file mode 100644 index 23535d9e6..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileView.js +++ /dev/null @@ -1,28 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var LanguageCollection = require('../Language/LanguageCollection'); -var Config = require('../../../Config'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/Edit/EditProfileViewTemplate', - - ui : { cutoff : '.x-cutoff' }, - - templateHelpers : function() { - return { - languages : LanguageCollection.toJSON() - }; - }, - - getCutoff : function() { - var self = this; - - return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id : parseInt(self.ui.cutoff.val(), 10) }); - } -}); - -AsValidatedView.call(view); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs b/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs deleted file mode 100644 index 894fb68cf..000000000 --- a/src/UI/Settings/Profile/Edit/EditProfileViewTemplate.hbs +++ /dev/null @@ -1,45 +0,0 @@ -<div class="form-group"> - <label class="col-sm-3 control-label">Name</label> - - <div class="col-sm-5"> - <input type="text" name="name" class="form-control"> - </div> -</div> - -<hr> - -<div class="form-group"> - <label class="col-sm-3 control-label">Language</label> - - <div class="col-sm-5"> - <select class="form-control" name="language"> - {{#each languages}} - {{#unless_eq nameLower compare="unknown"}} - <option value="{{nameLower}}">{{name}}</option> - {{/unless_eq}} - {{/each}} - </select> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Artists assigned this profile will be looking for albums with the selected language"/> - </div> -</div> - -<div class="form-group"> - <label class="col-sm-3 control-label">Cutoff</label> - - <div class="col-sm-5"> - <select class="form-control x-cutoff" name="cutoff.id" validation-name="cutoff"> - {{#eachReverse items}} - {{#if allowed}} - <option value="{{quality.id}}">{{quality.name}}</option> - {{/if}} - {{/eachReverse}} - </select> - </div> - - <div class="col-sm-1 help-inline"> - <i class="icon-lidarr-form-info" title="Once this quality is reached Lidarr will no longer download albums"/> - </div> -</div> diff --git a/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js b/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js deleted file mode 100644 index 6fc6253aa..000000000 --- a/src/UI/Settings/Profile/Edit/QualitySortableCollectionView.js +++ /dev/null @@ -1,17 +0,0 @@ -var BackboneSortableCollectionView = require('backbone.collectionview'); -var EditProfileItemView = require('./EditProfileItemView'); - -module.exports = BackboneSortableCollectionView.extend({ - className : 'qualities', - modelView : EditProfileItemView, - - attributes : { - 'validation-name' : 'items' - }, - - events : { - 'click li, td' : '_listItem_onMousedown', - 'dblclick li, td' : '_listItem_onDoubleClick', - 'keydown' : '_onKeydown' - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/Language/LanguageCollection.js b/src/UI/Settings/Profile/Language/LanguageCollection.js deleted file mode 100644 index d016c0441..000000000 --- a/src/UI/Settings/Profile/Language/LanguageCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LanguageModel = require('./LanguageModel'); - -var LanuageCollection = Backbone.Collection.extend({ - model : LanguageModel, - url : window.NzbDrone.ApiRoot + '/language' -}); - -var languages = new LanuageCollection(); -languages.fetch(); - -module.exports = languages; \ No newline at end of file diff --git a/src/UI/Settings/Profile/Language/LanguageModel.js b/src/UI/Settings/Profile/Language/LanguageModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Settings/Profile/Language/LanguageModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/LanguageLabel.js b/src/UI/Settings/Profile/LanguageLabel.js deleted file mode 100644 index b162d5683..000000000 --- a/src/UI/Settings/Profile/LanguageLabel.js +++ /dev/null @@ -1,15 +0,0 @@ -var _ = require('underscore'); -var Handlebars = require('handlebars'); -var LanguageCollection = require('./Language/LanguageCollection'); - -Handlebars.registerHelper('languageLabel', function() { - var wantedLanguage = this.language; - - var language = LanguageCollection.find(function(lang) { - return lang.get('nameLower') === wantedLanguage; - }); - - var result = '<span class="label label-primary">' + language.get('name') + '</span>'; - - return new Handlebars.SafeString(result); -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs b/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs deleted file mode 100644 index 194082875..000000000 --- a/src/UI/Settings/Profile/ProfileCollectionTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Profiles</legend> - <div class="row"> - <div class="col-md-12"> - <ul class="profiles thingies"> - <li> - <div class="profile-item thingy add-card x-add-card"> - <span class="center well"> - <i class="icon-lidarr-add"/> - </span> - </div> - </li> - </ul> - </div> - </div> -</fieldset> \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileCollectionView.js b/src/UI/Settings/Profile/ProfileCollectionView.js deleted file mode 100644 index 1f96fc44f..000000000 --- a/src/UI/Settings/Profile/ProfileCollectionView.js +++ /dev/null @@ -1,43 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var ProfileView = require('./ProfileView'); -var EditProfileView = require('./Edit/EditProfileLayout'); -var ProfileCollection = require('./ProfileSchemaCollection'); -var _ = require('underscore'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ProfileView, - itemViewContainer : '.profiles', - template : 'Settings/Profile/ProfileCollectionTemplate', - - ui : { - 'addCard' : '.x-add-card' - }, - - events : { - 'click .x-add-card' : '_addProfile' - }, - - appendHtml : function(collectionView, itemView, index) { - collectionView.ui.addCard.parent('li').before(itemView.el); - }, - - _addProfile : function() { - var self = this; - var schemaCollection = new ProfileCollection(); - schemaCollection.fetch({ - success : function(collection) { - var model = _.first(collection.models); - model.set('id', undefined); - model.set('name', ''); - model.collection = self.collection; - var view = new EditProfileView({ - model : model, - profileCollection : self.collection - }); - - AppLayout.modalRegion.show(view); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileLayout.js b/src/UI/Settings/Profile/ProfileLayout.js deleted file mode 100644 index d8f226271..000000000 --- a/src/UI/Settings/Profile/ProfileLayout.js +++ /dev/null @@ -1,28 +0,0 @@ -var Marionette = require('marionette'); -var ProfileCollection = require('../../Profile/ProfileCollection'); -var ProfileCollectionView = require('./ProfileCollectionView'); -var DelayProfileLayout = require('./Delay/DelayProfileLayout'); -var DelayProfileCollection = require('./Delay/DelayProfileCollection'); -var LanguageCollection = require('./Language/LanguageCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Profile/ProfileLayoutTemplate', - - regions : { - profile : '#profile', - delayProfile : '#delay-profile' - }, - - initialize : function(options) { - this.settings = options.settings; - ProfileCollection.fetch(); - - this.delayProfileCollection = new DelayProfileCollection(); - this.delayProfileCollection.fetch(); - }, - - onShow : function() { - this.profile.show(new ProfileCollectionView({ collection : ProfileCollection })); - this.delayProfile.show(new DelayProfileLayout({ collection : this.delayProfileCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs b/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs deleted file mode 100644 index 99adeab97..000000000 --- a/src/UI/Settings/Profile/ProfileLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="profile"/> - - <div class="col-md-12 delay-profile-region" id="delay-profile"/> -</div> diff --git a/src/UI/Settings/Profile/ProfileSchemaCollection.js b/src/UI/Settings/Profile/ProfileSchemaCollection.js deleted file mode 100644 index 6f32ff2e8..000000000 --- a/src/UI/Settings/Profile/ProfileSchemaCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var ProfileModel = require('../../Profile/ProfileModel'); - -module.exports = Backbone.Collection.extend({ - model : ProfileModel, - url : window.NzbDrone.ApiRoot + '/profile/schema' -}); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileView.js b/src/UI/Settings/Profile/ProfileView.js deleted file mode 100644 index 4241c3f12..000000000 --- a/src/UI/Settings/Profile/ProfileView.js +++ /dev/null @@ -1,35 +0,0 @@ -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditProfileView = require('./Edit/EditProfileLayout'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -require('./AllowedLabeler'); -require('./LanguageLabel'); -require('bootstrap'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Profile/ProfileViewTemplate', - tagName : 'li', - - ui : { - "progressbar" : '.progress .bar', - "deleteButton" : '.x-delete' - }, - - events : { - 'click' : '_editProfile' - }, - - initialize : function() { - this.listenTo(this.model, 'sync', this.render); - }, - - _editProfile : function() { - var view = new EditProfileView({ - model : this.model, - profileCollection : this.model.collection - }); - AppLayout.modalRegion.show(view); - } -}); - -module.exports = AsModelBoundView.call(view); \ No newline at end of file diff --git a/src/UI/Settings/Profile/ProfileViewTemplate.hbs b/src/UI/Settings/Profile/ProfileViewTemplate.hbs deleted file mode 100644 index 4f5b3eef0..000000000 --- a/src/UI/Settings/Profile/ProfileViewTemplate.hbs +++ /dev/null @@ -1,13 +0,0 @@ -<div class="profile-item thingy"> - <div> - <h3 name="name"></h3> - </div> - - <div class="language"> - {{languageLabel}} - </div> - - <ul class="allowed-qualities"> - {{allowedLabeler}} - </ul> -</div> \ No newline at end of file diff --git a/src/UI/Settings/Profile/profile.less b/src/UI/Settings/Profile/profile.less deleted file mode 100644 index df217a398..000000000 --- a/src/UI/Settings/Profile/profile.less +++ /dev/null @@ -1,43 +0,0 @@ -@import "../../Content/Bootstrap/mixins"; -@import "../../Content/FontAwesome/font-awesome"; -@import "../../Shared/Styles/clickable.less"; - -.profile-item { - .clickable; - - width: 300px; - height: 158px; - padding: 10px 15px; - - &.add-card { - .center { - margin-top: 30px; - } - } - - .allowed-qualities { - - padding-left: 0px; - - li { - list-style-type : none; - margin: 1px; - } - } - - .language { - margin-bottom: 3px; - } -} - -.delay-profile-region { - margin-top : 30px; -} - -.delay-profiles { - padding-left : 0px; - - li { - list-style-type : none; - } -} \ No newline at end of file diff --git a/src/UI/Settings/ProviderSettingsModelBase.js b/src/UI/Settings/ProviderSettingsModelBase.js deleted file mode 100644 index 674aba4e5..000000000 --- a/src/UI/Settings/ProviderSettingsModelBase.js +++ /dev/null @@ -1,71 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var DeepModel = require('backbone.deepmodel'); -var Messenger = require('../Shared/Messenger'); - -module.exports = DeepModel.extend({ - - getFieldValue : function(name) { - var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name); - return this.get('fields.' + index + '.value'); - }, - - setFieldValue : function(name, value) { - var index = _.indexOf(_.pluck(this.get('fields'), 'name'), name); - return this.set('fields.' + index + '.value', value); - }, - - requestAction : function(action, queryParams) { - var self = this; - - this.trigger('validation:sync'); - - var params = { - url : this.collection.url + '/action/' + action, - contentType : 'application/json', - data : JSON.stringify(this.toJSON()), - type : 'POST', - isValidatedCall : true - }; - - if (queryParams) { - params.url += '?' + $.param(queryParams, true); - } - - var promise = $.ajax(params); - - promise.fail(function(response) { - self.trigger('validation:failed', response); - }); - - return promise; - }, - - test : function() { - var self = this; - - this.trigger('validation:sync'); - - var params = {}; - - params.url = this.collection.url + '/test'; - params.contentType = 'application/json'; - params.data = JSON.stringify(this.toJSON()); - params.type = 'POST'; - params.isValidatedCall = true; - - var promise = $.ajax(params); - - Messenger.monitor({ - promise : promise, - successMessage : 'Testing \'{0}\' succeeded'.format(this.get('name')), - errorMessage : 'Testing \'{0}\' failed'.format(this.get('name')) - }); - - promise.fail(function(response) { - self.trigger('validation:failed', response); - }); - - return promise; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs deleted file mode 100644 index ee0a5faef..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<fieldset> - <legend>Quality Definitions</legend> - <div class="col-md-11"> - <div id="quality-definition-list"> - <div class="quality-header x-header hidden-xs"> - <div class="row"> - <span class="col-md-2 col-sm-3">Quality</span> - <span class="col-md-2 col-sm-3">Title</span> - <span class="col-md-4 col-sm-6">Size Limit <i class="icon-lidarr-info" title="Limits are automatically adjusted for the number of tracks in the file." /></span> - </div> - </div> - <div class="rows x-rows"> - </div> - </div> - </div> -</fieldset> diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js deleted file mode 100644 index be2743d5b..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionCollectionView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); -var QualityDefinitionItemView = require('./QualityDefinitionItemView'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Settings/Quality/Definition/QualityDefinitionCollectionTemplate', - - itemViewContainer : '.x-rows', - - itemView : QualityDefinitionItemView -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js b/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js deleted file mode 100644 index d7531ab8d..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemView.js +++ /dev/null @@ -1,95 +0,0 @@ -var Marionette = require('marionette'); -var AsModelBoundView = require('../../../Mixins/AsModelBoundView'); -require('jquery-ui'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/Quality/Definition/QualityDefinitionItemViewTemplate', - className : 'row', - - slider : { - min : 0, - max : 10, - step : 0.01 - }, - - ui : { - sizeSlider : '.x-slider', - thirtyMinuteMinSize : '.x-min-thirty', - sixtyMinuteMinSize : '.x-min-sixty', - thirtyMinuteMaxSize : '.x-max-thirty', - sixtyMinuteMaxSize : '.x-max-sixty' - }, - - events : { - 'slide .x-slider' : '_updateSize' - }, - - initialize : function(options) { - this.profileCollection = options.profiles; - }, - - onRender : function() { - if (this.model.get('quality').id === 0) { - this.$el.addClass('row advanced-setting'); - } - - this.ui.sizeSlider.slider({ - range : true, - min : this.slider.min, - max : this.slider.max, - step : this.slider.step, - values : [ - this.model.get('minSize') || this.slider.min, - this.model.get('maxSize') || this.slider.max - ] - }); - - this._changeSize(); - }, - - _updateSize : function(event, ui) { - var minSize = ui.values[0]; - var maxSize = ui.values[1]; - - if (maxSize === this.slider.max) { - maxSize = null; - } - - this.model.set('minSize', minSize); - this.model.set('maxSize', maxSize); - - this._changeSize(); - }, - - _changeSize : function() { - var minSize = this.model.get('minSize') || this.slider.min; - var maxSize = this.model.get('maxSize') || null; - { - var minBytes = minSize * 1024 * 1024; - var minThirty = FormatHelpers.bytes(minBytes * 30, 2); - var minSixty = FormatHelpers.bytes(minBytes * 60, 2); - - this.ui.thirtyMinuteMinSize.html(minThirty); - this.ui.sixtyMinuteMinSize.html(minSixty); - } - - { - if (maxSize === 0 || maxSize === null) { - this.ui.thirtyMinuteMaxSize.html('Unlimited'); - this.ui.sixtyMinuteMaxSize.html('Unlimited'); - } else { - var maxBytes = maxSize * 1024 * 1024; - var maxThirty = FormatHelpers.bytes(maxBytes * 30, 2); - var maxSixty = FormatHelpers.bytes(maxBytes * 60, 2); - - this.ui.thirtyMinuteMaxSize.html(maxThirty); - this.ui.sixtyMinuteMaxSize.html(maxSixty); - } - } - } -}); - -view = AsModelBoundView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs b/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs deleted file mode 100644 index a0eaac784..000000000 --- a/src/UI/Settings/Quality/Definition/QualityDefinitionItemViewTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ - <span class="col-md-2 col-sm-3"> - {{quality.name}} - </span> - <span class="col-md-2 col-sm-3"> - <input type="text" class="form-control" name="title"> - </span> - <span class="col-md-4 col-sm-6"> - <div class="x-slider"></div> - <div class="size-label-wrapper"> - <div class="pull-left"> - <span class="label label-warning x-min-thirty" - name="thirtyMinuteMinSize" - title="Minimum size for a 30 minute album"> - </span> - <span class="label label-info x-min-sixty" - name="sixtyMinuteMinSize" - title="Minimum size for a 60 minute album"> - </span> - </div> - <div class="pull-right"> - <span class="label label-warning x-max-thirty" - name="thirtyMinuteMaxSize" - title="Maximum size for a 30 minute album"> - </span> - <span class="label label-info x-max-sixty" - name="sixtyMinuteMaxSize" - title="Maximum size for a 60 minute album"> - </span> - </div> - </div> - </span> \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayout.js b/src/UI/Settings/Quality/QualityLayout.js deleted file mode 100644 index e93ca1854..000000000 --- a/src/UI/Settings/Quality/QualityLayout.js +++ /dev/null @@ -1,21 +0,0 @@ -var Marionette = require('marionette'); -var QualityDefinitionCollection = require('../../Quality/QualityDefinitionCollection'); -var QualityDefinitionCollectionView = require('./Definition/QualityDefinitionCollectionView'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/Quality/QualityLayoutTemplate', - - regions : { - qualityDefinition : '#quality-definition' - }, - - initialize : function(options) { - this.settings = options.settings; - this.qualityDefinitionCollection = new QualityDefinitionCollection(); - this.qualityDefinitionCollection.fetch(); - }, - - onShow : function() { - this.qualityDefinition.show(new QualityDefinitionCollectionView({ collection : this.qualityDefinitionCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/Quality/QualityLayoutTemplate.hbs b/src/UI/Settings/Quality/QualityLayoutTemplate.hbs deleted file mode 100644 index a12f1926a..000000000 --- a/src/UI/Settings/Quality/QualityLayoutTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="quality-definition"/> -</div> diff --git a/src/UI/Settings/Quality/quality.less b/src/UI/Settings/Quality/quality.less deleted file mode 100644 index 23732cdfd..000000000 --- a/src/UI/Settings/Quality/quality.less +++ /dev/null @@ -1,135 +0,0 @@ -@import "../../Content/Bootstrap/mixins"; -@import (reference) "../../Content/icons"; -@import "../../Shared/Styles/clickable.less"; - -ul.qualities { - .user-select(none); - - min-height: 100px; - margin: 0; - padding: 0; - list-style-type: none; - outline: none; - width: 220px; - display: inline-block; - - li { - margin: 2px; - padding: 2px 4px; - line-height: 20px; - border: 1px solid #aaaaaa; - border-radius: 4px; /* may need vendor varients */ - background: #fafafa; - cursor: pointer; - - &.selected { - .select-handle { - opacity: 1.0; - cursor: pointer; - } - - .quality-label { - color: #444444; - } - - .select-handle { - .fa-icon-content(@fa-var-check-square-o); - } - } - - &:hover { - border-color: #888888; - background: #eeeeee; - - .drag-handle { - opacity: 1.0; - cursor: move; - } - } - - .quality-label { - color: #c6c6c6; - } - - .drag-handle, .select-handle { - opacity: 0.2; - line-height: 20px; - cursor: pointer; - } - - .select-handle { - .fa-icon-content(@fa-var-square-o); - - &:before { - display : inline-block; - width : 14px; - margin-top : 3px; - } - } - } -} - -.qualities-controls { - .help-inline { - vertical-align: top; - margin-top: 5px; - } -} - -#quality-definition-list { - - .quality-header .row { - font-weight: bold; - line-height: 40px; - } - - .rows .row { - line-height: 30px; - border-top: 1px solid #ddd; - vertical-align: middle; - padding: 5px; - - input { - margin-bottom: 0px; - } - - .size-label-wrapper { - line-height: 20px; - } - - .label { - min-width: 70px; - text-align: center; - margin: 0px 1px; - padding: 1px 4px; - } - - .ui-slider { - position: relative; - text-align: left; - background-color: #f5f5f5; - border-radius: 3px; - border: 1px solid #ccc; - height: 8px; - - .ui-slider-range { - position: absolute; - display: block; - background-color: #ddd; - height: 100%; - } - - .ui-slider-handle { - position: absolute; - z-index: 2; - width: 6px; - height: 12px; - cursor: default; - background-color: #ccc; - border: 1px solid #aaa; - border-radius: 3px; - top: -3px; - } - } - } -} diff --git a/src/UI/Settings/SettingsLayout.js b/src/UI/Settings/SettingsLayout.js deleted file mode 100644 index 429d702cd..000000000 --- a/src/UI/Settings/SettingsLayout.js +++ /dev/null @@ -1,252 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var GeneralSettingsModel = require('./General/GeneralSettingsModel'); -var NamingModel = require('./MediaManagement/Naming/NamingModel'); -var MediaManagementLayout = require('./MediaManagement/MediaManagementLayout'); -var MediaManagementSettingsModel = require('./MediaManagement/MediaManagementSettingsModel'); -var ProfileLayout = require('./Profile/ProfileLayout'); -var QualityLayout = require('./Quality/QualityLayout'); -var IndexerLayout = require('./Indexers/IndexerLayout'); -var IndexerCollection = require('./Indexers/IndexerCollection'); -var IndexerSettingsModel = require('./Indexers/IndexerSettingsModel'); -var DownloadClientLayout = require('./DownloadClient/DownloadClientLayout'); -var DownloadClientSettingsModel = require('./DownloadClient/DownloadClientSettingsModel'); -var NotificationCollectionView = require('./Notifications/NotificationCollectionView'); -var NotificationCollection = require('./Notifications/NotificationCollection'); -var MetadataLayout = require('./Metadata/MetadataLayout'); -var GeneralView = require('./General/GeneralView'); -var UiView = require('./UI/UiView'); -var UiSettingsModel = require('./UI/UiSettingsModel'); -var LoadingView = require('../Shared/LoadingView'); -var Config = require('../Config'); - -module.exports = Marionette.Layout.extend({ - template : 'Settings/SettingsLayoutTemplate', - - regions : { - mediaManagement : '#media-management', - profiles : '#profiles', - quality : '#quality', - indexers : '#indexers', - downloadClient : '#download-client', - notifications : '#notifications', - metadata : '#metadata', - general : '#general', - uiRegion : '#ui', - loading : '#loading-region' - }, - - ui : { - mediaManagementTab : '.x-media-management-tab', - profilesTab : '.x-profiles-tab', - qualityTab : '.x-quality-tab', - indexersTab : '.x-indexers-tab', - downloadClientTab : '.x-download-client-tab', - notificationsTab : '.x-notifications-tab', - metadataTab : '.x-metadata-tab', - generalTab : '.x-general-tab', - uiTab : '.x-ui-tab', - advancedSettings : '.x-advanced-settings' - }, - - events : { - 'click .x-media-management-tab' : '_showMediaManagement', - 'click .x-profiles-tab' : '_showProfiles', - 'click .x-quality-tab' : '_showQuality', - 'click .x-indexers-tab' : '_showIndexers', - 'click .x-download-client-tab' : '_showDownloadClient', - 'click .x-notifications-tab' : '_showNotifications', - 'click .x-metadata-tab' : '_showMetadata', - 'click .x-general-tab' : '_showGeneral', - 'click .x-ui-tab' : '_showUi', - 'click .x-save-settings' : '_save', - 'change .x-advanced-settings' : '_toggleAdvancedSettings' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - - this.listenTo(vent, vent.Hotkeys.SaveSettings, this._save); - }, - - onRender : function() { - this.loading.show(new LoadingView()); - var self = this; - - this.mediaManagementSettings = new MediaManagementSettingsModel(); - this.namingSettings = new NamingModel(); - this.indexerSettings = new IndexerSettingsModel(); - this.downloadClientSettings = new DownloadClientSettingsModel(); - this.notificationCollection = new NotificationCollection(); - this.generalSettings = new GeneralSettingsModel(); - this.uiSettings = new UiSettingsModel(); - Backbone.$.when(this.mediaManagementSettings.fetch(), this.namingSettings.fetch(), this.indexerSettings.fetch(), this.downloadClientSettings.fetch(), - this.notificationCollection.fetch(), this.generalSettings.fetch(), this.uiSettings.fetch()).done(function() { - if (!self.isClosed) { - self.loading.$el.hide(); - self.mediaManagement.show(new MediaManagementLayout({ - settings : self.mediaManagementSettings, - namingSettings : self.namingSettings - })); - self.profiles.show(new ProfileLayout()); - self.quality.show(new QualityLayout()); - self.indexers.show(new IndexerLayout({ model : self.indexerSettings })); - self.downloadClient.show(new DownloadClientLayout({ model : self.downloadClientSettings })); - self.notifications.show(new NotificationCollectionView({ collection : self.notificationCollection })); - self.metadata.show(new MetadataLayout()); - self.general.show(new GeneralView({ model : self.generalSettings })); - self.uiRegion.show(new UiView({ model : self.uiSettings })); - } - }); - - this._setAdvancedSettingsState(); - }, - - onShow : function() { - switch (this.action) { - case 'profiles': - this._showProfiles(); - break; - case 'quality': - this._showQuality(); - break; - case 'indexers': - this._showIndexers(); - break; - case 'downloadclient': - this._showDownloadClient(); - break; - case 'connect': - this._showNotifications(); - break; - case 'notifications': - this._showNotifications(); - break; - case 'metadata': - this._showMetadata(); - break; - case 'general': - this._showGeneral(); - break; - case 'ui': - this._showUi(); - break; - default: - this._showMediaManagement(); - } - }, - - _showMediaManagement : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.mediaManagementTab.tab('show'); - this._navigate('settings/mediamanagement'); - }, - - _showProfiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.profilesTab.tab('show'); - this._navigate('settings/profiles'); - }, - - _showQuality : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.qualityTab.tab('show'); - this._navigate('settings/quality'); - }, - - _showIndexers : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.indexersTab.tab('show'); - this._navigate('settings/indexers'); - }, - - _showDownloadClient : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.downloadClientTab.tab('show'); - this._navigate('settings/downloadclient'); - }, - - _showNotifications : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.notificationsTab.tab('show'); - this._navigate('settings/connect'); - }, - - _showMetadata : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.metadataTab.tab('show'); - this._navigate('settings/metadata'); - }, - - _showGeneral : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.generalTab.tab('show'); - this._navigate('settings/general'); - }, - - _showUi : function(e) { - if (e) { - e.preventDefault(); - } - this.ui.uiTab.tab('show'); - this._navigate('settings/ui'); - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _save : function() { - vent.trigger(vent.Commands.SaveSettings); - }, - - _setAdvancedSettingsState : function() { - var checked = Config.getValueBoolean(Config.Keys.AdvancedSettings); - this.ui.advancedSettings.prop('checked', checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } - }, - - _toggleAdvancedSettings : function() { - var checked = this.ui.advancedSettings.prop('checked'); - Config.setValue(Config.Keys.AdvancedSettings, checked); - - if (checked) { - $('body').addClass('show-advanced-settings'); - } else { - $('body').removeClass('show-advanced-settings'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Settings/SettingsLayoutTemplate.hbs b/src/UI/Settings/SettingsLayoutTemplate.hbs deleted file mode 100644 index c69ba9f16..000000000 --- a/src/UI/Settings/SettingsLayoutTemplate.hbs +++ /dev/null @@ -1,49 +0,0 @@ -<ul class="nav nav-tabs nav-justified settings-tabs"> - <li><a href="#media-management" class="x-media-management-tab no-router">Media Management</a></li> - <li><a href="#profiles" class="x-profiles-tab no-router">Profiles</a></li> - <li><a href="#quality" class="x-quality-tab no-router">Quality</a></li> - <li><a href="#indexers" class="x-indexers-tab no-router">Indexers</a></li> - <li><a href="#download-client" class="x-download-client-tab no-router">Download Client</a></li> - <li><a href="#notifications" class="x-notifications-tab no-router">Connect</a></li> - <li><a href="#metadata" class="x-metadata-tab no-router">Metadata</a></li> - <li><a href="#general" class="x-general-tab no-router">General</a></li> - <li><a href="#ui" class="x-ui-tab no-router">UI</a></li> -</ul> - -<div class="row settings-controls"> - <div class="col-sm-4 col-sm-offset-7 col-md-3 col-md-offset-8"> - <div class="advanced-settings-toggle"> - <span class="help-inline-checkbox hidden-xs"> - Advanced Settings - </span> - <label class="checkbox toggle well"> - <input type="checkbox" class="x-advanced-settings"/> - <p> - <span>Shown</span> - <span>Hidden</span> - </p> - <div class="btn btn-warning slide-button"/> - </label> - <span class="help-inline-checkbox hidden-sm hidden-md hidden-lg"> - Advanced Settings - </span> - </div> - </div> - <div class="col-sm-1 col-md-1"> - <button class="btn btn-primary x-save-settings">Save</button> - </div> -</div> - -<div class="tab-content"> - <div class="tab-pane" id="media-management"></div> - <div class="tab-pane" id="profiles"></div> - <div class="tab-pane" id="quality"></div> - <div class="tab-pane" id="indexers"></div> - <div class="tab-pane" id="download-client"></div> - <div class="tab-pane" id="notifications"></div> - <div class="tab-pane" id="metadata"></div> - <div class="tab-pane" id="general"></div> - <div class="tab-pane" id="ui"></div> -</div> - -<div id="loading-region"></div> \ No newline at end of file diff --git a/src/UI/Settings/SettingsModelBase.js b/src/UI/Settings/SettingsModelBase.js deleted file mode 100644 index f08773f91..000000000 --- a/src/UI/Settings/SettingsModelBase.js +++ /dev/null @@ -1,34 +0,0 @@ -var vent = require('vent'); -var DeepModel = require('backbone.deepmodel'); -var AsChangeTrackingModel = require('../Mixins/AsChangeTrackingModel'); -var Messenger = require('../Shared/Messenger'); - -var model = DeepModel.extend({ - - initialize : function() { - this.listenTo(vent, vent.Commands.SaveSettings, this.saveSettings); - this.listenTo(this, 'destroy', this._stopListening); - }, - - saveSettings : function() { - if (!this.isSaved) { - var savePromise = this.save(); - - Messenger.monitor({ - promise : savePromise, - successMessage : this.successMessage, - errorMessage : this.errorMessage - }); - - return savePromise; - } - - return undefined; - }, - - _stopListening : function() { - this.stopListening(vent, vent.Commands.SaveSettings); - } -}); - -module.exports = AsChangeTrackingModel.call(model); diff --git a/src/UI/Settings/ThingyAddCollectionView.js b/src/UI/Settings/ThingyAddCollectionView.js deleted file mode 100644 index ecce0dd7b..000000000 --- a/src/UI/Settings/ThingyAddCollectionView.js +++ /dev/null @@ -1,13 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - itemViewOptions : function() { - return { - targetCollection : this.targetCollection || this.options.targetCollection - }; - }, - - initialize : function(options) { - this.targetCollection = options.targetCollection; - } -}); \ No newline at end of file diff --git a/src/UI/Settings/ThingyHeaderGroupView.js b/src/UI/Settings/ThingyHeaderGroupView.js deleted file mode 100644 index 0f7e9a2f8..000000000 --- a/src/UI/Settings/ThingyHeaderGroupView.js +++ /dev/null @@ -1,18 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - itemViewContainer : '.item-list', - template : 'Settings/ThingyHeaderGroupViewTemplate', - tagName : 'div', - - itemViewOptions : function() { - return { - targetCollection : this.targetCollection || this.options.targetCollection - }; - }, - - initialize : function() { - this.collection = new Backbone.Collection(this.model.get('collection')); - } -}); \ No newline at end of file diff --git a/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs b/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs deleted file mode 100644 index 310a29241..000000000 --- a/src/UI/Settings/ThingyHeaderGroupViewTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<legend>{{header}}</legend> -<ul class="item-list" /> \ No newline at end of file diff --git a/src/UI/Settings/UI/UiSettingsModel.js b/src/UI/Settings/UI/UiSettingsModel.js deleted file mode 100644 index baf6a5297..000000000 --- a/src/UI/Settings/UI/UiSettingsModel.js +++ /dev/null @@ -1,7 +0,0 @@ -var SettingsModelBase = require('../SettingsModelBase'); - -module.exports = SettingsModelBase.extend({ - url : window.NzbDrone.ApiRoot + '/config/ui', - successMessage : 'UI settings saved', - errorMessage : 'Failed to save UI settings' -}); \ No newline at end of file diff --git a/src/UI/Settings/UI/UiView.js b/src/UI/Settings/UI/UiView.js deleted file mode 100644 index 5e8664036..000000000 --- a/src/UI/Settings/UI/UiView.js +++ /dev/null @@ -1,22 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var UiSettingsModel = require('../../Shared/UiSettingsModel'); -var AsModelBoundView = require('../../Mixins/AsModelBoundView'); -var AsValidatedView = require('../../Mixins/AsValidatedView'); - -var view = Marionette.ItemView.extend({ - template : 'Settings/UI/UiViewTemplate', - - initialize : function() { - this.listenTo(this.model, 'sync', this._reloadUiSettings); - }, - - _reloadUiSettings : function() { - UiSettingsModel.fetch(); - } -}); - -AsModelBoundView.call(view); -AsValidatedView.call(view); - -module.exports = view; \ No newline at end of file diff --git a/src/UI/Settings/UI/UiViewTemplate.hbs b/src/UI/Settings/UI/UiViewTemplate.hbs deleted file mode 100644 index 4b5228870..000000000 --- a/src/UI/Settings/UI/UiViewTemplate.hbs +++ /dev/null @@ -1,124 +0,0 @@ -<div class="form-horizontal"> - <fieldset> - <legend>Calendar</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">First Day of Week</label> - - <div class="col-sm-4"> - <select name="firstDayOfWeek" class="form-control"> - <option value="0">Sunday</option> - <option value="1">Monday</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Week Column Header</label> - - <div class="col-sm-1 col-sm-push-4 help-inline"> - <i class="icon-lidarr-form-info" title="Shown above each column when week is the active view"/> - </div> - - <div class="col-sm-4 col-sm-pull-1"> - <select name="calendarWeekColumnHeader" class="form-control"> - <option value="ddd M/D">Tue 3/5</option> - <option value="ddd MM/DD">Tue 03/05</option> - <option value="ddd D/M">Tue 5/3</option> - <option value="ddd DD/MM">Tue 05/03</option> - </select> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Dates</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Short Date Format</label> - - <div class="col-sm-4"> - <select name="shortDateFormat" class="form-control"> - <option value="MMM D YYYY">Mar 5 2014</option> - <option value="DD MMM YYYY">05 Mar 2014</option> - <option value="MM/D/YYYY">03/5/2014</option> - <option value="MM/DD/YYYY">03/05/2014</option> - <option value="DD/MM/YYYY">05/03/2014</option> - <option value="YYYY-MM-DD">2014-03-05</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Long Date Format</label> - - <div class="col-sm-4"> - <select name="longDateFormat" class="form-control"> - <option value="dddd, MMMM D YYYY">Tuesday, March 5, 2014</option> - <option value="dddd, D MMMM YYYY">Tuesday, 5 March, 2014</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Time Format</label> - - <div class="col-sm-4"> - <select name="timeFormat" class="form-control"> - <option value="h(:mm)a">5pm/5:30pm</option> - <option value="HH:mm">17:00/17:30</option> - </select> - </div> - </div> - - <div class="form-group"> - <label class="col-sm-3 control-label">Show Relative Dates</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="showRelativeDates"/> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Show relative (Today/Yesterday/etc) or absolute dates"/> - </span> - </div> - </div> - </div> - </fieldset> - - <fieldset> - <legend>Style</legend> - - <div class="form-group"> - <label class="col-sm-3 control-label">Enable Color-Impaired mode</label> - - <div class="col-sm-8"> - <div class="input-group"> - <label class="checkbox toggle well"> - <input type="checkbox" name="enableColorImpairedMode" /> - - <p> - <span>Yes</span> - <span>No</span> - </p> - - <div class="btn btn-primary slide-button"/> - </label> - - <span class="help-inline-checkbox"> - <i class="icon-lidarr-form-info" title="Altered style to allow color-impaired users to better distinguish color coded information"/> - </span> - </div> - </div> - </div> - </fieldset> -</div> diff --git a/src/UI/Settings/settings.less b/src/UI/Settings/settings.less deleted file mode 100644 index ec6bd2a1c..000000000 --- a/src/UI/Settings/settings.less +++ /dev/null @@ -1,161 +0,0 @@ -@import "../Content/Bootstrap/variables"; -@import "../Shared/Styles/clickable.less"; -@import "Indexers/indexers"; -@import "Quality/quality"; -@import "Profile/profile"; -@import "Notifications/notifications"; -@import "Metadata/metadata"; -@import "DownloadClient/downloadclient"; -@import "thingy"; - -li.save-and-add { - .clickable; - - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 20px; - color: rgb(51, 51, 51); - white-space: nowrap; -} - -li.save-and-add:hover { - text-decoration: none; - color: rgb(255, 255, 255); - background-color: rgb(0, 129, 194); -} - -.add-card { - .clickable; - color: #adadad; - font-size: 50px; - text-align: center; - background-color: #f5f5f5; - - .center { - display: inline-block; - padding: 5px 20px 0px; - background-color: white; - } - - i { - .clickable; - } -} - -.naming-example { - display: inline-block; - margin-top: 5px; -} - -.naming-format { - width: 500px; -} - -.settings-controls { - margin-top: 10px; -} - -.advanced-settings-toggle { - display: inline-block; - margin-bottom: 10px; - - .checkbox { - width : 100px; - margin-left : 0px; - display : inline-block; - padding-top : 0px; - margin-bottom : -10px; - margin-top : -1px; - } - - .help-inline-checkbox { - display : inline-block; - margin-top : -3px; - margin-bottom : 0; - vertical-align : middle; - } -} - -.advanced-setting { - display: none; - - .control-label { - color: @brand-warning; - } -} - -.basic-setting { - display: block; -} - -.show-advanced-settings { - .advanced-setting { - display: block; - } - - .basic-setting { - display: none; - } -} - -.api-key { - - input { - width : 280px; - cursor : text; - } -} - -.settings-tabs { - li>a { - padding : 10px; - } - - @media (min-width: @screen-sm-min) and (max-width: @screen-md-max) { - li { - a { - white-space : nowrap; - padding : 10px; - } - } - } -} - -.indicator { - display : none; - padding-right : 5px; -} - -.add-rule-setting-mapping { - cursor : pointer; - font-size : 14px; - text-align : center; - display : inline-block; - padding : 2px 6px; - - i { - cursor : pointer; - } -} - -.rule-setting-list { - - .rule-setting-header .row { - font-weight : bold; - line-height : 40px; - } - - .rows .row { - line-height : 30px; - border-top : 1px solid #ddd; - vertical-align : middle; - padding : 5px; - - i { - cursor : pointer; - margin-left : 5px; - } - } -} diff --git a/src/UI/Settings/thingy.less b/src/UI/Settings/thingy.less deleted file mode 100644 index 2368240b7..000000000 --- a/src/UI/Settings/thingy.less +++ /dev/null @@ -1,65 +0,0 @@ -@import "../Shared/Styles/card"; -@import "../Shared/Styles/clickable"; - -.add-thingy { - .card; - cursor: pointer; - font-size: 24px; - font-weight: lighter; - text-align: center; - height: 85px; -} - -.add-thingies { - text-align: center; - - legend { - text-align: left; - text-transform: capitalize; - } - - ul.items { - list-style-type: none; - margin: 0px; - padding: 0px; - - li.add-thingy-item { - display: inline-block; - vertical-align: top; - } - } -} - -.thingy { - - .card; - - h3 { - margin-top: 0px; - display: inline-block; - white-space: nowrap; - overflow: hidden; - line-height: 30px; - text-overflow: ellipsis; - text-transform: none; - } - - .btn-group { - margin-top: 8px; - } - - .settings { - margin-top: 5px; - } -} - -.thingies { - li { - display: inline-block; - vertical-align: top; - } - - @media (max-width: @screen-xs-max) { - padding-left: 0px; - } -} \ No newline at end of file diff --git a/src/UI/Shared/ApiData.js b/src/UI/Shared/ApiData.js deleted file mode 100644 index 6d8e62043..000000000 --- a/src/UI/Shared/ApiData.js +++ /dev/null @@ -1,17 +0,0 @@ -var $ = require('jquery'); - -module.exports = { - get : function(resource) { - var url = window.NzbDrone.ApiRoot + '/' + resource; - var _data; - $.ajax({ - url : url, - async : false - }).done(function(data) { - _data = data; - }).error(function(xhr, status, error) { - throw error; - }); - return _data; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/ControlPanel/ControlPanelController.js b/src/UI/Shared/ControlPanel/ControlPanelController.js deleted file mode 100644 index c2a31c3cc..000000000 --- a/src/UI/Shared/ControlPanel/ControlPanelController.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - vent.on(vent.Commands.OpenControlPanelCommand, this._openControlPanel, this); - vent.on(vent.Commands.CloseControlPanelCommand, this._closeControlPanel, this); - }, - - _openControlPanel : function(view) { - AppLayout.controlPanelRegion.show(view); - }, - - _closeControlPanel : function() { - AppLayout.controlPanelRegion.closePanel(); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/ControlPanel/ControlPanelRegion.js b/src/UI/Shared/ControlPanel/ControlPanelRegion.js deleted file mode 100644 index e32c02552..000000000 --- a/src/UI/Shared/ControlPanel/ControlPanelRegion.js +++ /dev/null @@ -1,41 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var region = Marionette.Region.extend({ - el : '#control-panel-region', - - constructor : function() { - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showPanel, this); - }, - - getEl : function(selector) { - var $el = $(selector); - - return $el; - }, - - showPanel : function() { - $('body').addClass('control-panel-visible'); - this.$el.animate({ - 'margin-bottom' : 0, - 'opacity' : 1 - }, { - queue : false, - duration : 300 - }); - }, - - closePanel : function() { - $('body').removeClass('control-panel-visible'); - this.$el.animate({ - 'margin-bottom' : -100, - 'opacity' : 0 - }, { - queue : false, - duration : 300 - }); - this.reset(); - } -}); -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/EmptyView.js b/src/UI/Shared/FileBrowser/EmptyView.js deleted file mode 100644 index 3bd8ddc93..000000000 --- a/src/UI/Shared/FileBrowser/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.CompositeView.extend({ - template : 'Shared/FileBrowser/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs b/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs deleted file mode 100644 index 53469ac16..000000000 --- a/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="text-center col-md-12 file-browser-empty"> - <span>No files/folders were found, edit the path above, or clear to start again</span> -</div> diff --git a/src/UI/Shared/FileBrowser/FileBrowserCollection.js b/src/UI/Shared/FileBrowser/FileBrowserCollection.js deleted file mode 100644 index d2771b15d..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserCollection.js +++ /dev/null @@ -1,28 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var FileBrowserModel = require('./FileBrowserModel'); - -module.exports = Backbone.Collection.extend({ - model : FileBrowserModel, - url : window.NzbDrone.ApiRoot + '/filesystem', - - parse : function(response) { - var contents = []; - if (response.parent || response.parent === '') { - var type = 'parent'; - var name = '...'; - if (response.parent === '') { - type = 'computer'; - name = 'My Computer'; - } - contents.push({ - type : type, - name : name, - path : response.parent - }); - } - $.merge(contents, response.directories); - $.merge(contents, response.files); - return contents; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayout.js b/src/UI/Shared/FileBrowser/FileBrowserLayout.js deleted file mode 100644 index 82ae8b32b..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserLayout.js +++ /dev/null @@ -1,162 +0,0 @@ -var _ = require('underscore'); -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FileBrowserCollection = require('./FileBrowserCollection'); -var EmptyView = require('./EmptyView'); -var FileBrowserRow = require('./FileBrowserRow'); -var FileBrowserTypeCell = require('./FileBrowserTypeCell'); -var FileBrowserNameCell = require('./FileBrowserNameCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var FileSizeCell = require('../../Cells/FileSizeCell'); -var LoadingView = require('../LoadingView'); -require('../../Mixins/DirectoryAutoComplete'); - -module.exports = Marionette.Layout.extend({ - template : 'Shared/FileBrowser/FileBrowserLayoutTemplate', - - regions : { - browser : '#x-browser' - }, - - ui : { - path : '.x-path', - indicator : '.x-indicator' - }, - - events : { - 'typeahead:selected .x-path' : '_pathChanged', - 'typeahead:autocompleted .x-path' : '_pathChanged', - 'keyup .x-path' : '_inputChanged', - 'click .x-ok' : '_selectPath' - }, - - initialize : function(options) { - this.collection = new FileBrowserCollection(); - this.collection.showFiles = options.showFiles || false; - this.collection.showLastModified = options.showLastModified || false; - this.input = options.input; - this._setColumns(); - this.listenTo(this.collection, 'sync', this._showGrid); - this.listenTo(this.collection, 'filebrowser:row:folderselected', this._rowSelected); - this.listenTo(this.collection, 'filebrowser:row:fileselected', this._fileSelected); - }, - - onRender : function() { - this.browser.show(new LoadingView()); - this.ui.path.directoryAutoComplete(); - this._fetchCollection(this.input.val()); - this._updatePath(this.input.val()); - }, - - _setColumns : function() { - this.columns = [ - { - name : 'type', - label : '', - sortable : false, - cell : FileBrowserTypeCell - }, - { - name : 'name', - label : 'Name', - sortable : false, - cell : FileBrowserNameCell - } - ]; - if (this.collection.showLastModified) { - this.columns.push({ - name : 'lastModified', - label : 'Last Modified', - sortable : false, - cell : RelativeDateCell - }); - } - if (this.collection.showFiles) { - this.columns.push({ - name : 'size', - label : 'Size', - sortable : false, - cell : FileSizeCell - }); - } - }, - - _fetchCollection : function(path) { - this.ui.indicator.show(); - var data = { includeFiles : this.collection.showFiles }; - if (path) { - data.path = path; - } - this.collection.fetch({ data : data }); - }, - - _showGrid : function() { - this.ui.indicator.hide(); - if (this.collection.models.length === 0) { - this.browser.show(new EmptyView()); - return; - } - var grid = new Backgrid.Grid({ - row : FileBrowserRow, - collection : this.collection, - columns : this.columns, - className : 'table table-hover' - }); - this.browser.show(grid); - }, - - _rowSelected : function(model) { - var path = model.get('path'); - - this._updatePath(path); - this._fetchCollection(path); - }, - - _fileSelected : function(model) { - var path = model.get('path'); - var type = model.get('type'); - - this.input.val(path); - this.input.trigger('change'); - - this.input.trigger('filebrowser:fileselected', { - type : type, - path : path - }); - - vent.trigger(vent.Commands.CloseFileBrowser); - }, - - _pathChanged : function(e, path) { - this._fetchCollection(path.value); - this._updatePath(path.value); - }, - - _inputChanged : function() { - var path = this.ui.path.val(); - if (path === '' || path.endsWith('\\') || path.endsWith('/')) { - this._fetchCollection(path); - } - }, - - _updatePath : function(path) { - if (path !== undefined || path !== null) { - this.ui.path.val(path); - } - }, - - _selectPath : function() { - var path = this.ui.path.val(); - - this.input.val(path); - this.input.trigger('change'); - - this.input.trigger('filebrowser:folderselected', { - type: 'folder', - path: path - }); - - vent.trigger(vent.Commands.CloseFileBrowser); - } -}); diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs b/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs deleted file mode 100644 index 3c09e7ecc..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs +++ /dev/null @@ -1,26 +0,0 @@ -<div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button> - <h3>File Browser</h3> -</div> - <div class="modal-body"> - <div class="row"> - <div class="col-sm-12"> - <div class="form-group"> - <input type="text" class="form-control x-path" placeholder="Start typing or select a path below"/> - </div> - </div> - </div> - - <div class="row"> - <div class="col-sm-12"> - <div id="x-browser"></div> - </div> - </div> - </div> - <div class="modal-footer"> - <span class="indicator x-indicator"><i class="icon-lidarr-spinner fa-spin"></i></span> - <button class="btn" data-dismiss="modal">Close</button> - <button class="btn btn-primary x-ok">Ok</button> - </div> -</div> diff --git a/src/UI/Shared/FileBrowser/FileBrowserModel.js b/src/UI/Shared/FileBrowser/FileBrowserModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserNameCell.js b/src/UI/Shared/FileBrowser/FileBrowserNameCell.js deleted file mode 100644 index 90cb704be..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserNameCell.js +++ /dev/null @@ -1,18 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'file-browser-name-cell', - - render : function() { - this.$el.empty(); - - var name = this.model.get(this.column.get('name')); - - this.$el.html(name); - - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserRow.js b/src/UI/Shared/FileBrowser/FileBrowserRow.js deleted file mode 100644 index af982cf72..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserRow.js +++ /dev/null @@ -1,24 +0,0 @@ -var _ = require('underscore'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'file-browser-row', - - events : { - 'click' : '_selectRow' - }, - - _originalInit : Backgrid.Row.prototype.initialize, - - initialize : function() { - this._originalInit.apply(this, arguments); - }, - - _selectRow : function() { - if (this.model.get('type') === 'file') { - this.model.collection.trigger('filebrowser:row:fileselected', this.model); - } else { - this.model.collection.trigger('filebrowser:row:folderselected', this.model); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js b/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js deleted file mode 100644 index 81c31efcb..000000000 --- a/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js +++ /dev/null @@ -1,28 +0,0 @@ -var vent = require('vent'); -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'file-browser-type-cell', - - render : function() { - this.$el.empty(); - - var type = this.model.get(this.column.get('name')); - var icon = 'icon-lidarr-hdd'; - - if (type === 'computer') { - icon = 'icon-lidarr-browser-computer'; - } else if (type === 'parent') { - icon = 'icon-lidarr-browser-up'; - } else if (type === 'folder') { - icon = 'icon-lidarr-browser-folder'; - } else if (type === 'file') { - icon = 'icon-lidarr-browser-file'; - } - - this.$el.html('<i class="{0}"></i>'.format(icon)); - this.delegateEvents(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/filebrowser.less b/src/UI/Shared/FileBrowser/filebrowser.less deleted file mode 100644 index c8810147b..000000000 --- a/src/UI/Shared/FileBrowser/filebrowser.less +++ /dev/null @@ -1,24 +0,0 @@ -.file-browser-row { - cursor : pointer; - - .file-size-cell { - white-space : nowrap; - } - - .relative-date-cell { - width : 120px; - white-space : nowrap; - } -} - -.file-browser-type-cell { - width : 16px; -} - -.file-browser-name-cell { - word-break : break-all; -} - -.file-browser-empty { - margin-top : 20px; -} \ No newline at end of file diff --git a/src/UI/Shared/FormatHelpers.js b/src/UI/Shared/FormatHelpers.js deleted file mode 100644 index cf81a0702..000000000 --- a/src/UI/Shared/FormatHelpers.js +++ /dev/null @@ -1,93 +0,0 @@ -var moment = require('moment'); -var filesize = require('filesize'); -var UiSettings = require('./UiSettingsModel'); - -module.exports = { - bytes : function(sourceSize, sourceRounding) { - var size = Number(sourceSize); - var rounding = Number(sourceRounding); - - if (isNaN(size)) { - return ''; - } - - if (isNaN(rounding)) { - rounding = 1; - } - - return filesize(size, { - base : 2, - round : rounding - }); - }, - - relativeDate : function(sourceDate) { - if (!sourceDate) { - return ''; - } - - var date = moment(sourceDate); - var calendarDate = date.calendar(); - - //TODO: It would be nice to not have to hack this... - var strippedCalendarDate = calendarDate.substring(0, calendarDate.indexOf(' at ')); - - if (strippedCalendarDate) { - return strippedCalendarDate; - } - - if (date.isAfter(moment())) { - return 'in ' + date.fromNow(true); - } - - if (date.isBefore(moment().add('years', -1))) { - return date.format(UiSettings.get('shortDateFormat')); - } - - return date.fromNow(); - }, - - pad : function(n, width, z) { - z = z || '0'; - n = n + ''; - return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; - }, - - number : function(input) { - if (!input) { - return '0'; - } - - return input.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); - }, - - plural : function(input, unit) { - if (input === 1) { - return unit; - } - - return unit + 's'; - }, - - timeMinSec : function (s, format) { - - function pad(n, z) { - z = z || 2; - return ('00' + n).slice(-z); - } - - var ms = s % 1000; - s = (s - ms) / 1000; - var secs = s % 60; - s = (s - secs) / 60; - var mins = s; - - if (format === 'ms') { - return pad(mins) + ':' + pad(secs); - } else { - return Math.round(mins,0); - } - }, - - -}; \ No newline at end of file diff --git a/src/UI/Shared/Grid/HeaderCell.js b/src/UI/Shared/Grid/HeaderCell.js deleted file mode 100644 index 73a906c49..000000000 --- a/src/UI/Shared/Grid/HeaderCell.js +++ /dev/null @@ -1,155 +0,0 @@ -module.exports = function() { - var Backgrid = this; - - Backgrid.LidarrHeaderCell = Backgrid.HeaderCell.extend({ - events : { - 'click' : 'onClick' - }, - - _originalInit : Backgrid.HeaderCell.prototype.initialize, - - initialize : function(options) { - this._originalInit.call(this, options); - - this.listenTo(this.collection, 'drone:sort', this.render); - }, - - render : function() { - this.$el.empty(); - this.$el.append(this.column.get('label')); - if (this.column.get('tooltip')) { - this.$el.attr({ - 'title' : this.column.get('tooltip'), - 'data-container' : '.table' - }); - } - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, this.collection); - - if (sortable) { - this.$el.addClass('sortable'); - this.$el.prepend(' <i class="sort-direction-icon"></i>'); - } - - //Do we need this? - this.$el.addClass(column.get('name')); - - if (column.has('className')) { - this.$el.addClass(column.get('className')); - } - - this.delegateEvents(); - this.direction(column.get('direction')); - - if (this.collection.state) { - var name = this._getSortMapping().name; - var order = this.collection.state.order; - - if (name === column.get('name')) { - this._setSortIcon(order); - } else { - this._removeSortIcon(); - } - } - - return this; - }, - - direction : function(dir) { - this.$el.children('i.sort-direction-icon').removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - - if (arguments.length) { - if (dir) { - this._setSortIcon(dir); - } - - this.column.set('direction', dir); - } - - var columnDirection = this.column.get('direction'); - - if (!columnDirection && this.collection.state) { - var name = this._getSortMapping().name; - var order = this.collection.state.order; - - if (name === this.column.get('name')) { - columnDirection = order; - } - } - - return columnDirection; - }, - - _getSortMapping : function() { - var sortKey = this.collection.state.sortKey; - - if (this.collection._getSortMapping) { - return this.collection._getSortMapping(sortKey); - } - - return { - name : sortKey, - sortKey : sortKey - }; - }, - - onClick : function(e) { - e.preventDefault(); - - var collection = this.collection; - var event = 'backgrid:sort'; - - var column = this.column; - var sortable = Backgrid.callByNeed(column.sortable(), column, collection); - if (sortable) { - var isSorted = this.$el.children('.icon-lidarr-sort-asc,.icon-lidarr-sort-desc').length !== 0; - var direction = collection.state.order; - if (column.get('sortType') === 'fixed' || !isSorted) { - direction = column.get('direction') || 'ascending'; - } else { - if (direction === 'ascending' || direction === -1) { - direction = 'descending'; - } else { - direction = 'ascending'; - } - } - - if (collection.setSorting) { - collection.setSorting(column.get('name'), direction); - } else { - collection.state.sortKey = column.get('name'); - collection.state.order = direction; - } - collection.trigger(event, column, direction); - } - }, - - _resetCellDirection : function(columnToSort, direction) { - if (columnToSort !== this.column) { - this.direction(null); - } else { - this.direction(direction); - } - }, - - _convertDirectionToIcon : function(dir) { - if (dir === 'ascending' || dir === -1) { - return 'icon-lidarr-sort-asc'; - } - - return 'icon-lidarr-sort-desc'; - }, - - _setSortIcon : function(dir) { - this._removeSortIcon(); - this.$el.children('i.sort-direction-icon').addClass(this._convertDirectionToIcon(dir)); - }, - - _removeSortIcon : function() { - this.$el.children('i.sort-direction-icon').removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - } - }); - - return Backgrid.LidarrHeaderCell; -}; diff --git a/src/UI/Shared/Grid/JumpToPageTemplate.hbs b/src/UI/Shared/Grid/JumpToPageTemplate.hbs deleted file mode 100644 index 9a157ece6..000000000 --- a/src/UI/Shared/Grid/JumpToPageTemplate.hbs +++ /dev/null @@ -1,9 +0,0 @@ -<select class="x-page-select"> - {{#each pages}} - {{#if current}} - <option value="{{page}}" selected="selected">{{page}}</option> - {{else}} - <option value="{{page}}">{{page}}</option> - {{/if}} - {{/each}} -</select> \ No newline at end of file diff --git a/src/UI/Shared/Grid/Pager.js b/src/UI/Shared/Grid/Pager.js deleted file mode 100644 index 36cda08f7..000000000 --- a/src/UI/Shared/Grid/Pager.js +++ /dev/null @@ -1,188 +0,0 @@ -var $ = require('jquery'); -var Marionette = require('marionette'); -var Paginator = require('backgrid.paginator'); - -module.exports = Paginator.extend({ - template : 'Shared/Grid/PagerTemplate', - - events : { - 'click .pager-btn' : 'changePage', - 'click .x-page-number' : '_showPageJumper', - 'change .x-page-select' : '_jumpToPage', - 'blur .x-page-select' : 'render' - }, - - windowSize : 1, - - fastForwardHandleLabels : { - first : 'icon-lidarr-pager-first', - prev : 'icon-lidarr-pager-previous', - next : 'icon-lidarr-pager-next', - last : 'icon-lidarr-pager-last' - }, - - changePage : function(e) { - e.preventDefault(); - - var target = this.$(e.target); - - if (target.closest('li').hasClass('disabled')) { - return; - } - - var icon = target.closest('li i'); - var iconClasses = icon.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/); - var iconClass = $.trim(iconClasses[0]); - - icon.removeClass(iconClass); - icon.addClass('icon-lidarr-spinner fa-spin'); - - var label = target.attr('data-action'); - var ffLabels = this.fastForwardHandleLabels; - - var collection = this.collection; - - if (ffLabels) { - switch (label) { - case 'first': - collection.getFirstPage(); - return; - case 'prev': - if (collection.hasPrevious()) { - collection.getPreviousPage(); - } - return; - case 'next': - if (collection.hasNext()) { - collection.getNextPage(); - } - return; - case 'last': - collection.getLastPage(); - return; - } - } - - var state = collection.state; - var pageIndex = target.text(); - collection.getPage(state.firstPage === 0 ? pageIndex - 1 : pageIndex); - }, - - makeHandles : function() { - var handles = []; - - var collection = this.collection; - var state = collection.state; - - // convert all indices to 0-based here - var firstPage = state.firstPage; - var lastPage = +state.lastPage; - lastPage = Math.max(0, firstPage ? lastPage - 1 : lastPage); - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = firstPage ? currentPage - 1 : currentPage; - var windowStart = Math.floor(currentPage / this.windowSize) * this.windowSize; - var windowEnd = Math.min(lastPage + 1, windowStart + this.windowSize); - - if (collection.mode !== 'infinite') { - for (var i = windowStart; i < windowEnd; i++) { - handles.push({ - label : i + 1, - title : 'No. ' + (i + 1), - className : currentPage === i ? 'active' : undefined, - pageNumber : i + 1, - lastPage : lastPage + 1 - }); - } - } - - var ffLabels = this.fastForwardHandleLabels; - if (ffLabels) { - if (ffLabels.prev) { - handles.unshift({ - label : ffLabels.prev, - className : collection.hasPrevious() ? void 0 : 'disabled', - action : 'prev' - }); - } - - if (ffLabels.first) { - handles.unshift({ - label : ffLabels.first, - className : collection.hasPrevious() ? void 0 : 'disabled', - action : 'first' - }); - } - - if (ffLabels.next) { - handles.push({ - label : ffLabels.next, - className : collection.hasNext() ? void 0 : 'disabled', - action : 'next' - }); - } - - if (ffLabels.last) { - handles.push({ - label : ffLabels.last, - className : collection.hasNext() ? void 0 : 'disabled', - action : 'last' - }); - } - } - - return handles; - }, - - render : function() { - this.$el.empty(); - - var templateFunction = Marionette.TemplateCache.get(this.template); - - this.$el.html(templateFunction({ - handles : this.makeHandles(), - state : this.collection.state - })); - - this.delegateEvents(); - - return this; - }, - - _showPageJumper : function(e) { - if ($(e.target).is('select')) { - return; - } - - var templateFunction = Marionette.TemplateCache.get('Shared/Grid/JumpToPageTemplate'); - var state = this.collection.state; - var currentPage = Math.max(state.currentPage, state.firstPage); - currentPage = state.firstPage ? currentPage - 1 : currentPage; - - var pages = []; - - for (var i = 0; i < this.collection.state.lastPage; i++) { - if (i === currentPage) { - pages.push({ - page : i + 1, - current : true - }); - } else { - pages.push({ page : i + 1 }); - } - } - - this.$el.find('.x-page-number').html(templateFunction({ pages : pages })); - }, - - _jumpToPage : function() { - var target = this.$el.find('.x-page-select'); - - //Remove event handlers so the blur event is not triggered - this.undelegateEvents(); - - var selectedPage = parseInt(target.val(), 10); - - this.$el.find('.x-page-number').html('<i class="icon-lidarr-spinner fa-spin"></i>'); - this.collection.getPage(selectedPage); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Grid/PagerTemplate.hbs b/src/UI/Shared/Grid/PagerTemplate.hbs deleted file mode 100644 index 795e76e6e..000000000 --- a/src/UI/Shared/Grid/PagerTemplate.hbs +++ /dev/null @@ -1,16 +0,0 @@ -<ul> - {{#each handles}} - <li {{#if className}}class="{{className}}"{{/if}} > - {{#if pageNumber}} - <span class="x-page-number">{{pageNumber}} / {{lastPage}}</span> - {{else}} - <i class="pager-btn clickable {{label}}" data-action="{{action}}"/> - {{/if}} - </li> - {{/each}} -</ul> - -<span class="total-records"> - <span class="hidden-xs">Total records: {{Number state.totalRecords}}</span> - <span class="visible-xs label label-info" title="Total records">{{Number state.totalRecords}}</span> -</span> \ No newline at end of file diff --git a/src/UI/Shared/LoadingView.js b/src/UI/Shared/LoadingView.js deleted file mode 100644 index 1b703940e..000000000 --- a/src/UI/Shared/LoadingView.js +++ /dev/null @@ -1,6 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/LoadingViewTemplate', - className : 'nz-loading row' -}); \ No newline at end of file diff --git a/src/UI/Shared/LoadingViewTemplate.hbs b/src/UI/Shared/LoadingViewTemplate.hbs deleted file mode 100644 index 1ae3d7f54..000000000 --- a/src/UI/Shared/LoadingViewTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="followingBalls"> - <div id="ball-1" class="ball"></div> - <div id="ball-2" class="ball"></div> - <div id="ball-3" class="ball"></div> - <div id="ball-4" class="ball"></div> - </div> - </div> -</div> diff --git a/src/UI/Shared/Messenger.js b/src/UI/Shared/Messenger.js deleted file mode 100644 index 11837396a..000000000 --- a/src/UI/Shared/Messenger.js +++ /dev/null @@ -1,66 +0,0 @@ -require('messenger'); - -var messenger = require('messenger'); -module.exports = { - show : function(options) { - if (!options.type) { - options.type = 'info'; - } - - if (options.hideAfter === undefined) { - switch (options.type) { - case 'info': - options.hideAfter = 5; - break; - - case 'success': - options.hideAfter = 5; - break; - - default: - options.hideAfter = 5; - } - } - - options.hideOnNavigate = options.hideOnNavigate || false; - - return messenger().post({ - message : options.message, - type : options.type, - showCloseButton : true, - hideAfter : options.hideAfter, - id : options.id, - actions : options.actions, - hideOnNavigate : options.hideOnNavigate - }); - }, - - monitor : function(options) { - if (!options.promise) { - throw 'promise is required'; - } - - if (!options.successMessage) { - throw 'success message is required'; - } - - if (!options.errorMessage) { - throw 'error message is required'; - } - - var self = this; - - options.promise.done(function() { - self.show({ message : options.successMessage }); - }); - - options.promise.fail(function() { - self.show({ - message : options.errorMessage, - type : 'error' - }); - }); - - return options.promise; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js deleted file mode 100644 index 173399ade..000000000 --- a/src/UI/Shared/Modal/ModalController.js +++ /dev/null @@ -1,102 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../../AppLayout'); -var Marionette = require('marionette'); -var EditArtistView = require('../../Artist/Edit/EditArtistView'); -var DeleteArtistView = require('../../Artist/Delete/DeleteArtistView'); -var EpisodeDetailsLayout = require('../../Episode/EpisodeDetailsLayout'); -var AlbumDetailsLayout = require('../../Album/AlbumDetailsLayout'); -var HistoryDetailsLayout = require('../../Activity/History/Details/HistoryDetailsLayout'); -var LogDetailsView = require('../../System/Logs/Table/Details/LogDetailsView'); -var RenamePreviewLayout = require('../../Rename/RenamePreviewLayout'); -var ManualImportLayout = require('../../ManualImport/ManualImportLayout'); -var FileBrowserLayout = require('../FileBrowser/FileBrowserLayout'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - vent.on(vent.Commands.OpenModalCommand, this._openModal, this); - vent.on(vent.Commands.CloseModalCommand, this._closeModal, this); - vent.on(vent.Commands.OpenModal2Command, this._openModal2, this); - vent.on(vent.Commands.CloseModal2Command, this._closeModal2, this); - vent.on(vent.Commands.EditArtistCommand, this._editArtist, this); - vent.on(vent.Commands.DeleteArtistCommand, this._deleteArtist, this); - vent.on(vent.Commands.ShowEpisodeDetails, this._showEpisode, this); - vent.on(vent.Commands.ShowAlbumDetails, this._showAlbum, this); - vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); - vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); - vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); - vent.on(vent.Commands.ShowManualImport, this._showManualImport, this); - vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); - vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); - }, - - _openModal : function(view) { - AppLayout.modalRegion.show(view); - }, - - _closeModal : function() { - AppLayout.modalRegion.closeModal(); - }, - - _openModal2 : function(view) { - AppLayout.modalRegion2.show(view); - }, - - _closeModal2 : function() { - AppLayout.modalRegion2.closeModal(); - }, - - _editArtist : function(options) { - var view = new EditArtistView({ model : options.artist }); - AppLayout.modalRegion.show(view); - }, - - _deleteArtist : function(options) { - var view = new DeleteArtistView({ model : options.artist }); - AppLayout.modalRegion.show(view); - }, - - _showEpisode : function(options) { - var view = new EpisodeDetailsLayout({ - model : options.episode, - hideSeriesLink : options.hideSeriesLink, - openingTab : options.openingTab - }); - AppLayout.modalRegion.show(view); - }, - - _showAlbum : function(options) { - var view = new AlbumDetailsLayout({ - model : options.album - }); - AppLayout.modalRegion.show(view); - }, - - _showHistory : function(options) { - var view = new HistoryDetailsLayout({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showLogDetails : function(options) { - var view = new LogDetailsView({ model : options.model }); - AppLayout.modalRegion.show(view); - }, - - _showRenamePreview : function(options) { - var view = new RenamePreviewLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showManualImport : function(options) { - var view = new ManualImportLayout(options); - AppLayout.modalRegion.show(view); - }, - - _showFileBrowser : function(options) { - var view = new FileBrowserLayout(options); - AppLayout.modalRegion2.show(view); - }, - - _closeFileBrowser : function() { - AppLayout.modalRegion2.closeModal(); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegion.js b/src/UI/Shared/Modal/ModalRegion.js deleted file mode 100644 index 91fccccc5..000000000 --- a/src/UI/Shared/Modal/ModalRegion.js +++ /dev/null @@ -1,7 +0,0 @@ -var ModalRegionBase = require('./ModalRegionBase'); - -var region = ModalRegionBase.extend({ - el : '#modal-region' -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegion2.js b/src/UI/Shared/Modal/ModalRegion2.js deleted file mode 100644 index f9f38bea4..000000000 --- a/src/UI/Shared/Modal/ModalRegion2.js +++ /dev/null @@ -1,30 +0,0 @@ -var $ = require('jquery'); -var ModalRegionBase = require('./ModalRegionBase'); - -var region = ModalRegionBase.extend({ - el : '#modal-region2', - - initialize : function () { - this.listenTo(this, 'modal:beforeShow', this.onBeforeShow); - }, - - onBeforeShow : function () { - this.$el.addClass('modal fade'); - this.$el.attr('tabindex', '-1'); - this.$el.css('z-index', '1060'); - - this.$el.on('shown.bs.modal', function() { - $('.modal-backdrop:last').css('z-index', 1059); - }); - }, - - _closed : function () { - ModalRegionBase.prototype._closed.apply(this, arguments); - - if (require('../../AppLayout').modalRegion.currentView) { - $('body').addClass('modal-open'); - } - } -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalRegionBase.js b/src/UI/Shared/Modal/ModalRegionBase.js deleted file mode 100644 index 91c8ab32d..000000000 --- a/src/UI/Shared/Modal/ModalRegionBase.js +++ /dev/null @@ -1,65 +0,0 @@ -var _ = require('underscore'); -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -require('bootstrap'); -var region = Marionette.Region.extend({ - el : '#modal-region', - - constructor : function() { - Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); - this.on('show', this.showModal, this); - }, - - getEl : function(selector) { - var $el = $(selector); - $el.on('hidden', this.close); - return $el; - }, - - showModal : function() { - this.trigger('modal:beforeShow'); - this.$el.addClass('modal fade'); - - //need tab index so close on escape works - //https://github.com/twitter/bootstrap/issues/4663 - this.$el.attr('tabindex', '-1'); - this.$el.modal({ - show : true, - keyboard : true, - backdrop : true - }); - - this.$el.on('hide.bs.modal', $.proxy(this._closing, this)); - this.$el.on('hidden.bs.modal', $.proxy(this._closed, this)); - - this.currentView.$el.addClass('modal-dialog'); - - this.$el.on('shown.bs.modal', _.bind(function() { - this.trigger('modal:afterShow'); - this.currentView.trigger('modal:afterShow'); - }, this)); - }, - - closeModal : function() { - $(this.el).modal('hide'); - this.reset(); - }, - - _closing : function() { - if (this.$el) { - this.$el.off('hide.bs.modal'); - this.$el.off('shown.bs.modal'); - } - - this.reset(); - }, - - _closed: function () { - if (this.$el) { - this.$el.off('hidden.bs.modal'); - } - } -}); - -module.exports = region; \ No newline at end of file diff --git a/src/UI/Shared/NotFoundView.js b/src/UI/Shared/NotFoundView.js deleted file mode 100644 index f0b34039a..000000000 --- a/src/UI/Shared/NotFoundView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/NotFoundViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/Shared/NotFoundViewTemplate.hbs b/src/UI/Shared/NotFoundViewTemplate.hbs deleted file mode 100644 index 4073bba9f..000000000 --- a/src/UI/Shared/NotFoundViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<div> - <img src="{{UrlBase}}/Content/Images/404.png" style="height:400px; margin-top: 50px"/> - -</div> diff --git a/src/UI/Shared/NzbDroneController.js b/src/UI/Shared/NzbDroneController.js deleted file mode 100644 index 4cdbd2510..000000000 --- a/src/UI/Shared/NzbDroneController.js +++ /dev/null @@ -1,67 +0,0 @@ -var vent = require('vent'); -var AppLayout = require('../AppLayout'); -var Marionette = require('marionette'); -var NotFoundView = require('./NotFoundView'); -var Messenger = require('./Messenger'); - -module.exports = Marionette.AppRouter.extend({ - initialize : function() { - this.listenTo(vent, vent.Events.ServerUpdated, this._onServerUpdated); - }, - - showNotFound : function() { - this.setTitle('Not Found'); - this.showMainRegion(new NotFoundView(this)); - }, - - setTitle : function(title) { - title = title; - if (title === 'Lidarr') { - document.title = 'Lidarr'; - } else { - document.title = title + ' - Lidarr'; - } - - if (window.NzbDrone.Analytics && window.Piwik) { - try { - var piwik = window.Piwik.getTracker(window.location.protocol + '//piwik.nzbdrone.com/piwik.php', 1); - piwik.setReferrerUrl(''); - piwik.setCustomUrl('http://local' + window.location.pathname); - piwik.setCustomVariable(1, 'version', window.NzbDrone.Version, 'page'); - piwik.setCustomVariable(2, 'branch', window.NzbDrone.Branch, 'page'); - piwik.trackPageView(title); - } - catch (e) { - console.error(e); - } - } - }, - - _onServerUpdated : function() { - var label = window.location.pathname === window.NzbDrone.UrlBase + '/system/updates' ? 'Reload' : 'View Changes'; - - Messenger.show({ - message : 'Lidarr has been updated', - hideAfter : 0, - id : 'lidarrUpdated', - actions : { - viewChanges : { - label : label, - action : function() { - window.location = window.NzbDrone.UrlBase + '/system/updates'; - } - } - } - }); - - this.pendingUpdate = true; - }, - - showMainRegion : function(view) { - if (this.pendingUpdate) { - window.location.reload(); - } else { - AppLayout.mainRegion.show(view); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/SignalRBroadcaster.js b/src/UI/Shared/SignalRBroadcaster.js deleted file mode 100644 index 204f77ab5..000000000 --- a/src/UI/Shared/SignalRBroadcaster.js +++ /dev/null @@ -1,76 +0,0 @@ -var vent = require('vent'); -var $ = require('jquery'); -var Messenger = require('./Messenger'); -var StatusModel = require('../System/StatusModel'); -require('signalR'); - -module.exports = { - appInitializer : function() { - console.log('starting signalR'); - - var getStatus = function(status) { - switch (status) { - case 0: - return 'connecting'; - case 1: - return 'connected'; - case 2: - return 'reconnecting'; - case 4: - return 'disconnected'; - default: - throw 'invalid status ' + status; - } - }; - - var tryingToReconnect = false; - var messengerId = 'signalR'; - - this.signalRconnection = $.connection(StatusModel.get('urlBase') + '/signalr', { apiKey: window.NzbDrone.ApiKey }); - - this.signalRconnection.stateChanged(function(change) { - console.debug('SignalR: [{0}]'.format(getStatus(change.newState))); - }); - - this.signalRconnection.received(function(message) { - vent.trigger('server:' + message.name, message.body); - }); - - this.signalRconnection.reconnecting(function() { - if (window.NzbDrone.unloading) { - return; - } - - tryingToReconnect = true; - }); - - this.signalRconnection.reconnected(function() { - tryingToReconnect = false; - }); - - this.signalRconnection.disconnected(function() { - if (tryingToReconnect) { - $('<div class="modal-backdrop fade in"></div>').appendTo(document.body); - - Messenger.show({ - id : messengerId, - type : 'error', - hideAfter : 0, - message : 'Connection to backend lost', - actions : { - cancel : { - label : 'Reload', - action : function() { - window.location.reload(); - } - } - } - }); - } - }); - - this.signalRconnection.start({ transport : ['longPolling'] }); - - return this; - } -}; diff --git a/src/UI/Shared/Styles/card.less b/src/UI/Shared/Styles/card.less deleted file mode 100644 index 92c275a8b..000000000 --- a/src/UI/Shared/Styles/card.less +++ /dev/null @@ -1,10 +0,0 @@ -@import "../../Content/prefixer"; - -.card(@color : #e1e1e1 ) { - margin : 10px; - background-color : #ffffff; - padding : 10px; - color : #444444; - .box-shadow( 0px 0px 10px 1px @color); - .border-radius(3px); -} diff --git a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js b/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js deleted file mode 100644 index 097df89ab..000000000 --- a/src/UI/Shared/Toolbar/Button/ButtonCollectionView.js +++ /dev/null @@ -1,22 +0,0 @@ -var Marionette = require('marionette'); -var ButtonView = require('./ButtonView'); - -module.exports = Marionette.CollectionView.extend({ - className : 'btn-group', - itemView : ButtonView, - - initialize : function(options) { - this.menu = options.menu; - this.className = 'btn-group'; - - if (options.menu.collapse) { - this.className += ' btn-group-collapse'; - } - }, - - onRender : function() { - if (this.menu.collapse) { - this.$el.addClass('btn-group-collapse'); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Button/ButtonView.js b/src/UI/Shared/Toolbar/Button/ButtonView.js deleted file mode 100644 index 20e77e4e9..000000000 --- a/src/UI/Shared/Toolbar/Button/ButtonView.js +++ /dev/null @@ -1,85 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var _ = require('underscore'); -var CommandController = require('../../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/ButtonTemplate', - className : 'btn btn-default btn-icon-only-xs', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function() { - this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); - }, - - onRender : function() { - if (this.model.get('active')) { - this.$el.addClass('active'); - this.invokeCallback(); - } - - if (!this.model.get('title')) { - this.$el.addClass('btn-icon-only'); - } - - if (this.model.get('className')) { - this.$el.addClass(this.model.get('className')); - } - - if (this.model.get('tooltip')) { - this.$el.attr('title', this.model.get('tooltip')); - } - - var command = this.model.get('command'); - if (command) { - var properties = _.extend({ name : command }, this.model.get('properties')); - - CommandController.bindToCommand({ - command : properties, - element : this.$el - }); - } - }, - - onClick : function() { - if (this.$el.hasClass('disabled')) { - return; - } - - this.invokeCallback(); - this.invokeRoute(); - this.invokeCommand(); - }, - - invokeCommand : function() { - var command = this.model.get('command'); - if (command) { - CommandController.Execute(command, this.model.get('properties')); - } - }, - - invokeRoute : function() { - var route = this.model.get('route'); - if (route) { - Backbone.history.navigate(route, { trigger : true }); - } - }, - - invokeCallback : function() { - if (!this.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - var callback = this.model.get('callback'); - if (callback) { - callback.call(this.model.ownerContext, this); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonCollection.js b/src/UI/Shared/Toolbar/ButtonCollection.js deleted file mode 100644 index a48a04574..000000000 --- a/src/UI/Shared/Toolbar/ButtonCollection.js +++ /dev/null @@ -1,6 +0,0 @@ -var Backbone = require('backbone'); -var ButtonModel = require('./ButtonModel'); - -module.exports = Backbone.Collection.extend({ - model : ButtonModel -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonModel.js b/src/UI/Shared/Toolbar/ButtonModel.js deleted file mode 100644 index 055a4ef8f..000000000 --- a/src/UI/Shared/Toolbar/ButtonModel.js +++ /dev/null @@ -1,11 +0,0 @@ -var _ = require('underscore'); -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - defaults : { - 'target' : '/nzbdrone/route', - 'title' : '', - 'active' : false, - 'tooltip' : undefined - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ButtonTemplate.hbs b/src/UI/Shared/Toolbar/ButtonTemplate.hbs deleted file mode 100644 index d21cdbc9d..000000000 --- a/src/UI/Shared/Toolbar/ButtonTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js deleted file mode 100644 index 70c4fb188..000000000 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonCollectionView.js +++ /dev/null @@ -1,37 +0,0 @@ -var Marionette = require('marionette'); -var RadioButtonView = require('./RadioButtonView'); -var Config = require('../../../Config'); - -module.exports = Marionette.CollectionView.extend({ - className : 'btn-group', - itemView : RadioButtonView, - - attributes : { - 'data-toggle' : 'buttons' - }, - - initialize : function(options) { - this.menu = options.menu; - - this.setActive(); - }, - - setActive : function() { - var storedKey = this.menu.defaultAction; - - if (this.menu.storeState) { - storedKey = Config.getValue(this.menu.menuKey, storedKey); - } - - if (!storedKey) { - return; - } - this.collection.each(function(model) { - if (model.get('key').toLocaleLowerCase() === storedKey.toLowerCase()) { - model.set('active', true); - } else { - model.set('active, false'); - } - }); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js b/src/UI/Shared/Toolbar/Radio/RadioButtonView.js deleted file mode 100644 index 90fa0bd0c..000000000 --- a/src/UI/Shared/Toolbar/Radio/RadioButtonView.js +++ /dev/null @@ -1,50 +0,0 @@ -var Marionette = require('marionette'); -var Config = require('../../../Config'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/RadioButtonTemplate', - className : 'btn btn-default', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function() { - this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); - }, - - onRender : function() { - if (this.model.get('active')) { - this.$el.addClass('active'); - this.invokeCallback(); - } - - if (!this.model.get('title')) { - this.$el.addClass('btn-icon-only'); - } - - if (this.model.get('tooltip')) { - this.$el.attr('title', this.model.get('tooltip')); - } - }, - - onClick : function() { - Config.setValue(this.model.get('menuKey'), this.model.get('key')); - this.invokeCallback(); - }, - - invokeCallback : function() { - if (!this.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - var callback = this.model.get('callback'); - if (callback) { - callback.call(this.model.ownerContext, this); - } - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs b/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs deleted file mode 100644 index aaff67405..000000000 --- a/src/UI/Shared/Toolbar/RadioButtonTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<input type="radio"><i class="{{icon}} x-icon"/><span> {{title}}</span> diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js deleted file mode 100644 index 6db8995a2..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionView.js +++ /dev/null @@ -1,38 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var Marionette = require('marionette'); -var ButtonView = require('./SortingButtonView'); - -module.exports = Marionette.CompositeView.extend({ - itemView : ButtonView, - template : 'Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate', - itemViewContainer : '.dropdown-menu', - - initialize : function(options) { - this.viewCollection = options.viewCollection; - this.listenTo(this.viewCollection, 'drone:sort', this.sort); - }, - - itemViewOptions : function() { - return { - viewCollection : this.viewCollection - }; - }, - - sort : function(sortModel, sortDirection) { - var collection = this.viewCollection; - - var order; - if (sortDirection === 'ascending') { - order = -1; - } else if (sortDirection === 'descending') { - order = 1; - } else { - order = null; - } - - collection.setSorting(sortModel.get('name'), order); - collection.fullCollection.sort(); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs b/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs deleted file mode 100644 index 80d4888de..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonCollectionViewTemplate.hbs +++ /dev/null @@ -1,8 +0,0 @@ -<div class="btn-group sorting-buttons"> - <a class="btn btn-default dropdown-toggle" data-toggle="dropdown" href="#"> - Sort <span class="caret"></span> - </a> - <ul class="dropdown-menu"> - - </ul> -</div> diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js b/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js deleted file mode 100644 index 9ffb4f1e8..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonView.js +++ /dev/null @@ -1,70 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var _ = require('underscore'); - -module.exports = Marionette.ItemView.extend({ - template : 'Shared/Toolbar/Sorting/SortingButtonViewTemplate', - tagName : 'li', - - ui : { - icon : 'i' - }, - - events : { - 'click' : 'onClick' - }, - - initialize : function(options) { - this.viewCollection = options.viewCollection; - this.listenTo(this.viewCollection, 'drone:sort', this.render); - this.listenTo(this.viewCollection, 'backgrid:sort', this.render); - }, - - onRender : function() { - if (this.viewCollection.state) { - var sortKey = this.viewCollection.state.sortKey; - var name = this.viewCollection._getSortMapping(sortKey).name; - var order = this.viewCollection.state.order; - - if (name === this.model.get('name')) { - this._setSortIcon(order); - } else { - this._removeSortIcon(); - } - } - }, - - onClick : function(e) { - e.preventDefault(); - - var collection = this.viewCollection; - var event = 'drone:sort'; - - var direction = collection.state.order; - if (direction === 'ascending' || direction === -1) { - direction = 'descending'; - } else { - direction = 'ascending'; - } - - collection.setSorting(this.model.get('name'), direction); - collection.trigger(event, this.model, direction); - }, - - _convertDirectionToIcon : function(dir) { - if (dir === 'ascending' || dir === -1) { - return 'icon-lidarr-sort-asc'; - } - - return 'icon-lidarr-sort-desc'; - }, - - _setSortIcon : function(dir) { - this._removeSortIcon(); - this.ui.icon.addClass(this._convertDirectionToIcon(dir)); - }, - - _removeSortIcon : function() { - this.ui.icon.removeClass('icon-lidarr-sort-asc icon-lidarr-sort-desc'); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs b/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs deleted file mode 100644 index 57018028d..000000000 --- a/src/UI/Shared/Toolbar/Sorting/SortingButtonViewTemplate.hbs +++ /dev/null @@ -1,4 +0,0 @@ -<a href="#"> - <span class="sorting-title">{{title}}</span> - <i class=""></i> -</a> \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayout.js b/src/UI/Shared/Toolbar/ToolbarLayout.js deleted file mode 100644 index ebad4c789..000000000 --- a/src/UI/Shared/Toolbar/ToolbarLayout.js +++ /dev/null @@ -1,108 +0,0 @@ -var Marionette = require('marionette'); -var ButtonCollection = require('./ButtonCollection'); -var ButtonModel = require('./ButtonModel'); -var RadioButtonCollectionView = require('./Radio/RadioButtonCollectionView'); -var ButtonCollectionView = require('./Button/ButtonCollectionView'); -var SortingButtonCollectionView = require('./Sorting/SortingButtonCollectionView'); -var _ = require('underscore'); - -module.exports = Marionette.Layout.extend({ - template : 'Shared/Toolbar/ToolbarLayoutTemplate', - className : 'toolbar', - - ui : { - left_x : '.x-toolbar-left', - right_x : '.x-toolbar-right' - }, - - initialize : function(options) { - if (!options) { - throw 'options needs to be passed'; - } - - if (!options.context) { - throw 'context needs to be passed'; - } - - this.templateHelpers = { - floatOnMobile : options.floatOnMobile || false - }; - - this.left = options.left; - this.right = options.right; - this.toolbarContext = options.context; - }, - - onShow : function() { - if (this.left) { - _.each(this.left, this._showToolbarLeft, this); - } - if (this.right) { - _.each(this.right, this._showToolbarRight, this); - } - }, - - _showToolbarLeft : function(element, index) { - this._showToolbar(element, index, 'left'); - }, - - _showToolbarRight : function(element, index) { - this._showToolbar(element, index, 'right'); - }, - - _showToolbar : function(buttonGroup, index, position) { - var groupCollection = new ButtonCollection(); - - _.each(buttonGroup.items, function(button) { - if (buttonGroup.storeState && !button.key) { - throw 'must provide key for all buttons when storeState is enabled'; - } - - var model = new ButtonModel(button); - model.set('menuKey', buttonGroup.menuKey); - model.ownerContext = this.toolbarContext; - groupCollection.add(model); - }, this); - - var buttonGroupView; - - switch (buttonGroup.type) { - case 'radio': - { - buttonGroupView = new RadioButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup - }); - break; - } - case 'sorting': - { - buttonGroupView = new SortingButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup, - viewCollection : buttonGroup.viewCollection - }); - break; - } - default: - { - buttonGroupView = new ButtonCollectionView({ - collection : groupCollection, - menu : buttonGroup - }); - break; - } - } - - var regionId = position + '_' + (index + 1); - var region = this[regionId]; - - if (!region) { - var regionClassName = 'x-toolbar-' + position + '-' + (index + 1); - this.ui[position + '_x'].append('<div class="toolbar-group ' + regionClassName + '" />\r\n'); - region = this.addRegion(regionId, '.' + regionClassName); - } - - region.show(buttonGroupView); - } -}); \ No newline at end of file diff --git a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs b/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs deleted file mode 100644 index 0cd0e21e0..000000000 --- a/src/UI/Shared/Toolbar/ToolbarLayoutTemplate.hbs +++ /dev/null @@ -1,2 +0,0 @@ -<div class="page-toolbar pull-left {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-left" /> -<div class="page-toolbar pull-right {{#unless floatOnMobile}}pull-none-xs{{/unless}} x-toolbar-right" /> diff --git a/src/UI/Shared/Tooltip.js b/src/UI/Shared/Tooltip.js deleted file mode 100644 index c19b369fb..000000000 --- a/src/UI/Shared/Tooltip.js +++ /dev/null @@ -1,47 +0,0 @@ -var $ = require('jquery'); -require('bootstrap'); - -var Tooltip = $.fn.tooltip.Constructor; - -var origGetOptions = Tooltip.prototype.getOptions; -Tooltip.prototype.getOptions = function(options) { - var result = origGetOptions.call(this, options); - - if (result.container === false) { - - var container = this.$element.closest('.btn-group,.input-group').parent(); - - if (container.length) { - result.container = container; - } - } - - return result; -}; - -var onElementRemoved = function(event) { - event.data.hide(); -}; - -var origShow = Tooltip.prototype.show; -Tooltip.prototype.show = function() { - origShow.call(this); - - this.$element.on('remove', this, onElementRemoved); -}; - -var origHide = Tooltip.prototype.hide; -Tooltip.prototype.hide = function() { - origHide.call(this); - - this.$element.off('remove', onElementRemoved); -}; - -module.exports = { - appInitializer : function() { - - $('body').tooltip({ selector : '[title]' }); - - return this; - } -}; \ No newline at end of file diff --git a/src/UI/Shared/UiSettingsController.js b/src/UI/Shared/UiSettingsController.js deleted file mode 100644 index eb9210659..000000000 --- a/src/UI/Shared/UiSettingsController.js +++ /dev/null @@ -1,26 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var UiSettingsModel = require('./UiSettingsModel'); - -var Controller = { - - appInitializer : function() { - - UiSettingsModel.on('sync', this._updateUiSettings); - - this._updateUiSettings(); - }, - - _updateUiSettings: function() { - - if (UiSettingsModel.get('enableColorImpairedMode')) { - $('body').addClass('color-impaired-mode'); - } else { - $('body').removeClass('color-impaired-mode'); - } - } -}; - -_.bindAll(Controller, 'appInitializer'); - -module.exports = Controller; \ No newline at end of file diff --git a/src/UI/Shared/UiSettingsModel.js b/src/UI/Shared/UiSettingsModel.js deleted file mode 100644 index a517b5aba..000000000 --- a/src/UI/Shared/UiSettingsModel.js +++ /dev/null @@ -1,29 +0,0 @@ -var Backbone = require('backbone'); -var ApiData = require('./ApiData'); - -var UiSettings = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/config/ui', - - shortDateTime : function(includeSeconds) { - return this.get('shortDateFormat') + ' ' + this.time(true, includeSeconds); - }, - - longDateTime : function(includeSeconds) { - return this.get('longDateFormat') + ' ' + this.time(true, includeSeconds); - }, - - time : function(includeMinuteZero, includeSeconds) { - if (includeSeconds) { - return this.get('timeFormat').replace(/\(?\:mm\)?/, ':mm:ss'); - } - if (includeMinuteZero) { - return this.get('timeFormat').replace('(', '').replace(')', ''); - } - - return this.get('timeFormat').replace(/\(\:mm\)/, ''); - } -}); - -var instance = new UiSettings(ApiData.get('config/ui')); - -module.exports = instance; \ No newline at end of file diff --git a/src/UI/Shared/VersionChangeMonitor.js b/src/UI/Shared/VersionChangeMonitor.js deleted file mode 100644 index 932d97f6c..000000000 --- a/src/UI/Shared/VersionChangeMonitor.js +++ /dev/null @@ -1,13 +0,0 @@ -var $ = require('jquery'); -var vent = require('vent'); - -$(document).ajaxSuccess(function(event, xhr) { - var version = xhr.getResponseHeader('X-ApplicationVersion'); - if (!version || !window.NzbDrone || !window.NzbDrone.Version) { - return; - } - - if (version !== window.NzbDrone.Version) { - vent.trigger(vent.Events.ServerUpdated); - } -}); diff --git a/src/UI/Shared/piwikCheck.js b/src/UI/Shared/piwikCheck.js deleted file mode 100644 index fdda0639c..000000000 --- a/src/UI/Shared/piwikCheck.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -if(window.NzbDrone.Analytics) { - var d = document; - var g = d.createElement('script'); - var s = d.getElementsByTagName('script')[0]; - g.type = 'text/javascript'; - g.async = true; - g.defer = true; - g.src = '//piwik.lidarr.tv/piwik.js'; - s.parentNode.insertBefore(g, s); -} diff --git a/src/UI/Shims/backbone.backgrid.selectall.js b/src/UI/Shims/backbone.backgrid.selectall.js deleted file mode 100644 index 10b92f5d9..000000000 --- a/src/UI/Shims/backbone.backgrid.selectall.js +++ /dev/null @@ -1,4 +0,0 @@ -var backgrid = require('backgrid'); -require('../JsLibraries/backbone.backgrid.selectall'); - -module.exports = backgrid.Extension.SelectRowCell; \ No newline at end of file diff --git a/src/UI/Shims/backbone.collectionview.js b/src/UI/Shims/backbone.collectionview.js deleted file mode 100644 index a4080a462..000000000 --- a/src/UI/Shims/backbone.collectionview.js +++ /dev/null @@ -1,4 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.collectionview'); - -module.exports = window.Backbone.CollectionView; \ No newline at end of file diff --git a/src/UI/Shims/backbone.deep.model.js b/src/UI/Shims/backbone.deep.model.js deleted file mode 100644 index dc7b47265..000000000 --- a/src/UI/Shims/backbone.deep.model.js +++ /dev/null @@ -1,4 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.deep.model'); - -module.exports = window.Backbone.DeepModel; \ No newline at end of file diff --git a/src/UI/Shims/backbone.js b/src/UI/Shims/backbone.js deleted file mode 100644 index 0896076d8..000000000 --- a/src/UI/Shims/backbone.js +++ /dev/null @@ -1,7 +0,0 @@ -var jquery = require('jquery'); -var Backbone = require('../JsLibraries/backbone'); - -window.Backbone = Backbone; -Backbone.$ = jquery; - -module.exports = Backbone; \ No newline at end of file diff --git a/src/UI/Shims/backbone.marionette.js b/src/UI/Shims/backbone.marionette.js deleted file mode 100644 index 50b3bf182..000000000 --- a/src/UI/Shims/backbone.marionette.js +++ /dev/null @@ -1,10 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.marionette'); - -var templateMixin = require('../Handlebars/backbone.marionette.templates'); -var asNamedView = require('../Mixins/AsNamedView'); - -templateMixin.call(window.Marionette.TemplateCache); -asNamedView.call(window.Marionette.ItemView.prototype); - -module.exports = window.Marionette; \ No newline at end of file diff --git a/src/UI/Shims/backbone.validation.js b/src/UI/Shims/backbone.validation.js deleted file mode 100644 index 158e42265..000000000 --- a/src/UI/Shims/backbone.validation.js +++ /dev/null @@ -1,8 +0,0 @@ -require('backbone'); -require('../JsLibraries/backbone.validation'); -var $ = require('jquery'); - -var jqueryValidation = require('../jQuery/jquery.validation'); -jqueryValidation.call($); - -module.exports = window.Backbone.Validation; \ No newline at end of file diff --git a/src/UI/Shims/backgrid.js b/src/UI/Shims/backgrid.js deleted file mode 100644 index 40853cc90..000000000 --- a/src/UI/Shims/backgrid.js +++ /dev/null @@ -1,19 +0,0 @@ -require('backbone'); - -var backgrid = require('../JsLibraries/backbone.backgrid'); -var header = require('../Shared/Grid/HeaderCell'); - -header.call(backgrid); - -backgrid.Column.prototype.defaults = { - name : undefined, - label : undefined, - sortable : true, - editable : false, - renderable : true, - formatter : undefined, - cell : undefined, - headerCell : 'Lidarr', - sortType : 'toggle' -}; -module.exports = backgrid; \ No newline at end of file diff --git a/src/UI/Shims/backgrid.paginator.js b/src/UI/Shims/backgrid.paginator.js deleted file mode 100644 index 874fb4006..000000000 --- a/src/UI/Shims/backgrid.paginator.js +++ /dev/null @@ -1,5 +0,0 @@ -require('backbone'); -var backgrid = require('backgrid'); -require('../JsLibraries/backbone.backgrid.paginator'); - -module.exports = backgrid.Extension.Paginator; \ No newline at end of file diff --git a/src/UI/Shims/handlebars.js b/src/UI/Shims/handlebars.js deleted file mode 100644 index 539e73271..000000000 --- a/src/UI/Shims/handlebars.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = window.Handlebars; \ No newline at end of file diff --git a/src/UI/Shims/jquery.js b/src/UI/Shims/jquery.js deleted file mode 100644 index 19e901944..000000000 --- a/src/UI/Shims/jquery.js +++ /dev/null @@ -1,11 +0,0 @@ -var jquery = require('../JsLibraries/jquery'); -require('../Instrumentation/StringFormat'); -var spin = require('../jQuery/jquery.spin'); -var ajax = require('../jQuery/jquery.ajax'); - -spin.call(jquery); -ajax.call(jquery); - -window.$ = jquery; -window.jQuery = jquery; -module.exports = jquery; diff --git a/src/UI/Shims/jquery.signalR.js b/src/UI/Shims/jquery.signalR.js deleted file mode 100644 index 70139ccef..000000000 --- a/src/UI/Shims/jquery.signalR.js +++ /dev/null @@ -1,4 +0,0 @@ -require('jquery'); -var signalR = require('../JsLibraries/jquery.signalR'); - -module.exports = signalR; \ No newline at end of file diff --git a/src/UI/Shims/messenger.js b/src/UI/Shims/messenger.js deleted file mode 100644 index f070bb991..000000000 --- a/src/UI/Shims/messenger.js +++ /dev/null @@ -1,6 +0,0 @@ -require('jquery'); -var m = require('../JsLibraries/messenger'); - -window.Messenger.options = { theme : 'flat' }; - -module.exports = window.Messenger; \ No newline at end of file diff --git a/src/UI/Shims/underscore.js b/src/UI/Shims/underscore.js deleted file mode 100644 index 67b9b808b..000000000 --- a/src/UI/Shims/underscore.js +++ /dev/null @@ -1,4 +0,0 @@ -var _ = require('../JsLibraries/lodash.underscore'); -window._ = window._ || _; - -module.exports = window._; \ No newline at end of file diff --git a/src/UI/System/Backup/BackupCollection.js b/src/UI/System/Backup/BackupCollection.js deleted file mode 100644 index 5bee1fc35..000000000 --- a/src/UI/System/Backup/BackupCollection.js +++ /dev/null @@ -1,15 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var BackupModel = require('./BackupModel'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/system/backup', - model : BackupModel, - - state : { - sortKey : 'time', - order : 1, - pageSize : 100000 - }, - - mode : 'client' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupEmptyView.js b/src/UI/System/Backup/BackupEmptyView.js deleted file mode 100644 index a86ba42bc..000000000 --- a/src/UI/System/Backup/BackupEmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Backup/BackupEmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupEmptyViewTemplate.hbs b/src/UI/System/Backup/BackupEmptyViewTemplate.hbs deleted file mode 100644 index 2e14e7145..000000000 --- a/src/UI/System/Backup/BackupEmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No backups are available</div> \ No newline at end of file diff --git a/src/UI/System/Backup/BackupFilenameCell.js b/src/UI/System/Backup/BackupFilenameCell.js deleted file mode 100644 index c8a57d1f9..000000000 --- a/src/UI/System/Backup/BackupFilenameCell.js +++ /dev/null @@ -1,6 +0,0 @@ -var TemplatedCell = require('../../Cells/TemplatedCell'); - -module.exports = TemplatedCell.extend({ - className : 'series-title-cell', - template : 'System/Backup/BackupFilenameCellTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupFilenameCellTemplate.hbs b/src/UI/System/Backup/BackupFilenameCellTemplate.hbs deleted file mode 100644 index e129039c3..000000000 --- a/src/UI/System/Backup/BackupFilenameCellTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<a href="{{UrlBase}}/backup/{{type}}/{{name}}" class="no-router">{{name}}</a> diff --git a/src/UI/System/Backup/BackupLayout.js b/src/UI/System/Backup/BackupLayout.js deleted file mode 100644 index 3fa3be030..000000000 --- a/src/UI/System/Backup/BackupLayout.js +++ /dev/null @@ -1,94 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BackupCollection = require('./BackupCollection'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var BackupFilenameCell = require('./BackupFilenameCell'); -var BackupTypeCell = require('./BackupTypeCell'); -var EmptyView = require('./BackupEmptyView'); -var LoadingView = require('../../Shared/LoadingView'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Backup/BackupLayoutTemplate', - - regions : { - backups : '#x-backups', - toolbar : '#x-backup-toolbar' - }, - - columns : [ - { - name : 'type', - label : '', - sortable : false, - cell : BackupTypeCell - }, - { - name : 'this', - label : 'Name', - sortable : false, - cell : BackupFilenameCell - }, - { - name : 'time', - label : 'Time', - sortable : false, - cell : RelativeDateCell - } - ], - - leftSideButtons : { - type : 'default', - storeState : false, - collapse : false, - items : [ - { - title : 'Backup', - icon : 'icon-lidarr-file-text', - command : 'backup', - properties : { type : 'manual' }, - successMessage : 'Database and settings were backed up successfully', - errorMessage : 'Backup Failed!' - } - ] - }, - - initialize : function() { - this.backupCollection = new BackupCollection(); - - this.listenTo(this.backupCollection, 'sync', this._showBackups); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - this._showToolbar(); - this.backups.show(new LoadingView()); - - this.backupCollection.fetch(); - }, - - _showBackups : function() { - if (this.backupCollection.length === 0) { - this.backups.show(new EmptyView()); - } else { - this.backups.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.backupCollection, - className : 'table table-hover' - })); - } - }, - - _showToolbar : function() { - this.toolbar.show(new ToolbarLayout({ - left : [this.leftSideButtons], - context : this - })); - }, - _commandComplete : function(options) { - if (options.command.get('name') === 'backup') { - this.backupCollection.fetch(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupLayoutTemplate.hbs b/src/UI/System/Backup/BackupLayoutTemplate.hbs deleted file mode 100644 index b50db1799..000000000 --- a/src/UI/System/Backup/BackupLayoutTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-backup-toolbar"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-backups" class="table-responsive"/> - </div> -</div> diff --git a/src/UI/System/Backup/BackupModel.js b/src/UI/System/Backup/BackupModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Backup/BackupModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Backup/BackupTypeCell.js b/src/UI/System/Backup/BackupTypeCell.js deleted file mode 100644 index 36bee0ccf..000000000 --- a/src/UI/System/Backup/BackupTypeCell.js +++ /dev/null @@ -1,26 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'backup-type-cell', - - render : function() { - this.$el.empty(); - - var icon = 'icon-lidarr-backup-scheduled'; - var title = 'Scheduled'; - - var type = this.model.get(this.column.get('name')); - - if (type === 'manual') { - icon = 'icon-lidarr-backup-manual'; - title = 'Manual'; - } else if (type === 'update') { - icon = 'icon-lidarr-backup-update'; - title = 'Before update'; - } - - this.$el.html('<i class="{0}" title="{1}"></i>'.format(icon, title)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/About/AboutView.js b/src/UI/System/Info/About/AboutView.js deleted file mode 100644 index 494b9a3ef..000000000 --- a/src/UI/System/Info/About/AboutView.js +++ /dev/null @@ -1,10 +0,0 @@ -var Marionette = require('marionette'); -var StatusModel = require('../../StatusModel'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/About/AboutViewTemplate', - - initialize : function() { - this.model = StatusModel; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/About/AboutViewTemplate.hbs b/src/UI/System/Info/About/AboutViewTemplate.hbs deleted file mode 100644 index a7fff2483..000000000 --- a/src/UI/System/Info/About/AboutViewTemplate.hbs +++ /dev/null @@ -1,20 +0,0 @@ -<fieldset> - <legend>About</legend> - - <dl class="dl-horizontal info"> - <dt>Version</dt> - <dd>{{version}}</dd> - - {{#if isMonoRuntime}} - <dt>Mono Version</dt> - <dd>{{runtimeVersion}}</dd> - {{/if}} - - <dt>AppData directory</dt> - <dd>{{appData}}</dd> - - <dt>Startup directory</dt> - <dd>{{startupPath}}</dd> - </dl> -</fieldset> - diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js b/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js deleted file mode 100644 index 9769ba7fb..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var DiskSpaceModel = require('./DiskSpaceModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/diskspace', - model : DiskSpaceModel -}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js b/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js deleted file mode 100644 index bd3470750..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceLayout.js +++ /dev/null @@ -1,58 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var DiskSpaceCollection = require('./DiskSpaceCollection'); -var LoadingView = require('../../../Shared/LoadingView'); -var DiskSpacePathCell = require('./DiskSpacePathCell'); -var FileSizeCell = require('../../../Cells/FileSizeCell'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/DiskSpace/DiskSpaceLayoutTemplate', - - regions : { - grid : '#x-grid' - }, - - columns : [ - { - name : 'path', - label : 'Location', - cell : DiskSpacePathCell, - sortable : false - }, - { - name : 'freeSpace', - label : 'Free Space', - cell : FileSizeCell, - sortable : false - }, - { - name : 'totalSpace', - label : 'Total Space', - cell : FileSizeCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new DiskSpaceCollection(); - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - }, - - onShow : function() { - this.collection.fetch(); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs b/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs deleted file mode 100644 index 99c218b67..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<fieldset> - <legend>Disk Space</legend> - - <div id="x-grid"/> -</fieldset> \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpaceModel.js b/src/UI/System/Info/DiskSpace/DiskSpaceModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpaceModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js b/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js deleted file mode 100644 index de2ceb9b6..000000000 --- a/src/UI/System/Info/DiskSpace/DiskSpacePathCell.js +++ /dev/null @@ -1,22 +0,0 @@ -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Cell.extend({ - className : 'disk-space-path-cell', - - render : function() { - this.$el.empty(); - - var path = this.model.get('path'); - var label = this.model.get('label'); - - var contents = path; - - if (label) { - contents += ' ({0})'.format(label); - } - - this.$el.html(contents); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthCell.js b/src/UI/System/Info/Health/HealthCell.js deleted file mode 100644 index 594ece55c..000000000 --- a/src/UI/System/Info/Health/HealthCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-level-cell', - - render : function() { - var level = this._getValue(); - this.$el.html('<i class="icon-lidarr-health-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthLayout.js b/src/UI/System/Info/Health/HealthLayout.js deleted file mode 100644 index bc2bc33eb..000000000 --- a/src/UI/System/Info/Health/HealthLayout.js +++ /dev/null @@ -1,57 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var HealthCollection = require('../../../Health/HealthCollection'); -var HealthCell = require('./HealthCell'); -var HealthWikiCell = require('./HealthWikiCell'); -var HealthOkView = require('./HealthOkView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/Health/HealthLayoutTemplate', - - regions : { - grid : '#x-health-grid' - }, - - columns : [ - { - name : 'type', - label : '', - cell : HealthCell, - sortable : false - }, - { - name : 'message', - label : 'Message', - cell : 'string', - sortable : false - }, - { - name : 'wikiUrl', - label : '', - cell : HealthWikiCell, - sortable : false - } - ], - - initialize : function() { - this.listenTo(HealthCollection, 'sync', this.render); - HealthCollection.fetch(); - }, - - onRender : function() { - if (HealthCollection.length === 0) { - this.grid.show(new HealthOkView()); - } else { - this._showTable(); - } - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : Backgrid.Row, - columns : this.columns, - collection : HealthCollection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthLayoutTemplate.hbs b/src/UI/System/Info/Health/HealthLayoutTemplate.hbs deleted file mode 100644 index eda20b205..000000000 --- a/src/UI/System/Info/Health/HealthLayoutTemplate.hbs +++ /dev/null @@ -1,6 +0,0 @@ -<fieldset class="x-health"> - <legend>Health</legend> - - <div id="x-health-grid"/> -</fieldset> - diff --git a/src/UI/System/Info/Health/HealthOkView.js b/src/UI/System/Info/Health/HealthOkView.js deleted file mode 100644 index 662d9d278..000000000 --- a/src/UI/System/Info/Health/HealthOkView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/Health/HealthOkViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthOkViewTemplate.hbs b/src/UI/System/Info/Health/HealthOkViewTemplate.hbs deleted file mode 100644 index b33a62360..000000000 --- a/src/UI/System/Info/Health/HealthOkViewTemplate.hbs +++ /dev/null @@ -1,3 +0,0 @@ -<div class="row health-ok"> - <div class="col-md-12">No issues with your configuration</div> -</div> \ No newline at end of file diff --git a/src/UI/System/Info/Health/HealthWikiCell.js b/src/UI/System/Info/Health/HealthWikiCell.js deleted file mode 100644 index bb93f0606..000000000 --- a/src/UI/System/Info/Health/HealthWikiCell.js +++ /dev/null @@ -1,24 +0,0 @@ -var $ = require('jquery'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.UriCell.extend({ - className : 'wiki-link-cell', - - title : 'Read the Wiki for more information', - - text : 'Wiki', - - render : function() { - this.$el.empty(); - var rawValue = this.model.get(this.column.get('name')); - var formattedValue = this.formatter.fromRaw(rawValue, this.model); - this.$el.append($('<a>', { - tabIndex : -1, - href : rawValue, - title : this.title || formattedValue, - target : this.target - }).text(this.text)); - this.delegateEvents(); - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/MoreInfo/MoreInfoView.js b/src/UI/System/Info/MoreInfo/MoreInfoView.js deleted file mode 100644 index 0217ed742..000000000 --- a/src/UI/System/Info/MoreInfo/MoreInfoView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Info/MoreInfo/MoreInfoViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs b/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs deleted file mode 100644 index b74f00ac5..000000000 --- a/src/UI/System/Info/MoreInfo/MoreInfoViewTemplate.hbs +++ /dev/null @@ -1,32 +0,0 @@ -<fieldset> - <legend>More Info</legend> - - <dl class="dl-horizontal info"> - <dt>Home page</dt> - <dd><a href="https://lidarr.audio/">lidarr.audio</a></dd> - - <dt>Wiki</dt> - <dd><a href="https://wiki.lidarr.audio/">wiki.lidarr.audio</a></dd> - - <dt>Forums</dt> - <dd><a href="https://forums.lidarr.audio/">forums.lidarr.audio</a></dd> - - <dt>Reddit</dt> - <dd><a href="https://reddit.com/r/Lidarr/">reddit.com/r/Lidarr</a></dd> - - <dt>Source</dt> - <dd><a href="https://github.com/mattman86/Lidarr/">github.com/Lidarr/Lidarr</a></dd> - - <dt>Contributors</dt> - <dd>DB and API - <a href="https://github.com/majora2007">Majora2007</a></dd> - <dd>UI and Website - <a href="https://github.com/mattman86">Mattman86</a></dd> - <dd>UI and Logo - <a href="https://github.com/skoden">Skoden</a></dd> - <dd>DB and Search - <a href="https://github.com/runraid">Runraid</a></dd> - <dd>Consultation - <a href="https://github.com/galli-leo">Galli-leo</a></dd> - - <dt>Feature Requests</dt> - <dd><a href="http://feathub.com/lidarr/Lidarr/">feathub.com/lidarr/Lidarr</a></dd> - <dd><a href="https://github.com/lidarr/Lidarr/issues">github.com/lidarr/Lidarr/issues</a> <b>(Please post issues on the forum first and not on github)</b></dd> - </dl> -</fieldset> - diff --git a/src/UI/System/Info/SystemInfoLayout.js b/src/UI/System/Info/SystemInfoLayout.js deleted file mode 100644 index 0b56318ea..000000000 --- a/src/UI/System/Info/SystemInfoLayout.js +++ /dev/null @@ -1,24 +0,0 @@ -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var AboutView = require('./About/AboutView'); -var DiskSpaceLayout = require('./DiskSpace/DiskSpaceLayout'); -var HealthLayout = require('./Health/HealthLayout'); -var MoreInfoView = require('./MoreInfo/MoreInfoView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Info/SystemInfoLayoutTemplate', - - regions : { - about : '#about', - diskSpace : '#diskspace', - health : '#health', - moreInfo : '#more-info' - }, - - onRender : function() { - this.health.show(new HealthLayout()); - this.diskSpace.show(new DiskSpaceLayout()); - this.about.show(new AboutView()); - this.moreInfo.show(new MoreInfoView()); - } -}); \ No newline at end of file diff --git a/src/UI/System/Info/SystemInfoLayoutTemplate.hbs b/src/UI/System/Info/SystemInfoLayoutTemplate.hbs deleted file mode 100644 index d6eef7abd..000000000 --- a/src/UI/System/Info/SystemInfoLayoutTemplate.hbs +++ /dev/null @@ -1,15 +0,0 @@ -<div class="row"> - <div class="col-md-12" id="health"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="diskspace"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="about"></div> -</div> - -<div class="row"> - <div class="col-md-12" id="more-info"></div> -</div> diff --git a/src/UI/System/Info/info.less b/src/UI/System/Info/info.less deleted file mode 100644 index 59746b7d8..000000000 --- a/src/UI/System/Info/info.less +++ /dev/null @@ -1,3 +0,0 @@ -.health-ok { - margin-bottom: 30px; -} \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsModel.js b/src/UI/System/Logs/Files/ContentsModel.js deleted file mode 100644 index c9c47b1bb..000000000 --- a/src/UI/System/Logs/Files/ContentsModel.js +++ /dev/null @@ -1,13 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({ - url : function() { - return this.get('contentsUrl'); - }, - - parse : function(contents) { - var response = {}; - response.contents = contents; - return response; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsView.js b/src/UI/System/Logs/Files/ContentsView.js deleted file mode 100644 index 6b5b9e067..000000000 --- a/src/UI/System/Logs/Files/ContentsView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Logs/Files/ContentsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/ContentsViewTemplate.hbs b/src/UI/System/Logs/Files/ContentsViewTemplate.hbs deleted file mode 100644 index c9a21b736..000000000 --- a/src/UI/System/Logs/Files/ContentsViewTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <h3>{{filename}}</h3> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <pre>{{contents}}</pre> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/DownloadLogCell.js b/src/UI/System/Logs/Files/DownloadLogCell.js deleted file mode 100644 index 8be2d0176..000000000 --- a/src/UI/System/Logs/Files/DownloadLogCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'download-log-cell', - - render : function() { - this.$el.empty(); - this.$el.html('<a href="{0}" class="no-router" target="_blank">Download</a>'.format(this.cellValue)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/FilenameCell.js b/src/UI/System/Logs/Files/FilenameCell.js deleted file mode 100644 index aedbb8cfb..000000000 --- a/src/UI/System/Logs/Files/FilenameCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-filename-cell', - - render : function() { - var filename = this._getValue(); - this.$el.html(filename); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileCollection.js b/src/UI/System/Logs/Files/LogFileCollection.js deleted file mode 100644 index 590dda677..000000000 --- a/src/UI/System/Logs/Files/LogFileCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LogFileModel = require('./LogFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/log/file', - model : LogFileModel, - - state : { - sortKey : 'lastWriteTime', - order : 1 - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayout.js b/src/UI/System/Logs/Files/LogFileLayout.js deleted file mode 100644 index 39516091e..000000000 --- a/src/UI/System/Logs/Files/LogFileLayout.js +++ /dev/null @@ -1,135 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var FilenameCell = require('./FilenameCell'); -var RelativeDateCell = require('../../../Cells/RelativeDateCell'); -var DownloadLogCell = require('./DownloadLogCell'); -var LogFileRow = require('./Row'); -var ContentsView = require('./ContentsView'); -var ContentsModel = require('./ContentsModel'); -var ToolbarLayout = require('../../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../../Shared/LoadingView'); -require('../../../jQuery/jquery.spin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/Files/LogFileLayoutTemplate', - - regions : { - toolbar : '#x-toolbar', - grid : '#x-grid', - contents : '#x-contents' - }, - - columns : [ - { - name : 'filename', - label : 'Filename', - cell : FilenameCell, - sortable : false - }, - { - name : 'lastWriteTime', - label : 'Last Write Time', - cell : RelativeDateCell, - sortable : false - }, - { - name : 'downloadUrl', - label : '', - cell : DownloadLogCell, - sortable : false - } - ], - - initialize : function(options) { - this.collection = options.collection; - this.deleteFilesCommand = options.deleteFilesCommand; - - this.listenTo(vent, vent.Commands.ShowLogFile, this._fetchLogFileContents); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - this.listenTo(this.collection, 'sync', this._collectionSynced); - - this.collection.fetch(); - }, - - onShow : function() { - this._showToolbar(); - this._showTable(); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Refresh', - icon : 'icon-lidarr-refresh', - ownerContext : this, - callback : this._refreshTable - }, - { - title : 'Clear Log Files', - icon : 'icon-lidarr-clear', - command : this.deleteFilesCommand, - successMessage : 'Log files have been deleted', - errorMessage : 'Failed to delete log files' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - context : this - })); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : LogFileRow, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - }, - - _collectionSynced : function() { - if (!this.collection.any()) { - return; - } - - var model = this.collection.first(); - this._fetchLogFileContents({ model : model }); - }, - - _fetchLogFileContents : function(options) { - this.contents.show(new LoadingView()); - - var model = options.model; - var contentsModel = new ContentsModel(model.toJSON()); - - this.listenToOnce(contentsModel, 'sync', this._showDetails); - - contentsModel.fetch({ dataType : 'text' }); - }, - - _showDetails : function(model) { - this.contents.show(new ContentsView({ model : model })); - }, - - _refreshTable : function(buttonContext) { - this.contents.close(); - var promise = this.collection.fetch(); - - //Would be nice to spin the icon on the refresh button - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === this.deleteFilesCommand.toLowerCase()) { - this._refreshTable(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs b/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs deleted file mode 100644 index 0188f1d0e..000000000 --- a/src/UI/System/Logs/Files/LogFileLayoutTemplate.hbs +++ /dev/null @@ -1,12 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid"/> - </div> -</div> - -<div class="row"> - <div class="col-md-12"> - <div id="x-contents"/> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/Files/LogFileModel.js b/src/UI/System/Logs/Files/LogFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/Files/LogFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/Files/Row.js b/src/UI/System/Logs/Files/Row.js deleted file mode 100644 index 01e6bd55a..000000000 --- a/src/UI/System/Logs/Files/Row.js +++ /dev/null @@ -1,14 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'log-file-row', - - events : { - 'click' : '_showDetails' - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowLogFile, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsCollection.js b/src/UI/System/Logs/LogsCollection.js deleted file mode 100644 index c233a9d63..000000000 --- a/src/UI/System/Logs/LogsCollection.js +++ /dev/null @@ -1,64 +0,0 @@ -var PagableCollection = require('backbone.pageable'); -var LogsModel = require('./LogsModel'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/log', - model : LogsModel, - tableName : 'logs', - - state : { - pageSize : 50, - sortKey : 'time', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - // Filter Modes - filterModes : { - "all" : [ - null, - null - ], - "info" : [ - 'level', - 'Info' - ], - "warn" : [ - 'level', - 'Warn' - ], - "error" : [ - 'level', - 'Error' - ] - }, - - parseState : function(resp, queryParams, state) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -collection = AsFilteredCollection.apply(collection); - -module.exports = AsPersistedStateCollection.apply(collection); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsLayout.js b/src/UI/System/Logs/LogsLayout.js deleted file mode 100644 index d064cff64..000000000 --- a/src/UI/System/Logs/LogsLayout.js +++ /dev/null @@ -1,64 +0,0 @@ -var Marionette = require('marionette'); -var LogsTableLayout = require('./Table/LogsTableLayout'); -var LogsFileLayout = require('./Files/LogFileLayout'); -var LogFileCollection = require('./Files/LogFileCollection'); -var UpdateLogFileCollection = require('./Updates/LogFileCollection'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/LogsLayoutTemplate', - - ui : { - tableTab : '.x-table-tab', - filesTab : '.x-files-tab', - updateFilesTab : '.x-update-files-tab' - }, - - regions : { - table : '#table', - files : '#files', - updateFiles : '#update-files' - }, - - events : { - 'click .x-table-tab' : '_showTable', - 'click .x-files-tab' : '_showFiles', - 'click .x-update-files-tab' : '_showUpdateFiles' - }, - - onShow : function() { - this._showTable(); - }, - - _showTable : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.tableTab.tab('show'); - this.table.show(new LogsTableLayout()); - }, - - _showFiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.filesTab.tab('show'); - this.files.show(new LogsFileLayout({ - collection : new LogFileCollection(), - deleteFilesCommand : 'deleteLogFiles' - })); - }, - - _showUpdateFiles : function(e) { - if (e) { - e.preventDefault(); - } - - this.ui.updateFilesTab.tab('show'); - this.updateFiles.show(new LogsFileLayout({ - collection : new UpdateLogFileCollection(), - deleteFilesCommand : 'deleteUpdateLogFiles' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/LogsLayoutTemplate.hbs b/src/UI/System/Logs/LogsLayoutTemplate.hbs deleted file mode 100644 index 1bf663f29..000000000 --- a/src/UI/System/Logs/LogsLayoutTemplate.hbs +++ /dev/null @@ -1,17 +0,0 @@ -<div class="row"> - <div class="col-md-2 col-sm-2"> - <ul class="nav nav-pills nav-stacked"> - <li><a href="#table" class="x-table-tab no-router">Table</a></li> - <li><a href="#files" class="x-files-tab no-router">Files</a></li> - <li><a href="#update-files" class="x-update-files-tab no-router">Updates</a></li> - </ul> - </div> - - <div class="col-md-10 col-sm-10"> - <div class="tab-content"> - <div class="tab-pane" id="table"></div> - <div class="tab-pane" id="files"></div> - <div class="tab-pane" id="update-files"></div> - </div> - </div> -</div> \ No newline at end of file diff --git a/src/UI/System/Logs/LogsModel.js b/src/UI/System/Logs/LogsModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/LogsModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsView.js b/src/UI/System/Logs/Table/Details/LogDetailsView.js deleted file mode 100644 index dcdadcf0b..000000000 --- a/src/UI/System/Logs/Table/Details/LogDetailsView.js +++ /dev/null @@ -1,6 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Logs/Table/Details/LogDetailsViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs b/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs deleted file mode 100644 index 80a8f7d26..000000000 --- a/src/UI/System/Logs/Table/Details/LogDetailsViewTemplate.hbs +++ /dev/null @@ -1,23 +0,0 @@ -<div class="modal-content"> - <div class="log-details-modal"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - - <h3>Details</h3> - - </div> - <div class="modal-body"> - Message - <pre>{{message}}</pre> - - {{#if exception}} - <br/> - Exception - <pre>{{exception}}</pre> - {{/if}} - </div> - <div class="modal-footer"> - <button class="btn" data-dismiss="modal">Close</button> - </div> - </div> -</div> diff --git a/src/UI/System/Logs/Table/LogLevelCell.js b/src/UI/System/Logs/Table/LogLevelCell.js deleted file mode 100644 index e09264cd8..000000000 --- a/src/UI/System/Logs/Table/LogLevelCell.js +++ /dev/null @@ -1,12 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); - -module.exports = NzbDroneCell.extend({ - className : 'log-level-cell', - - render : function() { - var level = this._getValue(); - this.$el.html('<i class="icon-lidarr-log-{0}" title="{1}"/>'.format(this._getValue().toLowerCase(), level)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogRow.js b/src/UI/System/Logs/Table/LogRow.js deleted file mode 100644 index c8cf6eb97..000000000 --- a/src/UI/System/Logs/Table/LogRow.js +++ /dev/null @@ -1,14 +0,0 @@ -var vent = require('vent'); -var Backgrid = require('backgrid'); - -module.exports = Backgrid.Row.extend({ - className : 'log-row', - - events : { - 'click' : '_showDetails' - }, - - _showDetails : function() { - vent.trigger(vent.Commands.ShowLogDetails, { model : this.model }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogTimeCell.js b/src/UI/System/Logs/Table/LogTimeCell.js deleted file mode 100644 index 1adbab10e..000000000 --- a/src/UI/System/Logs/Table/LogTimeCell.js +++ /dev/null @@ -1,31 +0,0 @@ -var NzbDroneCell = require('../../../Cells/NzbDroneCell'); -var moment = require('moment'); -var FormatHelpers = require('../../../Shared/FormatHelpers'); -var UiSettings = require('../../../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'log-time-cell', - - render : function() { - var dateStr = this._getValue(); - var date = moment(dateStr); - var diff = date.diff(moment().zone(date.zone()).startOf('day'), 'days', true); - var result = '<span title="{0}">{1}</span>'; - var tooltip = date.format(UiSettings.longDateTime(true)); - var text; - - if (diff > 0 && diff < 1) { - text = date.format(UiSettings.time(true, false)); - } else { - if (UiSettings.get('showRelativeDates')) { - text = FormatHelpers.relativeDate(dateStr); - } else { - text = date.format(UiSettings.get('shortDateFormat')); - } - } - - this.$el.html(result.format(tooltip, text)); - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogsTableLayout.js b/src/UI/System/Logs/Table/LogsTableLayout.js deleted file mode 100644 index d5668a9de..000000000 --- a/src/UI/System/Logs/Table/LogsTableLayout.js +++ /dev/null @@ -1,175 +0,0 @@ -var vent = require('vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var LogTimeCell = require('./LogTimeCell'); -var LogLevelCell = require('./LogLevelCell'); -var LogRow = require('./LogRow'); -var GridPager = require('../../../Shared/Grid/Pager'); -var LogCollection = require('../LogsCollection'); -var ToolbarLayout = require('../../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../../Shared/LoadingView'); -require('../../../jQuery/jquery.spin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Logs/Table/LogsTableLayoutTemplate', - - regions : { - grid : '#x-grid', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - attributes : { - id : 'logs-screen' - }, - - columns : [ - { - name : 'level', - label : '', - sortable : true, - cell : LogLevelCell - }, - { - name : 'logger', - label : 'Component', - sortable : true, - cell : Backgrid.StringCell.extend({ - className : 'log-logger-cell' - }) - }, - { - name : 'message', - label : 'Message', - sortable : false, - cell : Backgrid.StringCell.extend({ - className : 'log-message-cell' - }) - }, - { - name : 'time', - label : 'Time', - cell : LogTimeCell - } - ], - - initialize : function() { - this.collection = new LogCollection(); - - this.listenTo(this.collection, 'sync', this._showTable); - this.listenTo(vent, vent.Events.CommandComplete, this._commandComplete); - }, - - onRender : function() { - this.grid.show(new LoadingView()); - }, - - onShow : function() { - this._showToolbar(); - }, - - _showTable : function() { - this.grid.show(new Backgrid.Grid({ - row : LogRow, - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - })); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var filterButtons = { - type : 'radio', - storeState : true, - menuKey : 'logs.filterMode', - defaultAction : 'all', - items : [ - { - key : 'all', - title : '', - tooltip : 'All', - icon : 'icon-lidarr-all', - callback : this._setFilter - }, - { - key : 'info', - title : '', - tooltip : 'Info', - icon : 'icon-lidarr-log-info', - callback : this._setFilter - }, - { - key : 'warn', - title : '', - tooltip : 'Warn', - icon : 'icon-lidarr-log-warn', - callback : this._setFilter - }, - { - key : 'error', - title : '', - tooltip : 'Error', - icon : 'icon-lidarr-log-error', - callback : this._setFilter - } - ] - }; - - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Refresh', - icon : 'icon-lidarr-refresh', - ownerContext : this, - callback : this._refreshTable - }, - { - title : 'Clear Logs', - icon : 'icon-lidarr-clear', - command : 'clearLog' - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterButtons], - context : this - })); - }, - - _refreshTable : function(buttonContext) { - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.setFilterMode(mode, { reset : false }); - - this.collection.state.currentPage = 1; - var promise = this.collection.fetch({ reset : true }); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _commandComplete : function(options) { - if (options.command.get('name') === 'clearlog') { - this._refreshTable(); - } - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs b/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs deleted file mode 100644 index 1d579ffcd..000000000 --- a/src/UI/System/Logs/Table/LogsTableLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-grid" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/System/Logs/Updates/LogFileCollection.js b/src/UI/System/Logs/Updates/LogFileCollection.js deleted file mode 100644 index 1f957dbf1..000000000 --- a/src/UI/System/Logs/Updates/LogFileCollection.js +++ /dev/null @@ -1,12 +0,0 @@ -var Backbone = require('backbone'); -var LogFileModel = require('./LogFileModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/log/file/update', - model : LogFileModel, - - state : { - sortKey : 'lastWriteTime', - order : 1 - } -}); \ No newline at end of file diff --git a/src/UI/System/Logs/Updates/LogFileModel.js b/src/UI/System/Logs/Updates/LogFileModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Logs/Updates/LogFileModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Logs/logs.less b/src/UI/System/Logs/logs.less deleted file mode 100644 index 7142583ad..000000000 --- a/src/UI/System/Logs/logs.less +++ /dev/null @@ -1,25 +0,0 @@ -@import "../../Shared/Styles/clickable"; - -#logs-screen { - - .log-time-cell{ - width: 100px; - } - - .log-level-cell{ - width: 12px; - font-size: 14px; - } - - td{ - font-size: 13px; - } -} - -.log-file-row { - .clickable; -} - -.log-row { - .clickable; -} \ No newline at end of file diff --git a/src/UI/System/StatusModel.js b/src/UI/System/StatusModel.js deleted file mode 100644 index 075dd0918..000000000 --- a/src/UI/System/StatusModel.js +++ /dev/null @@ -1,9 +0,0 @@ -var Backbone = require('backbone'); -var ApiData = require('../Shared/ApiData'); - -var StatusModel = Backbone.Model.extend({ - url : window.NzbDrone.ApiRoot + '/system/status' -}); -var instance = new StatusModel(ApiData.get('system/status')); - -module.exports = instance; \ No newline at end of file diff --git a/src/UI/System/SystemLayout.js b/src/UI/System/SystemLayout.js deleted file mode 100644 index 33e18db34..000000000 --- a/src/UI/System/SystemLayout.js +++ /dev/null @@ -1,150 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var SystemInfoLayout = require('./Info/SystemInfoLayout'); -var LogsLayout = require('./Logs/LogsLayout'); -var UpdateLayout = require('./Update/UpdateLayout'); -var BackupLayout = require('./Backup/BackupLayout'); -var TaskLayout = require('./Task/TaskLayout'); -var Messenger = require('../Shared/Messenger'); -var StatusModel = require('./StatusModel'); - -module.exports = Marionette.Layout.extend({ - template : 'System/SystemLayoutTemplate', - - regions : { - status : '#status', - logs : '#logs', - updates : '#updates', - backup : '#backup', - tasks : '#tasks' - }, - - ui : { - statusTab : '.x-status-tab', - logsTab : '.x-logs-tab', - updatesTab : '.x-updates-tab', - backupTab : '.x-backup-tab', - tasksTab : '.x-tasks-tab' - }, - - events : { - 'click .x-status-tab' : '_showStatus', - 'click .x-logs-tab' : '_showLogs', - 'click .x-updates-tab' : '_showUpdates', - 'click .x-backup-tab' : '_showBackup', - 'click .x-tasks-tab' : '_showTasks', - 'click .x-shutdown' : '_shutdown', - 'click .x-restart' : '_restart' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - - this.templateHelpers = { - authentication : StatusModel.get('authentication') - }; - }, - - onShow : function() { - switch (this.action) { - case 'logs': - this._showLogs(); - break; - case 'updates': - this._showUpdates(); - break; - case 'backup': - this._showBackup(); - break; - case 'tasks': - this._showTasks(); - break; - default: - this._showStatus(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : true, - replace : true - }); - }, - - _showStatus : function(e) { - if (e) { - e.preventDefault(); - } - - this.status.show(new SystemInfoLayout()); - this.ui.statusTab.tab('show'); - this._navigate('system/status'); - }, - - _showLogs : function(e) { - if (e) { - e.preventDefault(); - } - - this.logs.show(new LogsLayout()); - this.ui.logsTab.tab('show'); - this._navigate('system/logs'); - }, - - _showUpdates : function(e) { - if (e) { - e.preventDefault(); - } - - this.updates.show(new UpdateLayout()); - this.ui.updatesTab.tab('show'); - this._navigate('system/updates'); - }, - - _showBackup : function(e) { - if (e) { - e.preventDefault(); - } - - this.backup.show(new BackupLayout()); - this.ui.backupTab.tab('show'); - this._navigate('system/backup'); - }, - - _showTasks : function(e) { - if (e) { - e.preventDefault(); - } - - this.tasks.show(new TaskLayout()); - this.ui.tasksTab.tab('show'); - this._navigate('system/tasks'); - }, - - _shutdown : function() { - $.ajax({ - url : window.NzbDrone.ApiRoot + '/system/shutdown', - type : 'POST' - }); - - Messenger.show({ - message : 'Lidarr will shutdown shortly', - type : 'info' - }); - }, - - _restart : function() { - $.ajax({ - url : window.NzbDrone.ApiRoot + '/system/restart', - type : 'POST' - }); - - Messenger.show({ - message : 'Lidarr will restart shortly', - type : 'info' - }); - } -}); \ No newline at end of file diff --git a/src/UI/System/SystemLayoutTemplate.hbs b/src/UI/System/SystemLayoutTemplate.hbs deleted file mode 100644 index af2adf692..000000000 --- a/src/UI/System/SystemLayoutTemplate.hbs +++ /dev/null @@ -1,31 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#status" class="x-status-tab no-router">Status</a></li> - <li><a href="#updates" class="x-updates-tab no-router">Updates</a></li> - <li><a href="#tasks" class="x-tasks-tab no-router">Tasks</a></li> - <li><a href="#backup" class="x-backup-tab no-router">Backup</a></li> - <li><a href="#logs" class="x-logs-tab no-router">Logs</a></li> - <li class="lifecycle-controls pull-right"> - <div class="btn-group"> - <button class="btn btn-default btn-icon-only x-shutdown" title="Shutdown"> - <i class="icon-lidarr-shutdown"></i> - </button> - <button class="btn btn-default btn-icon-only x-restart" title="Restart"> - <i class="icon-lidarr-restart"></i> - </button> - - {{#if_eq authentication compare="forms"}} - <a href="{{UrlBase}}/logout" class="btn btn-default btn-icon-only" title="Logout"> - <i class="icon-lidarr-logout"></i> - </a> - {{/if_eq}} - </div> - </li> -</ul> - -<div class="tab-content"> - <div class="tab-pane" id="status"></div> - <div class="tab-pane" id="updates"></div> - <div class="tab-pane" id="tasks"></div> - <div class="tab-pane" id="backup"></div> - <div class="tab-pane" id="logs"></div> -</div> \ No newline at end of file diff --git a/src/UI/System/Task/ExecuteTaskCell.js b/src/UI/System/Task/ExecuteTaskCell.js deleted file mode 100644 index 10a510dbb..000000000 --- a/src/UI/System/Task/ExecuteTaskCell.js +++ /dev/null @@ -1,30 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = NzbDroneCell.extend({ - className : 'execute-task-cell', - - events : { - 'click .x-execute' : '_executeTask' - }, - - render : function() { - this.$el.empty(); - - var name = this.model.get('name'); - var task = this.model.get('taskName'); - - this.$el.html('<i class="icon-lidarr-refresh icon-can-spin x-execute" title="Execute {0}"></i>'.format(name)); - - CommandController.bindToCommand({ - element : this.$el.find('.x-execute'), - command : { name : task } - }); - - return this; - }, - - _executeTask : function() { - CommandController.Execute(this.model.get('taskName'), { name : this.model.get('taskName') }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/NextExecutionCell.js b/src/UI/System/Task/NextExecutionCell.js deleted file mode 100644 index 39140f5a9..000000000 --- a/src/UI/System/Task/NextExecutionCell.js +++ /dev/null @@ -1,34 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); -var UiSettings = require('../../Shared/UiSettingsModel'); - -module.exports = NzbDroneCell.extend({ - className : 'next-execution-cell', - - render : function() { - this.$el.empty(); - - var interval = this.model.get('interval'); - var nextExecution = moment(this.model.get('nextExecution')); - - if (interval === 0) { - this.$el.html('-'); - } else if (moment().isAfter(nextExecution)) { - this.$el.html('now'); - } else { - var result = '<span title="{0}">{1}</span>'; - var tooltip = nextExecution.format(UiSettings.longDateTime()); - var text; - - if (UiSettings.get('showRelativeDates')) { - text = nextExecution.fromNow(); - } else { - text = nextExecution.format(UiSettings.shortDateTime()); - } - - this.$el.html(result.format(tooltip, text)); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskCollection.js b/src/UI/System/Task/TaskCollection.js deleted file mode 100644 index bca599554..000000000 --- a/src/UI/System/Task/TaskCollection.js +++ /dev/null @@ -1,15 +0,0 @@ -var PageableCollection = require('backbone.pageable'); -var TaskModel = require('./TaskModel'); - -module.exports = PageableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/system/task', - model : TaskModel, - - state : { - sortKey : 'name', - order : -1, - pageSize : 100000 - }, - - mode : 'client' -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskIntervalCell.js b/src/UI/System/Task/TaskIntervalCell.js deleted file mode 100644 index b2f246c48..000000000 --- a/src/UI/System/Task/TaskIntervalCell.js +++ /dev/null @@ -1,21 +0,0 @@ -var NzbDroneCell = require('../../Cells/NzbDroneCell'); -var moment = require('moment'); - -module.exports = NzbDroneCell.extend({ - className : 'task-interval-cell', - - render : function() { - this.$el.empty(); - - var interval = this.model.get('interval'); - var duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1'); - - if (interval === 0) { - this.$el.html('disabled'); - } else { - this.$el.html(duration); - } - - return this; - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskLayout.js b/src/UI/System/Task/TaskLayout.js deleted file mode 100644 index 621797e3c..000000000 --- a/src/UI/System/Task/TaskLayout.js +++ /dev/null @@ -1,71 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var BackupCollection = require('./TaskCollection'); -var RelativeTimeCell = require('../../Cells/RelativeTimeCell'); -var TaskIntervalCell = require('./TaskIntervalCell'); -var ExecuteTaskCell = require('./ExecuteTaskCell'); -var NextExecutionCell = require('./NextExecutionCell'); -var LoadingView = require('../../Shared/LoadingView'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Task/TaskLayoutTemplate', - - regions : { - tasks : '#x-tasks' - }, - - columns : [ - { - name : 'name', - label : 'Name', - sortable : true, - cell : 'string' - }, - { - name : 'interval', - label : 'Interval', - sortable : true, - cell : TaskIntervalCell - }, - { - name : 'lastExecution', - label : 'Last Execution', - sortable : true, - cell : RelativeTimeCell - }, - { - name : 'nextExecution', - label : 'Next Execution', - sortable : true, - cell : NextExecutionCell - }, - { - name : 'this', - label : '', - sortable : false, - cell : ExecuteTaskCell - } - ], - - initialize : function() { - this.taskCollection = new BackupCollection(); - - this.listenTo(this.taskCollection, 'sync', this._showTasks); - this.taskCollection.bindSignalR(); - }, - - onRender : function() { - this.tasks.show(new LoadingView()); - - this.taskCollection.fetch(); - }, - - _showTasks : function() { - this.tasks.show(new Backgrid.Grid({ - columns : this.columns, - collection : this.taskCollection, - className : 'table table-hover' - })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Task/TaskLayoutTemplate.hbs b/src/UI/System/Task/TaskLayoutTemplate.hbs deleted file mode 100644 index 0a3631541..000000000 --- a/src/UI/System/Task/TaskLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-tasks" class="tasks table-responsive"/> - </div> -</div> diff --git a/src/UI/System/Task/TaskModel.js b/src/UI/System/Task/TaskModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Task/TaskModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Update/EmptyView.js b/src/UI/System/Update/EmptyView.js deleted file mode 100644 index a18f84f4d..000000000 --- a/src/UI/System/Update/EmptyView.js +++ /dev/null @@ -1,5 +0,0 @@ -var Marionette = require('marionette'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Update/EmptyViewTemplate' -}); \ No newline at end of file diff --git a/src/UI/System/Update/EmptyViewTemplate.hbs b/src/UI/System/Update/EmptyViewTemplate.hbs deleted file mode 100644 index 728e10d93..000000000 --- a/src/UI/System/Update/EmptyViewTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<div>No updates are available</div> \ No newline at end of file diff --git a/src/UI/System/Update/UpdateCollection.js b/src/UI/System/Update/UpdateCollection.js deleted file mode 100644 index 2b21a6616..000000000 --- a/src/UI/System/Update/UpdateCollection.js +++ /dev/null @@ -1,7 +0,0 @@ -var Backbone = require('backbone'); -var UpdateModel = require('./UpdateModel'); - -module.exports = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/update', - model : UpdateModel -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateCollectionView.js b/src/UI/System/Update/UpdateCollectionView.js deleted file mode 100644 index 7af0bfc73..000000000 --- a/src/UI/System/Update/UpdateCollectionView.js +++ /dev/null @@ -1,8 +0,0 @@ -var Marionette = require('marionette'); -var UpdateItemView = require('./UpdateItemView'); -var EmptyView = require('./EmptyView'); - -module.exports = Marionette.CollectionView.extend({ - itemView : UpdateItemView, - emptyView : EmptyView -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateItemView.js b/src/UI/System/Update/UpdateItemView.js deleted file mode 100644 index 73ed31e0c..000000000 --- a/src/UI/System/Update/UpdateItemView.js +++ /dev/null @@ -1,31 +0,0 @@ -var Marionette = require('marionette'); -var CommandController = require('../../Commands/CommandController'); - -module.exports = Marionette.ItemView.extend({ - template : 'System/Update/UpdateItemViewTemplate', - - events : { - 'click .x-install-update' : '_installUpdate' - }, - - initialize : function() { - this.updating = false; - }, - - _installUpdate : function() { - if (this.updating) { - return; - } - - this.updating = true; - var self = this; - - var promise = CommandController.Execute('applicationUpdate'); - - promise.done(function() { - window.setTimeout(function() { - self.updating = false; - }, 5000); - }); - } -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateItemViewTemplate.hbs b/src/UI/System/Update/UpdateItemViewTemplate.hbs deleted file mode 100644 index f9a2dce19..000000000 --- a/src/UI/System/Update/UpdateItemViewTemplate.hbs +++ /dev/null @@ -1,43 +0,0 @@ -<div class="update"> - <fieldset> - <legend>{{version}} - <span class="date"> - - {{ShortDate releaseDate}} - </span> - <span class="status"> - {{#unless_eq branch compare="master"}} - <span class="label label-default">{{branch}}</span> - {{/unless_eq}} - {{#if installed}} - <span class="label label-success">Installed</span> - {{else}} - {{#if latest}} - {{#if installable}} - <span class="label label-info install-update x-install-update">Install Latest</span> - {{else}} - <span class="label label-info label-disabled" title="Cannot install an older version">Install Latest</span> - {{/if}} - {{/if}} - {{/if}} - </span> - </legend> - - {{#with changes}} - {{#each new}} - <div class="change"> - <span class="label label-success">New</span> {{this}} - </div> - {{/each}} - - {{#each fixed}} - <div class="change"> - <span class="label label-info">Fixed</span> {{this}} - </div> - {{/each}} - {{/with}} - - {{#unless changes}} - Maintenance release - {{/unless}} - </fieldset> -</div> diff --git a/src/UI/System/Update/UpdateLayout.js b/src/UI/System/Update/UpdateLayout.js deleted file mode 100644 index a1fd84d06..000000000 --- a/src/UI/System/Update/UpdateLayout.js +++ /dev/null @@ -1,29 +0,0 @@ -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var UpdateCollection = require('./UpdateCollection'); -var UpdateCollectionView = require('./UpdateCollectionView'); -var LoadingView = require('../../Shared/LoadingView'); - -module.exports = Marionette.Layout.extend({ - template : 'System/Update/UpdateLayoutTemplate', - - regions : { - updates : '#x-updates' - }, - - initialize : function() { - this.updateCollection = new UpdateCollection(); - - this.listenTo(this.updateCollection, 'sync', this._showUpdates); - }, - - onRender : function() { - this.updates.show(new LoadingView()); - - this.updateCollection.fetch(); - }, - - _showUpdates : function() { - this.updates.show(new UpdateCollectionView({ collection : this.updateCollection })); - } -}); \ No newline at end of file diff --git a/src/UI/System/Update/UpdateLayoutTemplate.hbs b/src/UI/System/Update/UpdateLayoutTemplate.hbs deleted file mode 100644 index 0bd69dc20..000000000 --- a/src/UI/System/Update/UpdateLayoutTemplate.hbs +++ /dev/null @@ -1,5 +0,0 @@ -<div class="row"> - <div class="col-md-12"> - <div id="x-updates"/> - </div> -</div> diff --git a/src/UI/System/Update/UpdateModel.js b/src/UI/System/Update/UpdateModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/System/Update/UpdateModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/System/Update/update.less b/src/UI/System/Update/update.less deleted file mode 100644 index 1db354fd6..000000000 --- a/src/UI/System/Update/update.less +++ /dev/null @@ -1,51 +0,0 @@ -@import '../../Shared/Styles/clickable'; - -.update { - margin-bottom: 30px; - - legend { - cursor : default; - margin-bottom : 5px; - line-height : 30px; - display : inline-block; - - .date { - font-size : 16px; - display : inline-block; - } - - .status { - margin-left : 5px; - font-size : 14px; - margin-top : -3px; - display : inline-block; - vertical-align : middle; - } - - .install-update { - .clickable(); - } - } - - .changes-header { - font-size: 18px; - } - - .label { - width: 40px; - text-align: center; - } - .change { - margin-bottom: 2px; - font-size: 13px; - } - - a { - color: white; - text-decoration: none; - } - - a:hover { - text-decoration: none; - } -} \ No newline at end of file diff --git a/src/UI/Tags/TagCollection.js b/src/UI/Tags/TagCollection.js deleted file mode 100644 index 287f6eaef..000000000 --- a/src/UI/Tags/TagCollection.js +++ /dev/null @@ -1,14 +0,0 @@ -var Backbone = require('backbone'); -var TagModel = require('./TagModel'); -var ApiData = require('../Shared/ApiData'); - -var Collection = Backbone.Collection.extend({ - url : window.NzbDrone.ApiRoot + '/tag', - model : TagModel, - - comparator : function(model) { - return model.get('label'); - } -}); - -module.exports = new Collection(ApiData.get('tag')); diff --git a/src/UI/Tags/TagHelpers.js b/src/UI/Tags/TagHelpers.js deleted file mode 100644 index a745de057..000000000 --- a/src/UI/Tags/TagHelpers.js +++ /dev/null @@ -1,25 +0,0 @@ -var _ = require('underscore'); -var Handlebars = require('handlebars'); -var TagCollection = require('./TagCollection'); - -Handlebars.registerHelper('tagDisplay', function(tags) { - var tagLabels = _.map(TagCollection.filter(function(tag) { - return _.contains(tags, tag.get('id')); - }), function(tag) { - return '<span class="label label-info">{0}</span>'.format(tag.get('label')); - }); - - return new Handlebars.SafeString(tagLabels.join(' ')); -}); - -Handlebars.registerHelper('genericTagDisplay', function(tags, classes) { - if (!tags) { - return new Handlebars.SafeString(''); - } - - var tagLabels = _.map(tags.split(','), function(tag) { - return '<span class="{0}">{1}</span>'.format(classes, tag); - }); - - return new Handlebars.SafeString(tagLabels.join(' ')); -}); \ No newline at end of file diff --git a/src/UI/Tags/TagModel.js b/src/UI/Tags/TagModel.js deleted file mode 100644 index 3986a5948..000000000 --- a/src/UI/Tags/TagModel.js +++ /dev/null @@ -1,3 +0,0 @@ -var Backbone = require('backbone'); - -module.exports = Backbone.Model.extend({}); \ No newline at end of file diff --git a/src/UI/Wanted/ControlsColumnTemplate.hbs b/src/UI/Wanted/ControlsColumnTemplate.hbs deleted file mode 100644 index 75f689167..000000000 --- a/src/UI/Wanted/ControlsColumnTemplate.hbs +++ /dev/null @@ -1 +0,0 @@ -<i class="icon-lidarr-search x-search" title="Search"/> diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js b/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js deleted file mode 100644 index 5f2a6546f..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetCollection.js +++ /dev/null @@ -1,63 +0,0 @@ -var _ = require('underscore'); -var EpisodeModel = require('../../Series/EpisodeModel'); -var PagableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/wanted/cutoff', - model : EpisodeModel, - tableName : 'wanted.cutoff', - - state : { - pageSize : 15, - sortKey : 'airDateUtc', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - // Filter Modes - filterModes : { - 'monitored' : [ - 'monitored', - 'true' - ], - 'unmonitored' : [ - 'monitored', - 'false' - ], - }, - - sortMappings : { - 'series' : { sortKey : 'series.sortTitle' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); - -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); - -module.exports = AsPersistedStateCollection.call(Collection); \ No newline at end of file diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js b/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js deleted file mode 100644 index 69006e827..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayout.js +++ /dev/null @@ -1,188 +0,0 @@ -var _ = require('underscore'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var CutoffUnmetCollection = require('./CutoffUnmetCollection'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var SeriesTitleCell = require('../../Cells/SeriesTitleCell'); -var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var EpisodeTitleCell = require('../../Cells/EpisodeTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); -var Messenger = require('../../Shared/Messenger'); -var CommandController = require('../../Commands/CommandController'); -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/Cutoff/CutoffUnmetLayoutTemplate', - - regions : { - cutoff : '#x-cutoff-unmet', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - ui : { - searchSelectedButton : '.btn i.icon-lidarr-search' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'series', - label : 'Artist', - cell : SeriesTitleCell, - sortValue : 'series.sortTitle' - }, -// { -// name : 'this', -// label : 'Track Number', -// cell : EpisodeNumberCell, -// sortable : false -// }, - { - name : 'this', - label : 'Track Title', - cell : EpisodeTitleCell, - sortable : false - }, - { - name : 'airDateUtc', - label : 'Release Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new CutoffUnmetCollection().bindSignalR({ updateOnly : true }); - - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.cutoff.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function() { - this.cutoffGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - }); - - this.cutoff.show(this.cutoffGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - items : [ - { - title : 'Search Selected', - icon : 'icon-lidarr-search', - callback : this._searchSelected, - ownerContext : this, - className : 'x-search-selected' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - } - ] - }; - - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-lidarr-unmonitored', - callback : this._setFilter - } - ] - }; - - this.toolbar.show(new ToolbarLayout({ - left : [ - leftSideButtons - ], - right : [ - filterOptions - ], - context : this - })); - - CommandController.bindToCommand({ - element : this.$('.x-search-selected'), - command : { - name : 'episodeSearch' - } - }); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _searchSelected : function() { - var selected = this.cutoffGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No episodes selected' - }); - - return; - } - - var ids = _.pluck(selected, 'id'); - - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : ids - }); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs b/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs deleted file mode 100644 index 7c6d095c0..000000000 --- a/src/UI/Wanted/Cutoff/CutoffUnmetLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-cutoff-unmet" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Wanted/Missing/MissingCollection.js b/src/UI/Wanted/Missing/MissingCollection.js deleted file mode 100644 index eb2c8ae8d..000000000 --- a/src/UI/Wanted/Missing/MissingCollection.js +++ /dev/null @@ -1,61 +0,0 @@ -var _ = require('underscore'); -var EpisodeModel = require('../../Series/EpisodeModel'); -var PagableCollection = require('backbone.pageable'); -var AsFilteredCollection = require('../../Mixins/AsFilteredCollection'); -var AsSortedCollection = require('../../Mixins/AsSortedCollection'); -var AsPersistedStateCollection = require('../../Mixins/AsPersistedStateCollection'); - -var Collection = PagableCollection.extend({ - url : window.NzbDrone.ApiRoot + '/wanted/missing', - model : EpisodeModel, - tableName : 'wanted.missing', - - state : { - pageSize : 15, - sortKey : 'releaseDate', - order : 1 - }, - - queryParams : { - totalPages : null, - totalRecords : null, - pageSize : 'pageSize', - sortKey : 'sortKey', - order : 'sortDir', - directions : { - '-1' : 'asc', - '1' : 'desc' - } - }, - - filterModes : { - 'monitored' : [ - 'monitored', - 'true' - ], - 'unmonitored' : [ - 'monitored', - 'false' - ] - }, - - sortMappings : { - 'artist' : { sortKey : 'artist.sortName' } - }, - - parseState : function(resp) { - return { totalRecords : resp.totalRecords }; - }, - - parseRecords : function(resp) { - if (resp) { - return resp.records; - } - - return resp; - } -}); -Collection = AsFilteredCollection.call(Collection); -Collection = AsSortedCollection.call(Collection); - -module.exports = AsPersistedStateCollection.call(Collection); \ No newline at end of file diff --git a/src/UI/Wanted/Missing/MissingLayout.js b/src/UI/Wanted/Missing/MissingLayout.js deleted file mode 100644 index f7c750892..000000000 --- a/src/UI/Wanted/Missing/MissingLayout.js +++ /dev/null @@ -1,240 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); -var vent = require('../../vent'); -var Marionette = require('marionette'); -var Backgrid = require('backgrid'); -var MissingCollection = require('./MissingCollection'); -var SelectAllCell = require('../../Cells/SelectAllCell'); -var ArtistTitleCell = require('../../Cells/ArtistTitleCell'); -var EpisodeNumberCell = require('../../Cells/EpisodeNumberCell'); -var AlbumTitleCell = require('../../Cells/AlbumTitleCell'); -var RelativeDateCell = require('../../Cells/RelativeDateCell'); -var EpisodeStatusCell = require('../../Cells/EpisodeStatusCell'); -var GridPager = require('../../Shared/Grid/Pager'); -var ToolbarLayout = require('../../Shared/Toolbar/ToolbarLayout'); -var LoadingView = require('../../Shared/LoadingView'); -var Messenger = require('../../Shared/Messenger'); -var CommandController = require('../../Commands/CommandController'); - -require('backgrid.selectall'); -require('../../Mixins/backbone.signalr.mixin'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/Missing/MissingLayoutTemplate', - - regions : { - missing : '#x-missing', - toolbar : '#x-toolbar', - pager : '#x-pager' - }, - - ui : { - searchSelectedButton : '.btn i.icon-lidarr-search' - }, - - columns : [ - { - name : '', - cell : SelectAllCell, - headerCell : 'select-all', - sortable : false - }, - { - name : 'artist', - label : 'Artist', - cell : ArtistTitleCell, - sortValue : 'artist.sortName' - }, -// { -// name : 'this', -// label : 'Ttack Number', -// cell : EpisodeNumberCell, -// sortable : false -// }, - { - name : 'this', - label : 'Album Title', - cell : AlbumTitleCell, - sortable : false - }, - { - name : 'releaseDate', - label : 'Release Date', - cell : RelativeDateCell - }, - { - name : 'status', - label : 'Status', - cell : EpisodeStatusCell, - sortable : false - } - ], - - initialize : function() { - this.collection = new MissingCollection().bindSignalR({ updateOnly : true }); - - this.listenTo(this.collection, 'sync', this._showTable); - }, - - onShow : function() { - this.missing.show(new LoadingView()); - this._showToolbar(); - this.collection.fetch(); - }, - - _showTable : function() { - this.missingGrid = new Backgrid.Grid({ - columns : this.columns, - collection : this.collection, - className : 'table table-hover' - }); - - this.missing.show(this.missingGrid); - - this.pager.show(new GridPager({ - columns : this.columns, - collection : this.collection - })); - }, - - _showToolbar : function() { - var leftSideButtons = { - type : 'default', - storeState : false, - collapse : true, - items : [ - { - title : 'Search Selected', - icon : 'icon-lidarr-search', - callback : this._searchSelected, - ownerContext : this, - className : 'x-search-selected' - }, - { - title : 'Search All Missing', - icon : 'icon-lidarr-search', - callback : this._searchMissing, - ownerContext : this, - className : 'x-search-missing' - }, - { - title : 'Toggle Selected', - icon : 'icon-lidarr-monitored', - tooltip : 'Toggle monitored status of selected', - callback : this._toggleMonitoredOfSelected, - ownerContext : this, - className : 'x-unmonitor-selected' - }, - { - title : 'Album Studio', - icon : 'icon-lidarr-monitored', - route : 'albumstudio' - }, - { - title : 'Rescan Drone Factory Folder', - icon : 'icon-lidarr-refresh', - command : 'downloadedalbumsscan', - properties : { sendUpdates : true } - }, - { - title : 'Manual Import', - icon : 'icon-lidarr-search-manual', - callback : this._manualImport, - ownerContext : this - } - ] - }; - var filterOptions = { - type : 'radio', - storeState : false, - menuKey : 'wanted.filterMode', - defaultAction : 'monitored', - items : [ - { - key : 'monitored', - title : '', - tooltip : 'Monitored Only', - icon : 'icon-lidarr-monitored', - callback : this._setFilter - }, - { - key : 'unmonitored', - title : '', - tooltip : 'Unmonitored Only', - icon : 'icon-lidarr-unmonitored', - callback : this._setFilter - } - ] - }; - this.toolbar.show(new ToolbarLayout({ - left : [leftSideButtons], - right : [filterOptions], - context : this - })); - CommandController.bindToCommand({ - element : this.$('.x-search-selected'), - command : { name : 'episodeSearch' } - }); - CommandController.bindToCommand({ - element : this.$('.x-search-missing'), - command : { name : 'missingEpisodeSearch' } - }); - }, - - _setFilter : function(buttonContext) { - var mode = buttonContext.model.get('key'); - this.collection.state.currentPage = 1; - var promise = this.collection.setFilterMode(mode); - if (buttonContext) { - buttonContext.ui.icon.spinForPromise(promise); - } - }, - - _searchSelected : function() { - var selected = this.missingGrid.getSelectedModels(); - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No albums selected' - }); - return; - } - var ids = _.pluck(selected, 'id'); - CommandController.Execute('episodeSearch', { - name : 'episodeSearch', - episodeIds : ids - }); - }, - _searchMissing : function() { - if (window.confirm('Are you sure you want to search for {0} missing albums? '.format(this.collection.state.totalRecords) + - 'One API request to each indexer will be used for each album. ' + 'This cannot be stopped once started.')) { - CommandController.Execute('missingEpisodeSearch', { name : 'missingEpisodeSearch' }); - } - }, - _toggleMonitoredOfSelected : function() { - var selected = this.missingGrid.getSelectedModels(); - - if (selected.length === 0) { - Messenger.show({ - type : 'error', - message : 'No albums selected' - }); - return; - } - - var promises = []; - var self = this; - - _.each(selected, function (episode) { - episode.set('monitored', !episode.get('monitored')); - promises.push(episode.save()); - }); - - $.when(promises).done(function () { - self.collection.fetch(); - }); - }, - _manualImport : function () { - vent.trigger(vent.Commands.ShowManualImport); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs b/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs deleted file mode 100644 index 4fd573b09..000000000 --- a/src/UI/Wanted/Missing/MissingLayoutTemplate.hbs +++ /dev/null @@ -1,11 +0,0 @@ -<div id="x-toolbar"/> -<div class="row"> - <div class="col-md-12"> - <div id="x-missing" class="table-responsive"/> - </div> -</div> -<div class="row"> - <div class="col-md-12"> - <div id="x-pager"/> - </div> -</div> diff --git a/src/UI/Wanted/WantedLayout.js b/src/UI/Wanted/WantedLayout.js deleted file mode 100644 index f7cce2109..000000000 --- a/src/UI/Wanted/WantedLayout.js +++ /dev/null @@ -1,68 +0,0 @@ -var Marionette = require('marionette'); -var Backbone = require('backbone'); -var Backgrid = require('backgrid'); -var MissingLayout = require('./Missing/MissingLayout'); -var CutoffUnmetLayout = require('./Cutoff/CutoffUnmetLayout'); - -module.exports = Marionette.Layout.extend({ - template : 'Wanted/WantedLayoutTemplate', - - regions : { - content : '#content' - //missing : '#missing', - //cutoff : '#cutoff' - }, - - ui : { - missingTab : '.x-missing-tab', - cutoffTab : '.x-cutoff-tab' - }, - - events : { - 'click .x-missing-tab' : '_showMissing', - 'click .x-cutoff-tab' : '_showCutoffUnmet' - }, - - initialize : function(options) { - if (options.action) { - this.action = options.action.toLowerCase(); - } - }, - - onShow : function() { - switch (this.action) { - case 'cutoff': - this._showCutoffUnmet(); - break; - default: - this._showMissing(); - } - }, - - _navigate : function(route) { - Backbone.history.navigate(route, { - trigger : false, - replace : true - }); - }, - - _showMissing : function(e) { - if (e) { - e.preventDefault(); - } - - this.content.show(new MissingLayout()); - this.ui.missingTab.tab('show'); - this._navigate('/wanted/missing'); - }, - - _showCutoffUnmet : function(e) { - if (e) { - e.preventDefault(); - } - - this.content.show(new CutoffUnmetLayout()); - this.ui.cutoffTab.tab('show'); - this._navigate('/wanted/cutoff'); - } -}); \ No newline at end of file diff --git a/src/UI/Wanted/WantedLayoutTemplate.hbs b/src/UI/Wanted/WantedLayoutTemplate.hbs deleted file mode 100644 index 973feb838..000000000 --- a/src/UI/Wanted/WantedLayoutTemplate.hbs +++ /dev/null @@ -1,10 +0,0 @@ -<ul class="nav nav-tabs"> - <li><a href="#missing" class="x-missing-tab no-router">Missing</a></li> - <li><a href="#cutoff" class="x-cutoff-tab no-router">Cutoff Unmet</a></li> -</ul> - -<div class="tab-pane" id="content"></div> -<!--<div class="tab-content"> - <div class="tab-pane" id="missing"></div> - <div class="tab-pane" id="cutoff"></div> -</div>--> \ No newline at end of file diff --git a/src/UI/app.js b/src/UI/app.js deleted file mode 100644 index e2998dd72..000000000 --- a/src/UI/app.js +++ /dev/null @@ -1,160 +0,0 @@ -'use strict'; -require.config({ - - paths : { - 'backbone' : 'JsLibraries/backbone', - 'moment' : 'JsLibraries/moment', - 'filesize' : 'JsLibraries/filesize', - 'handlebars' : 'Shared/Shims/handlebars', - 'handlebars.helpers' : 'JsLibraries/handlebars.helpers', - 'bootstrap' : 'JsLibraries/bootstrap', - 'bootstrap.tagsinput' : 'JsLibraries/bootstrap.tagsinput', - 'backbone.deepmodel' : 'JsLibraries/backbone.deep.model', - 'backbone.pageable' : 'JsLibraries/backbone.pageable', - 'backbone.validation' : 'JsLibraries/backbone.validation', - 'backbone.modelbinder' : 'JsLibraries/backbone.modelbinder', - 'backbone.collectionview' : 'JsLibraries/backbone.collectionview', - 'backgrid' : 'JsLibraries/backbone.backgrid', - 'backgrid.paginator' : 'JsLibraries/backbone.backgrid.paginator', - 'backgrid.selectall' : 'JsLibraries/backbone.backgrid.selectall', - 'fullcalendar' : 'JsLibraries/fullcalendar', - 'backstrech' : 'JsLibraries/jquery.backstretch', - 'underscore' : 'JsLibraries/lodash.underscore', - 'marionette' : 'JsLibraries/backbone.marionette', - 'signalR' : 'JsLibraries/jquery.signalR', - 'jquery-ui' : 'JsLibraries/jquery-ui', - 'jquery.knob' : 'JsLibraries/jquery.knob', - 'jquery.easypiechart' : 'JsLibraries/jquery.easypiechart', - 'jquery.dotdotdot' : 'JsLibraries/jquery.dotdotdot', - 'messenger' : 'JsLibraries/messenger', - 'jquery' : 'JsLibraries/jquery', - 'typeahead' : 'JsLibraries/typeahead', - 'zero.clipboard' : 'JsLibraries/zero.clipboard', - 'jdu' : 'JsLibraries/jdu', - 'libs' : 'JsLibraries/' - }, - - shim : { - api : { - deps : ['jquery'] - }, - jquery : { - exports : '$' - }, - messenger : { - deps : ['jquery'], - exports : 'Messenger', - init : function() { - window.Messenger.options = { - theme : 'flat' - }; - } - }, - signalR : { - deps : ['jquery'] - }, - bootstrap : { - deps : ['jquery'] - }, - 'bootstrap.tagsinput' : { - deps : [ - 'bootstrap', - 'typeahead' - ] - }, - backstrech : { - deps : ['jquery'] - }, - underscore : { - deps : ['jquery'], - exports : '_' - }, - backbone : { - deps : [ - 'jquery', - 'Instrumentation/ErrorHandler', - 'underscore', - 'Mixins/jquery.ajax', - 'jQuery/ToTheTop' - ], - exports : 'Backbone' - }, - marionette : { - deps : [ - 'backbone', - 'Handlebars/backbone.marionette.templates', - 'Mixins/AsNamedView' - ], - exports : 'Marionette', - init : function(Backbone, TemplateMixin, AsNamedView) { - TemplateMixin.call(window.Marionette.TemplateCache); - AsNamedView.call(window.Marionette.ItemView.prototype); - } - }, - 'typeahead' : { - deps : ['jquery'] - }, - 'jquery-ui' : { - deps : ['jquery'] - }, - 'jquery.knob' : { - deps : ['jquery'] - }, - 'jquery.easypiechart' : { - deps : ['jquery'] - }, - 'jquery.dotdotdot' : { - deps : ['jquery'] - }, - 'backbone.pageable' : { - deps : ['backbone'] - }, - 'backbone.deepmodel' : { - deps : [ - 'backbone', - 'underscore' - ] - }, - 'backbone.validation' : { - deps : ['backbone'], - exports : 'Backbone.Validation' - }, - 'backbone.modelbinder' : { - deps : ['backbone'] - }, - 'backbone.collectionview' : { - deps : [ - 'backbone', - 'jquery-ui' - ], - exports : 'Backbone.CollectionView' - }, - backgrid : { - deps : ['backbone'], - exports : 'Backgrid', - init : function() { - require(['Shared/Grid/HeaderCell'], function() { - window.Backgrid.Column.prototype.defaults = { - name : undefined, - label : undefined, - sortable : true, - editable : false, - renderable : true, - formatter : undefined, - cell : undefined, - headerCell : 'NzbDrone', - sortType : 'toggle' - }; - }); - } - }, - 'backgrid.paginator' : { - deps : ['backgrid'], - exports : 'Backgrid.Extension.Paginator' - }, - 'backgrid.selectall' : { - deps : ['backgrid'], - exports : 'Backgrid.Extension.SelectRowCell' - } - } -}); \ No newline at end of file diff --git a/src/UI/index.html b/src/UI/index.html deleted file mode 100644 index a4ab8c22f..000000000 --- a/src/UI/index.html +++ /dev/null @@ -1,101 +0,0 @@ -<!doctype html> -<html> -<head> - <title>Lidarr - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
- -
- -
-
- - - - - - - - - diff --git a/src/UI/jQuery/RouteBinder.js b/src/UI/jQuery/RouteBinder.js deleted file mode 100644 index a09b42a41..000000000 --- a/src/UI/jQuery/RouteBinder.js +++ /dev/null @@ -1,63 +0,0 @@ -var Backbone = require('backbone'); -var $ = require('jquery'); -var StatusModel = require('../System/StatusModel'); - -//This module will automatically route all relative links through backbone router rather than -//causing links to reload pages. - -var routeBinder = { - - bind : function() { - var self = this; - $(document).on('click contextmenu', 'a[href]', function(event) { - self._handleClick(event); - }); - }, - - _handleClick : function(event) { - var $target = $(event.target); - - //check if tab nav - if ($target.parents('.nav-tabs').length) { - return; - } - - var linkElement = $target.closest('a').first(); - var href = linkElement.attr('href'); - - if (href && href.startsWith('http')) { - // Set noreferrer for external links. - if (!linkElement.attr('rel')) { - linkElement.attr('rel', 'noreferrer'); - } - // Open all external links in new windows. - if (!linkElement.attr('target')) { - linkElement.attr('target', '_blank'); - } - } - - if (linkElement.hasClass('no-router') || event.type !== 'click') { - return; - } - - if (!href) { - throw 'couldn\'t find route target'; - } - - if (!href.startsWith('http')) { - event.preventDefault(); - - if (event.ctrlKey) { - window.open(href, '_blank'); - } - - else { - var relativeHref = href.replace(StatusModel.get('urlBase'), ''); - - Backbone.history.navigate(relativeHref, { trigger : true }); - } - } - } -}; - -module.exports = routeBinder; \ No newline at end of file diff --git a/src/UI/jQuery/ToTheTop.js b/src/UI/jQuery/ToTheTop.js deleted file mode 100644 index 696f903a4..000000000 --- a/src/UI/jQuery/ToTheTop.js +++ /dev/null @@ -1,23 +0,0 @@ -var $ = require('jquery'); -var _ = require('underscore'); - -$(document).ready(function() { - var _window = $(window); - var _scrollContainer = $('#scroll-up'); - var _scrollButton = $('#scroll-up i'); - - var _scrollHandler = function() { - if (_window.scrollTop() > 400) { - _scrollContainer.fadeIn(); - } else { - _scrollContainer.fadeOut(); - } - }; - - $(window).scroll(_.throttle(_scrollHandler, 500)); - _scrollButton.click(function() { - $('html, body').animate({ scrollTop : 0 }, 600); - return false; - }); -}); - diff --git a/src/UI/jQuery/jquery.ajax.js b/src/UI/jQuery/jquery.ajax.js deleted file mode 100644 index 0073e8619..000000000 --- a/src/UI/jQuery/jquery.ajax.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = function() { - - var $ = this; - - var original = $.ajax; - $.ajax = function(xhr) { - 'use strict'; - if (xhr && xhr.data && xhr.type === 'DELETE') { - if (xhr.url.contains('?')) { - xhr.url += '&'; - } else { - xhr.url += '?'; - } - xhr.url += $.param(xhr.data); - delete xhr.data; - } - if (xhr) { - xhr.headers = xhr.headers || {}; - xhr.headers['X-Api-Key'] = window.NzbDrone.ApiKey; - } - return original.apply(this, arguments); - }; -}; diff --git a/src/UI/jQuery/jquery.spin.js b/src/UI/jQuery/jquery.spin.js deleted file mode 100644 index a5daaf671..000000000 --- a/src/UI/jQuery/jquery.spin.js +++ /dev/null @@ -1,62 +0,0 @@ -module.exports = function() { - 'use strict'; - - var $ = this; - - $.fn.spinForPromise = function(promise) { - var self = this; - - if (!promise || promise.state() !== 'pending') { - return this; - } - promise.always(function() { - self.stopSpin(); - }); - - return this.startSpin(); - }; - - $.fn.startSpin = function() { - var icon = this.find('i').andSelf('i'); - - if (!icon || !icon.attr('class')) { - return this; - } - - var iconClasses = icon.attr('class').match(/(?:^|\s)icon\-.+?(?:$|\s)/); - - if (!iconClasses || iconClasses.length === 0) { - return this; - } - - var iconClass = $.trim(iconClasses[0]); - - this.addClass('disabled'); - - if (icon.hasClass('icon-can-spin')) { - icon.addClass('fa-spin'); - } else { - icon.attr('data-idle-icon', iconClass); - icon.removeClass(iconClass); - icon.addClass('fa-spin-overlay'); - icon.html(''); - } - - return this; - }; - - $.fn.stopSpin = function() { - var icon = this.find('i').andSelf('i'); - - icon.empty(); - this.removeClass('disabled'); - icon.removeClass('fa-spin fa-spin-overlay'); - var idleIcon = icon.attr('data-idle-icon'); - - if (idleIcon) { - icon.addClass(idleIcon); - } - - return this; - }; -}; \ No newline at end of file diff --git a/src/UI/jQuery/jquery.validation.js b/src/UI/jQuery/jquery.validation.js deleted file mode 100644 index 7844c3a8c..000000000 --- a/src/UI/jQuery/jquery.validation.js +++ /dev/null @@ -1,105 +0,0 @@ -module.exports = function() { - 'use strict'; - var $ = this; - $.fn.processServerError = function(error) { - var validationName = error.propertyName.toLowerCase(); - - var errorMessage = this.formatErrorMessage(error); - - this.find('.validation-errors').addClass('alert alert-danger').append('
' + errorMessage + '
'); - - if (!validationName || validationName === '') { - this.addFormError(error); - return this; - } - - var input = this.find('[name]').filter(function() { - return this.name.toLowerCase() === validationName; - }); - - if (input.length === 0) { - input = this.find('[validation-name]').filter(function() { - return $(this).attr('validation-name').toLowerCase() === validationName; - }); - - //still not found? - if (input.length === 0) { - this.addFormError(error); - console.error('couldn\'t find input for ' + error.propertyName); - return this; - } - } - - var formGroup = input.parents('.form-group'); - - if (formGroup.length === 0) { - formGroup = input.parent(); - } else { - var inputGroup = formGroup.find('.input-group'); - - var validationClass = error.isWarning ? 'validation-warning' : 'validation-error'; - - if (inputGroup.length === 0) { - formGroup.append('{1}'.format(validationClass, errorMessage)); - } - else { - inputGroup.parent().append('{1}'.format(validationClass, errorMessage)); - } - } - - if (error.isWarning) { - formGroup.addClass('has-warning'); - } else { - formGroup.addClass('has-error'); - } - - return formGroup.find('.help-inline').text(); - }; - - $.fn.processClientError = function(error) { - - }; - - $.fn.addFormError = function(error) { - - var errorMessage = this.formatErrorMessage(error); - - var target = this.find('.modal-body'); - if (!target.length) { - target = this; - } - - var validationClass = error.isWarning ? 'alert alert-warning validation-warning' : 'alert alert-danger validation-error'; - - target.prepend('
{1}
'.format(validationClass, errorMessage)); - }; - - $.fn.removeAllErrors = function() { - this.removeClass('has-error'); - this.removeClass('has-warning'); - this.find('.has-error').removeClass('has-error'); - this.find('.has-warning').removeClass('has-warning'); - this.find('.error').removeClass('error'); - this.find('.validation-errors').removeClass('alert').removeClass('alert-danger').removeClass('alert-warning').html(''); - this.find('.validation-error').remove(); - this.find('.validation-warning').remove(); - return this.find('.help-inline.error-message').remove(); - }; - - $.fn.formatErrorMessage = function(error) { - - var errorMessage = error.errorMessage; - - if (error.infoLink) { - if (error.detailedDescription) { - errorMessage += ' '; - } else { - errorMessage += ' '; - } - } else if (error.detailedDescription) { - errorMessage += ' '; - } - - return errorMessage; - }; -}; \ No newline at end of file diff --git a/src/UI/login.html b/src/UI/login.html deleted file mode 100644 index 00c698dc1..000000000 --- a/src/UI/login.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - Lidarr - Login - - - - - - - - - - - - - - - -
- -
-
-
-
-
-
- -
-
-
-
-
- - diff --git a/src/UI/main.js b/src/UI/main.js deleted file mode 100644 index 40b9e9efe..000000000 --- a/src/UI/main.js +++ /dev/null @@ -1,60 +0,0 @@ -var $ = require('jquery'); -var Backbone = require('backbone'); -var Marionette = require('marionette'); -var RouteBinder = require('./jQuery/RouteBinder'); -var SignalRBroadcaster = require('./Shared/SignalRBroadcaster'); -var NavbarLayout = require('./Navbar/NavbarLayout'); -var AppLayout = require('./AppLayout'); -var ArtistController = require('./Artist/ArtistController'); -var Router = require('./Router'); -var ModalController = require('./Shared/Modal/ModalController'); -var ControlPanelController = require('./Shared/ControlPanel/ControlPanelController'); -var serverStatusModel = require('./System/StatusModel'); -var Tooltip = require('./Shared/Tooltip'); -var UiSettingsController = require('./Shared/UiSettingsController'); - -require('./jQuery/ToTheTop'); -require('./Instrumentation/StringFormat'); -require('./LifeCycle'); -require('./Hotkeys/Hotkeys'); -require('./Shared/piwikCheck'); -require('./Shared/VersionChangeMonitor'); - -new ArtistController(); -new ModalController(); -new ControlPanelController(); -new Router(); - -var app = new Marionette.Application(); - -app.addInitializer(function() { - console.log('starting application'); -}); - -app.addInitializer(SignalRBroadcaster.appInitializer, { app : app }); - -app.addInitializer(Tooltip.appInitializer, { app : app }); - -app.addInitializer(function() { - Backbone.history.start({ - pushState : true, - root : serverStatusModel.get('urlBase') - }); - RouteBinder.bind(); - AppLayout.navbarRegion.show(new NavbarLayout()); - $('body').addClass('started'); -}); - -app.addInitializer(UiSettingsController.appInitializer); - -app.addInitializer(function() { - var footerText = serverStatusModel.get('version'); - if (serverStatusModel.get('branch') !== 'master') { - footerText += '
' + serverStatusModel.get('branch'); - } - $('#footer-region .version').html(footerText); -}); - -app.start(); - -module.exports = app; diff --git a/src/UI/polyfills.js b/src/UI/polyfills.js deleted file mode 100644 index fef657524..000000000 --- a/src/UI/polyfills.js +++ /dev/null @@ -1,39 +0,0 @@ -window.console = window.console || {}; -window.console.log = window.console.log || function() {}; -window.console.group = window.console.group || function() {}; -window.console.groupEnd = window.console.groupEnd || function() {}; -window.console.debug = window.console.debug || function() {}; -window.console.warn = window.console.warn || function() {}; -window.console.assert = window.console.assert || function() {}; - -if (!String.prototype.startsWith) { - Object.defineProperty(String.prototype, 'startsWith', { - enumerable : false, - configurable : false, - writable : false, - value : function(searchString, position) { - position = position || 0; - return this.indexOf(searchString, position) === position; - } - }); -} - -if (!String.prototype.endsWith) { - Object.defineProperty(String.prototype, 'endsWith', { - enumerable : false, - configurable : false, - writable : false, - value : function(searchString, position) { - position = position || this.length; - position = position - searchString.length; - var lastIndex = this.lastIndexOf(searchString); - return lastIndex !== -1 && lastIndex === position; - } - }); -} - -if (!('contains' in String.prototype)) { - String.prototype.contains = function(str, startIndex) { - return -1 !== String.prototype.indexOf.call(this, str, startIndex); - }; -} \ No newline at end of file diff --git a/src/UI/reqres.js b/src/UI/reqres.js deleted file mode 100644 index 1904f3602..000000000 --- a/src/UI/reqres.js +++ /dev/null @@ -1,10 +0,0 @@ -var Wreqr = require('./JsLibraries/backbone.wreqr'); - -var reqres = new Wreqr.RequestResponse(); - -reqres.Requests = { - GetEpisodeFileById : 'GetEpisodeFileById', - GetAlternateNameBySeasonNumber : 'GetAlternateNameBySeasonNumber' -}; - -module.exports = reqres; \ No newline at end of file diff --git a/src/UI/vendor.js b/src/UI/vendor.js deleted file mode 100644 index dc343bb35..000000000 --- a/src/UI/vendor.js +++ /dev/null @@ -1,34 +0,0 @@ -/*Base*/ -require('jquery'); -require('underscore'); -require('messenger'); -require('moment'); -require('fullcalendar'); -require('backstrech'); -require('signalR'); -require('jquery-ui'); -require('jquery.knob'); -require('jquery.easypiechart'); -require('jquery.dotdotdot'); -require('typeahead'); -require('zero.clipboard'); - -/*Bootstrap*/ -require('bootstrap'); -require('bootstrap.tagsinput'); - -/*Backbone*/ -require('backbone'); -require('backbone.deepmodel'); -require('backbone.pageable'); -require('backbone-pageable'); -require('backbone.validation'); - -require('backbone.modelbinder'); -require('backbone.collectionview'); -require('backgrid'); -require('backgrid.paginator'); -require('backgrid.selectall'); - -require('marionette'); //this brings in a bunch of our code into this chunk because of template helpers. -require('vent'); diff --git a/src/UI/vent.js b/src/UI/vent.js deleted file mode 100644 index 7b5eaaef1..000000000 --- a/src/UI/vent.js +++ /dev/null @@ -1,42 +0,0 @@ -var Wreqr = require('./JsLibraries/backbone.wreqr'); - -var vent = new Wreqr.EventAggregator(); - -vent.Events = { - SeriesAdded : 'series:added', - SeriesDeleted : 'series:deleted', - ArtistAdded : 'artist:added', - ArtistDeleted : 'artist:deleted', - CommandComplete : 'command:complete', - ServerUpdated : 'server:updated', - EpisodeFileDeleted : 'episodefile:deleted' -}; - -vent.Commands = { - EditArtistCommand : 'EditArtistCommand', - DeleteArtistCommand : 'DeleteArtistCommand', - OpenModalCommand : 'OpenModalCommand', - CloseModalCommand : 'CloseModalCommand', - OpenModal2Command : 'OpenModal2Command', - CloseModal2Command : 'CloseModal2Command', - ShowEpisodeDetails : 'ShowEpisodeDetails', - ShowAlbumDetails : 'ShowAlbumDetails', - ShowHistoryDetails : 'ShowHistoryDetails', - ShowLogDetails : 'ShowLogDetails', - SaveSettings : 'saveSettings', - ShowLogFile : 'showLogFile', - ShowRenamePreview : 'showRenamePreview', - ShowManualImport : 'showManualImport', - ShowFileBrowser : 'showFileBrowser', - CloseFileBrowser : 'closeFileBrowser', - OpenControlPanelCommand : 'OpenControlPanelCommand', - CloseControlPanelCommand : 'CloseControlPanelCommand' -}; - -vent.Hotkeys = { - NavbarSearch : 'navbar:search', - SaveSettings : 'settings:save', - ShowHotkeys : 'hotkeys:show' -}; - -module.exports = vent; \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index 0239dd789..e47ff06a8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,5 +1,4 @@ var path = require('path'); -var stylish = require('jshint-stylish'); var webpack = require('webpack'); var uglifyJsPlugin = new webpack.optimize.UglifyJsPlugin(); @@ -17,38 +16,6 @@ module.exports = { resolve: { root: root, alias: { - 'vent': 'vent', - 'backbone': 'Shims/backbone', - 'moment': 'JsLibraries/moment', - 'filesize': 'JsLibraries/filesize', - 'handlebars': 'Shims/handlebars', - 'handlebars.helpers': 'JsLibraries/handlebars.helpers', - 'bootstrap': 'JsLibraries/bootstrap', - 'backbone.deepmodel': 'Shims/backbone.deep.model', - 'backbone.pageable': 'JsLibraries/backbone.pageable', - 'backbone-pageable': 'JsLibraries/backbone.pageable', - 'backbone.paginator': 'JsLibraries/backbone.paginator', - 'backbone.validation': 'Shims/backbone.validation', - 'backbone.modelbinder': 'JsLibraries/backbone.modelbinder', - 'backbone.collectionview': 'Shims/backbone.collectionview', - 'backgrid': 'Shims/backgrid', - 'backgrid.paginator': 'Shims/backgrid.paginator', - 'backgrid.selectall': 'Shims/backbone.backgrid.selectall', - 'fullcalendar': 'JsLibraries/fullcalendar', - 'backstrech': 'JsLibraries/jquery.backstretch', - 'underscore': 'Shims/underscore', - 'marionette': 'Shims/backbone.marionette', - 'signalR': 'Shims/jquery.signalR', - 'jquery-ui': 'JsLibraries/jquery-ui', - 'jquery.knob': 'JsLibraries/jquery.knob', - 'jquery.easypiechart': 'JsLibraries/jquery.easypiechart', - 'jquery.dotdotdot': 'JsLibraries/jquery.dotdotdot', - 'jquery.lazyload': 'JsLibraries/jquery.lazyload', - 'messenger': 'Shims/messenger', - 'jquery': 'Shims/jquery', - 'typeahead': 'JsLibraries/typeahead', - 'zero.clipboard': 'JsLibraries/zero.clipboard', - 'bootstrap.tagsinput': 'JsLibraries/bootstrap.tagsinput', 'jdu': 'JsLibraries/jdu', 'libs': 'JsLibraries/' }