diff options
| author | Einar Egilsson <einar@einaregilsson.com> | 2015-09-09 14:53:22 +0000 | 
|---|---|---|
| committer | Einar Egilsson <einar@einaregilsson.com> | 2015-09-09 14:53:22 +0000 | 
| commit | 8a77401f48360fe0d5501d4fd6b4cc194279d4f0 (patch) | |
| tree | 6c6069b54de898c72a2281caa9702039df8a2bfb | |
| parent | a7506a34544f4df3ba65a854c81fadcca2eb303f (diff) | |
Plenty of changes
29 files changed, 600 insertions, 329 deletions
| diff --git a/css/redirector.css b/css/redirector.css index 6997d32..a439196 100644 --- a/css/redirector.css +++ b/css/redirector.css @@ -13,7 +13,11 @@ h1, h2, h3, h5, h6 {  h1 {    font-size:55px; -  margin:20px 0px 0px 0px; +  margin:-15px 0px 0px 0px; +  -webkit-user-select:none; +  -moz-user-select:none; +  cursor:default; +  padding:0px;  }  h3 { @@ -30,7 +34,7 @@ h4 {  h5 {    font-size:20px; -  margin:0; +  margin:-20px 0 0 0;    color:#5e6163;  } @@ -84,6 +88,7 @@ input[type="radio"] {  .btn.grey:hover {    background:#333; +  border:solid 1px #333;  }  .btn.red { @@ -92,6 +97,7 @@ input[type="radio"] {  .btn.red:hover {    background:rgb(208,52,37); +  border:solid 1px rgb(208,52,37);  }  .btn.blue { @@ -100,6 +106,7 @@ input[type="radio"] {  .btn.blue:hover {    background:rgb(21,90,233); +  border:solid 1px rgb(21,90,233);  }  .btn.green { @@ -108,6 +115,7 @@ input[type="radio"] {  .btn.green:hover {    background:green; +  border:solid 1px green;  }  /* Main menu with buttons */ @@ -193,6 +201,7 @@ input[type="radio"] {  .redirect-row a, .redirect-row a:visited {    font-size:13px;    margin-top:5px; +  padding:2px;    width:60px;  } @@ -240,6 +249,7 @@ input[type="radio"] {  a.disabled:hover {    cursor:default;    color:#bbb !important; +  border:solid 1px #bbb !important;    background:white !important;  } @@ -287,14 +297,24 @@ a.disabled:hover {  #edit-redirect-form .second-column {    display:block; -  padding-left: 134px; +  margin-left: 133px;  }  #edit-redirect-form div input[type='text'] {    width:300px;  } +.error { +  color:red; +} + +.placeholder { +  color:#c0c0c0; +  font-size:11px; +} +  #advanced-toggle { +  padding-top:3px;    text-align: center;  } @@ -318,6 +338,7 @@ a[ng-click] {  .button-container {    margin-top:20px;    text-align: center; +  padding-bottom:10px;  }  /* Footer with link */ diff --git a/icon.html b/icon.html new file mode 100644 index 0000000..579a6a9 --- /dev/null +++ b/icon.html @@ -0,0 +1,68 @@ +<!DOCTYPE html> +<html> +<head> +	<meta charset="UTF-8"> +	<style> +		a { +			display:block; +			margin:10px auto; +			padding:0px; +		} + +		img { +			margin:0; +			padding:0; +			border:solid 1px green; +		} + +		 +	</style> +</head> +<body> + +	<script> +		var data = { +			 16: [ 32, 0,  16], +			 19: [ 35, 1,  18], +			 32: [ 64, 1,  32], +			 38: [ 75, 2,  38], +			 48: [ 95, 1,  48], +			128: [260, 2, 131], +		}; + + +		function createImageLink(size, logoFontSize, logoX, logoY) { +			var colors = {'#333' : 'active', '#bbb' : 'disabled'}; +			for (var color in colors) { +				var canvas = document.createElement('canvas'); +				canvas.width = canvas.height = size; +				ctx = canvas.getContext('2d'); +				ctx.fillStyle = color; +				ctx.font = 'Bold ' + logoFontSize + 'px Arial'; +				ctx.fillText('☈', logoX, logoY); +				 +				 +				var a = document.createElement('a'); +				var img = document.createElement('img'); +				img.src = canvas.toDataURL(); +				a.href = canvas.toDataURL(); +				a.download = 'icon' + size + colors[color] + '.png'; +				a.appendChild(img); +				a.style.width = size + 'px' +				document.body.appendChild(a); +			} +		} + +		document.addEventListener('DOMContentLoaded', function() { +			createImageLink(16, 32, 0, 16); +			createImageLink(19, 35, 1,  18); +			createImageLink(32, 64, 1,  32); +			createImageLink(38, 75, 2,  38); +			createImageLink(48, 95, 1,  48); +			createImageLink(128, 260, 2, 131); + +		}); +		 +	</script> +</body> +</html>
\ No newline at end of file diff --git a/images/icon128active.png b/images/icon128active.pngBinary files differ new file mode 100644 index 0000000..fa723bf --- /dev/null +++ b/images/icon128active.png diff --git a/images/icon128disabled.png b/images/icon128disabled.pngBinary files differ new file mode 100644 index 0000000..0f7d19a --- /dev/null +++ b/images/icon128disabled.png diff --git a/images/icon16active.png b/images/icon16active.pngBinary files differ new file mode 100644 index 0000000..9482d4b --- /dev/null +++ b/images/icon16active.png diff --git a/images/icon16disabled.png b/images/icon16disabled.pngBinary files differ new file mode 100644 index 0000000..23d6675 --- /dev/null +++ b/images/icon16disabled.png diff --git a/images/icon19active.png b/images/icon19active.pngBinary files differ new file mode 100644 index 0000000..def8e2f --- /dev/null +++ b/images/icon19active.png diff --git a/images/icon19disabled.png b/images/icon19disabled.pngBinary files differ new file mode 100644 index 0000000..f5eaa06 --- /dev/null +++ b/images/icon19disabled.png diff --git a/images/icon32active.png b/images/icon32active.pngBinary files differ new file mode 100644 index 0000000..2bc01ff --- /dev/null +++ b/images/icon32active.png diff --git a/images/icon32disabled.png b/images/icon32disabled.pngBinary files differ new file mode 100644 index 0000000..28d2dc0 --- /dev/null +++ b/images/icon32disabled.png diff --git a/images/icon38active.png b/images/icon38active.pngBinary files differ new file mode 100644 index 0000000..2e08aec --- /dev/null +++ b/images/icon38active.png diff --git a/images/icon38disabled.png b/images/icon38disabled.pngBinary files differ new file mode 100644 index 0000000..7e900ba --- /dev/null +++ b/images/icon38disabled.png diff --git a/images/icon48active.png b/images/icon48active.pngBinary files differ new file mode 100644 index 0000000..f63956b --- /dev/null +++ b/images/icon48active.png diff --git a/images/icon48disabled.png b/images/icon48disabled.pngBinary files differ new file mode 100644 index 0000000..e2826c4 --- /dev/null +++ b/images/icon48disabled.png diff --git a/images/redirector.png b/images/redirector.pngBinary files differ deleted file mode 100644 index f8de12c..0000000 --- a/images/redirector.png +++ /dev/null diff --git a/images/statusactive.png b/images/statusactive.pngBinary files differ deleted file mode 100644 index 3127229..0000000 --- a/images/statusactive.png +++ /dev/null diff --git a/images/statusinactive.png b/images/statusinactive.pngBinary files differ deleted file mode 100644 index 4c0438e..0000000 --- a/images/statusinactive.png +++ /dev/null @@ -1,276 +1,14 @@ -var redirectorApp = angular.module('redirectorApp', []) -.config( [ -    '$compileProvider', -    function( $compileProvider ) -    {    -        $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|data):/); -    } -]); +//Nothing really here except the app object. Filters, and directives are  +//include with the controllers that use them. If we need to add any that +//are used by multiple controllers then we'll define them here. +var redirectorApp = angular.module('redirectorApp', []); -//Directive for file upload: -redirectorApp.directive('fileselected', function() { -    return { -        restrict: 'A', -        link: function(scope, element, attr, ctrl) { -            element.bind('change', function(e) { -            	var f = element[0].files[0]; -            	element[0].value = ''; -            	scope.$eval(attr.fileselected, {'$file':f}); -            }); -        } -    } -}); -//Filter for displaying nice names for request types -redirectorApp.filter('requestTypeDisplay', function() { -	return function(input) { -		return input.map(function(key) { return requestTypes[key]; }).join(', '); -	} -}); -var storage = chrome.storage.local; //TODO: Change to sync when Firefox supports it... -var requestTypes = { -	main_frame: "Main window (address bar)", -	sub_frame: "IFrames", -	stylesheet : "Stylesheets", -	script : "Scripts", -	image : "Images", -	object : "Objects (e.g. Flash videos, Java applets)", -	xmlhttprequest : "XMLHttpRequests (Ajax)", -	other : "Other" -}; -redirectorApp.controller('redirectorController', ['$scope', '$timeout', function($s, $timeout) { -	$s.activeRedirect = null; -	$s.editIndex = -1; -	$s.requestTypes = requestTypes; -	$s.appliesTo = function(key) { -		if (!$s.activeRedirect) { -			return; -		} -		return $s.activeRedirect.appliesTo.indexOf(key) != -1; -	}; -	$s.toggleApplies = function(key) { -		if (!$s.activeRedirect) { -			return; -		} -		var arr = $s.activeRedirect.appliesTo; - -		var index = arr.indexOf(key); -		if (index == -1) { -			arr.push(key); -		} else { -			arr.splice(index, 1); -		} - -		var order = 'main_frame,sub_frame,stylesheet,script,image,object,xmlhttprequest,other'; - -		arr.sort(function(a,b) { -			return order.indexOf(a) - order.indexOf(b); -		}); -	}; - - -	function closeEditForm() { -		$s.editIndex = -1; -		$s.activeRedirect = null; -		$s.showAdvanced = false; -		$s.showModal = false; -	} - -	$s.createNew = function() { -		$s.activeRedirect = new Redirect({}).toObject(); -		$s.showModal = true; -	}; - -	$s.saveRedirect = function() { -		if ($s.editIndex >= 0) { -			$s.redirects[$s.editIndex] = $s.activeRedirect; -		} else { -			$s.redirects.push(new Redirect($s.activeRedirect).toObject()); -		} -		closeEditForm(); -		saveChanges(); -	}; - -	$s.cancelEdit = function() { -		closeEditForm(); -	} - -	function saveChanges() { - -		var arr = []; -		for (var i=0; i < $s.redirects.length; i++) { -			var r = $s.redirects[i]; -			arr.push(new Redirect(r).toObject()); -		} - -		storage.set({redirects:arr}, function() { -			console.log('Saved redirects'); -		}); -		updateExportLink(); -	} - -	function swap(arr, i, n) { -		var item = arr[i]; -		arr[i] = arr[n]; -		arr[n] = item; -	} - -	$s.moveUp = function(index) { -		if (index == 0) { -			return; -		} -		swap($s.redirects, index, index-1); -		saveChanges(); -	}; - -	$s.moveDown = function(index) { -		if (index == $s.redirects.length-1) { -			return; -		} -		swap($s.redirects, index, index+1); -		saveChanges(); -	}; - -	$s.confirmDelete = function(index) { -		$s.deleting = $s.redirects[index]; -		$s.deletingIndex = index; -		$s.showModal = true; -	} - -	$s.cancelDelete = function(index) { -		delete $s.deleting; -		delete $s.deletingIndex; -		$s.showModal = false; -	} - -	$s.deleteRedirect = function() { -		$s.redirects.splice($s.deletingIndex, 1); -		delete $s.deleting; -		delete $s.deletingIndex; -		$s.showModal = false; -		saveChanges(); -	}; - -	$s.editRedirect = function(index) { -		$s.activeRedirect = new Redirect($s.redirects[index]).toObject(); -		$s.editIndex = index; -		$s.showModal = true; -	}; - -	$s.toggleDisabled = function(redirect) { -		redirect.disabled = !redirect.disabled; -		saveChanges(); -	} -  - 	$s.redirects = []; - 	storage.get('redirects', function(results) { - 		if (!results || !results.redirects) { - 			return; - 		} - 		for (var i=0; i < results.redirects.length; i++) { -			$s.redirects.push(new Redirect(results.redirects[i]).toObject()); -		} -		updateExportLink(); -		$s.$apply(); - 	}); - - 	function msg(message, success) { - 		$s.message = message; - 		$s.messageType = success ? 'success' : 'error'; - - 		var m = message; - - 		//Remove the message in 5 seconds if it hasn't been changed... - 		$timeout(function() { - 			if ($s.message == m) { - 				$s.message = null; - 			} - 		}, 20 * 1000); - 	} - - 	/* Import/Export of Redirects */ - 	$s.importRedirects = function(file) { - 		if (!file) { - 			return; - 		} - 		var reader = new FileReader(); - 		 - 		reader.onload = function(e) { - 			var data; - 			try { -	 			var data = JSON.parse(reader.result); - 			} catch(e) { - 				msg('Failed to parse JSON data, invalid JSON: ' + (e.message||'').substr(0,100)); -	 			return $s.$apply(); - 			} - - 			if (!data.redirects) { - 				msg('Invalid JSON, missing "redirects" property'); - 				return $s.$apply(); - 			} - - 			var imported = 0, existing = 0; - 			for (var i = 0; i < data.redirects.length; i++) { - 				var r = new Redirect(data.redirects[i]); - - 				if ($s.redirects.some(function(i) { return new Redirect(i).equals(r);})) { - 					existing++; - 				} else { -	 				$s.redirects.push(r.toObject()); -	 				imported++; - 				} - 			} - 			if (imported == 0 && existing == 0) { - 				msg('No redirects existed in the file.'); - 			} - 			if (imported > 0 && existing == 0) { - 				msg('Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's.' : '.'), true); - 			} - 			if (imported == 0 && existing > 0) { - 				msg('All redirects in the file already existed and were ignored.'); - 			} - 			if (imported > 0 && existing > 0) { - 				var m = 'Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's' : '') + '. '; - 				if (existing == 1) { - 					m += '1 redirect already existed and was ignored.'; - 				} else { - 					m += existing + ' redirects already existed and were ignored.';  - 				} - 				msg(m, true); - 			} - - 			saveChanges(); - 			$s.$apply(); -  		}; -  		try { -	 		reader.readAsText(file, 'utf-8'); -  		} catch(e) { -  			msg('Failed to read import file'); -  		} - 	} - - 	function updateExportLink() { -		var redirects = $s.redirects.map(function(r) { - 			return new Redirect(r).toObject(); - 		}); - 		 - 		var exportObj = {  - 			createdBy : 'Redirector v' + chrome.app.getDetails().version,  - 			createdAt : new Date(),  - 			redirects : redirects  - 		}; - 		 - 		var json = JSON.stringify(exportObj, null, 4); - 		 - 		$s.redirectDownload = 'data:text/plain;charset=utf-8,' + encodeURIComponent(json);  - 	} - -}]); diff --git a/js/background.js b/js/background.js index b9ef47b..b6260d7 100644 --- a/js/background.js +++ b/js/background.js @@ -19,7 +19,7 @@ function checkForRedirect(details) {  var filter = {urls:["http://*/*", "https://*/*"]};  //TODO: Better browser detection... -var isChrome = !!navigator.userAgent.match(/ Chrome\//); +var isFirefox = !!navigator.userAgent.match(/Firefox\//); -var ev = isChrome ? chrome.webRequest.onBeforeRequest : chrome.webRequest.onBeforeSendHeaders; +var ev = isFirefox ? chrome.webRequest.onBeforeSendHeaders : chrome.webRequest.onBeforeRequest;  ev.addListener(checkForRedirect, filter, ["blocking"]); diff --git a/js/controllers/deleteredirect.js b/js/controllers/deleteredirect.js new file mode 100644 index 0000000..f480339 --- /dev/null +++ b/js/controllers/deleteredirect.js @@ -0,0 +1,25 @@ +redirectorApp.controller('DeleteRedirectCtrl', ['$scope', function($s) { + +	// Ok, this is pretty ugly. But I want to make this controller to control +	// everything about the deleting process, so I make this available on +	// the parent scope, so the RedirectListCtrl can access it. +	$s.$parent.confirmDeleteRedirect = function(index) { +		$s.redirect = $s.redirects[index]; +		$s.deleteIndex = index; +		$s.$parent.showDeleteForm = true; +	}; + +	$s.cancelDelete = function(index) { +		delete $s.redirect; +		delete $s.deletingIndex; +		$s.$parent.showDeleteForm = false; +	} + +	$s.deleteRedirect = function() { +		$s.redirects.splice($s.deletingIndex, 1); +		delete $s.redirect; +		delete $s.deletingIndex; +		$s.$parent.showDeleteForm = false; +		$s.saveChanges(); +	}; +}]);
\ No newline at end of file diff --git a/js/controllers/editredirect.js b/js/controllers/editredirect.js new file mode 100644 index 0000000..f24f3e6 --- /dev/null +++ b/js/controllers/editredirect.js @@ -0,0 +1,82 @@ +redirectorApp.controller('EditRedirectCtrl', ['$scope', function($s) { +	 + +	$s.requestTypes = Redirect.requestTypes; + +	// Ok, this is pretty ugly. But I want to make this controller to control +	// everything about the editing process, so I make this available on +	// the parent scope, so the RedirectListCtrl can access it. +	$s.$parent.editRedirect = function(index) { +		$s.redirect = new Redirect($s.redirects[index]);  +		$s.editIndex = index; +		$s.redirect.updateExampleResult(); +		if ($s.redirect.escapeMatches || $s.redirect.unescapeMatches || $s.redirect.excludePattern +			|| !($s.redirect.appliesTo.length == 1 && $s.redirect.appliesTo[0] == "main_frame")) { +			$s.showAdvanced = true; //Auto show advanced if redirect uses advanced options +		} +		$s.$parent.showEditForm = true; +	}; + +	// Same, this is for the Create New button, which is starting +	// the edit form, so I want to control it from here. +	$s.$parent.createNewRedirect = function() { +		$s.redirect = new Redirect({}); +		$s.$parent.showEditForm = true; +	}; + +	$s.saveRedirect = function() { +		if ($s.redirect.error) { +			return; //Button is already disabled, but we still get the click +		} + +		if ($s.editIndex >= 0) { +			$s.redirects[$s.editIndex] = $s.redirect; +		} else { +			$s.redirects.push($s.redirect); +		} +		closeEditForm(); +		$s.saveChanges(); +	}; + +	$s.cancelEdit = function() { +		closeEditForm(); +	} + +	// To bind a list of strings to a list of checkboxes +	$s.appliesTo = function(key) { +		if (!$s.redirect) { +			return; +		} +		return $s.redirect.appliesTo.indexOf(key) != -1; +	}; + +	// Add or remove string from array based on whether checkbox is checked +	$s.toggleApplies = function(key) { +		if (!$s.redirect) { +			return; +		} +		var arr = $s.redirect.appliesTo; + +		var index = arr.indexOf(key); +		if (index == -1) { +			arr.push(key); +		} else { +			arr.splice(index, 1); +		} + +		var order = 'main_frame,sub_frame,stylesheet,script,image,object,xmlhttprequest,other'; + +		arr.sort(function(a,b) { +			return order.indexOf(a) - order.indexOf(b); +		}); + +		$s.redirect.updateExampleResult(); +	}; +	 +	function closeEditForm() { +		$s.editIndex = -1; +		$s.redirect = null; +		$s.showAdvanced = false; +		$s.$parent.showEditForm = false; +	} +}]); diff --git a/js/controllers/importexport.js b/js/controllers/importexport.js new file mode 100644 index 0000000..191990c --- /dev/null +++ b/js/controllers/importexport.js @@ -0,0 +1,110 @@ + +//This controller, and associated directives and config, are responsible for import and exporting redirects +//from .json files. + +redirectorApp.config([ +    '$compileProvider', +    function($compileProvider) {    +        $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|chrome-extension|data):/); +    }] +).directive('fileselected', function() { //Directive for file upload: +    return { +        restrict: 'A', +        link: function(scope, element, attr, ctrl) { +            element.bind('change', function(e) { +            	var f = element[0].files[0]; +            	element[0].value = ''; +            	scope.$eval(attr.fileselected, {'$file':f}); +            }); +        } +    } +}).controller('ImportExportCtrl', ['$scope', function($s) { + +	// Shows a message explaining how many redirects were imported. +	function showImportedMessage(imported, existing) { +		if (imported == 0 && existing == 0) { +			$s.showMessage('No redirects existed in the file.'); +		} +		if (imported > 0 && existing == 0) { +			$s.showMessage('Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's.' : '.'), true); +		} +		if (imported == 0 && existing > 0) { +			$s.showMessage('All redirects in the file already existed and were ignored.'); +		} +		if (imported > 0 && existing > 0) { +			var m = 'Successfully imported ' + imported + ' redirect' + (imported > 1 ? 's' : '') + '. '; +			if (existing == 1) { +				m += '1 redirect already existed and was ignored.'; +			} else { +				m += existing + ' redirects already existed and were ignored.';  +			} +			$s.showMessage(m, true); +		} +	} + + 	$s.importRedirects = function(file) { + 		if (!file) { + 			return; + 		} + 		var reader = new FileReader(); + 		 + 		reader.onload = function(e) { + 			var data; + 			try { +	 			var data = JSON.parse(reader.result); + 			} catch(e) { + 				$s.showMessage('Failed to parse JSON data, invalid JSON: ' + (e.message||'').substr(0,100)); +	 			return $s.$parent.$apply(); + 			} + + 			if (!data.redirects) { + 				$s.showMessage('Invalid JSON, missing "redirects" property'); + 				return $s.$parent.$apply(); + 			} + + 			var imported = 0, existing = 0; + 			for (var i = 0; i < data.redirects.length; i++) { + 				var r = new Redirect(data.redirects[i]); + + 				if ($s.redirects.some(function(i) { return new Redirect(i).equals(r);})) { + 					existing++; + 				} else { +	 				$s.redirects.push(r.toObject()); +	 				imported++; + 				} + 			} + 			 + 			showImportedMessage(imported, existing); + + 			$s.saveChanges(); + 			$s.$parent.$apply(); +  		}; +  		try { +	 		reader.readAsText(file, 'utf-8'); +  		} catch(e) { +  			$s.showMessage('Failed to read import file'); +  		} + 	} + + 	// Updates the export link with a data url containing all the redirects. + 	// We want to have the href updated instead of just generated on click to + 	// allow people to right click and choose Save As... + 	$s.updateExportLink =  function() { +		var redirects = $s.redirects.map(function(r) { + 			return new Redirect(r).toObject(); + 		}); + 		 + 		var exportObj = {  + 			createdBy : 'Redirector v' + chrome.app.getDetails().version,  + 			createdAt : new Date(),  + 			redirects : redirects  + 		}; + 		 + 		var json = JSON.stringify(exportObj, null, 4); + 		 + 		//Using encodeURIComponent here instead of base64 because base64 always messed up our encoding for some reason... + 		$s.redirectDownload = 'data:text/plain;charset=utf-8,' + encodeURIComponent(json);  + 	} + + 	$s.updateExportLink(); //Run once so the a will have a href to begin with +}]);
\ No newline at end of file diff --git a/js/controllers/listredirects.js b/js/controllers/listredirects.js new file mode 100644 index 0000000..008a527 --- /dev/null +++ b/js/controllers/listredirects.js @@ -0,0 +1,44 @@ +// This controller is responsible for the list of redirects and the actions +// that can be taken from there. +redirectorApp.filter('requestTypeDisplay', function() { //Filter for displaying nice names for request types +	return function(input) { +		return input.map(function(key) { return Redirect.requestTypes[key]; }).join(', '); +	} +}).controller('ListRedirectsCtrl', ['$scope', function($s) { +	 +	function swap(arr, i, n) { +		var item = arr[i]; +		arr[i] = arr[n]; +		arr[n] = item; +	} + +	// Move the redirect at index up in the list, giving it higher priority +	$s.moveUp = function(index) { +		if (index == 0) { +			return; +		} +		swap($s.redirects, index, index-1); +		$s.saveChanges(); +	}; + +	// Move the redirect at index down in the list, giving it lower priority +	$s.moveDown = function(index) { +		if (index == $s.redirects.length-1) { +			return; +		} +		swap($s.redirects, index, index+1); +		$s.saveChanges(); +	}; + +	$s.toggleDisabled = function(redirect) { +		redirect.disabled = !redirect.disabled; +		$s.saveChanges(); +	}; + +	$s.example = function(redirect) { +		return new Redirect(redirect).getMatch(redirect.exampleUrl).redirectTo; +	}; + +	//Edit button is defined in EditRedirectCtrl +	//Delete button is defined in DeleteRedirectCtrl +}]);
\ No newline at end of file diff --git a/js/controllers/redirectorpage.js b/js/controllers/redirectorpage.js new file mode 100644 index 0000000..05fbe9f --- /dev/null +++ b/js/controllers/redirectorpage.js @@ -0,0 +1,50 @@ +// This is the main controller of the page. It is responsible for showing messages, +// modal windows and loading and saving the list of redirects, that all of the +// controllers work with. +redirectorApp.controller('RedirectorPageCtrl', ['$scope', '$timeout', function($s, $timeout) { +	 +	$s.deleting = null;  //Variable for redirect being edited, of the form { index:<nr>, redirect:<redirect>}; +	$s.showEditForm = $s.showDeleteForm = false; // Variables, child controllers can set them to show their forms + +	var storage = chrome.storage.local; //TODO: Change to sync when Firefox supports it... + +	function normalize(r) { +		return new Redirect(r).toObject(); //Cleans out any extra props, and adds default values for missing ones. +	} + +	// Saves the entire list of redirects to storage. +	$s.saveChanges = function() { + +		// Clean them up so angular $$hash things and stuff don't get serialized. +		var arr = $s.redirects.map(normalize); + +		storage.set({redirects:arr}, function() { +			console.log('Saved redirects at ' + new Date()); +		}); +	} +  + 	$s.redirects = []; + 	storage.get('redirects', function(results) { + 		if (!results || !results.redirects) { + 			return; + 		} + + 		for (var i=0; i < results.redirects.length; i++) { +			$s.redirects.push(normalize(results.redirects[i])); +		} +		$s.$apply(); + 	}); + + 	// Shows a message bar above the list of redirects. + 	$s.showMessage = function(message, success) { + 		$s.message = message; + 		$s.messageType = success ? 'success' : 'error'; + + 		//Remove the message in 20 seconds if it hasn't been changed... + 		$timeout(function() { + 			if ($s.message == message) { + 				$s.message = null; + 			} + 		}, 20 * 1000); + 	} +}]); diff --git a/js/popup.js b/js/popup.js new file mode 100644 index 0000000..fbff178 --- /dev/null +++ b/js/popup.js @@ -0,0 +1,13 @@ +function hi() { +	chrome.browserAction.setIcon({ +  		path: { +    		19: "images/icon19disabled.png", +    		38: "images/icon38disabled.png" +  		} +  	}); +  	open('redirector.html'); +} + +document.addEventListener('DOMContentLoaded', function() { +	document.getElementsByTagName('button')[0].addEventListener('click', hi); +})
\ No newline at end of file diff --git a/js/redirect.js b/js/redirect.js index 41e3c1d..b895ed9 100644 --- a/js/redirect.js +++ b/js/redirect.js @@ -7,11 +7,24 @@ function Redirect(o) {  Redirect.WILDCARD = 'W';  Redirect.REGEX = 'R'; +Redirect.requestTypes = { +	main_frame: "Main window (address bar)", +	sub_frame: "IFrames", +	stylesheet : "Stylesheets", +	script : "Scripts", +	image : "Images", +	object : "Objects (e.g. Flash videos, Java applets)", +	xmlhttprequest : "XMLHttpRequests (Ajax)", +	other : "Other" +}; +  Redirect.prototype = {  	//attributes  	description : '',  	exampleUrl : '', +	exampleResult : '', +	error : null,  	includePattern : '',  	excludePattern : '',  	redirectUrl : '', @@ -21,8 +34,16 @@ Redirect.prototype = {  	disabled : false,  	compile : function() { -		this._rxInclude = this._compile(this._includePattern);  -		this._rxExclude = this._compile(this._excludePattern);  + +		var incPattern = this._preparePattern(this.includePattern); +		var excPattern = this._preparePattern(this.excludePattern); + +		if (incPattern) { +			this._rxInclude = new RegExp(incPattern, 'gi'); +		} +		if (excPattern) { +			this._rxExclude = new RegExp(excPattern, 'gi'); +		}  	},  	equals : function(redirect) { @@ -41,6 +62,8 @@ Redirect.prototype = {  		return {  			description : this.description,  			exampleUrl : this.exampleUrl, +			exampleResult : this.exampleResult, +			error : this.error,  			includePattern : this.includePattern,  			excludePattern : this.excludePattern,  			redirectUrl : this.redirectUrl, @@ -52,7 +75,10 @@ Redirect.prototype = {  		};  	}, -	getMatch: function(url) { +	getMatch: function(url, forceIgnoreDisabled) { +		if (!this._rxInclude) { +			this.compile(); +		}  		var result = {   			isMatch : false,   			isExcludeMatch : false,  @@ -64,7 +90,7 @@ Redirect.prototype = {  		redirectTo = this._includeMatch(url);  		if (redirectTo !== null) { -			if (this.disabled) { +			if (this.disabled && !forceIgnoreDisabled) {  				result.isDisabledMatch = true;  			} else if (this._excludeMatch(url)) {  				result.isExcludeMatch = true; @@ -76,6 +102,66 @@ Redirect.prototype = {  		return result;	   	}, +	//Updates the .exampleResult field or the .error +	//field depending on if the example url and patterns match  +	//and make a good redirect +	updateExampleResult : function() { + +		//Default values +		this.error = null; +		this.exampleResult = ''; + + +		if (!this.exampleUrl) { +			this.error = 'No example URL defined'; +			return; +		} + +		if (this.patternType == Redirect.REGEX && this.includePattern) { +			try { +				new RegExp(this.includePattern, 'gi'); +			} catch(e) { +				this.error = 'Invalid regular expression in Include pattern.'; +				return; +			} +		} + +		if (this.patternType == Redirect.REGEX && this.excludePattern) { +			try { +				new RegExp(this.excludePattern, 'gi'); +			} catch(e) { +				this.error = 'Invalid regular expression in Exclude pattern.'; +				return; +			} +		} + +		if (!this.appliesTo || this.appliesTo.length == 0) { +			this.error = 'At least one request type must be chosen.'; +			return; +		} + +		this.compile(); + +		var match = this.getMatch(this.exampleUrl, true); + +		if (match.isExcludeMatch) { +			this.error = 'The exclude pattern excludes the example url.' +			return; +		} + +		if (!match.isMatch) { +			this.error = 'The include pattern does not match the example url.'; +			return; +		} + +		this.exampleResult = match.redirectTo; + +		if (this.getMatch(this.exampleResult, true).isMatch) { +			this.exampleResult = ''; +			this.error = 'Result matches the redirect again, causing endless loop.' +		} +	}, +  	isRegex: function() {  		return this.patternType == Redirect.REGEX;  	}, @@ -89,14 +175,13 @@ Redirect.prototype = {  	},  	//Private functions below	 - -	_includePattern : null, -	_excludePattern : null, -	_patternType : null,  	_rxInclude : null,  	_rxExclude : null,  	_preparePattern : function(pattern) { +		if (!pattern) { +			return null; +		}  		if (this.patternType == Redirect.REGEX) {  			return pattern;   		} else { //Convert wildcard to regex pattern @@ -115,13 +200,6 @@ Redirect.prototype = {  			return converted;  		}  	}, - -	_compile : function(pattern) { -		if (!pattern) { -			return null; -		} -		return new RegExp(this._preparePattern(pattern),"gi"); -	},  	_init : function(o) {  		this.description = o.description || '', @@ -141,16 +219,7 @@ Redirect.prototype = {  	},  	toString : function() { -		return 'REDIRECT: {' -			+  '\n\tExample url 	 : ' + this.exampleUrl -			+  '\n\tInclude pattern  : ' + this.includePattern -			+  '\n\tExclude pattern  : ' + this.excludePattern -			+  '\n\tRedirect url	 : ' + this.redirectUrl -			+  '\n\tPattern type	 : ' + this.patternType -			+  '\n\tUnescape matches : ' + this.unescapeMatches -			+  '\n\tEscape matches : ' + this.escapeMatches -			+  '\n\tDisabled		 : ' + this.disabled -			+  '\n}\n'; +		return JSON.stringify(this.toObject(), null, 2);  	},  	_includeMatch : function(url) { diff --git a/manifest.json b/manifest.json index 06ca42a..20d0b2f 100644 --- a/manifest.json +++ b/manifest.json @@ -3,6 +3,11 @@    "manifest_version": 2,    "name": "Redirector",    "version": "3.0", +   +  "icons": {   "16": "images/icon16active.png", +               "32": "images/icon32active.png", +               "48": "images/icon48active.png", +              "128": "images/icon128active.png" },    "permissions" : ["webRequest", "webRequestBlocking", "storage", "http://*/*", "https://*/*"],    "applications": { @@ -14,5 +19,19 @@   "background": {      "scripts": ["js/background.js"],      "persistent": true +  }, + +  "options_ui": { +    "page": "popup.html", +    "chrome_style": true +  }, + +  "browser_action": { +    "default_icon": { +      "19": "images/icon19active.png", +      "38": "images/icon38active.png"  +    }, +    "default_title": "Redirector", +    "default_popup": "popup.html"    }  } diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..5e7e81f --- /dev/null +++ b/popup.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +	<head> +		<title>Redirector</title> +		<link rel="stylesheet" href="css/popup.css" /> +		<meta charset="UTF-8"> +		<script src="js/angular.min.js"></script> +		<script src="js/popup.js"></script> +	</head> +	<body > +		<h1>Redirector</h1> +		<button>Disable</button> +	</body> +</html> diff --git a/redirector.html b/redirector.html index aa0ccbc..4f06438 100644 --- a/redirector.html +++ b/redirector.html @@ -7,62 +7,72 @@  		<script src="js/angular.min.js"></script>  		<script src="js/redirect.js"></script>  		<script src="js/app.js"></script> +		<script src="js/controllers/redirectorpage.js"></script> +		<script src="js/controllers/editredirect.js"></script> +		<script src="js/controllers/deleteredirect.js"></script> +		<script src="js/controllers/importexport.js"></script> +		<script src="js/controllers/listredirects.js"></script>  	</head> -	<body ng-app="redirectorApp" ng-controller="redirectorController"> -		<div id="cover" ng-show="showModal"> -		</div> -		<div id="delete-redirect-form" ng-show="deleting"> +	<body ng-app="redirectorApp" ng-controller="RedirectorPageCtrl"> +		<div id="cover" ng-show="showEditForm || showDeleteForm"></div> +		 +		<!-- Confirmation form for deleting redirects --> +		<div id="delete-redirect-form" ng-show="showDeleteForm" ng-controller="DeleteRedirectCtrl">  			<h3>Are you sure you want to delete this redirect?</h3>  			<div>  				<label>Description:</label> -				<span>{{deleting.description}}</span> +				<span>{{redirect.description}}</span>  			</div>  			<div>  				<label>Example URL:</label> -				<span>{{deleting.exampleUrl}}</span> +				<span>{{redirect.exampleUrl}}</span>  			</div>  			<div>  				<label>Include pattern:</label> -				<span>{{deleting.includePattern}} +				<span>{{redirect.includePattern}}  			</div>  			<div>  				<label>Redirect to:</label> -				<span>{{deleting.redirectUrl}}</span> +				<span>{{redirect.redirectUrl}}</span>  			</div>  			<div>  				<label>Pattern type:</label> -				<span>{{deleting.patternType == 'W' ? 'Wildcard' : 'Regular Expression'}}</span> +				<span>{{redirect.patternType == 'W' ? 'Wildcard' : 'Regular Expression'}}</span>  			</div>			  			<div class="button-container">  				<a class="btn red large" ng-click="deleteRedirect()">Yes, delete it</a>  				<a class="btn grey large" ng-click="cancelDelete()">No, don't delete it</a>  			</div>  		</div> -		<div id="edit-redirect-form" ng-show="activeRedirect"> +		 + +		<!-- Form for creating and editing redirects --> +		<div id="edit-redirect-form" ng-show="showEditForm" ng-controller="EditRedirectCtrl">  			<h3>{{editIndex >= 0 ? 'Edit' : 'Create'}} Redirect</h3>  			<div>  				<label>Description:</label> -				<input type="text" ng-model="activeRedirect.description" placeholder="Enter a description for your redirect rule" /> +				<input type="text" ng-model="redirect.description" placeholder="Enter a description for your redirect rule" />  			</div>  			<div>  				<label>Example URL:</label> -				<input type="text" ng-model="activeRedirect.exampleUrl" placeholder="http://example.com/some/path?a=1" /> +				<input type="text" ng-model="redirect.exampleUrl" ng-change="redirect.updateExampleResult()" placeholder="http://example.com/some/path?a=1" />  			</div>  			<div>  				<label>Include pattern:</label> -				<input type="text" ng-model="activeRedirect.includePattern" placeholder="Pattern that matches the urls you want to redirect" /> +				<input type="text" ng-model="redirect.includePattern" ng-change="redirect.updateExampleResult()" placeholder="Pattern that matches the urls you want to redirect" />  			</div>  			<div>  				<label>Redirect to:</label> -				<input type="text" ng-model="activeRedirect.redirectUrl" placeholder="The url you want to redirect requests to" /> +				<input type="text" ng-model="redirect.redirectUrl" ng-change="redirect.updateExampleResult()" placeholder="The url you want to redirect requests to" />  			</div>  			<div>  				<label>Pattern type:</label> -				<label id="wildcardtype"><input type="radio" ng-model="activeRedirect.patternType" name="patterntype" value="W">Wildcard</label> -				<label><input type="radio" ng-model="activeRedirect.patternType" name="patterntype" value="R">Regular Expression</label> +				<label id="wildcardtype"><input type="radio" ng-change="redirect.updateExampleResult()" ng-model="redirect.patternType" name="patterntype" value="W">Wildcard</label> +				<label><input type="radio" ng-change="redirect.updateExampleResult()" ng-model="redirect.patternType" name="patterntype" value="R">Regular Expression</label>  			</div>  			<div>  				<label>Example result:</label> +				<span class="error" ng-show="redirect.error">{{redirect.error}}</span><span ng-show="redirect.exampleResult">{{redirect.exampleResult}}</span>  			</div>  			<div id="advanced-toggle">  				<a ng-click="showAdvanced=true" ng-show="!showAdvanced">Show advanced options...</a> @@ -71,15 +81,15 @@  			<div id="advanced" ng-show="showAdvanced">  				<div>  					<label>Exclude pattern:</label> -					<input type="text" ng-model="activeRedirect.excludePattern" placeholder="Pattern to exclude certain urls from the redirection"/> +					<input type="text" ng-change="redirect.updateExampleResult()" ng-model="redirect.excludePattern" placeholder="Pattern to exclude certain urls from the redirection"/>  				</div>  				<div>  					<label for="unescape-matches">Unescape matches:</label> -					<input id="unescape-matches" ng-model="activeRedirect.unescapeMatches" type="checkbox"> +					<input id="unescape-matches" ng-change="redirect.updateExampleResult()" ng-model="redirect.unescapeMatches" type="checkbox"><span class="placeholder">E.g. turn %2Fbar%2Ffoo%3Fx%3D2 into /bar/foo?x=2</span>  				</div>	  				<div>  					<label for="escape-matches">Escape matches:</label> -					<input id="escape-matches" ng-model="activeRedirect.escapeMatches" type="checkbox"> +					<input id="escape-matches" ng-change="redirect.updateExampleResult()" ng-model="redirect.escapeMatches" type="checkbox"><span class="placeholder">E.g. turn /bar/foo?x=2 into %2Fbar%2Ffoo%3Fx%3D2</span>  				</div>  				<div>  					<label>Apply to:</label> @@ -89,23 +99,27 @@  				</div>  			</div>  			<div class="button-container"> -				<a class="btn green medium" ng-click="saveRedirect()">Save</a> -				<a class="btn red medium" ng-click="cancelEdit()">Cancel</a> +				<a ng-class="{disabled:redirect.error}" class="btn green large" ng-click="saveRedirect()">Save</a> +				<a class="btn red large" ng-click="cancelEdit()">Cancel</a>  			</div>  		</div> -		<div id="blur-wrapper" ng-class="{blur: showModal}"> -			<h1>Redirector</h1> -			<!--<h3>DISABLED</h3>--> +		<div id="blur-wrapper" ng-class="{blur: showEditForm || showDeleteForm}"> +			 +			<h1><span style="font-size:100px; position:relative; top:4px">☈</span>edirector</h1>  			<h5>Go where <em>YOU</em> want!</h5> -  			<div id="menu"> -				<input type="file" id="import-file" fileselected="importRedirects($file)" accept=".rjson,.json,.txt,text/*"  /> -				<a class="btn blue large" ng-click="createNew()">Create new redirect</a> -				<label for="import-file" class="btn blue large">Import</label> -				<a class="btn blue large" ng-href="{{redirectDownload}}" download="Redirector.rjson">Export</a> +				<a class="btn blue large" ng-click="createNewRedirect()">Create new redirect</a> +				 +				<!-- Importing/Exporting of redirects --> +				<span ng-controller="ImportExportCtrl"> +					<input type="file" id="import-file" fileselected="importRedirects($file)" accept=".rjson,.json,.txt,text/*"  /> +					<label for="import-file" class="btn blue large">Import</label> +					<a class="btn blue large" ng-mousedown="updateExportLink()" ng-href="{{redirectDownload}}" download="Redirector.json">Export</a> +				</span> +	  				<a class="btn blue large" href="help.html" target="_blank">Help</a>  			</div> @@ -113,20 +127,22 @@  				{{message}}  				<a ng-click="message=null">✖</a>  			</div> -			<div class="redirect-table"> + +			<!-- List of existing redirects --> +			<div class="redirect-table" ng-controller="ListRedirectsCtrl">  				<div class="redirect-row" ng-class="{disabled: r.disabled}" ng-repeat="r in redirects">  					<h4><span class="disabled-marker" ng-show="r.disabled">[Disabled] </span><span>{{r.description}}</span></h4>  					<div>  						<label>Redirect:</label> <span>{{r.includePattern}}</span>  					</div>  					<div> -						<label>to:</label> <span>foo.is</span> +						<label>to:</label> <span>{{r.redirectUrl}}</span>  					</div>  					<div ng-show="r.excludePattern">  						<label>excluding:</label> <span>asdfasdf</span>  					</div>  					<div> -						<label>Example:</label> <span></span> +						<label>Example:</label> <span>{{example(r)}}</span>  					</div>  					<div>  						<label>Applies to:</label> <span>{{r.appliesTo|requestTypeDisplay}}</span> @@ -134,15 +150,17 @@  					<div>  						<a class="btn blue" ng-click="toggleDisabled(r)">{{r.disabled ? "Enable" : "Disable"}}</a>  						<a class="btn green" ng-click="editRedirect($index)">Edit</a> -						<a class="btn red" ng-click="confirmDelete($index)">Delete</a> +						<a class="btn red" ng-click="confirmDeleteRedirect($index)">Delete</a>  						<a class="btn grey move-up-btn" ng-class="{disabled:$first}" ng-click="moveUp($index)">▲</a>  						<a class="btn grey move-down-btn" ng-class="{disabled:$last}" ng-click="moveDown($index)">▼</a>  					</div>  				</div>  			</div> +  			<footer>  				<small>Redirector is created by <a target="_blank" href="http://einaregilsson.com">Einar Egilsson</a></small>  			</footer> +		  		</div>  	</body>  </html> | 
