Browse Source

Change file browser to permit manually entering a path.

pull/604/head
JackDandy 9 years ago
parent
commit
a59fe898b5
  1. 4
      CHANGES.md
  2. 4
      gui/slick/css/dark.css
  3. BIN
      gui/slick/css/lib/images/ui-icons_09a2ff_256x240.png
  4. 2
      gui/slick/css/light.css
  5. 8
      gui/slick/css/style.css
  6. 383
      gui/slick/js/browser.js
  7. 82
      sickbeard/browser.py
  8. 7
      sickbeard/webserve.py

4
CHANGES.md

@ -108,7 +108,9 @@
* Fix issue on Add Existing Shows page where shows were listed that should not have been * Fix issue on Add Existing Shows page where shows were listed that should not have been
* Change get_size helper to also handle files * Change get_size helper to also handle files
* Change improve handling of a bad email notify setting * Change improve handling of a bad email notify setting
* Change give OMGWTFNZBS provider more time to respond * Fix provider MTV download URL
* Change give provider OMGWTFNZBS more time to respond
* Change file browser to permit manually entering a path
### 0.10.0 (2015-08-06 11:05:00 UTC) ### 0.10.0 (2015-08-06 11:05:00 UTC)

4
gui/slick/css/dark.css

@ -20,7 +20,7 @@ inc_top.tmpl
} }
.browserDialog.busy .ui-dialog-buttonpane{ .browserDialog.busy .ui-dialog-buttonpane{
background:url("../images/loading.gif") 10px 50% no-repeat !important background:url("../images/loading32-dark.gif") 10px 50% no-repeat !important
} }
.ui-progressbar .ui-progressbar-overlay{ .ui-progressbar .ui-progressbar-overlay{
@ -61,6 +61,8 @@ inc_top.tmpl
.ui-state-default, .ui-state-default,
.ui-widget-content .ui-state-default, .ui-widget-content .ui-state-default,
.ui-widget-header .ui-state-default{ .ui-widget-header .ui-state-default{
background:#3d3d3d;
color:#fff;
border:1px solid #111 border:1px solid #111
} }

BIN
gui/slick/css/lib/images/ui-icons_09a2ff_256x240.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

2
gui/slick/css/light.css

@ -16,7 +16,7 @@ inc_top.tmpl
} }
.browserDialog.busy .ui-dialog-buttonpane{ .browserDialog.busy .ui-dialog-buttonpane{
background:url("../images/loading.gif") 10px 50% no-repeat !important background:url("../images/loading32.gif") 10px 50% no-repeat !important
} }
.ui-progressbar .ui-progressbar-overlay{ .ui-progressbar .ui-progressbar-overlay{

8
gui/slick/css/style.css

@ -175,7 +175,7 @@ inc_top.tmpl
background:url("../css/lib/images/animated-overlay.gif") background:url("../css/lib/images/animated-overlay.gif")
} }
.ui-dialog, .ui-dialog,
.ui-dialog-buttonpane{ .ui-dialog-buttonpane{
background:#eceadf url("../css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png") 50% 50% repeat !important background:#eceadf url("../css/lib/images/ui-bg_fine-grain_10_eceadf_60x60.png") 50% 50% repeat !important
} }
@ -3032,6 +3032,10 @@ fieldset[disabled] .navbar-default .btn-link:focus{
display:inline display:inline
} }
#fileBrowserDialog .form-control{background-color:#f5f1e4}
#fileBrowserDialog .form-control:active,
#fileBrowserDialog .form-control:hover{background-color:#ffffca}
.btn{ .btn{
display:inline-block; display:inline-block;
*display:inline; *display:inline;
@ -3942,7 +3946,7 @@ jquery.confirm.css
box-shadow:inset 0 1px rgba(255, 255, 255, 0.1),inset 0 -1px 3px rgba(0, 0, 0, 0.3),inset 0 0 0 1px rgba(255, 255, 255, 0.08),0 1px 2px rgba(0, 0, 0, 0.15) box-shadow:inset 0 1px rgba(255, 255, 255, 0.1),inset 0 -1px 3px rgba(0, 0, 0, 0.3),inset 0 0 0 1px rgba(255, 255, 255, 0.08),0 1px 2px rgba(0, 0, 0, 0.15)
} }
#confirmBox .button:last-child{ #confirmBox .button:last-child{
margin-right:0 margin-right:0
} }

383
gui/slick/js/browser.js

@ -1,181 +1,208 @@
;(function($) { ;(function($) {
"use strict"; 'use strict';
$.Browser = { $.Browser = {
defaults: { defaults: {
title: 'Choose Directory', title: 'Choose Directory (or enter manually)',
url: sbRoot + '/browser/', url: sbRoot + '/browser/',
autocompleteURL: sbRoot + '/browser/complete', autocompleteURL: sbRoot + '/browser/complete',
includeFiles: 0 includeFiles: 0,
} showBrowseButton: !0
}; }
};
var fileBrowserDialog, currentBrowserPath, currentRequest = null;
var fileBrowserDialog, currentBrowserPath, currentRequest = null;
function browse(path, endpoint, includeFiles) {
function browse(path, endpoint, includeFiles) {
if (currentBrowserPath == path) {
return; if (path === currentBrowserPath) {
} return;
}
currentBrowserPath = path;
currentBrowserPath = path;
if (currentRequest) {
currentRequest.abort(); if (currentRequest) {
} currentRequest.abort();
}
fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy');
fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog busy');
currentRequest = $.getJSON(endpoint, { path: path, includeFiles: includeFiles }, function (data) {
fileBrowserDialog.empty(); currentRequest = $.getJSON(endpoint, {path: path, includeFiles: includeFiles}, function(data){
var first_val = data[0]; fileBrowserDialog.empty();
var i = 0; var firstVal = data[0], i = 0, list, link = null;
var list, link = null; data = $.grep(data, function(){
data = $.grep(data, function (value) { return i++ != 0;
return i++ != 0; });
}); $('<input type="text" class="form-control input-sm">')
$('<h2>').text(first_val.current_path).appendTo(fileBrowserDialog); .val(firstVal.currentPath)
list = $('<ul>').appendTo(fileBrowserDialog); .on('keypress', function(e){
$.each(data, function (i, entry) { if (13 === e.which) {
link = $("<a href='javascript:void(0)' />").click(function () { browse(entry.path, endpoint, includeFiles); }).text(entry.name); browse(e.target.value, endpoint, includeFiles);
$('<span class="ui-icon ui-icon-folder-collapsed"></span>').prependTo(link); }
link.hover( })
function () {$("span", this).addClass("ui-icon-folder-open"); }, .appendTo(fileBrowserDialog)
function () {$("span", this).removeClass("ui-icon-folder-open"); } .fileBrowser({showBrowseButton: !1})
); .on('autocompleteselect',
link.appendTo(list); function(e, ui){browse(ui.item.value, endpoint, includeFiles);
}); });
$("a", list).wrap('<li class="ui-state-default ui-corner-all">');
fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog'); list = $('<ul>').appendTo(fileBrowserDialog);
}); $.each(data, function(i, entry){
} link = $('<a href="javascript:void(0)">').on('click',
function(){
$.fn.nFileBrowser = function (callback, options) { if (entry.isFile) {
options = $.extend({}, $.Browser.defaults, options); currentBrowserPath = entry.path;
$('.browserDialog .ui-button:contains("Ok")').click();
// make a fileBrowserDialog object if one doesn't exist already } else {
if (!fileBrowserDialog) { browse(entry.path, endpoint, includeFiles);
}
// set up the jquery dialog }).text(entry.name);
fileBrowserDialog = $('<div id="fileBrowserDialog" style="display:hidden"></div>').appendTo('body').dialog({
dialogClass: 'browserDialog', if (entry.isFile) {
title: options.title, link.prepend('<span class="ui-icon ui-icon-blank"></span>');
position: ['center', 40], } else {
minWidth: Math.min($(document).width() - 80, 650), link.prepend('<span class="ui-icon ui-icon-folder-collapsed"></span>')
height: Math.min($(document).height() - 80, $(window).height() - 80), .on('mouseenter', function(){$('span', this).addClass('ui-icon-folder-open');})
maxHeight: Math.min($(document).height() - 80, $(window).height() - 80), .on('mouseleave', function(){$('span', this).removeClass('ui-icon-folder-open');});
maxWidth: $(document).width() - 80, }
modal: true, link.appendTo(list);
autoOpen: false });
}); $('a', list).wrap('<li class="ui-state-default ui-corner-all">');
} fileBrowserDialog.dialog('option', 'dialogClass', 'browserDialog');
});
fileBrowserDialog.dialog('option', 'buttons', [ }
{
text: "Ok", $.fn.nFileBrowser = function(callback, options){
"class": "btn", options = $.extend({}, $.Browser.defaults, options);
click: function() {
// store the browsed path to the associated text field // make a fileBrowserDialog object if one doesn't exist already
callback(currentBrowserPath, options); if (fileBrowserDialog) {
$(this).dialog("close"); fileBrowserDialog.dialog('option', 'title', options.title);
} } else {
}, // set up the jquery dialog
{ var docWidth = $(document).width(), dlgWidth = Math.min(docWidth - 80, 650),
text: "Cancel", docHeight = $(document).height() - 80, winHeight = $(window).height() - 80;
"class": "btn", fileBrowserDialog = $('<div id="fileBrowserDialog" style="display:none"></div>').appendTo('body').dialog({
click: function() { dialogClass: 'browserDialog',
$(this).dialog("close"); title: options.title,
} position: [(docWidth - dlgWidth)/2, 60],
} minWidth: dlgWidth,
]); height: Math.min(docHeight, winHeight),
maxHeight: Math.min(docHeight, winHeight),
// set up the browser and launch the dialog maxWidth: docWidth - 80,
var initialDir = ''; modal: true,
if (options.initialDir) { autoOpen: false
initialDir = options.initialDir; });
} }
browse(initialDir, options.url, options.includeFiles); fileBrowserDialog.dialog('option', 'buttons',
fileBrowserDialog.dialog('open'); [{
text: 'Ok',
return false; 'class': 'btn',
}; click: function(){
// store the browsed path to the associated text field
$.fn.fileBrowser = function (options) { callback(currentBrowserPath, options);
options = $.extend({}, $.Browser.defaults, options); $(this).dialog('close');
// text field used for the result }
options.field = $(this); },
{
if (options.field.autocomplete && options.autocompleteURL) { text: 'Cancel',
var query = ''; 'class': 'btn',
options.field.autocomplete({ click: function(){
position: { my : "top", at: "bottom", collision: "flipfit" }, $(this).dialog('close');
source: function (request, response) { }
//keep track of user submitted search term }]);
query = $.ui.autocomplete.escapeRegex(request.term, options.includeFiles);
$.ajax({ // set up the browser and launch the dialog
url: options.autocompleteURL, var initialDir = '';
data: request, if (options.initialDir) {
dataType: "json", initialDir = options.initialDir;
success: function (data, item) { }
//implement a startsWith filter for the results
var matcher = new RegExp("^" + query, "i"); browse(initialDir, options.url, options.includeFiles);
var a = $.grep(data, function (item, index) { fileBrowserDialog.dialog('open');
return matcher.test(item);
}); return false;
response(a); };
}
}); $.fn.fileBrowser = function(options){
}, options = $.extend({}, $.Browser.defaults, options);
open: function (event, ui) { // text field used for the result
$(".ui-autocomplete li.ui-menu-item a").removeClass("ui-corner-all"); options.field = $(this);
$(".ui-autocomplete li.ui-menu-item:odd a").addClass("ui-menu-item-alternate");
} if (options.field.autocomplete && options.autocompleteURL) {
}) var query = '';
.data("ui-autocomplete")._renderItem = function (ul, item) { options.field.autocomplete({
//highlight the matched search term from the item -- note that this is global and will match anywhere position: {my: 'top', at: 'bottom', collision: 'flipfit'},
var result_item = item.label; source: function(request, response){
var x = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + query + ")(?![^<>]*>)(?![^&;]+;)", "gi"); //keep track of user submitted search term
result_item = result_item.replace(x, function (FullMatch, n) { query = $.ui.autocomplete.escapeRegex(request.term, options.includeFiles);
return '<b>' + FullMatch + '</b>'; $.ajax({
}); url: options.autocompleteURL,
return $("<li></li>") data: request,
.data("ui-autocomplete-item", item) dataType: 'json',
.append("<a class='nowrap'>" + result_item + "</a>") success: function(data){
.appendTo(ul); //implement a startsWith filter for the results
}; var matcher = new RegExp('^' + query, 'i');
} var a = $.grep(data, function(item){
return matcher.test(item);
var initialDir, path, callback, ls = false; });
// if the text field is empty and we're given a key then populate it with the last browsed value from localStorage response(a);
try { ls = !!(localStorage.getItem); } catch (e) {} }
if (ls && options.key) { });
path = localStorage['fileBrowser-' + options.key]; },
} open: function(){
if (options.key && options.field.val().length == 0 && (path)) { $('.ui-autocomplete li.ui-menu-item a').removeClass('ui-corner-all');
options.field.val(path); $('.ui-autocomplete li.ui-menu-item:odd a').addClass('ui-menu-item-alternate');
} }
}).data('ui-autocomplete')._renderItem = function(ul, item){
callback = function (path, options) { //highlight the matched search term from the item -- note that this is global and will match anywhere
// store the browsed path to the associated text field var resultItem = item.label;
options.field.val(path); var x = new RegExp('(?![^&;]+;)(?!<[^<>]*)(' + query + ')(?![^<>]*>)(?![^&;]+;)', 'gi');
resultItem = resultItem.replace(x, function(fullMatch){
// use a localStorage to remember for next time -- no ie6/7 return '<b>' + fullMatch + '</b>';
if (ls && options.key) { });
localStorage['fileBrowser-' + options.key] = path; return $('<li></li>')
} .data('ui-autocomplete-item', item)
.append('<a class="nowrap">' + resultItem + '</a>')
}; .appendTo(ul);
};
initialDir = options.field.val() || (options.key && path) || ''; }
options = $.extend(options, {initialDir: initialDir}); var path, callback, ls = false;
// if empty text field and given a key then populate it with the last browsed value from localStorage
// append the browse button and give it a click behaviour try { ls = !!(localStorage.getItem); } catch (e) {}
return options.field.addClass('fileBrowserField').after($('<input type="button" value="Browse&hellip;" class="btn btn-inline fileBrowser" />').click(function () { if (ls && options.key) {
$(this).nFileBrowser(callback, options); path = localStorage['fileBrowser-' + options.key];
return false; }
})); if (options.key && options.field.val().length == 0 && (path)) {
}; options.field.val(path);
}
callback = function(path, options){
// store the browsed path to the associated text field
options.field.val(path);
// use a localStorage to remember for next time -- no ie6/7
if (ls && options.key) {
localStorage['fileBrowser-' + options.key] = path;
}
};
options.field.addClass('fileBrowserField');
if (options.showBrowseButton) {
// append the browse button and give it a click behaviour
options.field.after(
$('<input type="button" value="Browse&hellip;" class="btn btn-inline fileBrowser">').on('click',
function(){
$(this).nFileBrowser(callback, $.extend(
{}, options, {initialDir: options.field.val() || (options.key && path) || ''}
));
return false;
}));
}
return options.field;
};
})(jQuery); })(jQuery);

82
sickbeard/browser.py

@ -30,17 +30,18 @@ except ImportError:
from lib import simplejson as json from lib import simplejson as json
# this is for the drive letter code, it only works on windows # this is for the drive letter code, it only works on windows
if os.name == 'nt': if 'nt' == os.name:
from ctypes import windll from ctypes import windll
# adapted from http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490 # adapted from
# http://stackoverflow.com/questions/827371/is-there-a-way-to-list-all-the-available-drive-letters-in-python/827490
def getWinDrives(): def getWinDrives():
""" Return list of detected drives """ """ Return list of detected drives """
assert os.name == 'nt' assert 'nt' == os.name
drives = [] drives = []
bitmask = windll.kernel32.GetLogicalDrives() #@UndefinedVariable bitmask = windll.kernel32.GetLogicalDrives()
for letter in string.uppercase: for letter in string.uppercase:
if bitmask & 1: if bitmask & 1:
drives.append(letter) drives.append(letter)
@ -56,54 +57,67 @@ def foldersAtPath(path, includeParent=False, includeFiles=False):
""" """
# walk up the tree until we find a valid path # walk up the tree until we find a valid path
while path and not os.path.isdir(path): while path and not ek.ek(os.path.isdir, path):
if path == os.path.dirname(path): if path == ek.ek(os.path.dirname, path):
path = '' path = ''
break break
else: else:
path = os.path.dirname(path) path = ek.ek(os.path.dirname, path)
if path == '': if '' == path:
if os.name == 'nt': if 'nt' == os.name:
entries = [{'current_path': 'Root'}] entries = [{'currentPath': '\My Computer'}]
for letter in getWinDrives(): for letter in getWinDrives():
letterPath = '%s:\\' % letter letter_path = '%s:\\' % letter
entries.append({'name': letterPath, 'path': letterPath}) entries.append({'name': letter_path, 'path': letter_path})
return entries return entries
else: else:
path = '/' path = '/'
# fix up the path and find the parent # fix up the path and find the parent
path = os.path.abspath(os.path.normpath(path)) path = ek.ek(os.path.abspath, ek.ek(os.path.normpath, path))
parentPath = os.path.dirname(path) parent_path = ek.ek(os.path.dirname, path)
# if we're at the root then the next step is the meta-node showing our drive letters # if we're at the root then the next step is the meta-node showing our drive letters
if path == parentPath and os.name == 'nt': if 'nt' == os.name and path == parent_path:
parentPath = '' parent_path = ''
try: try:
fileList = [{'name': filename, 'path': ek.ek(os.path.join, path, filename)} for filename in file_list = get_file_list(path, includeFiles)
ek.ek(os.listdir, path)]
except OSError as e: except OSError as e:
logger.log(u'Unable to open %s: %r / %s' % (path, e, e), logger.WARNING) logger.log(u'Unable to open %s: %r / %s' % (path, e, e), logger.WARNING)
fileList = [{'name': filename, 'path': ek.ek(os.path.join, parentPath, filename)} for filename in file_list = get_file_list(parent_path, includeFiles)
ek.ek(os.listdir, parentPath)]
if not includeFiles: file_list = sorted(file_list, lambda x, y: cmp(ek.ek(os.path.basename, x['name']).lower(),
fileList = filter(lambda entry: ek.ek(os.path.isdir, entry['path']), fileList) ek.ek(os.path.basename, y['path']).lower()))
# prune out directories to proect the user from doing stupid things (already lower case the dir to reduce calls) entries = [{'currentPath': path}]
hideList = ['boot', 'bootmgr', 'cache', 'msocache', 'recovery', '$recycle.bin', 'recycler', if includeParent and path != parent_path:
'system volume information', 'temporary internet files'] # windows specific entries.append({'name': '..', 'path': parent_path})
hideList += ['.fseventd', '.spotlight', '.trashes', '.vol', 'cachedmessages', 'caches', 'trash'] # osx specific entries.extend(file_list)
fileList = filter(lambda entry: entry['name'].lower() not in hideList, fileList)
fileList = sorted(fileList, return entries
lambda x, y: cmp(os.path.basename(x['name']).lower(), os.path.basename(y['path']).lower()))
entries = [{'current_path': path}]
if includeParent and parentPath != path:
entries.append({'name': '..', 'path': parentPath})
entries.extend(fileList)
return entries def get_file_list(path, include_files):
result = []
hide_names = [
# windows specific
'boot', 'bootmgr', 'cache', 'config.msi', 'msocache', 'recovery', '$recycle.bin', 'recycler',
'system volume information', 'temporary internet files',
# osx specific
'.fseventd', '.spotlight', '.trashes', '.vol', 'cachedmessages', 'caches', 'trash',
# general
'.git']
# filter directories to protect
for name in ek.ek(os.listdir, path):
if name.lower() not in hide_names:
path_file = ek.ek(os.path.join, path, name)
is_dir = ek.ek(os.path.isdir, path_file)
if include_files or is_dir:
result.append({'name': name, 'path': path_file, 'isFile': (1, 0)[is_dir]})
return result

7
sickbeard/webserve.py

@ -3366,6 +3366,13 @@ class Manage(MainHandler):
cur_show_dir = ek.ek(os.path.basename, showObj._location) cur_show_dir = ek.ek(os.path.basename, showObj._location)
if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]:
new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir)
if 'nt' != os.name and ':\\' in cur_show_dir:
cur_show_dir = showObj._location.split('\\')[-1]
try:
base_dir = dir_map[cur_root_dir].rsplit(cur_show_dir)[0].rstrip('/')
except IndexError:
base_dir = dir_map[cur_root_dir]
new_show_dir = ek.ek(os.path.join, base_dir, cur_show_dir)
logger.log( logger.log(
u'For show ' + showObj.name + ' changing dir from ' + showObj._location + ' to ' + new_show_dir) u'For show ' + showObj.name + ' changing dir from ' + showObj._location + ' to ' + new_show_dir)
else: else:

Loading…
Cancel
Save