Nico Nico Ranking NG

By kengo321 Last update Jan 31, 2012 — Installed 1,544 times.

There are 17 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name Nico Nico Ranking NG
// @namespace http://userscripts.org/users/121129 
// @description ニコニコ動画のランキングにNG機能を追加
// @include http://www.nicovideo.jp/ranking/*
// @version 1.3
// ==/UserScript==

Util = {};

Util.throwErrorIfHasNotPropertyChangedFunc = function(listener) {
	if (typeof(listener.propertyChanged) !== "function") {
		throw new Error("the listener must have a propertyChanged function");
	}
};

Util.emptyPropertyChangeListener = {
	propertyChanged: function(source, changedPropertyName) {}
};

Util.getEmptyPropertyChangeListener = function() {
	return Util.emptyPropertyChangeListener;
};

Util.isEmptyString = function(value) {
	return typeof(value) === "string" && value.length === 0;
};

Util.throwErrorIfEmptyString = function(value, name) {
	if (Util.isEmptyString(value)) {
		throw new Error("the " + name + " must not be an empty string");
	}
};

Util.removeAllChildren = function(parent) {
	while (parent.firstChild) {
		parent.removeChild(parent.firstChild);
	}
};

Util.contains = function(start, end, value) {
	return start <= value && value <= end;
};

function View() {
	this.reduceRadio = View.newVisitedMovieViewModeRadio(View.Mode.REDUCE.name);
	this.hideRadio = View.newVisitedMovieViewModeRadio(View.Mode.HIDE.name);
	this.doNothingRadio = View.newVisitedMovieViewModeRadio(View.Mode.DO_NOTHING.name);
	this.ngMovieVisibilityCheckBox = View.newInput("checkbox");
	this.configButton = View.newLinkSpan(View.CONFIG_TEXT);
	this.movieIdToData = {};
	this.toolboxAdded = false;
}

View.PREPEND_TEXT = "閲覧済みの動画を";
View.REDUCE_TEXT = " 縮小";
View.HIDE_TEXT = " 非表示";
View.DO_NOTHING_TEXT = " 通常表示";
View.NG_MOVIES_SHOW_TEXT = " NG動画を表示";
View.CONFIG_TEXT = "設定";
View.PROMPT_MESSAGE = "NGタイトルを入力";

View.VISITED_MOVIE_VIEW_MODE = "visitedMovieViewMode";

View.ADDITION = {
	ngMovieIdButtonType: "NG登録",
	ngMovieTitleButtonType: "NGタイトル追加",
	visitButtonText: "閲覧済み",
	visitButtonClicked: function(model, movieId) {
		model.addVisitedMovieId(movieId);
	},
	ngMovieIdButtonClicked: function(model, movieId) {
		model.addNgMovieId(movieId);
	},
	ngMovieTitleButtonClicked: function(model, movieId) {
		var promptResult = View.promptNgMovieTitle(
				model.getMovieById(movieId).getTitle());
		if (promptResult) {
			model.addNgMovieTitle(promptResult);
		}
	}
};

View.REMOVAL = {
	ngMovieIdButtonType: "NG解除",
	ngMovieTitleButtonType: "NGタイトル削除",
	visitButtonText: "未閲覧",
	visitButtonClicked: function(model, movieId) {
		model.removeVisitedMovieId(movieId);
	},
	ngMovieIdButtonClicked: function(model, movieId) {
		model.removeNgMovieId(movieId);
	},
	ngMovieTitleButtonClicked: function(model, movieId) {
		model.removeNgMovieTitle(
				model.getMovieById(movieId).getMatchedNgTitle());
	}
};

View.Mode = {};

View.Mode.DO_NOTHING = {
	name: "doNothing",
	restoreViewState: function(movieId) {
	},
	setViewState: function(movieId) {
	},
	viewModeChanged: function(view) {
		view.getDoNothingRadio().checked = true;
	}
};

View.Mode.HIDE = {
	name: "hide",
	restoreViewState: function(movieId) {
		View.Mode.setMovieRootVisible(movieId, true);
	},
	setViewState: function(movieId) {
		View.Mode.setMovieRootVisible(movieId, false);
	},
	viewModeChanged: function(view) {
		view.getHideRadio().checked = true;
	}
};

View.Mode.REDUCE = {
	name: "reduce",
	restoreViewState: function(movieId) {
		new View.Unreduction().perform(movieId);
	},
	setViewState: function(movieId) {
		new View.Reduction().perform(movieId);
	},
	viewModeChanged: function(view) {
		view.getReduceRadio().checked = true;
	}
};

View.Mode.setMovieRootVisible = function(movieId, visible) {
	var r = View.getMovieRoot(View.getWatchAnchor(movieId));
	if (!r) {
		throw new Error("View.getMovieRoot(child) return null");
	}
	if (visible) {
		r.style.removeProperty("display");
	} else {
		r.style.display = "none";
	}
};

View.Mode.getMode = function(name) {
	switch (name) {
	case View.Mode.DO_NOTHING.name:
		return View.Mode.DO_NOTHING;
	case View.Mode.HIDE.name:
		return View.Mode.HIDE;
	case View.Mode.REDUCE.name:
		return View.Mode.REDUCE;
	default:
		throw new Error(name);
	}
};

View.newElement = function(tagName, attrMap, styleMap, children) {
	var result = document.createElement(tagName);
	for (var attrName in attrMap) {
		result.setAttribute(attrName, attrMap[attrName]);
	}
	for (var styleName in styleMap) {
		result.style.setProperty(styleName, styleMap[styleName], null);
	}
	for (var i = 3; i < arguments.length; i++) {
		if (typeof(arguments[i]) === "string") {
			result.appendChild(document.createTextNode(arguments[i]));
		} else {
			result.appendChild(arguments[i]);
		}
	}
	return result;
};

View.newAnchorButton = function(text) {
	return View.newElement("a",
		{ "href": "javascript:void(0)", "style": "color:#FFF;" }, null,	text);
};

View.prototype.getDataByMovieId = function(movieId) {
	var data = this.movieIdToData[String(movieId)];
	if (data)
		return data;
	return this.movieIdToData[String(movieId)] = {
		viewState: View.Mode.DO_NOTHING,
		ngMovieId: false,
		ngMovieTitle: "",
		ngMovieIdType: View.ADDITION,
		ngMovieTitleType: View.ADDITION,
		visitType: View.ADDITION,
		ngMovieIdButton: View.newAnchorButton(View.ADDITION.ngMovieIdButtonType),
		ngMovieTitleButton: View.newAnchorButton(View.ADDITION.ngMovieTitleButtonType),
		visitButton: View.newAnchorButton(View.ADDITION.visitButtonText),
		movieButtonsAppended: false
	};
};

View.prototype.getReduceRadio = function() {
	return this.reduceRadio;
};

View.prototype.getHideRadio = function() {
	return this.hideRadio;
};

View.prototype.getDoNothingRadio = function() {
	return this.doNothingRadio;
};

View.prototype.getNgMovieVisibilityCheckBox = function() {
	return this.ngMovieVisibilityCheckBox;
};

View.prototype.getConfigButton = function() {
	return this.configButton;
};

View.getMovieIdInHRef = function(href) {
	var execResult = /^watch\/([^?]+)/.exec(href);
	return execResult ? execResult[1] : null;
};

View.getWatchAnchors = function() {
	var a = document.querySelectorAll("#PAGEBODY div.content_672 a.watch") || [];
	var result = [];
	Array.forEach(a, function(e) { result.push(e); });
	return result;
};

View.getMovies = function() {
	var result = [];
	View.getWatchAnchors().forEach(function(a) {
		var movieId = View.getMovieIdInHRef(a.getAttribute("href"));
		if (movieId) {
			result.push(new Movie(movieId, a.firstChild.nodeValue));
		}
	});
	return result;
};

View.newVisitedMovieViewModeRadio = function(value) {
	return View.newElement(
			"input",
			{ "type": "radio",
				"name": View.VISITED_MOVIE_VIEW_MODE,
				"value": value });
};

View.newLabel = function(element, text) {
	return View.newElement("label", null, null, element, text);
};

View.appendTextSeparator = function(element, separator) {
	element.appendChild(document.createTextNode(separator || " | "));
};

View.newInput = function(type) {
	return View.newElement("input", { "type": type });
};

View.newLinkSpan = function(text) {
	return View.newElement("span", null,
		{ "text-decoration": "underline", "cursor": "pointer"}, text);
};

View.prototype.newVisitedMovieViewModeFragment = function() {
	var result = document.createDocumentFragment();
	result.appendChild(document.createTextNode(View.PREPEND_TEXT));
	result.appendChild(View.newLabel(this.reduceRadio, View.REDUCE_TEXT));
	result.appendChild(View.newLabel(this.hideRadio, View.HIDE_TEXT));
	result.appendChild(View.newLabel(this.doNothingRadio, View.DO_NOTHING_TEXT));
	return result;
};

View.prototype.newToolsDiv = function() {
	var result = document.createElement("div");
	result.appendChild(this.newVisitedMovieViewModeFragment());
	View.appendTextSeparator(result);
	result.appendChild(View.newLabel(
			this.ngMovieVisibilityCheckBox, View.NG_MOVIES_SHOW_TEXT));
	View.appendTextSeparator(result);
	result.appendChild(this.configButton);
	return result;
};

View.prototype.addToolbox = function() {
	if (this.toolboxAdded) {
		throw new Error("the toolbox has already been added");
	}
	this.toolboxAdded = true;
	
	var c672 = document.querySelector("#PAGEBODY div.content_672");
	c672.insertBefore(this.newToolsDiv(), c672.firstChild);
};

View.prototype.getViewState = function(movieId) {
	Util.throwErrorIfEmptyString(movieId, "movieId");
	return this.getDataByMovieId(movieId).viewState;
};

View.prototype.setViewState = function(movieId, viewState) {
	Util.throwErrorIfEmptyString(movieId, "movieId");
	Store.throwErrorIfNotVisitedMovidViewMode(viewState, "viewState");
	
	var currentViewState = this.getDataByMovieId(movieId).viewState;
	if (currentViewState === viewState) {
		return;
	}
	currentViewState.restoreViewState(movieId);
	this.getDataByMovieId(movieId).viewState = viewState;
	viewState.setViewState(movieId);
};

View.getMovieRoot = function(child) {
	for (var parent = child; parent; parent = parent.parentNode) {
		if (parent.id && parent.id.indexOf("item") === 0) {
			return parent.parentNode;
		}
	}
	return null;
};

View.getMovieIdOfMovieRoot = function(movieRoot) {
	var a = movieRoot.querySelector("a.watch");
	if (!a) {
		throw new Error("the movieRoot is not a movie root");
	}
	return View.getMovieIdInHRef(a.getAttribute("href"));
};

View.watchAnchorsCache = null;
View.newWatchAnchorsCache = function() {
	var result = {};
	var anchors = View.getWatchAnchors();
	for (var i = 0; i < anchors.length; i++) {
		var a = anchors[i];
		var movieId = View.getMovieIdInHRef(a.getAttribute("href"));
		result[movieId] = a;
	}
	return result;
};
View.getWatchAnchor = function(movieId) {
	if (!View.watchAnchorsCache)
		View.watchAnchorsCache = View.newWatchAnchorsCache();
	var a = View.watchAnchorsCache[movieId];
	if (a)
		return a;
	throw new Error(movieId + " not found");
};

View.prototype.isNgMovieId = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieId;
};

View.prototype.setNgMovieId = function(movieId, ngMovieId) {
	this.getDataByMovieId(movieId).ngMovieId = Boolean(ngMovieId);
	if (ngMovieId) {
		View.getWatchAnchor(movieId).style.textDecoration = "line-through";
	} else {
		View.getWatchAnchor(movieId).style.removeProperty("text-decoration");
	}
};

View.prototype.getNgMovieTitle = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieTitle;
};

View.prototype.setNgMovieTitle = function(movieId, ngMovieTitle) {
	this.getDataByMovieId(movieId).ngMovieTitle = String(ngMovieTitle);
	var a = View.getWatchAnchor(movieId);
	var t = a.textContent;
	var i = t.indexOf(ngMovieTitle);
	if (i === -1) {
		throw new Error("'" + ngMovieTitle + "' not found");
	}
	Util.removeAllChildren(a);
	if (ngMovieTitle.length === 0) {
		a.textContent = t;
		return;
	}
	if (i !== 0) {
		a.appendChild(document.createTextNode(t.substring(0, i)));
	}
	a.appendChild(View.newElement("span", null,
			{ "color": "white", "background-color": "fuchsia" },
			t.substring(i, i + ngMovieTitle.length)));
	if (i + ngMovieTitle.length !== t.length) {
		a.appendChild(document.createTextNode(
				t.substring(i + ngMovieTitle.length)));
	}
	
};

View.prototype.appendMovieButtons = function(movieId) {
	if (this.getDataByMovieId(movieId).movieButtonsAppended) {
		throw new Error("movie buttons have already been appended");
	}
	this.getDataByMovieId(movieId).movieButtonsAppended = true;
	
	var selector = "#MENU_" + movieId + " p.menu_palet";
	var menu = document.querySelector(selector);
	if (!menu) {
		throw new Error("'" + selector + "' not found");
	}
	
	View.appendTextSeparator(menu, "| ");
	menu.appendChild(this.getVisitButton(movieId));
	View.appendTextSeparator(menu);
	menu.appendChild(this.getNgMovieIdButton(movieId));
	View.appendTextSeparator(menu);
	menu.appendChild(this.getNgMovieTitleButton(movieId));
};

View.prototype.appendAllMovieButtons = function() {
	View.getMovies().forEach(function(movie) {
		this.appendMovieButtons(movie.getId());
	}, this);
};

View.prototype.getNgMovieIdButton = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieIdButton;
};

View.prototype.getNgMovieTitleButton = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieTitleButton;
};

View.prototype.getVisitButton = function(movieId) {
	return this.getDataByMovieId(movieId).visitButton;
};

View.prototype.getNgMovieIdType = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieIdType;
};

View.throwErrorIfNotType = function(type) {
	if (!(type === View.ADDITION || type === View.REMOVAL)) {
		throw new Error("the type must be ADDITION or REMOVAL");
	}
};

View.prototype.setNgMovieIdType = function(movieId, type) {
	View.throwErrorIfNotType(type);
	var d = this.getDataByMovieId(movieId);
	d.ngMovieIdType = type;
	d.ngMovieIdButton.textContent = type.ngMovieIdButtonType;
};

View.prototype.getNgMovieTitleType = function(movieId) {
	return this.getDataByMovieId(movieId).ngMovieTitleType;
	
};

View.prototype.setNgMovieTitleType = function(movieId, type) {
	View.throwErrorIfNotType(type);
	var d = this.getDataByMovieId(movieId);
	d.ngMovieTitleType = type;
	d.ngMovieTitleButton.textContent = type.ngMovieTitleButtonType;
};

View.prototype.getVisitType = function(movieId) {
	return this.getDataByMovieId(movieId).visitType;
};

View.prototype.setVisitType = function(movieId, type) {
	View.throwErrorIfNotType(type);
	var d = this.getDataByMovieId(movieId);
	d.visitType = type;
	d.visitButton.textContent = type.visitButtonText;
};

View.getMovieImages = function() {
	var result = [];
	var imgs = document.querySelectorAll("#PAGEBODY div.content_672 a > img");
	Array.forEach(imgs || [], function(img) {
		if (img.id && img.id.indexOf("video_img_") === 0) {
			result.push(img);
		}
	});
	return result;
};

View.getMovieAnchors = function() {
	var result = [];
	Array.forEach(View.getMovieImages(), function(img) {
		result.push(img.parentNode);
	});
	return result.concat(View.getWatchAnchors());
};

View.setTargetAttrsOfMovieAnchors = function(blank) {
	View.getMovieAnchors().forEach(function(movieAnchor) {
		if (blank) {
			movieAnchor.setAttribute("target", "_blank");
		} else {
			movieAnchor.removeAttribute("target");
		}
	});
};

View.newButton = function(text) {
	return View.newElement("button", null, null, text);
};

View.promptNgMovieTitle = function(initValue) {
	return window.prompt(View.PROMPT_MESSAGE, initValue);
};

View.getAbsoluteTop = function(target) {
	var result = target.offsetTop;
	var parent = target.offsetParent;
	while (parent && parent !== document.body) {
		result += parent.offsetTop;
		parent = parent.offsetParent;
	}
	return result;
};

View.moveTitleToSrc = function(img) {
	img.src = img.title;
	img.title = "";
};

View.loadLazyImageIfRequired = function(pageYOffset, innerHeight, img) {
	if (img.title &&
			Util.contains(pageYOffset,
					pageYOffset + innerHeight,
					View.getAbsoluteTop(img))) {
		View.moveTitleToSrc(img);
	}
};

View.loadLazyImagesIfRequired = function() {
	View.getMovieImages().forEach(function(img) {
		View.loadLazyImageIfRequired(
				window.pageYOffset, window.innerHeight, img);
	});
};

View.ConfigDialog = function() {
	this.ngMovieTitlesSelect = View.newElement(
		"select",
		{ "size": "10", "multiple": "multiple" },
		{ "width": "250px", "float": "left", "margin-bottom": "10px" });
	
	this.ngMovieTitlesRemovalButton = View.newElement(
			"button",
			null,
			{ "width": "70px" },
			View.ConfigDialog.NG_TITLES_REMOVAL_BUTTON_TEXT);
	
	this.ngTitleAllRemovalButton = View.newButton(
			View.ConfigDialog.NG_TITLE_ALL_REMOVAL_BUTTON_TEXT);
	
	this.ngMovieIdAllRemovalButton = View.newButton(
			View.ConfigDialog.NG_MOVIE_ID_ALL_REMOVAL_BUTTON_TEXT);
	
	this.visitedMovieAllRemovalButton = View.newButton(
			View.ConfigDialog.VISITED_MOVIE_ALL_REMOVAL_BUTTON_TEXT);
	
	this.newWindowOpenCheckBox = View.newInput("checkbox");
	
	this.closeButton = View.newButton(
			View.ConfigDialog.CLOSE_BUTTON_TEXT);
	
	this.rootElement = View.newElement("div", null,
		{ "background-color": "white", "z-index": View.ConfigDialog.Z_INDEX,
			"position": "fixed", "padding": "15px",
			"border": "medium solid black", "overflow": "auto",
			"width": "320px" },
		View.newElement("div", null, null, View.ConfigDialog.NG_TITLE_TEXT),
		this.ngMovieTitlesSelect,
		this.ngMovieTitlesRemovalButton,
		View.newElement("div", null, { "clear": "left" },
			this.ngTitleAllRemovalButton),
		View.newElement("div", null, { "margin-top": "10px" },
			this.ngMovieIdAllRemovalButton),
		View.newElement("div", null, { "margin-top": "10px" },
			this.visitedMovieAllRemovalButton),
		View.newElement("div", null, { "margin-top": "10px" },
			View.newLabel(
				this.newWindowOpenCheckBox,
				View.ConfigDialog.NEW_WINDOW_OPEN_CHECK_BOX_TEXT)),
		View.newElement("div", null,
			{ "text-align": "center", "margin-top": "20px" },
			this.closeButton));
};

View.ConfigDialog.NG_TITLE_TEXT = "NGタイトル";
View.ConfigDialog.NG_TITLES_REMOVAL_BUTTON_TEXT = "削除";
View.ConfigDialog.NG_TITLE_ALL_REMOVAL_BUTTON_TEXT = "NGタイトルを全て削除";
View.ConfigDialog.NG_MOVIE_ID_ALL_REMOVAL_BUTTON_TEXT = "NG登録を全て削除";
View.ConfigDialog.VISITED_MOVIE_ALL_REMOVAL_BUTTON_TEXT = "訪問履歴を全て削除";
View.ConfigDialog.NEW_WINDOW_OPEN_CHECK_BOX_TEXT = " 動画を別窓で開く";
View.ConfigDialog.CLOSE_BUTTON_TEXT = "閉じる";
View.ConfigDialog.Z_INDEX = "10000";

View.ConfigDialog.prototype.show = function(owner) {
	var e = this.rootElement;
	owner.appendChild(e);
	e.style.left = ((window.innerWidth - e.offsetWidth) / 2) + "px";
	e.style.top = ((window.innerHeight - e.offsetHeight) / 2) + "px";
};

View.ConfigDialog.prototype.hide = function() {
	this.rootElement.parentNode.removeChild(this.rootElement);
};

View.ConfigDialog.prototype.getRootElement = function() {
	return this.rootElement;
};

View.ConfigDialog.prototype.getNgMovieTitlesSelect = function() {
	return this.ngMovieTitlesSelect;
};

View.ConfigDialog.prototype.getNgMovieTitlesRemovalButton = function() {
	return this.ngMovieTitlesRemovalButton;
};

View.ConfigDialog.prototype.getNgTitleAllRemovalButton = function() {
	return this.ngTitleAllRemovalButton;
};

View.ConfigDialog.prototype.getNgMovieIdAllRemovalButton = function() {
	return this.ngMovieIdAllRemovalButton;
};

View.ConfigDialog.prototype.getVisitedMovieAllRemovalButton = function() {
	return this.visitedMovieAllRemovalButton;
};

View.ConfigDialog.prototype.getNewWindowOpenCheckBox = function() {
	return this.newWindowOpenCheckBox;
};

View.ConfigDialog.prototype.getCloseButton = function() {
	return this.closeButton;
};

View.DialogBackground = function() {
	var e = document.createElement("div");
	e.style.backgroundColor = "black";
	e.style.opacity = "0.5";
	e.style.zIndex = String(View.ConfigDialog.Z_INDEX - 1);
	e.style.position = "fixed";
	e.style.left = "0px";
	e.style.top = "0px";
	e.style.width = "100%";
	e.style.height = "100%";
	this.rootElement = e;
};

View.DialogBackground.prototype.getRootElement = function() {
	return this.rootElement;
};

View.DialogBackground.prototype.show = function(owner) {
	owner.appendChild(this.rootElement);
};

View.DialogBackground.prototype.hide = function() {
	this.rootElement.parentNode.removeChild(this.rootElement);
};

View.AbstractReduction = function() {
};

View.AbstractReduction.prototype.perform = function(movieId) {
	var p = View.getWatchAnchor(movieId).parentNode;
	this.setVisibility(p.previousSibling.previousSibling);
	for (var n = p.nextSibling; n; n = n.nextSibling) {
		if (n.tagName === "P") {
			this.setDisplay(n);
		}
	}
	var img = document.getElementById("video_img_" + movieId);
	var style = window.getComputedStyle(img, null);
	img.style.width = this.computeLength(parseInt(style.width)) + "px";
	img.style.height = this.computeLength(parseInt(style.height)) + "px";
};

View.AbstractReduction.prototype.setVisibility = function(target) {
	throw new Error("not implemented yet");
};

View.AbstractReduction.prototype.setDisplay = function(target) {
	throw new Error("not implemented yet");
};

View.AbstractReduction.prototype.computeLength = function(srcLength) {
	throw new Error("not implemented yet");
};

View.Reduction = function() {
};

View.Reduction.prototype = new View.AbstractReduction();

View.Reduction.prototype.setVisibility = function(target) {
	target.style.visibility = "hidden";
};

View.Reduction.prototype.setDisplay = function(target) {
	target.style.display = "none";
};

View.Reduction.prototype.computeLength = function(srcLength) {
	return srcLength / 2;
};

View.Unreduction = function() {
};

View.Unreduction.prototype = new View.AbstractReduction();

View.Unreduction.prototype.setVisibility = function(target) {
	target.style.removeProperty("visibility");
};

View.Unreduction.prototype.setDisplay = function(target) {
	target.style.removeProperty("display");
};

View.Unreduction.prototype.computeLength = function(srcLength) {
	return srcLength * 2;
};

function Store() {
	var data = {};
	
	var checkName = function(name) {
		if (typeof(name) !== "string") {
			throw new Error("the name must be the string type");
		}
		Util.throwErrorIfEmptyString(name, "name");
	};
	
	this.getter = function(name, def) {
		checkName(name);
		return data.hasOwnProperty(name) ? data[name] : def;
	};
	
	this.setter = function(name, value) {
		checkName(name);
		if (typeof(value) === "boolean" || typeof(value) === "string") {
			data[name] = value;
		} else if (value === parseInt(value)) {
			data[name] = parseInt(value);
		} else {
			throw new Error("the value must be the type of Boolean, Integer or String");
		}
	};
	
	this.remover = function(name) {
		checkName(name);
		delete data[name];
	};
}

Store.WAY_OF_PROCESSING_VISITED_ITEMS = "wayOfProcessingVisitedItems";
Store.OPEN_NEW_WINDOW = "openNewWindow";
Store.VISITED_MOVIES = "visitedMovies";
Store.NG_MOVIES = "ngMovies";
Store.NG_TITLES = "ngTitles";

Store.throwErrorIfNotFunction = function(value, name) {
	if (typeof(value) !== "function") {
		throw new Error("the " + name + " must be a function");
	}
};

Store.prototype.getGetter = function() {
	return this.getter;
};

Store.prototype.setGetter = function(getter) {
	Store.throwErrorIfNotFunction(getter, "getter");
	this.getter = getter;
};

Store.prototype.getSetter = function() {
	return this.setter;
};

Store.prototype.setSetter = function(setter) {
	Store.throwErrorIfNotFunction(setter, "setter");
	this.setter = setter;
};

Store.prototype.getRemover = function() {
	return this.remover;
};

Store.prototype.setRemover = function(remover) {
	Store.throwErrorIfNotFunction(remover, "remover");
	this.remover = remover;
};

Store.prototype.getValues = function(name) {
	return JSON.parse(this.getter(name, "[]"));
};

Store.containsValue = function(values, value) {
	return values.some(function(element) {
		return element === value;
	});
};

Store.prototype.addValue = function(name, value, valueName) {
	Util.throwErrorIfEmptyString(value, valueName);
	var values = this.getValues(name);
	if (Store.containsValue(values, value)) {
		return false;
	}
	values.push(String(value));
	this.setter(name, JSON.stringify(values));
	return true;
};

Store.prototype.removeValue = function(name, value) {
	var newValue = this.getValues(name).filter(function(element) {
		return element !== value;
	});
	this.setter(name, JSON.stringify(newValue));
};

Store.prototype.getVisitedMovieIds = function() {
	return this.getValues(Store.VISITED_MOVIES);
};

Store.prototype.containsVisitedMovieId = function(movieId) {
	return Store.containsValue(this.getVisitedMovieIds(), movieId);
};

Store.prototype.addVisitedMovieId = function(movieId) {
	return this.addValue(Store.VISITED_MOVIES, movieId, "movieId");
};

Store.prototype.removeVisitedMovieId = function(movieId) {
	this.removeValue(Store.VISITED_MOVIES, movieId);
};

Store.prototype.clearVisitedMovieIds = function() {
	this.remover(Store.VISITED_MOVIES);
};

Store.prototype.getNgMovieIds = function() {
	return this.getValues(Store.NG_MOVIES);
};

Store.prototype.containsNgMovieId = function(movieId) {
	return Store.containsValue(this.getNgMovieIds(), movieId);
};

Store.prototype.addNgMovieId = function(movieId) {
	return this.addValue(Store.NG_MOVIES, movieId, "movieId");
};

Store.prototype.removeNgMovieId = function(movieId) {
	this.removeValue(Store.NG_MOVIES, movieId);
};

Store.prototype.clearNgMovieIds = function() {
	this.remover(Store.NG_MOVIES);
};

Store.prototype.getNgMovieTitles = function() {
	return this.getValues(Store.NG_TITLES);
};

Store.prototype.containsNgMovieTitle = function(movieTitle) {
	return Store.containsValue(this.getNgMovieTitles(), movieTitle);
};

Store.prototype.addNgMovieTitle = function(movieTitle) {
	return this.addValue(Store.NG_TITLES, movieTitle, "movieTitle");
};

Store.prototype.removeNgMovieTitle = function(movieTitle) {
	this.removeValue(Store.NG_TITLES, movieTitle);
};

Store.prototype.removeNgMovieTitles = function(movieTitles) {
	var newValue = this.getValues(Store.NG_TITLES).filter(function(element) {
		return movieTitles.indexOf(element) === -1;
	});
	this.setter(Store.NG_TITLES, JSON.stringify(newValue));
};

Store.prototype.clearNgMovieTitles = function() {
	this.remover(Store.NG_TITLES);
};

Store.prototype.getVisitedMovieViewMode = function() {
	return View.Mode.getMode(this.getter(
			Store.WAY_OF_PROCESSING_VISITED_ITEMS, View.Mode.REDUCE.name));
};

Store.throwErrorIfNotVisitedMovidViewMode = function(visitedMovieViewMode, name) {
	try {
		View.Mode.getMode(visitedMovieViewMode.name);
	} catch (e) {
		throw new Error("the " + name + " must be" +
		" one of HIDE, DO_NOTHING and REDUCE");
	}
};

Store.prototype.setVisitedMovieViewMode = function(visitedMovieViewMode) {
	Store.throwErrorIfNotVisitedMovidViewMode(
			visitedMovieViewMode, "visitedMovieViewMode");
	this.setter(Store.WAY_OF_PROCESSING_VISITED_ITEMS, visitedMovieViewMode.name);
};

Store.prototype.isNewWindowOpen = function() {
	return this.getter(Store.OPEN_NEW_WINDOW, true);
};

Store.prototype.setNewWindowOpen = function(newWindowOpen) {
	this.setter(Store.OPEN_NEW_WINDOW, Boolean(newWindowOpen));
};

function Movie(id, title) {
	Util.throwErrorIfEmptyString(title, "title");
	Util.throwErrorIfEmptyString(id, "id");
	
	this.title = String(title);
	this.id = String(id);
	
	this.visited = false;
	this.ngId = false;
	this.ngTitle = false;
	this.listener = Util.getEmptyPropertyChangeListener();
	this.matchedNgTitle = "";
}

Movie.prototype.getTitle = function() {
	return this.title;
};

Movie.prototype.getId = function() {
	return this.id;
};

Movie.prototype.isVisited = function() {
	return this.visited;
};

Movie.prototype.setVisited = function(visited) {
	if (this.visited !== Boolean(visited)) {
		this.visited = Boolean(visited);
		this.listener.propertyChanged(this, "visited");
	}
};

Movie.prototype.setVisitedByMatch = function(regExp) {
	this.setVisited(regExp.test(this.id));
};

Movie.prototype.getPropertyChangeListener = function() {
	return this.listener;
};

Movie.prototype.setPropertyChangeListener = function(listener) {
	Util.throwErrorIfHasNotPropertyChangedFunc(listener);
	this.listener = listener;
};

Movie.prototype.isNgId = function() {
	return this.ngId;
};

Movie.prototype.setNgId = function(ngId) {
	if (this.ngId !== Boolean(ngId)) {
		this.ngId = Boolean(ngId);
		this.listener.propertyChanged(this, "ngId");
	}
};

Movie.prototype.setNgIdByMatch = function(regExp) {
	this.setNgId(regExp.test(this.id));
};

Movie.prototype.isNgTitle = function() {
	return this.ngTitle;
};

Movie.prototype.setNgTitle = function(ngTitle) {
	if (this.ngTitle !== Boolean(ngTitle)) {
		this.ngTitle = Boolean(ngTitle);
		this.listener.propertyChanged(this, "ngTitle");
	}
};

Movie.prototype.setNgTitleByMatch = function(regExp) {
	var execResult = regExp.exec(this.title);
	
	var oldMatchedNgTitle = this.matchedNgTitle;
	this.matchedNgTitle = Boolean(execResult) ? execResult[0] : "";
	
	var previousNgTitle = this.ngTitle;
	this.setNgTitle(Boolean(execResult));
	if (previousNgTitle && this.ngTitle &&
			oldMatchedNgTitle !== this.matchedNgTitle) {
		this.listener.propertyChanged(this, "matchedNgTitle");
	}
};

Movie.prototype.isNg = function() {
	return this.ngId || this.ngTitle;
};

Movie.prototype.getMatchedNgTitle = function() {
	if (this.ngTitle) {
		return this.matchedNgTitle;
	}
	throw new Error("#getMatchedNgTitle() must be called when the ngTitle is true");
};

function Model(movies, store) {
	if (!Model.isArray(movies)) {
		throw new Error("the movies must be an array");
	}
	if (!(store instanceof Store)) {
		throw new Error("the store must be an instance of Store");
	}
	this.movies = movies;
	this.store = store;

	this.idToMovie = {};
	movies.forEach(function(movie) {
		this.idToMovie[movie.getId()] = movie;
	}, this);
	
	this.listener = Util.getEmptyPropertyChangeListener();
	this.ngMovieVisible = false;
}

Model.isArray = function(value) {
	return Object.prototype.toString.apply(value) === "[object Array]";
};

Model.escapeNoWordCharacters = function(s) {
	return s.replace(/\W/g, "\\$&");
};

Model.newTitlePattern = function(rawComponents) {
	if (rawComponents.length === 0) {
		throw new Error("the rawComponents must not be empty");
	}
	var s = "(";
	rawComponents.forEach(function(c) {
		if (Util.isEmptyString(c)) {
			throw new Error("the rawComponents must not contain an empty string");
		}
		s += Model.escapeNoWordCharacters(String(c)) + "|";
	});
	return s.slice(0, -1) + ")";
};

Model.newIdPattern = function(rawComponents) {
	return "^" + Model.newTitlePattern(rawComponents) + "$";
};

Model.prototype.getMovies = function() {
	return this.movies;
};

Model.prototype.getMovieById = function(movieId) {
	var movie = this.idToMovie[movieId];
	return movie ? movie : null;
};

Model.prototype.getStore = function() {
	return this.store;
};

Model.prototype.setAllMoviesPropertyChangeListener = function(listener) {
	this.movies.forEach(function(movie) {
		movie.setPropertyChangeListener(listener);
	});
};

Model.prototype.getPropertyChangeListener = function() {
	return this.listener;
};

Model.prototype.setPropertyChangeListener = function(listener) {
	Util.throwErrorIfHasNotPropertyChangedFunc(listener);
	this.listener = listener;
};

Model.prototype.getVisitedMovieViewMode = function() {
	return this.store.getVisitedMovieViewMode();
};

Model.prototype.setVisitedMovieViewMode = function(visitedMovieViewMode) {
	if (this.store.getVisitedMovieViewMode() === visitedMovieViewMode) {
		return;
	}
	this.store.setVisitedMovieViewMode(visitedMovieViewMode);
	this.listener.propertyChanged(this, "visitedMovieViewMode");
};

Model.prototype.isNgMovieVisible = function() {
	return this.ngMovieVisible;
};

Model.prototype.setNgMovieVisible = function(ngMovieVisible) {
	if (this.ngMovieVisible === Boolean(ngMovieVisible)) {
		return;
	}
	this.ngMovieVisible = Boolean(ngMovieVisible);
	this.listener.propertyChanged(this, "ngMovieVisible");
};

Model.prototype.isNewWindowOpen = function() {
	return this.store.isNewWindowOpen();
};

Model.prototype.setNewWindowOpen = function(newWindowOpen) {
	if (this.store.isNewWindowOpen() === newWindowOpen) {
		return;
	}
	this.store.setNewWindowOpen(newWindowOpen);
	this.listener.propertyChanged(this, "newWindowOpen");
};

Model.prototype.addVisitedMovieId = function(movieId) {
	this.store.addVisitedMovieId(movieId);
	if (this.getMovieById(movieId)) {
		this.getMovieById(movieId).setVisited(true);
	}
};

Model.prototype.removeVisitedMovieId = function(movieId) {
	this.store.removeVisitedMovieId(movieId);
	if (this.getMovieById(movieId)) {
		this.getMovieById(movieId).setVisited(false);
	}
};

Model.prototype.clearVisitedMovieIds = function() {
	this.store.clearVisitedMovieIds();
	this.movies.forEach(function(movie) {
		movie.setVisited(false);
	});
};

Model.prototype.addNgMovieId = function(movieId) {
	this.store.addNgMovieId(movieId);
	if (this.getMovieById(movieId)) {
		this.getMovieById(movieId).setNgId(true);
	}
};

Model.prototype.removeNgMovieId = function(movieId) {
	this.store.removeNgMovieId(movieId);
	if (this.getMovieById(movieId)) {
		this.getMovieById(movieId).setNgId(false);
	}
};

Model.prototype.clearNgMovieIds = function() {
	this.store.clearNgMovieIds();
	this.movies.forEach(function(movie) {
		movie.setNgId(false);
	});
};

Model.prototype.addNgMovieTitle = function(movieTitle) {
	this.store.addNgMovieTitle(movieTitle);
	
	var regExp = new RegExp(Model.escapeNoWordCharacters(movieTitle));
	this.movies.forEach(function(movie) {
		movie.isNgTitle() || movie.setNgTitleByMatch(regExp);
	});
};

Model.prototype.removeNgMovieTitle = function(movieTitle) {
	this.removeNgMovieTitles([movieTitle]);
};

Model.prototype.getAndSetValues = function(
		values, setPropertyToFalse, newCallbackWithRegExp, newPattern) {
	if (values.length === 0) {
		this.movies.forEach(setPropertyToFalse);
	} else {
		var regExp = new RegExp(newPattern(values));
		this.movies.forEach(newCallbackWithRegExp(regExp));
	}
};

Model.prototype.removeNgMovieTitles = function(movieTitles) {
	if (movieTitles.length !== 0) {
		this.store.removeNgMovieTitles(movieTitles);
		this.getAndSetAllNgMovieTitles();
	}
};

Model.prototype.clearNgMovieTitles = function() {
	this.store.clearNgMovieTitles();
	this.movies.forEach(function(movie) {
		movie.setNgTitle(false);
	});
};

Model.prototype.getAndSetAllVisitedMovies = function() {
	this.getAndSetValues(
		this.store.getVisitedMovieIds(),
		function(movie) { movie.setVisited(false); },
		function(regExp) {
			return function(movie) { movie.setVisitedByMatch(regExp); };
		},
		Model.newIdPattern
	);
};

Model.prototype.getAndSetAllNgMovieIds = function() {
	this.getAndSetValues(
		this.store.getNgMovieIds(),
		function(movie) { movie.setNgId(false); },
		function(regExp) {
			return function(movie) { movie.setNgIdByMatch(regExp); };
		},
		Model.newIdPattern
	);
};

Model.prototype.getAndSetAllNgMovieTitles = function() {
	this.getAndSetValues(
		this.store.getNgMovieTitles(),
		function(movie) { movie.setNgTitle(false); },
		function(regExp) {
			return function(movie) { movie.setNgTitleByMatch(regExp); };
		},
		Model.newTitlePattern
	);
};

function Controller(model, view) {
	if (!(model instanceof Model)) {
		throw new Error("the model must be an instance of Model");
	}
	if (!(view instanceof View)) {
		throw new Error("the view must be an instance of View");
	}
	this.model = model;
	this.view = view;
	this.configDialog = new View.ConfigDialog();
	this.dialogBackground = new View.DialogBackground();
	this.configDialogOwner = document.body;
}

Controller.prototype.getModel = function() {
	return this.model;
};

Controller.prototype.getView = function() {
	return this.view;
};

Controller.prototype.getConfigDialog = function() {
	return this.configDialog;
};

Controller.prototype.getDialogBackground = function() {
	return this.dialogBackground;
};

Controller.prototype.isVisible = function(movie) {
	return movie.isNg() && !this.model.isNgMovieVisible();
};

Controller.prototype.isHideRequired = function(movie) {
	return this.isVisible(movie) || (movie.isVisited() &&
			this.model.getVisitedMovieViewMode() === View.Mode.HIDE);
};

Controller.prototype.isReduceRequired = function(movie) {
	return !this.isVisible(movie) && movie.isVisited() &&
			this.model.getVisitedMovieViewMode() === View.Mode.REDUCE;
};

Controller.prototype.setListenersToAllMovies = function() {
	var self = this;
	var listener = {
		propertyChanged: function(source) { self.synchMovie(source); }
	};
	this.model.setAllMoviesPropertyChangeListener(listener);
};

Controller.prototype.modelPropertyChanged = function(changedPropertyName) {
	switch (changedPropertyName) {
	case "newWindowOpen":
		this.synchNewWindowOpen();
		break;
	case "visitedMovieViewMode":
		this.synchVisitedMovieViewMode();
		this.synchAllMovies();
		View.loadLazyImagesIfRequired();
		break;
	case "ngMovieVisible":
		this.view.getNgMovieVisibilityCheckBox().checked =
				this.model.isNgMovieVisible();
		this.synchAllMovies();
		View.loadLazyImagesIfRequired();
		break;
	}
};

Controller.prototype.setListenerToModel = function() {
	var self = this;
	this.model.setPropertyChangeListener({
		propertyChanged: function(source, changedPropertyName) {
			self.modelPropertyChanged(changedPropertyName);
		}
	});
};

Controller.prototype.viewModeRadioClicked = function() {
	if (this.view.getReduceRadio().checked) {
		this.model.setVisitedMovieViewMode(View.Mode.REDUCE);
	} else if (this.view.getHideRadio().checked) {
		this.model.setVisitedMovieViewMode(View.Mode.HIDE);
	} else {
		this.model.setVisitedMovieViewMode(View.Mode.DO_NOTHING);
	}
};

Controller.prototype.ngMovieVisibilityCheckBoxClicked = function() {
	this.model.setNgMovieVisible(
			this.view.getNgMovieVisibilityCheckBox().checked);
};

Controller.prototype.configButtonClicked = function() {
	this.dialogBackground.show(this.configDialogOwner);
	this.configDialog.show(this.configDialogOwner);
	
	var select = this.configDialog.getNgMovieTitlesSelect();
	Util.removeAllChildren(select);
	this.model.getStore().getNgMovieTitles().forEach(function(ngMovieTitle) {
		var option = document.createElement("option");
		option.textContent = ngMovieTitle;
		select.appendChild(option);
	});
};

Controller.prototype.movieAnchorClicked = function(movieAnchor) {
	var movieId = View.getMovieIdOfMovieRoot(View.getMovieRoot(movieAnchor));
	View.ADDITION.visitButtonClicked(this.model, movieId);
	if (this.model.getVisitedMovieViewMode() !== View.Mode.DO_NOTHING) {
		View.loadLazyImagesIfRequired();
	}
};

Controller.prototype.addListenersToView = function() {
	this.addListenersToViewModeRadios();
	this.addListenersToConfigDialog();
	this.addListenersToMovieButtons();
	
	var self = this;
	this.view.getNgMovieVisibilityCheckBox().addEventListener("click", function() {
		self.ngMovieVisibilityCheckBoxClicked();
	}, false);
	this.view.getConfigButton().addEventListener("click", function() {
		self.configButtonClicked();
	}, false);
	View.getMovieAnchors().forEach(function(movieAnchor) {
		movieAnchor.addEventListener("click", function(event) {
			self.movieAnchorClicked(event.target);
		}, false);
	});
};

Controller.prototype.addListenersToViewModeRadios = function() {
	var self = this;
	var l = function() {
		self.viewModeRadioClicked();
	};
	this.view.getReduceRadio().addEventListener("click", l, false);
	this.view.getHideRadio().addEventListener("click", l, false);
	this.view.getDoNothingRadio().addEventListener("click", l, false);
};

Controller.prototype.closeButtonClicked = function() {
	this.configDialog.hide();
	this.dialogBackground.hide();
};

Controller.prototype.ngMovieTitlesRemovalButtonClicked = function() {
	var ngTitles = [];
	var selectedOptions = [];
	Array.forEach(this.configDialog.getNgMovieTitlesSelect().options, function(e) {
		if (e.selected) {
			ngTitles.push(e.textContent);
			selectedOptions.push(e);
		}
	});
	this.model.removeNgMovieTitles(ngTitles);
	selectedOptions.forEach(function(option) {
		option.parentNode.removeChild(option);
	});
	if (ngTitles.length !== 0) {
		View.loadLazyImagesIfRequired();
	}
};

Controller.prototype.ngMovieTitleAllRemovalButtonClicked = function() {
	this.model.clearNgMovieTitles();
	Util.removeAllChildren(this.configDialog.getNgMovieTitlesSelect());
	View.loadLazyImagesIfRequired();
};

Controller.prototype.ngMovieIdAllRemovalButtonClicked = function() {
	this.model.clearNgMovieIds();
	View.loadLazyImagesIfRequired();
};

Controller.prototype.visitedMovieAllRemovalButtonClicked = function() {
	this.model.clearVisitedMovieIds();
	View.loadLazyImagesIfRequired();
};

Controller.prototype.newWindowOpenCheckBoxClicked = function() {
	this.model.setNewWindowOpen(
			this.configDialog.getNewWindowOpenCheckBox().checked);
};

Controller.prototype.addListenersToConfigDialog = function() {
	var self = this;
	
	this.configDialog.getCloseButton().addEventListener("click", function() {
		self.closeButtonClicked();
	}, false);
	
	this.configDialog.getNgMovieTitlesRemovalButton().addEventListener("click", function() {
		self.ngMovieTitlesRemovalButtonClicked();
	}, false);
	
	this.configDialog.getNgTitleAllRemovalButton().addEventListener("click", function() {
		self.ngMovieTitleAllRemovalButtonClicked();
	}, false);
	
	this.configDialog.getNgMovieIdAllRemovalButton().addEventListener("click", function() {
		self.ngMovieIdAllRemovalButtonClicked();
	}, false);
	
	this.configDialog.getVisitedMovieAllRemovalButton().addEventListener("click", function() {
		self.visitedMovieAllRemovalButtonClicked();
	}, false);
	
	this.configDialog.getNewWindowOpenCheckBox().addEventListener("click", function() {
		self.newWindowOpenCheckBoxClicked();
	}, false);
};

Controller.prototype.visitButtonClicked = function(visitButton) {
	var movieId = View.getMovieIdOfMovieRoot(View.getMovieRoot(visitButton));
	this.view.getVisitType(movieId).visitButtonClicked(this.model, movieId);
	if (this.model.getVisitedMovieViewMode() !== View.Mode.DO_NOTHING) {
		View.loadLazyImagesIfRequired();
	}
};

Controller.prototype.ngMovieIdButtonClicked = function(ngMovieButton) {
	var movieId = View.getMovieIdOfMovieRoot(View.getMovieRoot(ngMovieButton));
	this.view.getNgMovieIdType(movieId).ngMovieIdButtonClicked(this.model, movieId);
	if (!this.model.isNgMovieVisible()) {
		View.loadLazyImagesIfRequired();
	}
};

Controller.prototype.ngMovieTitleButtonClicked = function(ngTitleButton) {
	var movieId = View.getMovieIdOfMovieRoot(View.getMovieRoot(ngTitleButton));
	this.view.getNgMovieTitleType(movieId)
			.ngMovieTitleButtonClicked(this.model, movieId);
	if (!this.model.isNgMovieVisible()) {
		View.loadLazyImagesIfRequired();
	}
};

Controller.prototype.addListenersToMovieButtons = function() {
	var self = this;
	var ngMovieIdButtonListener = function(event) {
		self.ngMovieIdButtonClicked(event.target);
	};
	var ngMovieTitleButtonListener = function(event) {
		self.ngMovieTitleButtonClicked(event.target);
	};
	var visitButtonListener = function(event) {
		self.visitButtonClicked(event.target);
	};
	View.getMovies().forEach(function(movie) {
		this.view.getNgMovieIdButton(movie.getId())
				.addEventListener("click", ngMovieIdButtonListener, false);
		this.view.getNgMovieTitleButton(movie.getId())
				.addEventListener("click", ngMovieTitleButtonListener, false);
		this.view.getVisitButton(movie.getId())
				.addEventListener("click", visitButtonListener, false);
	}, this);
};

Controller.prototype.synchVisitedMovieViewMode = function() {
	this.model.getVisitedMovieViewMode().viewModeChanged(this.view);
};

Controller.prototype.synchNewWindowOpen = function() {
	View.setTargetAttrsOfMovieAnchors(this.model.isNewWindowOpen());
	this.configDialog.getNewWindowOpenCheckBox().checked =
			this.model.isNewWindowOpen();
};

Controller.prototype.synchMovie = function(movie) {
	if (this.isHideRequired(movie)) {
		this.view.setViewState(movie.getId(), View.Mode.HIDE);
	} else if (this.isReduceRequired(movie)) {
		this.view.setViewState(movie.getId(), View.Mode.REDUCE);
	} else {
		this.view.setViewState(movie.getId(), View.Mode.DO_NOTHING);
	}
	
	this.view.setNgMovieTitle(movie.getId(),
			movie.isNgTitle() ? movie.getMatchedNgTitle() : "");
	this.view.setNgMovieId(movie.getId(), movie.isNgId());
	this.view.setVisitType(movie.getId(),
			movie.isVisited() ? View.REMOVAL : View.ADDITION);
	this.view.setNgMovieIdType(movie.getId(),
			movie.isNgId() ? View.REMOVAL : View.ADDITION);
	this.view.setNgMovieTitleType(movie.getId(),
			movie.isNgTitle() ? View.REMOVAL : View.ADDITION);
};

Controller.prototype.synchAllMovies = function() {
	this.model.getMovies().forEach(function(movie) {
		this.synchMovie(movie);
	}, this);
};

function main() {
	var view = new View();
	view.addToolbox();
	view.appendAllMovieButtons();
	
	var store = new Store();
	store.setGetter(GM_getValue);
	store.setSetter(GM_setValue);
	store.setRemover(GM_deleteValue);
	
	var model = new Model(View.getMovies(), store);
	model.getAndSetAllNgMovieIds();
	model.getAndSetAllNgMovieTitles();
	model.getAndSetAllVisitedMovies();
	
	var ctrl = new Controller(model, view);
	ctrl.synchVisitedMovieViewMode();
	ctrl.synchNewWindowOpen();
	ctrl.synchAllMovies();
	ctrl.setListenersToAllMovies();
	ctrl.setListenerToModel();
	ctrl.addListenersToView();
	
	View.loadLazyImagesIfRequired();
	window.addEventListener("scroll", function() {
		View.loadLazyImagesIfRequired();
	}, false);
}

main();