Compare commits
	
		
			35 Commits
		
	
	
		
			deprecate-
			...
			ui-improve
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | b77a470d6f | ||
|   | 2acdc9f2c7 | ||
|   | 08671e4068 | ||
|   | 1b2420ac03 | ||
|   | ca91f732b8 | ||
|   | 9f2806062b | ||
|   | 6ecfc3c843 | ||
|   | d24cd28523 | ||
|   | 508cc1dbd2 | ||
|   | 2e8e27dc07 | ||
|   | 3b02b89a63 | ||
|   | 7236572de6 | ||
|   | fe037064d8 | ||
|   | 51acfbdbda | ||
|   | bb5221d2c8 | ||
|   | e5add6c773 | ||
|   | b61928037b | ||
|   | 471c5533ee | ||
|   | 2e411e1ff4 | ||
|   | a7763ae9a3 | ||
|   | b1292908e2 | ||
|   | b01fded6eb | ||
|   | 30c763fed9 | ||
|   | 0302b7f801 | ||
|   | b6bd57a85d | ||
|   | cf09767b48 | ||
|   | 1468b3a374 | ||
|   | 2bb3d5c8ad | ||
|   | cb1fe50a88 | ||
|   | b5c2e13285 | ||
|   | 37842e6ea6 | ||
|   | 54e79268c1 | ||
|   | fe10d289a0 | ||
|   | a2568129f6 | ||
|   | 66064efce3 | 
| @@ -323,6 +323,7 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|     @app.route("/", methods=['GET']) | ||||
|     @login_required | ||||
|     def index(): | ||||
|  | ||||
|         limit_tag = request.args.get('tag') | ||||
|         pause_uuid = request.args.get('pause') | ||||
|  | ||||
| @@ -332,12 +333,17 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         if pause_uuid: | ||||
|             try: | ||||
|                 datastore.data['watching'][pause_uuid]['paused'] ^= True | ||||
|                 if pause_uuid == 'pause-all' or pause_uuid == 'resume-all': | ||||
|                     action = True if pause_uuid == 'pause-all' else False | ||||
|                     for watch_uuid, watch in datastore.data['watching'].items(): | ||||
|                         if datastore.data['watching'][watch_uuid]['tag'] == limit_tag or limit_tag is None : | ||||
|                             datastore.data['watching'][watch_uuid]['paused'] = action | ||||
|                 else : | ||||
|                     datastore.data['watching'][pause_uuid]['paused'] ^= True | ||||
|                 datastore.needs_write = True | ||||
|  | ||||
|                 return redirect(url_for('index', tag = limit_tag)) | ||||
|             except KeyError: | ||||
|                 pass | ||||
|                 flash("No watch by that UUID found, or error setting paused state.", 'error'); | ||||
|             return redirect(url_for('index', tag = limit_tag)) | ||||
|  | ||||
|         # Sort by last_changed and add the uuid which is usually the key.. | ||||
|         sorted_watches = [] | ||||
| @@ -362,6 +368,12 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|         from changedetectionio import forms | ||||
|         form = forms.quickWatchForm(request.form) | ||||
|  | ||||
|         # Extra page <title> (n) unviewed | ||||
|         extra_title = "" | ||||
|         if datastore.data['unviewed_count'] > 0: | ||||
|             extra_title = " ({})".format(str(datastore.data['unviewed_count'])) | ||||
|  | ||||
|  | ||||
|         output = render_template("watch-overview.html", | ||||
|                                  form=form, | ||||
|                                  watches=sorted_watches, | ||||
| @@ -371,7 +383,9 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                                  has_unviewed=datastore.data['has_unviewed'], | ||||
|                                  # Don't link to hosting when we're on the hosting environment | ||||
|                                  hosted_sticky=os.getenv("SALTED_PASS", False) == False, | ||||
|                                  guid=datastore.data['app_guid']) | ||||
|                                  guid=datastore.data['app_guid'], | ||||
|                                  extra_title=extra_title | ||||
|                                 ) | ||||
|  | ||||
|         return output | ||||
|  | ||||
| @@ -664,8 +678,10 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|         if request.method == 'POST': | ||||
|             urls = request.values.get('urls').split("\n") | ||||
|             for url in urls: | ||||
|                 url = url.strip() | ||||
|                | ||||
|                 url, *tags = url.split(" ") | ||||
|                 url = url.strip() | ||||
|  | ||||
|                 # Flask wtform validators wont work with basic auth, use validators package | ||||
|                 if len(url) and validators.url(url): | ||||
|                     new_uuid = datastore.add_watch(url=url.strip(), tag=" ".join(tags)) | ||||
| @@ -693,12 +709,130 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|     @login_required | ||||
|     def mark_all_viewed(): | ||||
|  | ||||
|         # Save the current newest history as the most recently viewed | ||||
|         for watch_uuid, watch in datastore.data['watching'].items(): | ||||
|             datastore.set_last_viewed(watch_uuid, watch['newest_history_key']) | ||||
|         limit_tag = request.args.get('tag') | ||||
|  | ||||
|         flash("Cleared all statuses.") | ||||
|         return redirect(url_for('index')) | ||||
|         # Save the current newest history as the most recently viewed | ||||
|         try:  | ||||
|             for watch_uuid, watch in datastore.data['watching'].items(): | ||||
|                 if datastore.data['watching'][watch_uuid]['tag'] == limit_tag or limit_tag is None : | ||||
|                     datastore.set_last_viewed(watch_uuid, watch['newest_history_key']) | ||||
|              | ||||
|             datastore.needs_write = True | ||||
|  | ||||
|             return redirect(url_for('index', tag = limit_tag)) | ||||
|         except KeyError: | ||||
|             pass | ||||
|  | ||||
|     # process selected | ||||
|     @app.route("/api/process-selected", methods=["POST"]) | ||||
|     @login_required | ||||
|     def process_selected(): | ||||
|  | ||||
|         func = request.form.get('func') | ||||
|         limit_tag = request.form.get('tag') | ||||
|         uuids = request.form.get('uuids') | ||||
|  | ||||
|         if uuids == '' : | ||||
|             flash("No watches selected.") | ||||
|              | ||||
|         else : | ||||
|  | ||||
|             if func == 'recheck_selected' : | ||||
|  | ||||
|                 i = 0 | ||||
|  | ||||
|                 running_uuids = [] | ||||
|                 for t in running_update_threads: | ||||
|                     running_uuids.append(t.current_uuid) | ||||
|  | ||||
|                 try : | ||||
|                     for uuid in uuids.split(',') : | ||||
|                         if uuid not in running_uuids and not datastore.data['watching'][uuid]['paused']: | ||||
|                             update_q.put(uuid) | ||||
|                             i += 1 | ||||
|                      | ||||
|                 except KeyError : | ||||
|                     pass | ||||
|  | ||||
|                 flash("{} watch{} {} rechecking.".format(i, "" if (i == 1) else "es", "is" if (i == 1) else "are"), "notice") | ||||
|                 #flash("{} watches are rechecking.".format(i)) | ||||
|              | ||||
|             # Clear selected statuses, so we do not see the 'unviewed' class | ||||
|             elif func == 'mark_selected_viewed' : | ||||
|  | ||||
|                 try : | ||||
|                     for uuid in uuids.split(',') : | ||||
|                         datastore.data['watching'][uuid]['last_viewed'] = datastore.data['watching'][uuid]['newest_history_key'] | ||||
|                 | ||||
|                 except KeyError : | ||||
|                     pass | ||||
|  | ||||
|                 datastore.needs_write = True | ||||
|  | ||||
|             # Reset selected statuses, so we see the 'unviewed' class | ||||
|             # both funcs will contain the uuid list from the processChecked javascript function | ||||
|             elif func == 'mark_selected_notviewed' or func == 'mark_all_notviewed' : | ||||
|  | ||||
|                 # count within limit_tag and count successes and capture unchanged | ||||
|                 tagged = 0 | ||||
|                 marked = 0 | ||||
|                 unchanged = [] | ||||
|                  | ||||
|                 try : | ||||
|                     for uuid in uuids.split(',') : | ||||
|                         # increment count with limit_tag | ||||
|                         tagged += 1 | ||||
|                         dates = list(datastore.data['watching'][uuid]['history'].keys()) | ||||
|                         # Convert to int, sort and back to str again | ||||
|                         dates = [int(i) for i in dates] | ||||
|                         dates.sort(reverse=True) | ||||
|                         dates = [str(i) for i in dates] | ||||
|  | ||||
|                         # must be more than 1 history to mark as not viewed | ||||
|                         if len(dates) > 1 : | ||||
|                             # Save the next earliest history as the most recently viewed | ||||
|                             datastore.set_last_viewed(uuid, dates[1]) | ||||
|                             # increment successes | ||||
|                             marked += 1 | ||||
|                              | ||||
|                         else :  | ||||
|                             if datastore.data['watching'][uuid]['title'] : | ||||
|                                 unchanged.append(datastore.data['watching'][uuid]['title']) | ||||
|                             else : | ||||
|                                 unchanged.append(datastore.data['watching'][uuid]['url']) | ||||
|  | ||||
|                 except KeyError : | ||||
|                         pass | ||||
|  | ||||
|                 datastore.needs_write = True | ||||
|  | ||||
|                 if marked < tagged : | ||||
|                     flash("The following {} not have enough history to be remarked:".format("watch does" if len(unchanged) == 1 else "watches do"), "notice") | ||||
|                     for i in range(len(unchanged)): | ||||
|                         flash(unchanged[i], "notice") | ||||
|                  | ||||
|             elif func == 'delete_selected' : | ||||
|  | ||||
|                 # reachable only after confirmation in javascript processChecked(func, tag) function | ||||
|                 try : | ||||
|                     i = 0 | ||||
|                     for uuid in uuids.split(',') : | ||||
|                         datastore.delete(uuid) | ||||
|                         i += 1 | ||||
|                          | ||||
|                 except KeyError : | ||||
|                     pass | ||||
|  | ||||
|                 datastore.needs_write = True | ||||
|                  | ||||
|                 flash("{0} {1} deleted.".format(i, "watch was" if (i) == 1 else "watches were"))  | ||||
|              | ||||
|             else : | ||||
|                  | ||||
|                 flash("Invalid parameter received.") | ||||
|  | ||||
|         render_template(url_for('index'), tag = limit_tag) | ||||
|         return index() | ||||
|  | ||||
|     @app.route("/diff/<string:uuid>", methods=['GET']) | ||||
|     @login_required | ||||
| @@ -954,6 +1088,10 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|         if form.validate(): | ||||
|  | ||||
|             # get action parameter (add paused button value is 'add', watch button value is 'watch' | ||||
|             #action = request.form.get('action') | ||||
|             add_paused = request.form.get('add-paused') | ||||
|              | ||||
|             url = request.form.get('url').strip() | ||||
|             if datastore.url_exists(url): | ||||
|                 flash('The URL {} already exists'.format(url), "error") | ||||
| @@ -961,10 +1099,17 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|  | ||||
|             # @todo add_watch should throw a custom Exception for validation etc | ||||
|             new_uuid = datastore.add_watch(url=url, tag=request.form.get('tag').strip()) | ||||
|             # Straight into the queue. | ||||
|             update_q.put(new_uuid) | ||||
|              | ||||
|             if add_paused : | ||||
|                 datastore.data['watching'][new_uuid]['paused'] = True | ||||
|                 datastore.needs_write = True | ||||
|                 flash("Watch added in a paused state.") | ||||
|                                  | ||||
|             else : # watch now | ||||
|                 # Straight into the queue.  | ||||
|                 update_q.put(new_uuid) | ||||
|  | ||||
|             flash("Watch added.") | ||||
|                 flash("Watch added.") | ||||
|             return redirect(url_for('index')) | ||||
|         else: | ||||
|             flash("Error") | ||||
| @@ -1027,7 +1172,9 @@ def changedetection_app(config=None, datastore_o=None): | ||||
|                 if watch_uuid not in running_uuids and not datastore.data['watching'][watch_uuid]['paused']: | ||||
|                     update_q.put(watch_uuid) | ||||
|                     i += 1 | ||||
|  | ||||
|         flash("{} watches are queued for rechecking.".format(i)) | ||||
|  | ||||
|         return redirect(url_for('index', tag=tag)) | ||||
|  | ||||
|     # @todo handle ctrl break | ||||
|   | ||||
							
								
								
									
										1
									
								
								changedetectionio/static/images/notviewed.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg id="svg2" xmlns="http://www.w3.org/2000/svg" width="9.5" height="15" viewBox="0 0 9.5 15"><path id="path3740" d="M2.2,0A2.41,2.41,0,0,0,0,1.5V2.8H2.2V0Z" transform="translate(0 0)" style="fill:#0078e7"/><path id="rect3728" d="M.3,1.7l-.2,1H2.2V.2A2.76,2.76,0,0,0,.3,1.7Z" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3655" d="M9.5,2.6h0L2.3,0,2,.2,9.2,2.8v.1" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3645" d="M9.2,2.8V13.3l.2-.3V2.5" transform="translate(0 0)" style="fill:#0078e7"/><path id="rect3517" d="M2,.2,9.2,2.8V13.4L2,10.8Z" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3657" d="M2.1.2,9.2,2.8" transform="translate(0 0)" style="fill:#0078e7;stroke:#0078e7;stroke-miterlimit:4.660399913787842;stroke-width:0.10000000149011612px"/><path id="path3684" d="M.5,9.6.6,2.4.4,2.3S1.2,1.1,2,1L8.8,3.4v.1" transform="translate(0 0)" style="fill:#fff;fill-rule:evenodd"/><path id="path3679" d="M8.9,3.4,8.7,13,7.3,14.3.5,11.9V9.5" transform="translate(0 0)" style="fill:#fff;fill-rule:evenodd"/><path id="path3669" d="M7.5,4.2h0L.3,1.6,0,1.9,7.2,4.5v.1" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3671" d="M7.2,4.5V15l.2-.3V4.2" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3673" d="M0,1.9,7.2,4.5V15L0,12.4Z" transform="translate(0 0)" style="fill:#0078e7"/><path id="path3675" d="M.1,1.9,7.2,4.5" transform="translate(0 0)" style="fill:#0078e7;stroke:#0078e7;stroke-miterlimit:4.660399913787842;stroke-width:0.10000000149011612px"/></svg> | ||||
| After Width: | Height: | Size: 1.5 KiB | 
| @@ -1,84 +1 @@ | ||||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    version="1.1" | ||||
|    id="Capa_1" | ||||
|    x="0px" | ||||
|    y="0px" | ||||
|    viewBox="0 0 15 14.998326" | ||||
|    xml:space="preserve" | ||||
|    width="15" | ||||
|    height="14.998326"><metadata | ||||
|    id="metadata39"><rdf:RDF><cc:Work | ||||
|        rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | ||||
|          rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs | ||||
|    id="defs37" /> | ||||
| <path | ||||
|    id="path2" | ||||
|    style="fill:#1b98f8;fill-opacity:1;stroke-width:0.0292893" | ||||
|    d="M 7.4975161,6.5052867e-4 C 4.549072,-0.04028702 1.7055675,1.8548221 0.58868606,4.5801341 -0.57739762,7.2574642 0.02596981,10.583326 2.069916,12.671949 4.0364753,14.788409 7.2763651,15.56067 9.989207,14.57284 12.801145,13.617602 14.87442,10.855325 14.985833,7.8845744 15.172496,4.9966544 13.49856,2.1100704 10.911002,0.8209349 9.8598067,0.28073592 8.6791261,-0.00114855 7.4975161,6.5052867e-4 Z M 6.5602569,10.251923 c -0.00509,0.507593 -0.5693885,0.488472 -0.9352002,0.468629 -0.3399386,0.0018 -0.8402048,0.07132 -0.9297965,-0.374189 -0.015842,-1.8973128 -0.015872,-3.7979649 0,-5.6952784 0.1334405,-0.5224315 0.7416869,-0.3424086 1.1377562,-0.374189 0.3969969,-0.084515 0.8245634,0.1963256 0.7272405,0.6382917 0,1.7789118 0,3.5578239 0,5.3367357 z m 3.7490371,0 c -0.0051,0.507593 -0.5693888,0.488472 -0.9352005,0.468629 -0.3399386,0.0018 -0.8402048,0.07132 -0.9297965,-0.374189 -0.015842,-1.8973128 -0.015872,-3.7979649 0,-5.6952784 0.1334405,-0.5224315 0.7416869,-0.3424086 1.1377562,-0.374189 0.3969969,-0.084515 0.8245638,0.1963256 0.7272408,0.6382917 0,1.7789118 0,3.5578239 0,5.3367357 z" /> | ||||
| <g | ||||
|    id="g4" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g6" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g8" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g10" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g12" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g14" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g16" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g18" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g20" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g22" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g24" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g26" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g28" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g30" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| <g | ||||
|    id="g32" | ||||
|    transform="translate(-0.01903604,0.02221043)"> | ||||
| </g> | ||||
| </svg> | ||||
| <svg id="Capa_1" data-name="Capa 1" xmlns="http://www.w3.org/2000/svg" width="15.03" height="15.03" viewBox="0 0 15.03 15.03"><path id="path2" d="M7.5,0A7.56,7.56,0,0,0,.6,4.6a7.37,7.37,0,0,0,1.5,8.1A7.52,7.52,0,0,0,10,14.6,7.53,7.53,0,0,0,10.9.8,7.73,7.73,0,0,0,7.5,0ZM6.6,10.3c0,.5-.6.5-.9.5s-.8.1-.9-.4V4.7c.1-.5.7-.3,1.1-.4a.53.53,0,0,1,.7.6Zm3.7,0c0,.5-.6.5-.9.5s-.8.1-.9-.4V4.7c.1-.5.7-.3,1.1-.4a.53.53,0,0,1,.7.6Z" transform="translate(0.01 0)" style="fill:#0078e7"/></svg> | ||||
| Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 480 B | 
							
								
								
									
										1
									
								
								changedetectionio/static/images/play.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg id="Capa_1" data-name="Capa 1" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 15 15"><path id="path2" d="M7.5,0A7.62,7.62,0,0,0,0,7.5,7.55,7.55,0,0,0,7.5,15,7.55,7.55,0,0,0,15,7.5,7.6,7.6,0,0,0,10.9.8,9.42,9.42,0,0,0,7.5,0Z" transform="translate(0 0)" style="fill:#0078e7"/><polygon points="11.4 8 5.8 4.8 5.8 11.3 11.4 8" style="fill:#fff"/></svg> | ||||
| After Width: | Height: | Size: 377 B | 
							
								
								
									
										1
									
								
								changedetectionio/static/images/search.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="16" style="fill:#0078e7"/><path d="M24,26.85l-4.93-5a8.53,8.53,0,0,1-4.71,1.41,8.63,8.63,0,1,1,7.32-4.12l5,5c.26.26,0,.9-.49,1.44l-.74.74C24.86,26.88,24.23,27.11,24,26.85Zm-3.9-12.23a5.75,5.75,0,1,0-5.74,5.79A5.76,5.76,0,0,0,20.07,14.62Z" style="fill:#fff"/></svg> | ||||
| After Width: | Height: | Size: 407 B | 
							
								
								
									
										1
									
								
								changedetectionio/static/images/sortable.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="10.93" height="14.99" viewBox="0 0 10.93 14.99"><path d="M5.5,1,9.6,6.1H1.3Zm0,13L1.3,8.9H9.5Z" transform="translate(0.02 -0.01)" style="fill:#0078e7;stroke:#0078e7;stroke-miterlimit:10;stroke-width:1.25px"/></svg> | ||||
| After Width: | Height: | Size: 294 B | 
							
								
								
									
										1
									
								
								changedetectionio/static/images/viewed.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | ||||
| <svg id="svg2" xmlns="http://www.w3.org/2000/svg" width="16.33" height="11.64" viewBox="0 0 16.33 11.64"><path id="path2416" d="M14.2,6.5l1.4,3.1-6.2.8s0,.2-.4.1a.65.65,0,0,1-.6-.6L1.8,11.1.6,4.2H.9v.1H.8L2,10.9,8.4,9.8V10h.4s0,.4.5.4l.1-.1,6-.8-1.2-3Z" transform="translate(-0.01 -0.04)" style="fill:#0078e7;stroke:#007ec0;fill-rule:evenodd"/><path id="path2400" d="M1,4.3H.9L2,10.9,8.4,9.8s-1.7-.9-6.1,1L1,4.3Z" transform="translate(-0.01 -0.04)" style="fill:#1187c3;fill-rule:evenodd"/><path id="path2402" d="M1,4.1V3.6h.1V3.2l.2-.3h.1V2.5L2.8,8.9l-.1.3v.2l-.1.1-.2.1-.1,1.1L1,4.1Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2388" d="M2.3,10.8l.1-1.2.3-.3V9.2l.2-.3s5.5-.2,5.9.8l-.4.1s-1.7-.9-6.1,1Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2394" d="M2.2,2.5H1.5L2.9,8.9s5.3-.2,5.9.8c0,0-.1-1.1-5.4-1.6L2.2,2.5Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2398" d="M2,1.3,3.4,8s5,.5,5.4,1.7l-2-6.3c0-.1,0-1.1-4.8-2.1Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2403" d="M6.9,3.3l5-2.9,2.5,5.9L8.8,9.7,6.9,3.3Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2411" d="M8.5,10V9.8l.3-.1s.2.6.6.6v.1a.49.49,0,0,1-.5-.5l-.4.1Z" transform="translate(-0.01 -0.04)" style="fill:#1187c3;stroke:#007ec0;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2415" d="M8.8,9.7s.2.6.6.5l6-.8-.3-.6h-.3c.1,0-5,.2-6,.9Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/><path id="path2404" d="M15.1,8.9l-1-2.3L8.8,9.7v.1s.5-.6,6-.9V9Z" transform="translate(-0.01 -0.04)" style="fill:#fff;stroke:#0078e7;stroke-width:0.5px;fill-rule:evenodd"/></svg> | ||||
| After Width: | Height: | Size: 1.9 KiB | 
| @@ -1,10 +1,13 @@ | ||||
| // Rewrite this is a plugin.. is all this JS really 'worth it?' | ||||
|  | ||||
|  | ||||
| // display correct label and messages for minutes or seconds  | ||||
| document.addEventListener("DOMContentLoaded", function(event) { | ||||
| 	use_seconds_change(); | ||||
| }); | ||||
| window.addEventListener('hashchange', function() { | ||||
|   var tabs = document.getElementsByClassName('active'); | ||||
|   while (tabs[0]) { | ||||
|     tabs[0].classList.remove('active') | ||||
|     tabs[0].classList.remove('active'); | ||||
|   } | ||||
|   set_active_tab(); | ||||
| }, false); | ||||
| @@ -37,7 +40,7 @@ function focus_error_tab() { | ||||
|     var tabs = document.querySelectorAll('.tabs li a'),i; | ||||
|     for (i = 0; i < tabs.length; ++i) { | ||||
|       var tab_name=tabs[i].hash.replace('#',''); | ||||
|       var pane_errors=document.querySelectorAll('#'+tab_name+' .error') | ||||
|       var pane_errors=document.querySelectorAll('#'+tab_name+' .error'); | ||||
|       if (pane_errors.length) { | ||||
|         document.location.hash = '#'+tab_name; | ||||
|         return true; | ||||
| @@ -45,7 +48,3 @@ function focus_error_tab() { | ||||
|     } | ||||
|     return false; | ||||
| } | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										406
									
								
								changedetectionio/static/js/tbltools.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,406 @@ | ||||
| // table tools | ||||
|  | ||||
| // must be a var for keyChar and keyCode use | ||||
| var CONSTANT_ESCAPE_KEY = 27; | ||||
| var CONSTANT_S_KEY = 83; | ||||
| var CONSTANT_s_KEY = 115; | ||||
|  | ||||
| // globals | ||||
| var loading; | ||||
| var sort_column; // new window or tab is always last_changed | ||||
| var sort_order;  // new window or tab is always descending | ||||
|  | ||||
| // restore scroll position on submit/reload  | ||||
| document.addEventListener("DOMContentLoaded", function(event) { | ||||
| 	load_functions(); | ||||
| 	var scrollpos = sessionStorage.getItem('scrollpos'); | ||||
| 	if (scrollpos) window.scrollTo(0, scrollpos); | ||||
| }); | ||||
|  | ||||
| // mobile scroll position retention  | ||||
| if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | ||||
| 	document.addEventListener("visibilitychange", function() { | ||||
| 		storeScrollAndSearch(); | ||||
| 	}); | ||||
| } else { | ||||
| 	// non-mobile scroll position retention  | ||||
| 	window.onbeforeunload = function(e) { | ||||
| 		storeScrollAndSearch(); | ||||
| 	}; | ||||
| } | ||||
| function storeScrollAndSearch() { | ||||
| 	sessionStorage.setItem('scrollpos', window.pageYOffset); | ||||
| 	sessionStorage.setItem('searchtxt', document.getElementById("txtInput").value); | ||||
| } | ||||
|  | ||||
| // mobile positioning of checkbox-controls grid popup | ||||
| document.addEventListener("touchstart", touchStartHandler, false); | ||||
| var touchXY = {}; | ||||
| function touchStartHandler(event) { | ||||
| 	var touches = event.changedTouches; | ||||
| 	touchXY = { | ||||
| 		clientX : touches[0].clientX, | ||||
| 		clientY : touches[0].clientY | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| // (ctl)-alt-s search hotkey | ||||
| document.onkeyup = function(e) { | ||||
| 	var e = e || window.event; // for IE to cover IEs window event-object | ||||
| 	if (e.altKey && (e.which == CONSTANT_S_KEY || e.which == CONSTANT_s_KEY)) { | ||||
| 		document.getElementById("txtInput").focus(); | ||||
| 		return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // new window or tab loading | ||||
| function load_functions() { | ||||
| 	// loading | ||||
| 	loading = true; | ||||
| 	// retain checked items | ||||
| 	checkChange(); | ||||
| 	// retrieve saved sorting | ||||
| 	getSort(); | ||||
| 	// sort if not default | ||||
| 	sortTable(sort_column); | ||||
| 	// search | ||||
| 	if (isSessionStorageSupported()) { | ||||
| 		// retrieve search | ||||
| 		if (sessionStorage.getItem("searchtxt") != null) { | ||||
| 			document.getElementById("txtInput").value = sessionStorage.getItem("searchtxt"); | ||||
| 			tblSearch(this); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // sorting | ||||
| function sortTable(n) { | ||||
| 	var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0, | ||||
| 		sortimgs, sortableimgs; | ||||
| 	table = document.getElementById("watch-table"); | ||||
| 	switching = true; | ||||
| 	//Set the sorting direction, either default 9, 1 or saved | ||||
| 	if (loading) { | ||||
| 		getSort(); | ||||
| 		dir = (sort_order == 0) ? "asc" : "desc"; | ||||
| 		loading = false; | ||||
| 	} else { | ||||
| 		dir = "asc"; | ||||
| 	} | ||||
| 	/*Make a loop that will continue until | ||||
| 	no switching has been done:*/ | ||||
| 	while (switching) { | ||||
| 		//start by saying: no switching is done: | ||||
| 		switching = false; | ||||
| 		rows = table.rows; | ||||
| 		/*Loop through all table rows (except the | ||||
| 		first, which contains table headers):*/ | ||||
| 		for (i = 1; i < (rows.length - 1); i++) { | ||||
| 			//start by saying there should be no switching: | ||||
| 			shouldSwitch = false; | ||||
| 			/*Get the two elements you want to compare, | ||||
| 			one from current row and one from the next:*/ | ||||
| 			x = rows[i].getElementsByTagName("TD")[n]; | ||||
| 			y = rows[i + 1].getElementsByTagName("TD")[n]; | ||||
| 			x = x.innerHTML.toLowerCase(); | ||||
| 			y = y.innerHTML.toLowerCase(); | ||||
| 			if (!isNaN(x)) { // handle numeric columns | ||||
| 				x = parseFloat(x); | ||||
| 				y = parseFloat(y); | ||||
| 			} | ||||
| 			if (n == 1) { // handle play/pause column | ||||
| 				x = rows[i].getElementsByTagName("TD")[n].getElementsByTagName("img")[0].src; | ||||
| 				y = rows[i + 1].getElementsByTagName("TD")[n].getElementsByTagName("img")[0].src; | ||||
| 			} | ||||
| 			/*check if the two rows should switch place, | ||||
| 			based on the direction, asc or desc:*/ | ||||
| 			if (dir == "asc") { | ||||
| 				if (x > y) { | ||||
| 					//if so, mark as a switch and break the loop: | ||||
| 					shouldSwitch = true; | ||||
| 					break; | ||||
| 				} | ||||
| 			} else if (dir == "desc") { | ||||
| 				if (x < y) { | ||||
| 					//if so, mark as a switch and break the loop: | ||||
| 					shouldSwitch = true; | ||||
| 					break; | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 		if (shouldSwitch) { | ||||
| 			/*If a switch has been marked, make the switch | ||||
| 			and mark that a switch has been done:*/ | ||||
| 			rows[i].parentNode.insertBefore(rows[i + 1], rows[i]); | ||||
| 			switching = true; | ||||
| 			//Each time a switch is done, increase this count by 1: | ||||
| 			switchcount++; | ||||
| 		} else { | ||||
| 			/*If no switching has been done AND the direction is "asc", | ||||
| 			set the direction to "desc" and run the while loop again.*/ | ||||
| 			if (switchcount == 0 && dir == "asc") { | ||||
| 				dir = "desc"; | ||||
| 				switching = true; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// hide all asc/desc sort arrows | ||||
| 	sortimgs = document.querySelectorAll('[id^="sort-"]'); | ||||
| 	for (i = 0; i < sortimgs.length; i++) { | ||||
| 		sortimgs[i].style.display = "none"; | ||||
| 	} | ||||
| 	// show current asc/desc sort arrow and set sort_order var | ||||
| 	if (dir == "asc") { | ||||
| 		document.getElementById("sort-" + n + "a").style.display = ""; | ||||
| 	} else { | ||||
| 		document.getElementById("sort-" + n + "d").style.display = ""; | ||||
| 	} | ||||
| 	// show all sortable indicators | ||||
| 	sortableimgs = document.querySelectorAll('[id^="sortable-"]'); | ||||
| 	for (i = 0; i < sortableimgs.length; i++) { | ||||
| 		sortableimgs[i].style.display = ""; | ||||
| 	} | ||||
| 	// hide sortable indicator from current column | ||||
| 	document.getElementById("sortable-" + n).style.display = "none"; | ||||
| 	// save sorting | ||||
| 	sessionStorage.setItem("sort_column", n); | ||||
| 	sessionStorage.setItem("sort_order", (dir == "asc") ? 0 : 1); | ||||
| 	// restripe rows | ||||
| 	restripe(); | ||||
| } | ||||
|  | ||||
| // check/uncheck all checkboxes | ||||
| function checkAll(e) { | ||||
| 	var elemID = event.srcElement.id; | ||||
| 	if (!elemID) return; | ||||
| 	var elem = document.getElementById(elemID); | ||||
| 	var rect = elem.getBoundingClientRect(); | ||||
| 	var offsetLeft = document.documentElement.scrollLeft + rect.left; | ||||
| 	var offsetTop; | ||||
| 	if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | ||||
| 		offsetTop = touchXY.clientY; // + rect.top; | ||||
| 	} | ||||
| 	else { | ||||
| 		offsetTop = document.documentElement.scrollTop + rect.top; | ||||
| 	} | ||||
| 	var i; | ||||
| 	var checkboxes = document.getElementsByName('check'); | ||||
| 	var checkboxFunctions = document.getElementById('checkbox-functions'); | ||||
| 	if (e.checked) { | ||||
| 		for (i = 0; i < checkboxes.length; i++) { | ||||
| 			checkboxes[i].checked = true; | ||||
| 		} | ||||
| 		checkboxFunctions.style.display = ""; | ||||
| 		checkboxFunctions.style.left = offsetLeft + 30 + "px"; | ||||
| 		checkboxFunctions.style.top = offsetTop + "px"; | ||||
| 	} else { | ||||
| 		for (i = 0; i < checkboxes.length; i++) { | ||||
| 			checkboxes[i].checked = false; | ||||
| 		} | ||||
| 		checkboxFunctions.style.display = "none"; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // show/hide checkbox controls grid popup and check/uncheck checkall checkbox if all other checkboxes are checked/unchecked | ||||
| function checkChange(e) { | ||||
| 	var elemID = event.srcElement.id; | ||||
| 	if (!elemID) return; | ||||
| 	var elem = document.getElementById(elemID); | ||||
| 	var rect = elem.getBoundingClientRect(); | ||||
| 	var offsetLeft = document.documentElement.scrollLeft + rect.left; | ||||
| 	var offsetTop; | ||||
| 	if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) { | ||||
| 		offsetTop = touchXY.clientY; // + rect.top; | ||||
| 	} | ||||
| 	else { | ||||
| 		offsetTop = document.documentElement.scrollTop + rect.top; | ||||
| 	} | ||||
| 	var i; | ||||
| 	var totalCheckbox = document.querySelectorAll('input[name="check"]').length; | ||||
| 	var totalChecked = document.querySelectorAll('input[name="check"]:checked').length; | ||||
| 	var checkboxFunctions = document.getElementById('checkbox-functions'); | ||||
| 	if(totalCheckbox == totalChecked) { | ||||
| 		document.getElementsByName("showhide")[0].checked=true; | ||||
| 	} | ||||
| 	else { | ||||
| 		document.getElementsByName("showhide")[0].checked=false; | ||||
| 	} | ||||
| 	if (totalChecked > 0) { | ||||
| 		checkboxFunctions.style.display = ""; | ||||
| 		checkboxFunctions.style.left = offsetLeft + 30 + "px"; | ||||
| 		if ( offsetTop > ( window.innerHeight - checkboxFunctions.offsetHeight) ) { | ||||
| 			checkboxFunctions.style.top = (window.innerHeight - checkboxFunctions.offsetHeight) + "px"; | ||||
| 		} | ||||
| 		else { | ||||
| 			checkboxFunctions.style.top = offsetTop + "px"; | ||||
| 		} | ||||
| 	} else { | ||||
| 		checkboxFunctions.style.display = "none"; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // search watches in Title column | ||||
| function tblSearch(evt) { | ||||
| 	var code = evt.charCode || evt.keyCode; | ||||
| 	if (code == CONSTANT_ESCAPE_KEY) { | ||||
| 		document.getElementById("txtInput").value = ''; | ||||
| 	} | ||||
| 	var input, filter, table, tr, td, i, txtValue; | ||||
| 	input = document.getElementById("txtInput"); | ||||
| 	filter = input.value.toUpperCase(); | ||||
| 	table = document.getElementById("watch-table"); | ||||
| 	tr = table.getElementsByTagName("tr"); | ||||
| 	for (i = 1; i < tr.length; i++) { // skip header | ||||
| 		td = tr[i].getElementsByTagName("td")[3]; // col 3 is the hidden title/url column | ||||
| 		if (td) { | ||||
| 			txtValue = td.textContent || td.innerText; | ||||
| 			if (txtValue.toUpperCase().indexOf(filter) > -1) { | ||||
| 				tr[i].style.display = ""; | ||||
| 			} else { | ||||
| 				tr[i].style.display = "none"; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	// restripe rows | ||||
| 	restripe(); | ||||
| 	if (code == CONSTANT_ESCAPE_KEY) { | ||||
| 		document.getElementById("watch-table-wrapper").focus(); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // restripe after searching or sorting | ||||
| function restripe() { | ||||
| 	var i, visrows = []; | ||||
| 	var table = document.getElementById("watch-table"); | ||||
| 	var rows = table.getElementsByTagName("tr"); | ||||
|  | ||||
| 	for (i = 1; i < rows.length; i++) { // skip header | ||||
| 		if (rows[i].style.display !== "none") { | ||||
| 			visrows.push(rows[i]); | ||||
| 		} | ||||
| 	} | ||||
| 	for (i = 0; i < visrows.length; i++) { | ||||
| 		var row = visrows[i]; | ||||
| 		if (i % 2 == 0) { | ||||
| 			row.classList.remove('pure-table-odd'); | ||||
| 			row.classList.add('pure-table-even'); | ||||
| 		} else { | ||||
| 			row.classList.remove('pure-table-even'); | ||||
| 			row.classList.add('pure-table-odd'); | ||||
| 		} | ||||
| 		var cells = row.getElementsByTagName("td"); | ||||
| 		for (var j = 0; j < cells.length; j++) { | ||||
| 			if (i % 2 == 0) { | ||||
| 				cells[j].style.background = "#f2f2f2"; | ||||
| 			} else { | ||||
| 				cells[j].style.background = "#ffffff"; | ||||
| 			} | ||||
| 		} | ||||
| 		// uncomment to renumber rows ascending:    var cells = row.getElementsByTagName("td"); | ||||
| 		// uncomment to renumber rows ascending:    cells[0].innerText = i+1; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // get checked or all uuids | ||||
| function getChecked(items) { | ||||
| 	var i, checkedArr, uuids = ''; | ||||
|  | ||||
| 	if (items === undefined) { | ||||
| 		checkedArr = document.querySelectorAll('input[name="check"]:checked'); | ||||
| 	} else { | ||||
| 		checkedArr = document.querySelectorAll('input[name="check"]'); | ||||
| 	} | ||||
| 	if (checkedArr.length > 0) { | ||||
| 		let output = []; | ||||
| 		for (i = 0; i < checkedArr.length; i++) { | ||||
| 			output.push(checkedArr[i].parentNode.parentNode.getAttribute("id")); | ||||
| 		} | ||||
| 		for (i = 0; i < checkedArr.length; i++) { | ||||
| 			if (i < checkedArr.length - 1) { | ||||
| 				uuids += output[i] + ","; | ||||
| 			} else { | ||||
| 				uuids += output[i]; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return uuids; | ||||
| } | ||||
|  | ||||
| // process selected watches  | ||||
| function processChecked(func, tag) { | ||||
| 	var uuids, result; | ||||
|  | ||||
| 	if (func == 'mark_all_notviewed') { | ||||
| 		uuids = getChecked('all'); | ||||
| 	} else { | ||||
| 		uuids = getChecked(); | ||||
| 	} | ||||
| 	// confirm if deleting | ||||
| 	if (func == 'delete_selected' && uuids.length > 0) { | ||||
| 		result = confirm('Deletions cannot be undone.\n\nAre you sure you want to continue?'); | ||||
| 		if (result == false) { | ||||
| 			return; | ||||
| 		} | ||||
| 	} | ||||
| 	// href locations | ||||
| 	var currenturl = window.location; | ||||
| 	var posturl = location.protocol + '//' + location.host + '/api/process-selected'; | ||||
| 	// posting vars | ||||
| 	const XHR = new XMLHttpRequest(), | ||||
| 		FD = new FormData(); | ||||
| 	// fill form data | ||||
| 	FD.append('func', func); | ||||
| 	FD.append('tag', tag); | ||||
| 	FD.append('uuids', uuids); | ||||
| 	// success | ||||
| 	XHR.addEventListener('load', function(event) { | ||||
| 		window.location = currenturl; | ||||
| 	}); | ||||
| 	// error | ||||
| 	XHR.addEventListener(' error', function(event) { | ||||
| 		alert('Error posting request.'); | ||||
| 	}); | ||||
| 	// set up request | ||||
| 	XHR.open('POST', posturl); | ||||
| 	// send | ||||
| 	XHR.send(FD); | ||||
| } | ||||
|  | ||||
| function clearSearch() { | ||||
| 	document.getElementById("txtInput").value = ''; | ||||
| 	tblSearch(CONSTANT_ESCAPE_KEY); | ||||
| } | ||||
|  | ||||
| function isSessionStorageSupported() { | ||||
| 	var storage = window.sessionStorage; | ||||
| 	try { | ||||
| 		storage.setItem('test', 'test'); | ||||
| 		storage.removeItem('test'); | ||||
| 		return true; | ||||
| 	} catch (e) { | ||||
| 		return false; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function getSort() { | ||||
| 	if (isSessionStorageSupported()) { | ||||
| 		// retrieve sort settings if set | ||||
| 		if (sessionStorage.getItem("sort_column") != null) { | ||||
| 			sort_column = sessionStorage.getItem("sort_column"); | ||||
| 			sort_order = sessionStorage.getItem("sort_order"); | ||||
| 		} else { | ||||
| 			sort_column = 7; // last changed | ||||
| 			sort_order = 1; // desc | ||||
| 			//alert("Your web browser does not support retaining sorting and page position."); | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| function closeGridDisplay() { | ||||
| 	document.getElementsByName("showhide")[0].checked = false; | ||||
| 	var checkboxes = document.getElementsByName('check'); | ||||
| 	for (i = 0; i < checkboxes.length; i++) { | ||||
| 		checkboxes[i].checked = false; | ||||
| 	} | ||||
| 	document.getElementById("checkbox-functions").style.display = "none"; | ||||
| } | ||||
| @@ -201,6 +201,7 @@ class ChangeDetectionStore: | ||||
|     @property | ||||
|     def data(self): | ||||
|         has_unviewed = False | ||||
|         unviewed_count = 0 | ||||
|         for uuid, v in self.__data['watching'].items(): | ||||
|             self.__data['watching'][uuid]['newest_history_key'] = self.get_newest_history_key(uuid) | ||||
|             if int(v['newest_history_key']) <= int(v['last_viewed']): | ||||
| @@ -209,6 +210,7 @@ class ChangeDetectionStore: | ||||
|             else: | ||||
|                 self.__data['watching'][uuid]['viewed'] = False | ||||
|                 has_unviewed = True | ||||
|                 unviewed_count += 1 | ||||
|  | ||||
|             # #106 - Be sure this is None on empty string, False, None, etc | ||||
|             # Default var for fetch_backend | ||||
| @@ -221,6 +223,7 @@ class ChangeDetectionStore: | ||||
|           self.__data['settings']['application']['base_url'] = env_base_url.strip('" ') | ||||
|  | ||||
|         self.__data['has_unviewed'] = has_unviewed | ||||
|         self.__data['unviewed_count'] = unviewed_count | ||||
|  | ||||
|         return self.__data | ||||
|  | ||||
|   | ||||
| @@ -34,6 +34,9 @@ | ||||
|         <ul class="pure-menu-list"  id="top-right-menu"> | ||||
|         {% if current_user.is_authenticated or not has_password %} | ||||
|             {% if not current_diff_url %} | ||||
|             <li class="pure-menu-item"> | ||||
|                 <span class="search-box"><input type="text" id="txtInput" onkeyup="tblSearch(event)" onmouseup="clearSearch(event)" placeholder="Title..." /></span> | ||||
|             </li> | ||||
|             <li class="pure-menu-item"> | ||||
|                 <a href="{{ url_for('get_backup')}}" class="pure-menu-link">BACKUP</a> | ||||
|             </li> | ||||
|   | ||||
| @@ -2,103 +2,146 @@ | ||||
| {% block content %} | ||||
| {% from '_helpers.jinja' import render_simple_field %} | ||||
|  | ||||
| <script src="{{url_for('static_content', group='js', filename='tbltools.js')}}"></script> | ||||
|  | ||||
| <div class="box"> | ||||
| 	<form class="pure-form" action="{{ url_for('api_watch_add') }}" method="POST" id="new-watch-form"> | ||||
| 		<fieldset> | ||||
| 			<legend>Add a new change detection watch</legend> | ||||
| 				{{ render_simple_field(form.url, placeholder="https://...", required=true) }} | ||||
| 				{{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="tag") }} | ||||
| 			<div id="watch-actions"><button type="submit" class="pure-button pure-button-primary" name="action" value="watch">Watch</button>  | ||||
| 			<span id="add-paused"><label><input type="checkbox" name="add-paused"> Add Paused</label></span></div> | ||||
| 	   </fieldset> | ||||
| 		<!-- add extra stuff, like do a http POST and send headers --> | ||||
| 		<!-- user/pass r = requests.get('https://api.github.com/user', auth=('user', 'pass')) --> | ||||
| 	</form> | ||||
|     <div id="watch-table-wrapper" tabindex="-1"> | ||||
| 		<div id="categories"> | ||||
| 			<a href="{{url_for('index')}}" class="pure-button button-tag {{'active' if not active_tag }}">All</a> | ||||
| 			{% for tag in tags %} | ||||
| 				{% if tag != "" %} | ||||
| 					<a href="{{url_for('index', tag=tag) }}" class="pure-button button-tag {{'active' if active_tag == tag }}">{{ tag }}</a> | ||||
| 				{% endif %} | ||||
| 			{% endfor %} | ||||
| 		</div> | ||||
| 			 | ||||
| 		<div id="controls-top"> | ||||
| 			<div id="checkbox-functions" style="display: none;"> | ||||
| 				<ul id="post-list-buttons-top-grid"> | ||||
| 					<li id="grid-item-1"> | ||||
| 						<a href="javascript:processChecked('recheck_selected', '{{ active_tag }}');" class="pure-button button-tag " title="Recheck Selected{%if active_tag%} in "{{active_tag}}"{%endif%}">Recheck </a> | ||||
| 					</li> | ||||
| 					<li id="grid-item-2"> | ||||
| 						<a href="javascript:processChecked('mark_selected_notviewed', '{{ active_tag }}');" class="pure-button button-tag " title="Mark Selected as Unviewed{%if active_tag%} in "{{active_tag}}"{%endif%}">Unviewed</a> | ||||
| 					</li> | ||||
| 					<li id="grid-item-3"> | ||||
| 						<a href="javascript:processChecked('mark_selected_viewed', '{{ active_tag }}');" class="pure-button button-tag " title="Mark Selected as Viewed{%if active_tag%} in "{{active_tag}}"{%endif%}">Viewed  </a> | ||||
| 					</li> | ||||
| 					<li id="grid-item-4"> | ||||
| 						<a href="javascript:processChecked('delete_selected', '{{ active_tag }}');" class="pure-button button-tag danger " title="Delete Selected{%if active_tag%} in "{{active_tag}}"{%endif%}">Delete  </a> | ||||
| 					</li> | ||||
| 					<li id="grid-item-5"> | ||||
| 					   <a href="javascript:closeGridDisplay();" class="pure-button button-tag ">Cancel  </a> | ||||
| 					</li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | ||||
|     <form class="pure-form" action="{{ url_for('api_watch_add') }}" method="POST" id="new-watch-form"> | ||||
|         <fieldset> | ||||
|             <legend>Add a new change detection watch</legend> | ||||
|                 {{ render_simple_field(form.url, placeholder="https://...", required=true) }} | ||||
|                 {{ render_simple_field(form.tag, value=active_tag if active_tag else '', placeholder="tag") }} | ||||
|             <button type="submit" class="pure-button pure-button-primary">Watch</button> | ||||
|         </fieldset> | ||||
|         <!-- add extra stuff, like do a http POST and send headers --> | ||||
|         <!-- user/pass r = requests.get('https://api.github.com/user', auth=('user', 'pass')) --> | ||||
|     </form> | ||||
|     <div> | ||||
|         <a href="{{url_for('index')}}" class="pure-button button-tag {{'active' if not active_tag }}">All</a> | ||||
|         {% for tag in tags %} | ||||
|             {% if tag != "" %} | ||||
|                 <a href="{{url_for('index', tag=tag) }}" class="pure-button button-tag {{'active' if active_tag == tag }}">{{ tag }}</a> | ||||
|             {% endif %} | ||||
|         {% endfor %} | ||||
|     </div> | ||||
| 		<div> | ||||
| 			<table class="pure-table pure-table-striped watch-table" id="watch-table"> | ||||
| 				<thead> | ||||
| 				<tr id="header"> | ||||
| 					<th class="inline chkbox-header"><input id="chk-all" type="checkbox" name="showhide" onchange="checkAll(this)" title="Check/Uncheck All">  #</th> | ||||
| 					<th class="pause-resume-header" onclick="sortTable(1)"> | ||||
| 						<span class="clickable" | ||||
| 																				 title="Sort by Pause/Resume"><a | ||||
| 							href="{{url_for('index', pause='pause-all', tag=active_tag)}}"><img | ||||
| 							src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause" | ||||
| 							title="Pause All {%if active_tag%}in "{{active_tag}}" {%endif%}"/></a> <a | ||||
| 							href="{{url_for('index', pause='resume-all', tag=active_tag)}}"><img | ||||
| 							src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="Resume" | ||||
| 							title="Resume All {%if active_tag%}in "{{active_tag}}" {%endif%}"/></a>  <span | ||||
| 							id="sortable-1"><img | ||||
| 							src="{{url_for('static_content', group='images', filename='sortable.svg')}}" | ||||
| 							alt="sort"/></span><span class="sortarrow"><span id="sort-1a" | ||||
| 																			 style="display:none;">▲</span><span | ||||
| 							id="sort-1d" style="display:none;">▼</span></span></span></th> | ||||
| 					<th onclick="sortTable(3)"><span class="clickable" title="Sort by Title">Title  <span id="sortable-3"><img src="{{url_for('static_content', group='images', filename='sortable.svg')}}" alt="sort" /></span><span class="sortarrow"><span id="sort-3a" style="display:none;">▲</span><span id="sort-3d" style="display:none;">▼</span></span></span></th> | ||||
| 					<th onclick="sortTable(5)"><span class="clickable" title="Sort by Last Checked">Checked  <span id="sortable-5"><img src="{{url_for('static_content', group='images', filename='sortable.svg')}}" alt="sort" /></span><span class="sortarrow"><span id="sort-5a" style="display:none;">▲</span><span id="sort-5d" style="display:none;">▼</span></span></span></th> | ||||
| 					<th onclick="sortTable(7)"><span class="clickable" title="Sort by Last Changed">Changed  <span id="sortable-7" style="display:none;"><img src="{{url_for('static_content', group='images', filename='sortable.svg')}}" alt="sort" /></span><span class="sortarrow"><span id="sort-7a" style="display:none;">▲</span><span id="sort-7d">▼</span></span></span></th> | ||||
| 					<th>Actions</th> | ||||
| 				</tr> | ||||
| 				</thead> | ||||
| 				<tbody> | ||||
| 					{% for watch in watches %} | ||||
| 						<tr id="{{ watch.uuid }}" | ||||
| 							class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} | ||||
| 																								   | ||||
| 							{% if watch.last_error is defined and watch.last_error != False %}error{% endif %} | ||||
| 							{% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %} | ||||
| 							{% if watch.paused is defined and watch.paused != False %}paused{% endif %} | ||||
| 							{% if watch.newest_history_key| int > watch.last_viewed| int %}unviewed{% endif %}"> | ||||
| 							<td class="inline chkbox"><input id="chk-{{ loop.index }}" type="checkbox" name="check" onchange="checkChange(this);">  {{ loop.index }}</td> | ||||
| 							<td class="inline pause-resume"> | ||||
| 							{% if watch.paused %} | ||||
| 								<a href="{{url_for('index', pause=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='play.svg')}}" alt="Resume" title="Resume"/></a> | ||||
| 							{% else %} | ||||
| 								<a href="{{url_for('index', pause=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause" title="Pause"/></a> | ||||
| 							{% endif %} | ||||
| 							</td> | ||||
| 							<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}} | ||||
| 								<a class="external inline-hover-img" target="_blank" rel="noopener" href="{{ watch.url }}"></a> | ||||
| 								{%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" />{% endif %} | ||||
|  | ||||
|     <div id="watch-table-wrapper"> | ||||
|         <table class="pure-table pure-table-striped watch-table"> | ||||
|             <thead> | ||||
|             <tr> | ||||
|                 <th>#</th> | ||||
|                 <th></th> | ||||
|                 <th></th> | ||||
|                 <th>Last Checked</th> | ||||
|                 <th>Last Changed</th> | ||||
|                 <th></th> | ||||
|             </tr> | ||||
|             </thead> | ||||
|             <tbody> | ||||
|  | ||||
|  | ||||
|             {% for watch in watches %} | ||||
|             <tr id="{{ watch.uuid }}" | ||||
|                 class="{{ loop.cycle('pure-table-odd', 'pure-table-even') }} | ||||
|                 {% if watch.last_error is defined and watch.last_error != False %}error{% endif %} | ||||
|                 {% if watch.last_notification_error is defined and watch.last_notification_error != False %}error{% endif %} | ||||
|                 {% if watch.paused is defined and watch.paused != False %}paused{% endif %} | ||||
|                 {% if watch.newest_history_key| int > watch.last_viewed| int %}unviewed{% endif %}"> | ||||
|                 <td class="inline">{{ loop.index }}</td> | ||||
|                 <td class="inline paused-state state-{{watch.paused}}"><a href="{{url_for('index', pause=watch.uuid, tag=active_tag)}}"><img src="{{url_for('static_content', group='images', filename='pause.svg')}}" alt="Pause" title="Pause"/></a></td> | ||||
|  | ||||
|                 <td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}} | ||||
|                     <a class="external" target="_blank" rel="noopener" href="{{ watch.url }}"></a> | ||||
|                     {%if watch.fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" />{% endif %} | ||||
|  | ||||
|                     {% if watch.last_error is defined and watch.last_error != False %} | ||||
|                     <div class="fetch-error">{{ watch.last_error }}</div> | ||||
|                     {% endif %} | ||||
|                     {% if watch.last_notification_error is defined and watch.last_notification_error != False %} | ||||
|                     <div class="fetch-error notification-error">{{ watch.last_notification_error }}</div> | ||||
|                     {% endif %} | ||||
|                     {% if not active_tag %} | ||||
|                     <span class="watch-tag-list">{{ watch.tag}}</span> | ||||
|                     {% endif %} | ||||
|                 </td> | ||||
|                 <td class="last-checked">{{watch|format_last_checked_time}}</td> | ||||
|                 <td class="last-changed">{% if watch.history|length >= 2 and watch.last_changed %} | ||||
|                     {{watch.last_changed|format_timestamp_timeago}} | ||||
|                     {% else %} | ||||
|                     Not yet | ||||
|                     {% endif %} | ||||
|                 </td> | ||||
|                 <td> | ||||
|                     <a href="{{ url_for('api_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" | ||||
|                        class="pure-button button-small pure-button-primary">Recheck</a> | ||||
|                     <a href="{{ url_for('edit_page', uuid=watch.uuid)}}" class="pure-button button-small pure-button-primary">Edit</a> | ||||
|                     {% if watch.history|length >= 2 %} | ||||
|                     <a href="{{ url_for('diff_history_page', uuid=watch.uuid) }}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary">Diff</a> | ||||
|                     {% else %} | ||||
|                         {% if watch.history|length == 1 %} | ||||
|                             <a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary">Preview</a> | ||||
|                         {% endif %} | ||||
|                     {% endif %} | ||||
|                 </td> | ||||
|             </tr> | ||||
|             {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <ul id="post-list-buttons"> | ||||
|             {% if has_unviewed %} | ||||
|             <li> | ||||
|                 <a href="{{url_for('mark_all_viewed', tag=request.args.get('tag')) }}" class="pure-button button-tag ">Mark all viewed</a> | ||||
|             </li> | ||||
|             {% endif %} | ||||
|             <li> | ||||
|                <a href="{{ url_for('api_watch_checknow', tag=active_tag) }}" class="pure-button button-tag ">Recheck | ||||
|                 all {% if active_tag%}in "{{active_tag}}"{%endif%}</a> | ||||
|             </li> | ||||
|             <li> | ||||
|                 <a href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| 								{% if watch.last_error is defined and watch.last_error != False %} | ||||
| 								<div class="fetch-error">{{ watch.last_error }}</div> | ||||
| 								{% endif %} | ||||
| 								{% if watch.last_notification_error is defined and watch.last_notification_error != False %} | ||||
| 								<div class="fetch-error notification-error">{{ watch.last_notification_error }}</div> | ||||
| 								{% endif %} | ||||
| 								{% if not active_tag %} | ||||
| 								<span class="watch-tag-list">{{ watch.tag}}</span> | ||||
| 								{% endif %} | ||||
| 							</td> | ||||
| 							<td class="last-checked">{{watch|format_last_checked_time}}</td> | ||||
| 							<td class="last-changed">{% if watch.history|length >= 2 and watch.last_changed %} | ||||
| 								{{watch.last_changed|format_timestamp_timeago}} | ||||
| 								{% else %} | ||||
| 								Not yet | ||||
| 								{% endif %} | ||||
| 							</td> | ||||
| 							<td> | ||||
| 								<a href="{{ url_for('api_watch_checknow', uuid=watch.uuid, tag=request.args.get('tag')) }}" | ||||
| 								   class="pure-button button-small pure-button-primary">Recheck</a> | ||||
| 								<a href="{{ url_for('edit_page', uuid=watch.uuid)}}" class="pure-button button-small pure-button-primary">Edit</a> | ||||
| 								{% if watch.history|length >= 2 %} | ||||
| 								<a href="{{ url_for('diff_history_page', uuid=watch.uuid) }}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary">Diff</a> | ||||
| 								{% else %} | ||||
| 									{% if watch.history|length == 1 %} | ||||
| 										<a href="{{ url_for('preview_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button button-small pure-button-primary">Preview</a> | ||||
| 									{% endif %} | ||||
| 								{% endif %} | ||||
| 							</td> | ||||
| 						</tr> | ||||
| 					{% endfor %} | ||||
| 				</tbody> | ||||
| 			</table> | ||||
| 			<ul id="post-list-buttons"> | ||||
| 				{% if has_unviewed %} | ||||
| 				<li> | ||||
| 					<a href="{{url_for('mark_all_viewed', tag=request.args.get('tag')) }}" class="pure-button button-tag ">Mark all viewed</a> | ||||
| 				</li> | ||||
| 				{% endif %} | ||||
| 				<li> | ||||
| 				   <a href="{{ url_for('api_watch_checknow', tag=active_tag) }}" class="pure-button button-tag ">Recheck | ||||
| 					all {% if active_tag%}in "{{active_tag}}"{%endif%}</a> | ||||
| 				</li> | ||||
| 				<li> | ||||
| 					<a href="{{ url_for('rss', tag=active_tag , token=app_rss_token)}}"><img alt="RSS Feed" id="feed-icon" src="{{url_for('static_content', group='images', filename='Generic_Feed-icon.svg')}}" height="15"></a> | ||||
| 				</li> | ||||
| 			</ul> | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| {% endblock %} | ||||
|   | ||||