1
0
mirror of https://github.com/aquatix/digimarks.git synced 2025-12-06 23:05:10 +01:00

8 Commits

25 changed files with 513 additions and 1337 deletions

View File

@@ -39,8 +39,6 @@ Usage / example configuration
Copy ``settings.py`` from example_config to the parent directory and
configure to your needs (*at the least* change the value of `SYSTEMKEY`).
Do not forget to fill in the `MASHAPE_API_KEY` value, which you [can request on the RapidAPI website](https://rapidapi.com/realfavicongenerator/api/realfavicongenerator).
Run digimarks as a service under nginx or apache and call the appropriate
url's when wanted.

View File

@@ -1,6 +1,5 @@
from __future__ import print_function
import binascii
import datetime
import gzip
import hashlib
@@ -10,11 +9,12 @@ import sys
import bs4
import requests
from dateutil import tz
from feedgen.feed import FeedGenerator
from flask import (Flask, abort, jsonify, make_response, redirect,
render_template, request, url_for)
from flask import (Flask, abort, jsonify, redirect, render_template, request,
url_for)
from peewee import * # noqa
from werkzeug.contrib.atom import AtomFeed
from digimarks import models, themes, views
try:
# Python 3
@@ -26,130 +26,6 @@ except ImportError:
DIGIMARKS_USER_AGENT = 'digimarks/1.2.0-dev'
DEFAULT_THEME = 'freshgreen'
themes = {
'green': {
'BROWSERCHROME': '#2e7d32', # green darken-2
'BODY': 'grey lighten-4',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-3',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-3',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'freshgreen': {
'BROWSERCHROME': '#43a047', # green darken-1
'BODY': 'grey lighten-5',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-1',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-1',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'lightblue': {
'BROWSERCHROME': '#0288d1', # light-blue darken-2
'BODY': 'white',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'light-blue darken-2',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#FFF', # white
'CARD_BACKGROUND': 'light-blue lighten-2',
'CARD_TEXT': 'black-text',
'CARD_LINK': '#263238', # blue-grey-text darken-4
'CHIP_TEXT': '#FFF', # white
'FAB': 'light-blue darken-4',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'dark': {
'BROWSERCHROME': '#212121', # grey darken-4
'BODY': 'grey darken-4',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'amoled': {
'BROWSERCHROME': '#000', # grey darken-4
'BODY': 'black',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
}
}
try:
import settings
except ImportError:
@@ -160,17 +36,10 @@ except ImportError:
APP_ROOT = os.path.dirname(os.path.realpath(__file__))
MEDIA_ROOT = os.path.join(APP_ROOT, 'static')
MEDIA_URL = '/static/'
DATABASE = {
'name': os.path.join(APP_ROOT, 'bookmarks.db'),
'engine': 'peewee.SqliteDatabase',
}
#PHANTOM = '/usr/local/bin/phantomjs'
#SCRIPT = os.path.join(APP_ROOT, 'screenshot.js')
# create our flask app and a database wrapper
app = Flask(__name__)
app.config.from_object(__name__)
database = SqliteDatabase(os.path.join(APP_ROOT, 'bookmarks.db'))
# Strip unnecessary whitespace due to jinja2 codeblocks
app.jinja_env.trim_blocks = True
@@ -187,299 +56,6 @@ all_tags = {}
usersettings = {}
def ifilterfalse(predicate, iterable):
# ifilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
if predicate is None:
predicate = bool
for x in iterable:
if not predicate(x):
yield x
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
b"\x1f\x8b\x08": "gz",
b"\x42\x5a\x68": "bz2",
b"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename, "rb") as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class BaseModel(Model):
class Meta:
database = database
class User(BaseModel):
""" User account """
username = CharField()
key = CharField()
theme = CharField(default=DEFAULT_THEME)
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = binascii.hexlify(os.urandom(24))
return self.key
class Bookmark(BaseModel):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url.encode('utf-8')).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200 or self.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def _set_favicon_with_iconsbetterideaorg(self, domain):
""" Fetch favicon for the domain """
fileextension = '.png'
meta = requests.head(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
allow_redirects=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def _set_favicon_with_realfavicongenerator(self, domain):
""" Fetch favicon for the domain """
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=android_chrome&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
if response.status_code == 404:
# Fall back to desktop favicon
response = requests.get(
'https://realfavicongenerator.p.rapidapi.com/favicon/icon?platform=desktop&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
# Debug for the moment
print(domain)
print(response.headers)
if 'Content-Length' in response.headers and response.headers['Content-Length'] == '0':
# No favicon found, likely
print('Skipping this favicon, needs fallback')
return
# Default to 'image/png'
fileextension = '.png'
if response.headers['content-type'] == 'image/jpeg':
fileextension = '.jpg'
if response.headers['content-type'] == 'image/x-icon':
fileextension = '.ico'
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def set_favicon(self):
""" Fetch favicon for the domain """
u = urlparse(self.url)
domain = u.netloc
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
# If file exists, don't re-download it
self.favicon = domain + '.png'
return
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
# If file exists, don't re-download it
self.favicon = domain + '.ico'
return
#self._set_favicon_with_iconsbetterideaorg(domain)
self._set_favicon_with_realfavicongenerator(domain)
def set_tags(self, newtags):
""" Set tags from `tags`, strip and sort them """
tags_split = newtags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': self.created_date.strftime('%Y-%m-%d %H:%M:%S'),
'url_hash': self.url_hash,
'tags': self.tags,
}
return result
@property
def serialize(self):
return self.to_dict()
class PublicTag(BaseModel):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = binascii.hexlify(os.urandom(16))
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)
def get_cached_tags(userkey):
""" Fail-safe way to get the cached tags for `userkey` """
try:
@@ -605,12 +181,10 @@ def bookmarks_js(userkey):
Bookmark.userkey == userkey,
Bookmark.status == Bookmark.VISIBLE
).order_by(Bookmark.created_date.desc())
resp = make_response(render_template(
return render_template(
'bookmarks.js',
bookmarks=bookmarks
))
resp.headers['Content-type'] = 'text/javascript; charset=utf-8'
return resp
)
@app.route('/r/<userkey>/<urlhash>')
@@ -963,34 +537,23 @@ def publictag_feed(tagkey):
Bookmark.tags.contains(this_tag.tag),
Bookmark.status == Bookmark.VISIBLE
)
feed = FeedGenerator()
feed.title(this_tag.tag)
feed.id(request.url)
feed.link(href=request.url, rel='self')
feed.link(href=make_external(url_for('publictag_page', tagkey=tagkey)))
feed = AtomFeed(this_tag.tag, feed_url=request.url, url=make_external(url_for('publictag_page', tagkey=tagkey)))
for bookmark in bookmarks:
entry = feed.add_entry()
updated_date = bookmark.modified_date
if not bookmark.modified_date:
updated_date = bookmark.created_date
bookmarktitle = '{} (no title)'.format(bookmark.url)
if bookmark.title:
bookmarktitle = bookmark.title
entry.id(bookmark.url)
entry.title(bookmarktitle)
entry.link(href=bookmark.url)
entry.author(name='digimarks')
entry.pubdate(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
entry.published(bookmark.created_date.replace(tzinfo=tz.tzlocal()))
entry.updated(updated_date.replace(tzinfo=tz.tzlocal()))
response = make_response(feed.atom_str(pretty=True))
response.headers.set('Content-Type', 'application/atom+xml')
return response
feed.add(
bookmarktitle,
content_type='html',
author='digimarks',
url=bookmark.url,
updated=updated_date,
published=bookmark.created_date
)
return feed.get_response()
except PublicTag.DoesNotExist:
abort(404)

321
digimarks/models.py Normal file
View File

@@ -0,0 +1,321 @@
"""digimarks data models and accompanying convenience functions"""
import binascii
import os
import datetime
from peewee import * # noqa
from . import themes
try:
# Python 3
from urllib.parse import urljoin, urlparse, urlunparse
except ImportError:
# Python 2
from urlparse import urljoin, urlparse, urlunparse
DATABASE_PATH = os.path.dirname(os.path.realpath(__file__))
if 'DIGIMARKS_DB_PATH' in os.environ:
DATABASE_PATH = os.environ['DIGIMARKS_DB_PATH']
database = SqliteDatabase(os.path.join(DATABASE_PATH, 'bookmarks.db'))
def ifilterfalse(predicate, iterable):
# ifilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
if predicate is None:
predicate = bool
for x in iterable:
if not predicate(x):
yield x
def unique_everseen(iterable, key=None):
"List unique elements, preserving order. Remember all elements ever seen."
# unique_everseen('AAAABBBCCDAABBB') --> A B C D
# unique_everseen('ABBCcAD', str.lower) --> A B C D
seen = set()
seen_add = seen.add
if key is None:
for element in ifilterfalse(seen.__contains__, iterable):
seen_add(element)
yield element
else:
for element in iterable:
k = key(element)
if k not in seen:
seen_add(k)
yield element
def clean_tags(tags_list):
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_everseen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
magic_dict = {
b"\x1f\x8b\x08": "gz",
b"\x42\x5a\x68": "bz2",
b"\x50\x4b\x03\x04": "zip"
}
max_len = max(len(x) for x in magic_dict)
def file_type(filename):
with open(filename, "rb") as f:
file_start = f.read(max_len)
for magic, filetype in magic_dict.items():
if file_start.startswith(magic):
return filetype
return "no match"
class BaseModel(Model):
class Meta:
database = database
class User(BaseModel):
""" User account """
username = CharField()
key = CharField()
theme = CharField(default=themes.DEFAULT_THEME)
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = binascii.hexlify(os.urandom(24))
return self.key
class Bookmark(BaseModel):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
note = TextField(default='')
#image = CharField(default='')
url_hash = CharField(default='')
tags = CharField(default='')
starred = BooleanField(default=False)
# Website (domain) favicon
favicon = CharField(null=True)
# Status code: 200 is OK, 404 is not found, for example (showing an error)
HTTP_CONNECTIONERROR = 0
HTTP_OK = 200
HTTP_ACCEPTED = 202
HTTP_MOVEDTEMPORARILY = 304
HTTP_NOTFOUND = 404
http_status = IntegerField(default=200)
redirect_uri = None
created_date = DateTimeField(default=datetime.datetime.now)
modified_date = DateTimeField(null=True)
deleted_date = DateTimeField(null=True)
# Bookmark status; deleting doesn't remove from DB
VISIBLE = 0
DELETED = 1
status = IntegerField(default=VISIBLE)
class Meta:
ordering = (('created_date', 'desc'),)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url.encode('utf-8')).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except:
# For example 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
self.http_status = 404
if self.http_status == 200 or self.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
self.title = html.title.text.strip()
except AttributeError:
self.title = ''
return self.title
def set_status_code(self):
""" Check the HTTP status of the url, as it might not exist for example """
try:
result = requests.head(self.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
except requests.ConnectionError:
self.http_status = self.HTTP_CONNECTIONERROR
return self.http_status
def _set_favicon_with_iconsbetterideaorg(self, domain):
""" Fetch favicon for the domain """
fileextension = '.png'
meta = requests.head(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
allow_redirects=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
if meta.url[-3:].lower() == 'ico':
fileextension = '.ico'
response = requests.get(
'http://icons.better-idea.org/icon?size=60&url=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT}
)
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def _set_favicon_with_realfavicongenerator(self, domain):
""" Fetch favicon for the domain """
response = requests.get(
'https://realfavicongenerator.p.mashape.com/favicon/icon?platform=android_chrome&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
if response.status_code == 404:
# Fall back to desktop favicon
response = requests.get(
'https://realfavicongenerator.p.mashape.com/favicon/icon?platform=desktop&site=' + domain,
stream=True,
headers={'User-Agent': DIGIMARKS_USER_AGENT, 'X-Mashape-Key': settings.MASHAPE_API_KEY}
)
# Debug for the moment
print(domain)
print(response.headers)
if 'Content-Length' in response.headers and response.headers['Content-Length'] == '0':
# No favicon found, likely
print('Skipping this favicon, needs fallback')
return
# Default to 'image/png'
fileextension = '.png'
if response.headers['content-type'] == 'image/jpeg':
fileextension = '.jpg'
if response.headers['content-type'] == 'image/x-icon':
fileextension = '.ico'
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + fileextension)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
filetype = file_type(filename)
if filetype == 'gz':
# decompress
orig = gzip.GzipFile(filename, 'rb')
origcontent = orig.read()
orig.close()
os.remove(filename)
with open(filename, 'wb') as new:
new.write(origcontent)
self.favicon = domain + fileextension
def set_favicon(self):
""" Fetch favicon for the domain """
u = urlparse(self.url)
domain = u.netloc
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')):
# If file exists, don't re-download it
self.favicon = domain + '.png'
return
if os.path.isfile(os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.ico')):
# If file exists, don't re-download it
self.favicon = domain + '.ico'
return
#self._set_favicon_with_iconsbetterideaorg(domain)
self._set_favicon_with_realfavicongenerator(domain)
def set_tags(self, newtags):
""" Set tags from `tags`, strip and sort them """
tags_split = newtags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
def get_redirect_uri(self):
if self.redirect_uri:
return self.redirect_uri
if self.http_status == 301 or self.http_status == 302:
result = requests.head(self.url, allow_redirects=True, headers={'User-Agent': DIGIMARKS_USER_AGENT})
self.http_status = result.status_code
self.redirect_uri = result.url
return result.url
return None
def get_uri_domain(self):
parsed = urlparse(self.url)
return parsed.hostname
@classmethod
def strip_url_params(cls, url):
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
@property
def tags_list(self):
""" Get the tags as a list, iterable in template """
if self.tags:
return self.tags.split(',')
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': self.created_date.strftime('%Y-%m-%d %H:%M:%S'),
'url_hash': self.url_hash,
'tags': self.tags.split(','),
'favicon': self.favicon,
'http_status': self.http_status,
'redirect_uri': self.redirect_uri,
}
return result
@property
def serialize(self):
return self.to_dict()
class PublicTag(BaseModel):
""" Publicly shared tag """
tagkey = CharField()
userkey = CharField()
tag = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate hash-based key for publicly shared tag """
self.tagkey = binascii.hexlify(os.urandom(16))
def get_tags_for_user(userkey):
""" Extract all tags from the bookmarks """
bookmarks = Bookmark.select().filter(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)

126
digimarks/themes.py Normal file
View File

@@ -0,0 +1,126 @@
"""digimarks theme definitions"""
DEFAULT_THEME = 'freshgreen'
themes = {
'green': {
'BROWSERCHROME': '#2e7d32', # green darken-2
'BODY': 'grey lighten-4',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-3',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-3',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'freshgreen': {
'BROWSERCHROME': '#43a047', # green darken-1
'BODY': 'grey lighten-5',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'green darken-1',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#1b5e20', # green darken-4
'BUTTON_ACTIVE': '#43a047', # green darken-1
'LINK_TEXT': '#1b5e20', # green darken-4
'CARD_BACKGROUND': 'green darken-1',
'CARD_TEXT': 'white-text',
'CARD_LINK': '#FFF', # white-text
'CHIP_TEXT': '#1b5e20', # green darken-4
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'lightblue': {
'BROWSERCHROME': '#0288d1', # light-blue darken-2
'BODY': 'white',
'TEXT': 'black-text',
'TEXTHEX': '#000',
'NAV': 'light-blue darken-2',
'PAGEHEADER': 'grey-text lighten-5',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#FFF', # white
'CARD_BACKGROUND': 'light-blue lighten-2',
'CARD_TEXT': 'black-text',
'CARD_LINK': '#263238', # blue-grey-text darken-4
'CHIP_TEXT': '#FFF', # white
'FAB': 'light-blue darken-4',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'dark': {
'BROWSERCHROME': '#212121', # grey darken-4
'BODY': 'grey darken-4',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
},
'amoled': {
'BROWSERCHROME': '#000', # grey darken-4
'BODY': 'black',
'TEXT': 'grey-text lighten-1',
'TEXTHEX': '#bdbdbd',
'NAV': 'grey darken-3',
'PAGEHEADER': 'grey-text lighten-1',
'MESSAGE_BACKGROUND': 'orange lighten-2',
'MESSAGE_TEXT': 'white-text',
'ERRORMESSAGE_BACKGROUND': 'red darken-1',
'ERRORMESSAGE_TEXT': 'white-text',
'BUTTON': '#fb8c00', # orange darken-1
'BUTTON_ACTIVE': '#ffa726', # orange lighten-1
'LINK_TEXT': '#fb8c00', # orange-text darken-1
'CARD_BACKGROUND': 'grey darken-3',
'CARD_TEXT': 'grey-text lighten-1',
'CARD_LINK': '#fb8c00', # orange-text darken-1
'CHIP_TEXT': '#fb8c00', # orange-text darken-1
'FAB': 'red',
'STAR': 'yellow-text',
'PROBLEM': 'red-text',
'COMMENT': '',
}
}

2
digimarks/views.py Normal file
View File

@@ -0,0 +1,2 @@
"""digimarks views"""

View File

@@ -10,10 +10,6 @@ DEBUG = False
# echo -n "yourstring" | sha1sum
SYSTEMKEY = 'S3kr1t'
# RapidAPI key for favicons
# https://rapidapi.com/realfavicongenerator/api/realfavicongenerator
MASHAPE_API_KEY = 'your_MASHAPE_key'
LOG_LOCATION = 'digimarks.log'
#LOG_LOCATION = '/var/log/digimarks/digimarks.log'
# How many logs to keep in log rotation:

View File

@@ -1,61 +1,27 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-dev.in
astroid==3.3.11
# via pylint
beautifulsoup4==4.13.5
# via bs4
blinker==1.9.0
# via flask
bs4==0.0.2
# via -r requirements.in
certifi==2025.8.3
# via requests
charset-normalizer==3.4.3
# via requests
click==8.2.1
# via flask
dill==0.4.0
# via pylint
feedgen==1.0.0
# via -r requirements.in
flask==3.1.2
# via -r requirements.in
idna==3.10
# via requests
isort==6.0.1
# via pylint
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
lxml==6.0.1
# via feedgen
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
mccabe==0.7.0
# via pylint
peewee==3.18.2
# via -r requirements.in
platformdirs==4.4.0
# via pylint
pylint==3.3.8
# via -r requirements-dev.in
python-dateutil==2.9.0.post0
# via feedgen
requests==2.32.5
# via -r requirements.in
six==1.17.0
# via python-dateutil
soupsieve==2.8
# via beautifulsoup4
tomlkit==0.13.3
# via pylint
typing-extensions==4.15.0
# via beautifulsoup4
urllib3==2.5.0
# via requests
werkzeug==3.1.3
# via flask
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements-dev.txt requirements-dev.in
#
astroid==2.0.4 # via pylint
beautifulsoup4==4.6.3 # via bs4
bs4==0.0.1
certifi==2018.10.15 # via requests
chardet==3.0.4 # via requests
click==7.0 # via flask
flask==1.0.2
idna==2.7 # via requests
isort==4.3.4 # via pylint
itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask
lazy-object-proxy==1.3.1 # via astroid
markupsafe==1.1.0 # via jinja2
mccabe==0.6.1 # via pylint
peewee==3.7.1
pylint==2.1.1
requests==2.20.1
six==1.11.0 # via astroid
urllib3==1.24.1 # via requests
werkzeug==0.14.1 # via flask
wrapt==1.10.11 # via astroid

View File

@@ -1,10 +1,4 @@
# Core application
flask
peewee
# Fetch title etc from links
bs4
requests
# Generate (atom) feeds for tags and such
feedgen

View File

@@ -1,47 +1,20 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.in
beautifulsoup4==4.13.5
# via bs4
blinker==1.9.0
# via flask
bs4==0.0.2
# via -r requirements.in
certifi==2025.8.3
# via requests
charset-normalizer==3.4.3
# via requests
click==8.2.1
# via flask
feedgen==1.0.0
# via -r requirements.in
flask==3.1.2
# via -r requirements.in
idna==3.10
# via requests
itsdangerous==2.2.0
# via flask
jinja2==3.1.6
# via flask
lxml==6.0.1
# via feedgen
markupsafe==3.0.2
# via
# flask
# jinja2
# werkzeug
peewee==3.18.2
# via -r requirements.in
python-dateutil==2.9.0.post0
# via feedgen
requests==2.32.5
# via -r requirements.in
six==1.17.0
# via python-dateutil
soupsieve==2.8
# via beautifulsoup4
typing-extensions==4.15.0
# via beautifulsoup4
urllib3==2.5.0
# via requests
werkzeug==3.1.3
# via flask
#
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements.txt requirements.in
#
beautifulsoup4==4.6.3 # via bs4
bs4==0.0.1
certifi==2018.10.15 # via requests
chardet==3.0.4 # via requests
click==7.0 # via flask
flask==1.0.2
idna==2.7 # via requests
itsdangerous==1.1.0 # via flask
jinja2==2.10 # via flask
markupsafe==1.1.0 # via jinja2
peewee==3.7.1
requests==2.20.1
urllib3==1.24.1 # via requests
werkzeug==0.14.1 # via flask

View File

@@ -1,67 +0,0 @@
/**
* digimarks styling
*/
/** Navigation **/
nav .sidenav-trigger
{
/* Fix for misalignment of hamburger icon */
margin: 0;
}
nav .sidenav-trigger i
{
/* Make the hamburger icon great again */
font-size: 2.7rem;
}
/** Cards and tags **/
.card .card-content,
.card .card-reveal
{
padding: 12px;
}
.card.tiny
{
height: 140px;
overflow: hidden;
}
.card.tiny .card-title
{
font-size: 18px;
}
.card .card-reveal .digimark-card-header,
.card .digimark-card-header.activator,
.chip.clickable
{
cursor: pointer;
/*display: block;*/
}
.card .digimark-card-header-tags
{
padding-top: 10px;
}
.card-image
{
min-width: 60px;
}
.card-image i,
.list-image i
{
padding: 5px 0 0 15px;
}
.card.horizontal .card-image img.favicon,
.list-image img.favicon
{
height: 60px;
width: 60px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 B

View File

@@ -1,11 +0,0 @@
/* global M */
var options = {};
var elem = document.querySelector(".sidenav");
var instance = M.Sidenav.init(elem, options);
elem = document.querySelector(".collapsible");
instance = M.Collapsible.init(elem, {
// inDuration: 1000,
// outDuration: 1000
});

View File

@@ -1,6 +0,0 @@
{% extends "base.html" %}
{% block title %}404: Page not found{% endblock %}
{% block pageheader %}404: Page not found{% endblock %}
{% block pagecontent %}
The page you requested was not found.
{% endblock %}

View File

@@ -1,126 +0,0 @@
<!doctype html>
<html>
<head>
<title>{% block title %}{% endblock %} - digimarks</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0"/>
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}"/>
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="{{ theme.BROWSERCHROME }}" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="{{ theme.BROWSERCHROME }}">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="referrer" content="never">
<meta name="referrer" content="no-referrer">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href='https://fonts.googleapis.com/css?family=Roboto+Mono&subset=latin,latin-ext' rel='stylesheet' type='text/css'>
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
<style>
a
{
color: {{ theme.LINK_TEXT }};
}
.card-content a
{
color: {{ theme.CARD_LINK }};
}
.chip a
{
color: {{ theme.CHIP_TEXT }};
}
/* label color */
.input-field .prefix ~ input, .input-field .prefix ~ textarea, .input-field .prefix ~ label, .input-field .prefix ~ .validate ~ label, .input-field .prefix ~ .autocomplete-content, .input-field input[type=text]
{
color: {{ theme.TEXTHEX }};
}
/* label focus color */
.input-field input[type=text]:focus + label,
.input-field .prefix ~ input[type=text]:focus + label
{
color: {{ theme.BUTTON }};
}
/* label underline focus color */
.input-field input[type=text]:focus,
.input-field .prefix ~ input[type=text]:focus,
.input-field input[type=text].autocomplete:focus
{
border-bottom: 1px solid {{ theme.BUTTON }};
box-shadow: 0 1px 0 0 {{ theme.BUTTON }};
}
/* icon prefix focus color */
.input-field .prefix.active
{
color: {{ theme.BUTTON }};
}
.btn, .btn:visited
{
background-color: {{ theme.BUTTON }};
}
.btn:hover, .btn:active
{
background-color: {{ theme.BUTTON_ACTIVE }};
}
.deletebtn
{
background-color: red;
}
.deletebtn:hover
{
background-color: #ef5350; /* red lighten-1 */
}
</style>
<link href="{{ url_for('static', filename='css/digimarks.css') }}?20180330" type="text/css" rel="stylesheet" media="screen,projection"/>
{% if not sortmethod %}
{% set sortmethod = None %}
{% endif %}
{% if not show_as %}
{% set show_as = None %}
{% endif %}
</head>
<body class="{{ theme.BODY }} {{ theme.TEXT }}">
<nav class="{{ theme.NAV }}" role="navigation">
<div class="nav-wrapper container"><a id="logo-container" href="{% if userkey %}{{ url_for('bookmarks_page', userkey=userkey, sortmethod=sortmethod, show_as=show_as) }}{% else %}{{ url_for('index') }}{% endif %}" class="brand-logo">digimarks</a>
<ul class="right hide-on-med-and-down">
{% if userkey %}
<li><a href="{{ url_for('tags_page', userkey=userkey) }}" class="waves-effect waves-light btn"><i class="material-icons left">label</i>Tags</a></li>
<li><a href="{{ url_for('addbookmark', userkey=userkey) }}" class="waves-effect waves-light btn"><i class="material-icons left">add</i>Add bookmark</a></li>
{% endif %}
</ul>
{% if userkey %}
<ul id="nav-mobile" class="sidenav">
<li><a class="waves-effect" href="{{ url_for('bookmarks_page', userkey=userkey) }}"><i class="material-icons left">turned_in</i>Home</a></li>
<li><a class="waves-effect" href="{{ url_for('tags_page', userkey=userkey) }}"><i class="material-icons left">label</i>Tags</a></li>
<li><a class="waves-effect" href="{{ url_for('addbookmark', userkey=userkey) }}"><i class="material-icons left">add</i>Add bookmark</a></li>
</ul>
<a href="#" data-target="nav-mobile" class="sidenav-trigger"><i class="material-icons">menu</i></a>
{% endif %}
</div>
</nav>
<div class="section no-pad-bot" id="index-banner">
<div class="container">
<div class="header {{ theme.PAGEHEADER }}">
<h1>{% block pageheader %}Bookmarks{% endblock %}</h1>
</div>
</div>
</div>
<div class="container">
<div class="section">
{% block pagecontent %}
{% endblock %}
</div>
</div>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="{{ url_for('static', filename='js/init.js') }}?20180309"></script>
{% block extrajs %}{% endblock %}
</body>
</html>

View File

@@ -1,113 +0,0 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if tag and not publictag %}
<div class="row">
<div class="col s12">
<a href="{{ url_for('addpublictag', userkey=userkey, tag=tag) }}">Create public page <i class="material-icons right">tag</i></a>
</div>
</div>
{% endif %}
{% if tag and publictag %}
<div class="row">
<div class="col s12"><a href="{{ url_for('publictag_page', tagkey=publictag.tagkey) }}">Public link</a></div>
</div>
{% endif %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel {{ theme.MESSAGE_BACKGROUND }}">
<span class="{{ theme.MESSAGE_TEXT }}">
{{ message|safe }}
</span>
</div>
</div>
</div>
{% endif %}
<div class="row">
<form action="{{ url_for('bookmarks_page', userkey=userkey) }}" name="filterForm" method="POST" autocomplete="off">
<div class="input-field col l9 m9 s8">
<input placeholder="search text" type="text" name="filter_text" id="filter_text" class="autocomplete" value="{{ filter_text }}" autocomplete="false" />
</div>
<div class="input-field col l3 m3 s4">
<p class="right-align"><button class="btn waves-effect waves-light" type="submit" name="submitBtn" title="Find"><i class="material-icons">search</i></button>
{% if show_as and show_as == 'list' %}
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod=filtermethod, sortmethod=sortmethod, show_as=None) }}" class="waves-effect waves-light btn" title="Show as cards"><i class="material-icons">apps</i></a>
{% else %}
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod=filtermethod, sortmethod=sortmethod, show_as='list') }}" class="waves-effect waves-light btn" title="Show as list"><i class="material-icons">reorder</i></a>
{% endif %}
</p>
</div>
</form>
</div>
{% if tags %}
<div class="row">
<div class="col s12">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header"><i class="material-icons">label</i>Filter on star/problem/comment/tag</div>
<div class="collapsible-body" style="padding: 10px;">
<div class="chip">
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='starred') }}"><i class="tiny material-icons {{ theme.STAR }}">star</i></a>
</div>
<div class="chip">
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='broken') }}"><i class="tiny material-icons {{ theme.PROBLEM }}">report_problem</i></a>
</div>
<div class="chip">
<a href="{{ url_for('bookmarks_page', userkey=userkey, filtermethod='note') }}"><i class="tiny material-icons {{ theme.COMMENT }}">comment</i></a>
</div>
{% for tag in tags %}
<div class="chip">
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</li>
</ul>
</div>
</div>
{% endif %}
{% if show_as and show_as == 'list' %}
{% include 'list.html' %}
{% else %}
{% include 'cards.html' %}
{% endif %}
<div class="fixed-action-btn" style="bottom: 20px; right: 20px;">
<a class="btn-floating btn-large {{ theme.FAB }}" href="{{ url_for('addbookmark', userkey=userkey) }}">
<i class="large material-icons">add</i>
</a>
</div>
{% endblock %}
{% block extrajs %}
<script>
function submitFilter() {
document.filterForm.submit();
}
/* Search filter autocomplete */
var options = {
onAutocomplete: submitFilter,
minLength: 3,
limit: 10,
data: {
},
}
var elem = document.querySelector('.autocomplete');
var instance = M.Autocomplete.init(elem, options);
/* TODO: fetch from API
instance.updateData({
});
*/
</script>
<script src="{{ url_for('bookmarks_js', userkey=userkey) }}" ></script>
{% endblock %}

View File

@@ -1,11 +0,0 @@
var elem = document.querySelector('.autocomplete');
var instance = M.Autocomplete.getInstance(elem);
instance.updateData({
{% for bookmark in bookmarks %}
{% if bookmark.favicon %}
"{{ bookmark.title | replace('"', '\\"') | replace('\n', '') | replace('\r', '') }}": "{{ url_for('static', filename='favicons/' + bookmark.favicon) }}",
{% else %}
"{{ bookmark.title | replace('"', '\\"') | replace('\n', '') | replace('\r', '') }}": null,
{% endif %}
{% endfor %}
});

View File

@@ -1,65 +0,0 @@
<div class="row">
{% for bookmark in bookmarks %}
<div class="col s12 m6 l4">
<div class="card horizontal tiny {{ theme.CARD_BACKGROUND }}">
<div class="card-image">
{% if bookmark.favicon %}
<div><img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" /></div>
{% else %}
<div><img src="{{ url_for('static', filename='faviconfallback.png') }}" class="favicon" /></div>
{% endif %}
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
<div><i class="small material-icons {{ theme.PROBLEM }}" title="HTTP status {{ bookmark.http_status }}">report_problem</i></div>
{% endif %}
{% if bookmark.starred == True %}
<div><i class="small material-icons {{ theme.STAR }}">star</i></div>
{% endif %}
{% if bookmark.note %}
<div><i class="small material-icons {{ theme.CARD_TEXT }}" title="{{ bookmark.note|truncate(100) }}">comment</i></div>
{% endif %}
</div>
<div class="card-stacked">
<div class="card-content {{ theme.CARD_TEXT }}">
<span class="digimark-card-header activator">
<i class="material-icons right">more_vert</i>
</span>
<div class="digimark-card-content">
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer noopener" target="_blank">
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
{{ bookmark.get_uri_domain() }} (no title)
{% endif %}
</a>
</div>
</div>
</div>
<div class="card-reveal {{ theme.CARD_BACKGROUND }}">
<span class="card-title {{ theme.CARD_TEXT }}">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
{% if editable %}
<div class="{{ theme.CARD_TEXT }}" style="padding-top: 10px;">
<a href="{{ url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" style="padding: 3px"><i class="tiny material-icons">mode_edit</i> EDIT</a>
<a href="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" style="padding: 3px" class="red-text"><i class="tiny material-icons">delete</i> DELETE</a>
</div>
{% endif %}
{% if showtags %}
<div class="digimark-card-header-tags">
{% for tag in bookmark.tags_list %}
<div class="chip">
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
{% endfor %}
{#
<div class="pagination">
{% if page > 1 %}<a href="./?page={{ page - 1 }}">Previous</a>{% endif %}
{% if pagination.get_pages() > page %}<a href="./?page={{ page + 1 }}">Next</a>{% endif %}
</div>
#}
</div>

View File

@@ -1,175 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if bookmark.http_status != 200 and bookmark.http_status != 202 and bookmark.http_status != 304 %}
<div class="row">
<div class="col s12">
<div class="card-panel {{ theme.ERRORMESSAGE_BACKGROUND }}">
<span class="{{ theme.ERRORMESSAGE_TEXT }}">
{% if bookmark.http_status == 404 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;URL not found (404), broken/outdated link?
{% elif bookmark.http_status == 301 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;HTTP status (301), moved permanently. Use button for new target
{% elif bookmark.http_status == 302 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;HTTP status (302), moved temporarily. Use button for new target
{% elif bookmark.http_status == bookmark.HTTP_CONNECTIONERROR %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;Connection error, server might have been offline at the time of last edit
{% else %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;HTTP status {{ bookmark.http_status }}
{% endif %}
</span>
</div>
</div>
</div>
{% endif %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel {{ theme.MESSAGE_BACKGROUND }}">
<span class="{{ theme.MESSAGE_TEXT }}">
{{ message }}
</span>
</div>
</div>
</div>
{% endif %}
{% if formaction and formaction == 'edit' %}
<form class="digimark" id="digimark" action="{{ url_for('editingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST" onsubmit="return onSubmitForm();" autocomplete="off">
{% else %}
<form class="digimark" id="digimark" action="{{ url_for('addingbookmark', userkey=userkey) }}" method="POST" onsubmit="return onSubmitForm();" autocomplete="off">
{% endif %}
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">description</i>
<input placeholder="title (leave empty for autofetch)" type="text" name="title" id="title" value="{{ bookmark.title }}" autocomplete="false" />
<label for="title">Title</label>
{# <span class="helper-text">Leave title empty for autofetching from the page</span>#}
</div>
<div class="input-field col s12">
<i class="material-icons prefix">turned_in</i>
<input placeholder="url" type="text" name="url" id="url" value="{{ bookmark.url }}" autocomplete="false" />
<label for="url">URL</label>
{% if bookmark.get_redirect_uri() %}
<div>
<a class="waves-effect waves-light btn" id="btn_urlupdate" onclick="updateURL()"><i class="material-icons left">turned_in</i>{{ bookmark.get_redirect_uri() }}</a>
</div>
<script type="text/javascript">
function updateURL() {
var text = document.getElementById('url');
text.value = '{{ bookmark.get_redirect_uri() }}';
}
</script>
{% endif %}
</div>
<div class="input-field col s12">
<i class="material-icons prefix">comment</i>
<input placeholder="note" type="text" name="note" id="note" value="{{ bookmark.note }}" autocomplete="false" />
<label for="note">Note</label>
</div>
<div class="input-field col s12">
<i class="material-icons prefix">label</i>
<input placeholder="tags, divided by comma's" type="text" name="tags" id="tags" value="{{ bookmark.tags }}" autocomplete="false" />
<label for="tags">Tags</label>
</div>
</div>
{% if tags %}
<div class="row">
<div class="col s12">
<ul class="collapsible" data-collapsible="expandable">
<li>
<div class="collapsible-header"><i class="material-icons">label</i>Existing tags</div>
<div class="collapsible-body" style="padding: 10px;">
{% for tag in tags %}
<div class="chip clickable" id="chip_{{ tag }}" onclick="addTag('{{ tag }}');">
{{ tag }}
</div>
{% endfor %}
</div>
</li>
</ul>
</div>
</div>
{% endif %}
<div class="row">
<div class="col s12">
{#<i class="material-icons prefix">star</i>#}
<label>
<input type="checkbox" name="starred" id="starred" {% if bookmark.starred == True %}checked{% endif %} />
<span>Starred</span>
</label>
</div>
<div class="col s12">
<label>
<input type="checkbox" name="strip" id="strip" />
<span>Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</span>
</label>
</div>
{% if bookmark.url_hash %}
</div>
<div class="row">
<div class="col l4 m6 s12">
<table>
<tr>
<th>Added</th>
<td>{{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% if bookmark.modified_date %}
<tr>
<th>Modified</th>
<td>{{ bookmark.modified_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endif %}
{% if bookmark.deleted_date %}
<tr>
<th>Deleted</th>
<td>{{ bookmark.deleted_date.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="row">
{% endif %}
<div class="input-field col l2 m3 s4">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="submit" id="submit">Save <i class="material-icons right">send</i></button></p>
</div>
{% if bookmark.url_hash %}
</form>
<div class="input-field col l4 m4 s6">
<form action="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
<p class="left-align"><button class="btn waves-effect waves-light deletebtn" type="submit" name="delete">Delete <i class="material-icons right">delete</i></button></p>
</form>
</div>
</div>
{% else %}
</div>
</form>
{% endif %}
<script>
function onSubmitForm()
{
var theForm = document.getElementById('digimark');
var submitButton = document.getElementById('submit');
theForm.onsubmit = submitButton.setAttribute("disabled", true);
return true;
}
function addTag(tagText)
{
var text = document.getElementById('tags');
text.value = text.value + ', ' + tagText;
}
</script>
{% endblock %}

View File

@@ -1,17 +0,0 @@
{% extends "base.html" %}
{% block title %}digimarks{% endblock %}
{% block pageheader %}digimarks{% endblock %}
{% block pagecontent %}
<p>Please visit your personal url, or <a href="https://github.com/aquatix/digimarks">see the digimarks project page</a>.</p>
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="black-text">
If you forgot/lost your personal url, contact your digimarks administrator. On startup, the personal codes are printed to the standard output (so should be findable in a log). Of course, bookmarks.db contains the user information too.
</span>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,62 +0,0 @@
<div class="row">
<table>
<thead>
<tr>
<th>&nbsp;</th>
<th>Bookmark</th>
<th>Added</th>
{% if showtags %}
<th>Tags</th>
{% endif %}
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for bookmark in bookmarks %}
<tr>
<td class="list-image">
{% if bookmark.favicon %}
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" class="favicon" />
{% else %}
<img src="{{ url_for('static', filename='faviconfallback.png') }}" class="favicon" />
{% endif %}
{% if bookmark.http_status != 200 and bookmark.http_status != 304 %}
<i class="small material-icons {{ theme.PROBLEM }}" title="HTTP status {{ bookmark.http_status }}">report_problem</i>
{% endif %}
{% if bookmark.starred == True %}
<i class="small material-icons {{ theme.STAR }}">star</i>
{% endif %}
{% if bookmark.note %}
<i class="small material-icons {{ theme.CARD_TEXT }}" title="{{ bookmark.note|truncate(100) }}">comment</i>
{% endif %}
</td>
<td>
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer noopener" target="_blank">
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
{{ bookmark.get_uri_domain() }} (no title)
{% endif %}
</a>
</td>
<td>{{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}</td>
{% if showtags %}
<td>
{% for tag in bookmark.tags_list %}
<div class="chip">
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</td>
{% endif %}
<td>
{% if editable %}
<a href="{{ url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" class="waves-effect waves-light btn" title="Edit"><i class="tiny material-icons">mode_edit</i></a>
<a href="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" class="waves-effect waves-light btn red" title="DELETE"><i class="tiny material-icons">delete</i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

View File

@@ -1,29 +0,0 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if message %}
<div class="row">
<div class="col s12">
<div class="card-panel orange lighten-2">
<span class="white-text">
{{ message }}
</span>
</div>
</div>
</div>
{% endif %}
<div class="row">
<div class="col s12">
<a href="{{ url_for('publictag_feed', tagkey=tagkey) }}"><i class="material-icons tiny">rss_feed</i> feed</a>
</div>
</div>
{% include 'cards.html' %}
{% endblock %}

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html>
<head>
<title>Redirecting - digimarks</title>
<meta name="referrer" content="never">
<meta name="robots" content="noindex, nofollow">
<meta http-equiv=refresh content="3; URL={{ url }}">
<style>
body { background-color: #000; color: #FFF; }
a { color: #fb8c00; }
</style>
</head>
<body>
<p>You're being redirected. If nothing happens, <a href="{{ url }}">click here instead</a>.</p>
</body>
</html>

View File

@@ -1,65 +0,0 @@
{% extends "base.html" %}
{% block title %}Tags{% endblock %}
{% block pageheader %}Tags{% endblock %}
{% block pagecontent %}
<div class="row">
<div class="col s12">
<table class="centered">
<thead>
<tr>
<th><i class="material-icons" title="Unique labels">label</i></th>
<th><i class="material-icons green-text" title="Public tag pages">present_to_all</i></th>
<th><i class="material-icons" title="Total bookmarks">turned_in</i></th>
<th><i class="material-icons" title="Bookmarks with notes">comment</i></th>
<th><i class="material-icons yellow-text" title="Starred bookmarks">star</i></th>
<th><i class="material-icons orange-text" title="HTTP status is not 200 OK">warning</i></th>
<th><i class="material-icons red-text" title="Deleted bookmarks">delete</i></th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ totaltags }}</td>
<td>{{ totalpublic }}</td>
<td>{{ totalbookmarks }}</td>
<td>{{ totalnotes }}</td>
<td>{{ totalstarred }}</td>
<td>{{ totalhttperrorstatus }}</td>
<td>{{ totaldeleted }}</td>
</tr>
</tbody>
</table>
<br /><br />
<table>
<thead>
<tr>
<th>Tag</th>
<th>Public link</th>
<th>Number of bookmarks</th>
</tr>
</thead>
<tbody>
{% for tag in tags %}
<tr>
<td>
<a href="{{ url_for('tag_page', userkey=userkey, tag=tag['tag']) }}">{{ tag['tag'] }}</a>
</td>
<td>
{% if tag['publictag'] %}
<a href="{{ url_for('publictag_page', tagkey=tag['publictag'].tagkey) }}">Public link</a> (<a href="{{ url_for('removepublictag', tag=tag['tag'], tagkey=tag['publictag'].tagkey, userkey=userkey) }}">Delete</a> <i class="tiny material-icons red-text">warning</i>)
{% else %}
<a href="{{ url_for('addpublictag', userkey=userkey, tag=tag['tag']) }}">Create</a>
{% endif %}
</td>
<td>
{{ tag['total'] }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}