Compare commits

...

35 Commits

Author SHA1 Message Date
dgtlmoon
b77a470d6f WIP - remove column, use a smart 'data-int' to sort on instead 2022-03-04 10:54:09 +01:00
dgtlmoon
2acdc9f2c7 Add comment 2022-03-04 10:54:03 +01:00
dgtlmoon
08671e4068 Merge branch 'master' into ui-improvements 2022-03-04 09:38:22 +01:00
dgtlmoon
1b2420ac03 Merge branch 'master' into ui-improvements 2022-02-13 23:46:11 +01:00
ntmmfts
ca91f732b8 UI improvements (#412)
* Update CONTRIBUTING.md

* Add option for tags on import (#377)

* Add option for tags on import and backup

* .add_watch() can accept empty tag
Use https://changedetection.io/CHANGELOG.txt as a nice default page to watch

* plaintext mime type fix - Don't attempt to extract HTML content from plaintext, this will remove lines and break changedetection (#391)

* #323 Adding note about discord:// 2000 char limit (#392)

* Adding note about discord:// 2000 char limit

* Ability to use a generated salted password in deployments as env var SALTED_PASS (#397)

* Ability to use a generated salted password in deployments as env var SALTED_PASS

* Offer instance on Lemonade
Tidy README

* Update README - Tidy up sections

* Update README - Fix docker section

* Update README.md

* /preview format doesnt need <pre> - fixing too many returnlines in content on diff/preview page

* fixed the reference to wiki for rpi section (#402)

* Add notification note - tgram:// bots cant send messages to other bots, so you should specify chat ID of non-bot user.

* Notification error log handler (#403)

* Add a notifications debug/error log interface (Link available under the notification URLs list)

* Refactor tests for notification error log handler (#404)

* Introduce -h option to allow listening not on 0.0.0.0. (#406)

* Fix typo in the startup create-directory command suggestion (#405)

* Use flask url_for() for webdriver chrome icon instead of relative path

* merging latest upstream changes

Co-authored-by: dgtlmoon <dgtlmoon@gmail.com>
Co-authored-by: Tim Loderhose <timlod@users.noreply.github.com>
Co-authored-by: Radu Ursache <3800336+rursache@users.noreply.github.com>
Co-authored-by: Alexander Aleksandrovič Klimov <al2klimov@gmail.com>
2022-02-13 23:44:12 +01:00
dgtlmoon
9f2806062b Merge branch 'master' into ui-improvements 2022-01-24 10:59:36 +01:00
ntmmfts
6ecfc3c843 increased checkbox functions grid popup font size 2022-01-08 10:57:47 -10:00
ntmmfts
d24cd28523 fixed test failure 2022-01-08 10:16:05 -10:00
ntmmfts
508cc1dbd2 saving edits 2022-01-08 01:43:21 -10:00
ntmmfts
2e8e27dc07 merging v0.39.6 2022-01-08 01:29:05 -10:00
ntmmfts
3b02b89a63 Merge branch 'master' of https://github.com/dgtlmoon/changedetection.io into ui-improvements 2022-01-08 01:22:57 -10:00
ntmmfts
7236572de6 merging v0.39.6 2022-01-08 00:39:44 -10:00
ntmmfts
fe037064d8 Merge branch 'seconds' of https://github.com/ntmmfts/changedetection.io into seconds 2022-01-08 00:29:43 -10:00
ntmmfts
51acfbdbda before v0.39.6 2022-01-08 00:29:37 -10:00
ntmmfts
bb5221d2c8 pulling upgrade 2022-01-06 20:54:48 -10:00
ntmmfts
e5add6c773 Revert to b129290 2022-01-06 07:33:23 -10:00
ntmmfts
b61928037b Revert "fixed backend test failure - had to remove grammar formatting replacement for flash message in api_watch_checknow."
This reverts commit b1292908e2.
2022-01-06 07:04:03 -10:00
ntmmfts
471c5533ee fixed check behavior and added cancel button to checkbox functions menu 2022-01-05 04:59:26 -10:00
ntmmfts
2e411e1ff4 Merge branch 'dgtlmoon:master' into seconds 2022-01-03 16:16:50 -10:00
ntmmfts
a7763ae9a3 Proof of concept for #160, 'Allow recheck time in seconds'. Currently retains 'minutes_between_check' key:value, so I'm requesting a review before globally renaming it 'to duration_between_check' or similar. 2022-01-02 09:56:54 -10:00
ntmmfts
b1292908e2 fixed backend test failure - had to remove grammar formatting replacement for flash message in api_watch_checknow. 2021-12-31 12:23:30 -10:00
ntmmfts
b01fded6eb latest fixes and improvements 2021-12-30 00:55:08 -10:00
ntmmfts
30c763fed9 latest fixes and improvements 2021-12-30 00:49:14 -10:00
ntmmfts
0302b7f801 Merge branch 'master' of https://github.com/ntmmfts/changedetection.io into ui-improvements
catching up to origin master before push
2021-12-29 17:29:52 -10:00
ntmmfts
b6bd57a85d latest fixes and improvements 2021-12-29 17:29:07 -10:00
ntmmfts
cf09767b48 fixes/adjustments 12/28 2021-12-28 09:34:36 -10:00
ntmmfts
1468b3a374 adding fixed search.svg 2021-12-28 03:58:39 -10:00
ntmmfts
2bb3d5c8ad fix watch-overview.html conflict 2021-12-28 03:56:43 -10:00
ntmmfts
cb1fe50a88 ui-improvement changes and fixes 12/28 2021-12-28 03:32:46 -10:00
ntmmfts
b5c2e13285 ui-improvement changes and fixes 12/28 2021-12-28 03:15:54 -10:00
ntmmfts
37842e6ea6 Merge branch 'master' of https://github.com/dgtlmoon/changedetection.io into ui-improvements 2021-12-25 12:35:01 -10:00
ntmmfts
54e79268c1 code cleanup 2021-12-25 12:33:20 -10:00
ntmmfts
fe10d289a0 minor fix 2021-12-24 09:08:37 -10:00
ntmmfts
a2568129f6 ui-imporovement adjustments 2021-12-24 08:35:34 -10:00
ntmmfts
66064efce3 ui-improvements updates 2021-12-19 21:59:23 -10:00
12 changed files with 723 additions and 200 deletions

View File

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

View 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

View File

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

View 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

View 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

View 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

View 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

View File

@@ -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;
}

View 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";
}

View File

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

View File

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

View File

@@ -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>&nbsp;
<span id="add-paused"><label><input type="checkbox" name="add-paused">&nbsp;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 &quot;{{active_tag}}&quot;{%endif%}">Recheck&nbsp;</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 &quot;{{active_tag}}&quot;{%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 &quot;{{active_tag}}&quot;{%endif%}">Viewed&nbsp;&nbsp;</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 &quot;{{active_tag}}&quot;{%endif%}">Delete&nbsp;&nbsp;</a>
</li>
<li id="grid-item-5">
<a href="javascript:closeGridDisplay();" class="pure-button button-tag ">Cancel&nbsp;&nbsp;</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">&nbsp;&nbsp;#</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 &quot;{{active_tag}}&quot; {%endif%}"/></a>&nbsp;<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 &quot;{{active_tag}}&quot; {%endif%}"/></a>&nbsp;&nbsp;<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;">&#9650;</span><span
id="sort-1d" style="display:none;">&#9660;</span></span></span></th>
<th onclick="sortTable(3)"><span class="clickable" title="Sort by Title">Title&nbsp;&nbsp;<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;">&#9650;</span><span id="sort-3d" style="display:none;">&#9660;</span></span></span></th>
<th onclick="sortTable(5)"><span class="clickable" title="Sort by Last Checked">Checked&nbsp;&nbsp;<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;">&#9650;</span><span id="sort-5d" style="display:none;">&#9660;</span></span></span></th>
<th onclick="sortTable(7)"><span class="clickable" title="Sort by Last Changed">Changed&nbsp;&nbsp;<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;">&#9650;</span><span id="sort-7d">&#9660;</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);">&nbsp;&nbsp;{{ 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 %}