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

342 Commits

Author SHA1 Message Date
e55b3c0ea1 Only show dropshadow on nav on the correct themes 2025-07-07 21:54:54 +02:00
6ed1269711 Better size and umwelt for modal dialog 2025-06-17 22:30:43 +02:00
36370cfad4 Use Javascript camelCase in variables 2025-06-16 22:38:54 +02:00
63a86a7090 Edit/Add bookmark modal 2025-06-16 22:32:43 +02:00
18b40ed485 Tables, template reflowing, docstrings, better index page 2025-06-15 22:07:02 +02:00
fc0c668ba1 Sections 2025-06-13 16:55:56 +02:00
fe360a1c7a Inputs like having a dropshadow too 2025-06-10 22:21:14 +02:00
231620ca3b Use palette colours 2025-06-10 22:19:23 +02:00
925aeba18e Lots of input styling and some argonaut colours 2025-06-10 22:16:11 +02:00
3a566f60ca Support HEAD on root route, for monitoring etc 2025-06-10 16:04:33 +02:00
5adbb8dce0 Default sizes for H1 per specification 2025-06-09 22:08:52 +02:00
284768d9d9 Lighter muted 2025-06-06 17:18:33 +02:00
1229680abe Namespace certain types of variables 2025-06-06 15:36:53 +02:00
4dc46ba3c7 Muted text color and some structuring 2025-06-06 15:28:06 +02:00
3e917efe37 Reworked image cards, added footer layout to digui 2025-05-24 22:38:14 +02:00
343ac01086 Fix for property after refactoring the model 2025-05-13 22:18:41 +02:00
d8c8d87568 Update bookmark and autofill certain fields 2025-05-13 19:01:13 +02:00
22e73bc991 Explicit button colours for these themes 2025-05-13 18:37:05 +02:00
f750a547d3 Sort bookmarks correctly on load 2025-05-12 22:58:32 +02:00
d96bde9bb1 Tags/chips 2025-05-12 22:50:56 +02:00
70979b3350 Support for filtering on tags 2025-05-12 16:13:28 +02:00
1836eedfe8 Better loading indicator 2025-05-11 09:19:53 +02:00
5efc12e698 blip-preventer 2025-05-11 09:19:51 +02:00
eb8e764a61 Card layouting, introduced Fontawesome icons 2025-05-09 23:13:39 +02:00
d9b1d99e32 20240331: just use ruff, and document the other deps 2025-05-09 20:44:50 +02:00
e7021cdf3d silo improvements, fix for nebula 2025-05-09 17:03:08 +02:00
45d8743b65 Moved themes to digui 2025-05-09 16:35:39 +02:00
4d5aae7881 Removed light/dark modes, introduced list of themes 2025-05-09 15:34:29 +02:00
3f5d43b0fa Theming trials 2025-05-09 14:58:42 +02:00
d9e8ca76fe Initial theme toggler 2025-05-08 19:13:17 +02:00
faae900b06 Configure for server 2025-05-07 16:47:27 +02:00
0f7e280bb3 Show bookmarks in a grid 2025-05-06 23:15:09 +02:00
d28228fc03 Better (semantic) HTML structure, load indicator in nav 2025-05-06 16:59:58 +02:00
5d71250408 Toggle between showing the bookmarks and a list of the tags 2025-05-06 16:21:56 +02:00
d073bc079a Started fleshing out digui 2025-05-06 15:57:27 +02:00
72b131c77f Better sort buttons 2025-05-06 15:40:58 +02:00
5943265687 Ignore PyCharm config 2025-05-06 14:49:18 +02:00
3642753266 Only fetch new bookmarks and tags when cache is not up-to-date 2025-05-06 14:44:25 +02:00
127284716e Also order z-a 2025-05-06 08:02:53 +02:00
40a0f773c4 Navigation stub, sortable bookmarks 2025-05-05 21:55:28 +02:00
b364f865b1 Show and filter/search bookmarks 2025-05-05 20:08:34 +02:00
324c77f985 Refactoring to simpler html with alpine.js 2025-05-04 22:39:37 +02:00
9b11ae65c3 Moved templates and static into the project dir 2025-05-04 21:40:31 +02:00
85639b128f Public tags, cleanups, config for templates 2025-05-04 21:38:58 +02:00
3369ee3b89 Tabula rasa rewrite with SQLModel 2025-05-04 19:05:54 +02:00
3a17b5aa63 More refactoring for sqlalchemy 2025-05-04 14:25:52 +02:00
bf36d726ac Merge branch 'fastapi' of github.com:aquatix/digimarks into fastapi 2025-05-04 13:18:12 +02:00
8e8d3bfcdc Better docstring 2025-05-04 13:18:06 +02:00
d89e8be72e Formatting fixes 2025-04-29 21:18:19 +02:00
e4d303a72b More refactoring 2025-02-02 22:06:23 +01:00
cfcca3e161 Use preferred name of the library 2024-08-08 17:31:29 +02:00
533e21141f More refactoring 2024-08-08 17:30:15 +02:00
c695972a94 Stubs for the rewrite 2024-08-08 15:00:09 +02:00
8968b47ddd Check on and fixed imports with ruff 2024-03-31 21:31:06 +02:00
7cbde4cc37 Use async httpx instead of requests to do external calls 2024-03-31 21:30:28 +02:00
29c3ccca59 Use async httpx instead of requests to do external calls 2024-03-31 21:23:36 +02:00
71b0543771 Introducing pytest 2024-03-31 21:23:12 +02:00
374db18181 Better naming, docstrings and more 2024-02-11 21:25:18 +01:00
0351760d3f Ruff config and such 2024-02-11 21:24:46 +01:00
a3cdccdb8a More refactoring 2023-12-12 22:46:19 +01:00
65af6b5762 Unnecessary exclude 2023-12-12 22:44:44 +01:00
81825379cb Docstring fixes according to ruff 2023-11-02 13:26:58 +01:00
f6f129d67c Refactoring to fastapi, reformatting with ruff 2023-10-30 21:51:55 +01:00
30bd835e41 Refactoring to FastAPI 2023-08-01 22:42:24 +02:00
7e397f9d2b Refactoring to FastAPI and SQLAlchemy 2023-07-30 21:19:51 +02:00
96e7ef16d4 New project structure, in line with modern Python projects 2023-07-29 16:05:05 +02:00
0b49186559 First work on refactoring to fastapi 2022-08-04 13:27:38 +02:00
3092f83c8b Serve the search JS with the correct mimetype 2022-07-23 11:10:55 +02:00
a27787e956 Dependency updates 2022-07-23 11:10:50 +02:00
21800911db Security: lxml; more updates 2022-07-13 18:45:34 +02:00
20a3d9f838 Latest everything 2022-04-23 09:09:50 +02:00
0ab3bd2263 Security: lxml; more updates 2021-12-13 21:28:43 +01:00
32b074b859 Security: urllib3; more updates 2021-07-06 15:10:18 +02:00
5789bbe006 Latest everything 2021-03-25 10:16:17 +01:00
2ef7358ac7 Jinja2 security fix; new-style pip-tools requirements 2021-02-02 08:51:18 +01:00
d1e590390c lxml security update and more 2020-12-02 09:19:05 +01:00
7a1bc11004 Replaced deprecated AtomFeed by feedgen; some dep updates 2020-07-28 14:52:44 +02:00
315c664fcc Document the RapidAPI (MashApe) key for favicons 2020-05-06 14:04:33 +02:00
db5944cec4 Werkzeug 1.0 has deprecated the AtomFeed 2020-05-06 14:02:09 +02:00
Michiel Scholten
becb734d17 Merge pull request #19 from mnishig/doc-MASHAPE_API_KEY
Add description for 'MASHAPE_API_KEY'
2020-05-06 13:56:57 +02:00
64ee0856c5 (Security) bumps 2020-05-05 19:59:27 +02:00
Masahide Nishihama
6c2be3070e add description 'MASHAPE_API_KEY' 2020-04-28 10:30:52 +09:00
426c2eda68 RapidAPI all the things 2020-04-16 13:34:10 +02:00
b0e53d4a85 Back to a version that actually works 2020-03-20 20:47:58 +01:00
1f69d9e53f Lots of updates 2020-03-20 20:44:28 +01:00
Michiel Scholten
fc27d9f186 Merge pull request #17 from aquatix/snyk-fix-e8cd4803e12d5fe482405c22b3c5b385
[Snyk] Security upgrade urllib3 from 1.25.3 to 1.25.8
2020-03-07 09:22:34 +01:00
snyk-bot
6341b384bf fix: requirements.txt to reduce vulnerabilities
The following vulnerabilities are fixed by pinning transitive dependencies:
- https://snyk.io/vuln/SNYK-PYTHON-URLLIB3-559452
2020-03-07 07:48:53 +00:00
f698ebfe18 Security updates for Flask&Werkzeug, more version bumps 2019-08-12 19:27:49 +02:00
9f736ffe82 Move to RapidAPI url for favicons API 2019-07-27 10:45:04 +02:00
e1a45a21b5 urllib3 security update, more version bumps 2019-05-04 10:41:49 +02:00
9492d26511 Security update for urllib3, more bumps 2019-04-19 21:08:49 +02:00
f7762ebc7b Updates, amongst which a security update for Jinja2 2019-04-10 12:07:14 +02:00
1c4bc61494 Minor version bumps 2019-02-28 12:48:44 +01:00
2a87e0aa1f Dependency version bumps 2019-02-11 13:23:03 +01:00
0a24c7d170 (Security) updates 2018-10-18 20:56:51 +02:00
2615089acd Updates 2018-09-11 13:10:48 +02:00
14bf22f3e5 Updated to 1.0.0 release of MaterializeCSS 2018-09-11 13:10:06 +02:00
e55fb7bd5f Updated some urls 2018-08-29 16:36:23 +02:00
4e1261857d Updates 2018-08-13 10:32:16 +02:00
f9861c1491 Dependency bumps 2018-07-10 22:12:57 +02:00
836077ad12 New MaterializeCSS release 2018-07-10 22:07:45 +02:00
0913ffca2c Minor version bumps 2018-06-13 09:40:13 +02:00
834f95c34b Do not import reserved name 2018-05-25 16:21:53 +02:00
c82e3a02d4 ESLint hint about 'M' being global (MaterializeCSS) 2018-05-25 16:17:29 +02:00
7874002fef pypi downloads is not available any longer 2018-05-25 16:06:36 +02:00
6db0355cc7 Move badge to the same line as the others 2018-05-25 13:10:54 +02:00
Michiel Scholten
0495296f0f Merge pull request #15 from codacy-badger/codacy-badge
Add a Codacy badge to README.rst
2018-05-25 13:09:10 +02:00
The Codacy Badger
a5d225fb56 Add Codacy badge 2018-05-25 10:15:26 +00:00
f6401a3e9f Problems found by Codacy 2018-05-25 12:11:17 +02:00
46e7fc9899 Updates 2018-05-25 12:10:07 +02:00
f6befd0700 Note about themed links 2018-05-22 09:54:09 +02:00
95ff9c01ca Properly theme regular links 2018-05-15 11:51:46 +02:00
b36cd8db6b Also strip enter characters 2018-05-15 11:44:53 +02:00
2b19b770dc Regular links (in tables) should be themed too 2018-05-15 10:58:42 +02:00
6dc74c7102 Autocomplete has been implemented; fix globbering of <systemkey> 2018-05-09 16:07:10 +02:00
ac7906d781 New version of peewee ORM 2018-05-09 16:03:19 +02:00
5d2329ff90 Release candidate of MaterializeCSS 2018-05-09 16:01:37 +02:00
a29e14b7a7 Upgrades 2018-05-03 09:19:33 +02:00
5e4a35527b Fix for bytes/str discrepancy 2018-04-12 15:58:32 +02:00
55609aa353 Dependency updates 2018-04-12 15:52:58 +02:00
0d86c2608a MaterializeCSS update 2018-04-12 15:33:35 +02:00
dd1e3a19ff Run app on 0.0.0.0 instead of 127.0.0.1 (e.g., in termux) 2018-03-30 16:01:50 +02:00
9694ca566b Nicer fallback favicon image 2018-03-30 15:40:29 +02:00
17caef1aed Fix 'home button' link in digimarks title 2018-03-30 09:41:08 +02:00
52a01794f6 Strip unnecessary whitespace 2018-03-30 09:40:53 +02:00
8148a79d28 Fix for narrow favicons if title contains long words 2018-03-30 09:40:19 +02:00
d6e74ff328 'Home' link now takes view type into account 2018-03-28 15:33:51 +02:00
540fd6ba91 Find button now is icon, form size tweaking 2018-03-27 11:15:30 +02:00
3becf27b42 Styling fixes: nice buttons for edit/delete bookmark, button for
list/cards
2018-03-27 09:59:39 +02:00
fe990ecf63 Implemented listing of bookmarks 2018-03-27 09:17:48 +02:00
75080579fd Fix some url lookups 2018-03-26 16:18:59 +02:00
94eb42a882 Note removal of jQuery as a Removed thing 2018-03-26 16:18:52 +02:00
d501c6b4db De-jQuery'd a forgetten piece of Javascript 2018-03-26 15:53:34 +02:00
11e159db8d Re-enable the automatic redirect; some dark styling 2018-03-26 15:42:14 +02:00
6516c4af1d Moved autocompletion items to seperate javascript call 2018-03-26 15:33:07 +02:00
63636d3355 More goodness 2018-03-26 15:18:39 +02:00
52cc93d4c3 Switch default theme to the fresher green variant 2018-03-26 15:11:24 +02:00
6f9d44ce86 Fixed API endpoint for single bookmark lookup 2018-03-26 15:11:07 +02:00
718b39a267 Reminder 2018-03-26 15:00:09 +02:00
bfc4fb702a Some more security against referrers from digimarks 2018-03-26 14:59:49 +02:00
7e2f2f6f6e Support for redirecting a bookmark to its url 2018-03-26 14:58:21 +02:00
29c8c875be More room for delete button (icon stays shown), red colour 2018-03-26 14:25:48 +02:00
0f0caed748 Also theme (autocomplete) search inputfield 2018-03-25 22:22:23 +02:00
971590196e Label, icon and underline are now themed in edit bookmark form 2018-03-25 22:08:37 +02:00
3653b5e424 Disable browser autocomplete for forms, as it interferes 2018-03-25 16:23:56 +02:00
330523ba3f Submit search on autocomplete, limit results in autocomplete 2018-03-24 21:58:53 +01:00
199b641a38 Removed limit for feeds 2018-03-24 21:39:53 +01:00
c0c8e35246 Fix for case where a bookmark has no title 2018-03-24 21:33:15 +01:00
6de9ba2642 Lots of styling fixes, name clash resolutions 2018-03-24 21:31:17 +01:00
e48f2c98c3 Search API endpoint 2018-03-24 21:06:30 +01:00
554f651ec8 Filter autocomplete 2018-03-24 20:48:41 +01:00
6def8d60a5 Buttons and the intention to make collections more accessible 2018-03-24 11:37:35 +01:00
1d531989bb Main nav items to buttons, theme support for buttons 2018-03-24 11:17:26 +01:00
fc2712f5e3 Fix for Python 3 and encoding 2018-03-23 15:10:13 +01:00
76ef520815 Favicon changes and fixes 2018-03-23 14:40:37 +01:00
ea4a7bdcd7 Crash fix 2018-03-23 14:27:47 +01:00
aee0515eae Support for fallback icon, clear empty icon; don't re-download existing 2018-03-23 14:21:15 +01:00
3835497918 Really skip 2018-03-23 13:32:59 +01:00
d7b2c28c96 Case where no icon is supplied 2018-03-23 13:17:21 +01:00
fac3a4f747 Catch case where favicon property is empty 2018-03-23 13:11:06 +01:00
0548f35b39 Endpoint to download all missing icons 2018-03-23 13:03:08 +01:00
8372d6e2a5 Implemented realfavicongenerator API through Mashape 2018-03-23 12:58:27 +01:00
cd2911e7f0 Better description, added missing bs4 dependency 2018-03-23 11:17:16 +01:00
127d99b1e0 We're getting somewhere 2018-03-23 11:08:39 +01:00
bb4f81262e Add icons to the top navbar too 2018-03-23 11:06:02 +01:00
3c697d3162 Find text in url and note too, apart from title 2018-03-23 10:52:09 +01:00
fefb317ddf Use a custom User Agent string to prevent server blockage 2018-03-23 10:44:23 +01:00
37ebdda933 Reference fix 2018-03-17 18:41:27 +01:00
3516fbfbb2 Serialisation fix 2018-03-17 18:41:24 +01:00
c4f921ac68 Refactored the public tag overviews 2018-03-17 18:34:58 +01:00
fec54c51f7 Show tags and edit buttons when on a tag overview page 2018-03-17 18:19:47 +01:00
9f467f8a09 Some more readability fixes 2018-03-17 16:21:31 +01:00
cac31e40c9 Cleanup and better API endpoint 2018-03-17 16:18:53 +01:00
1e14163d42 Lots of indentation and other readability & lintian fixes 2018-03-17 16:14:52 +01:00
88eee28b88 Latest changes 2018-03-17 15:50:49 +01:00
45c95f5f17 Properly serialise bookmarks 2018-03-17 15:46:54 +01:00
9539c48b7b Split fetching of bookmarks into generic and view-specific functions 2018-03-17 15:42:39 +01:00
46aa230fae Support read-only/editable and show/hide tags per view 2018-03-17 15:01:44 +01:00
c6089f1caa Moved tags to the metadata card reveal 2018-03-17 14:56:38 +01:00
45d44d3bdf Moved the cards to their own template 2018-03-17 14:38:49 +01:00
e4662351c2 Non-validating input field 2018-03-17 13:58:58 +01:00
62f3ddf654 Fix for dark-on-dark text in filter input 2018-03-17 13:58:56 +01:00
072ec6c426 Updated dependencies 2018-03-17 13:49:20 +01:00
5055947351 Indentation fix 2018-03-09 15:25:35 +01:00
5f131b15ef Bye bye flask-peewee 2018-03-02 21:10:24 +01:00
bd808a9e1d Reverting to older peewee's, as peewee-flask is hosed 2018-03-02 20:48:23 +01:00
3b019e4368 Updated to the latest MaterializeCSS alpha 2018-03-02 20:24:32 +01:00
3af1239326 Updated dependencies 2018-03-02 20:21:28 +01:00
e8ea948566 Improved ignores 2018-01-12 12:23:58 +01:00
1c2090f300 Cleanups, clarification 2018-01-12 12:21:09 +01:00
cdfd0341f0 Non-validating text inputs 2018-01-03 14:51:54 +01:00
01d6525861 Fixed dark text on dark background 2018-01-03 14:51:35 +01:00
c70e53a658 MaterializeCSS 1.0-alpha-3 2018-01-03 14:40:08 +01:00
dc76a592e0 Updated dependencies 2018-01-03 14:40:03 +01:00
a4aae2d6c4 Note about jQuery being booted 2017-12-25 13:43:24 +01:00
389e63bdbb Removed jQuery code, replaced with pure JavaScript 2017-12-25 11:39:59 +01:00
6ba4803ed2 jQuery -> new MaterializeCSS way 2017-12-24 20:58:15 +01:00
71756f9ea0 Make the Hamburger great again 2017-12-24 14:28:06 +01:00
67635c199a Fixed favicon type sniffing and writing to file in Python 3 2017-12-20 21:01:53 +01:00
7b2a861652 Ch-ch-ch-changes 2017-12-20 20:46:14 +01:00
53887c8ece Also accept http202; try fixing encoding issue 2017-12-19 22:03:51 +01:00
2cc2d382c1 Merge branch 'master' of github.com:aquatix/digimarks 2017-12-19 22:03:16 +01:00
fa033452f1 Fixed checkboxes 2017-12-19 14:58:45 +01:00
c14e24430b Initialise collapsible block so it actually unfolds/folds 2017-12-19 14:55:36 +01:00
cca4504fd7 Updates after Python 3 and latest pip-tools 2017-12-17 15:15:56 +01:00
7f6dc3f3df Python 2+3 2017-12-17 13:52:00 +01:00
4170a7818b Needed for importing settings.py 2017-12-17 13:44:41 +01:00
Michiel Scholten
48e77f551d Merge pull request #13 from aquatix/materialize1.0
Materialize1.0 and Python3
2017-12-17 11:51:24 +01:00
e35d9952bd Not needed with newer pip-tools 2017-12-13 11:47:38 +01:00
6933357a61 Python 3 compatibility fix 2017-12-13 11:47:36 +01:00
9578ee624b Silence 2017-12-13 11:37:26 +01:00
ded047d749 Fixes for MaterializeCSS 1.0.0 changes (based on alpha 2) 2017-12-13 11:37:01 +01:00
57226b88f5 Updates 2017-12-13 11:36:58 +01:00
de5d4d30ef Updated dependencies 2017-11-21 16:18:16 +01:00
60f5a48d89 Black amoled theme 2017-11-21 16:15:50 +01:00
47f6e36e4b Fixed order of imports 2017-11-03 16:55:40 +01:00
5402dfc320 Some updates 2017-11-03 16:53:15 +01:00
90f1322c48 Development requirements 2017-11-03 16:52:48 +01:00
d1aef0284f MaterializeCSS update 2017-08-27 13:49:46 +02:00
d7a5bd921f Recent updates 2017-08-15 10:46:27 +02:00
b85ee43cc7 Better error message on 301, with hint on using the button 2017-08-14 14:24:12 +02:00
8077499eae Something with name clashes 2017-08-14 13:41:58 +02:00
ab06f7e583 Recent changes 2017-07-31 16:34:34 +02:00
f309d4acf2 Make 404 page theme aware (falls back to default) 2017-07-31 16:33:07 +02:00
88a9806d44 Fix clickability of checkboxes 2017-07-31 16:29:27 +02:00
6f4d270858 Don't override the colour of form widgets 2017-07-28 13:49:24 +02:00
c354613b60 Changes 2017-07-26 08:25:47 +02:00
383c77ee8b Fixed padding for card reveal 2017-07-23 14:30:50 +02:00
0e77afd000 Modified card padding so it fits more content 2017-07-23 14:28:20 +02:00
f617fb8190 Moved chip link colour to theme 2017-07-23 13:34:34 +02:00
34af0e9ab7 Added lightblue theme and changed link colour 2017-07-23 13:11:27 +02:00
7f866658e3 (mobile) browser chrome theming 2017-07-23 08:30:26 +02:00
abf9141019 Release 1.1.0 2017-07-22 22:09:42 +02:00
Michiel Scholten
8ca54bf364 Merge pull request #7 from aquatix/theme
Theme
2017-07-22 21:55:03 +02:00
0be05d07e9 Default theme is now a constant; linting fixes 2017-07-22 21:51:07 +02:00
a66360a0e3 Note on the added DB field 2017-07-22 21:44:06 +02:00
1545c17472 Moved label colours to template/theme; added freshgreen and dark 2017-07-22 21:42:12 +02:00
dd0a9d8283 Typo 2017-07-22 21:16:33 +02:00
652e3a89af Theme support, with default 'green' 2017-07-22 21:11:09 +02:00
133a139b79 Merge branch 'master' into theme 2017-07-22 20:36:05 +02:00
b556978640 Updated MaterializeCSS 2017-07-22 19:39:52 +02:00
8bb9e11088 More changes 2017-07-05 12:45:09 +02:00
85e94b71e5 Some reformatting according to keepachangelog.com 2017-07-05 12:32:00 +02:00
a895774001 Changes 2017-07-03 15:08:50 +02:00
7598ba6a61 Updated requirements 2017-07-03 15:07:45 +02:00
c5dcf579d3 Updated MaterializeCSS 2017-07-03 15:07:19 +02:00
590095659e 20170519: default to 'green' 2017-06-15 08:41:01 +02:00
d5d71b4f51 Cache User settings 2017-04-28 16:33:42 +02:00
7f2da49d26 Update Materialize to 0.98.2 2017-04-26 15:22:20 +02:00
0f0e4e03c3 Small requirements updates 2017-04-26 15:20:23 +02:00
044f507aa2 Started with theming support 2017-04-15 09:01:49 +02:00
b0da3a4454 Updated Materialize and jQuery 2017-03-22 12:34:33 +01:00
17e1db12bf Minor requirements updates 2017-03-22 12:32:57 +01:00
480f27797f requirements updates 2017-03-11 21:32:32 +01:00
1729ecd540 Add pkg-resources==0.0.0 as otherwise venv will break 2017-03-01 21:06:59 +01:00
d54b26dafb Updated requirements 2017-03-01 21:03:16 +01:00
2c76ffaf12 Cache buster to force loading of the latest styling 2017-02-04 10:28:07 +01:00
e54a033984 Indentation and styling fixes, comments 2017-02-04 10:27:05 +01:00
4e0d89c25e Better size for the hamburger icon 2017-02-04 10:24:39 +01:00
30e4f74ed9 Fix for misalignment of hamburger icon 2017-02-04 09:20:42 +01:00
1beadb676c Updated MaterializeCSS 2017-02-04 09:20:40 +01:00
eea63398ba Try-out of scrolling tags at 2016-11-07, did not work out yet 2017-02-04 09:18:32 +01:00
d5021fd7c4 Try-out of scrolling tags at 2016-11-07, did not work out yet 2017-02-04 09:18:10 +01:00
c03d0c2458 Recompiled requirements, lowercase package names 2017-01-24 09:48:09 +01:00
ff08ea4930 Explicit requirements through pip-compile from pip-tools 2017-01-16 12:01:08 +01:00
2e9457ee1a Catch case where favicon could not be saved 2017-01-16 11:38:43 +01:00
63dd636c25 Show 404 page if bookmark is not found when editing 2017-01-16 11:22:56 +01:00
412b4a93c7 Fixed indention 2016-12-29 20:59:45 +01:00
abdc11361a Added missing itertools function 2016-12-29 20:59:04 +01:00
Michiel Scholten
7a98de4b3f Merge pull request #3 from jelmer/optional-venv
Make running in a virtualenv optional.
2016-12-29 20:49:16 +01:00
Michiel Scholten
59f5365edc Merge pull request #2 from jelmer/utilkit
Drop dependency on utilkit.
2016-12-29 20:48:42 +01:00
Michiel Scholten
b0aedcd6e4 Merge pull request #1 from jelmer/unique-everseen
Avoid dependency on more_itertools.
2016-12-29 20:47:32 +01:00
6b61e43c5d Upcoming changes 2016-12-29 17:01:55 +01:00
edc1a86b75 Rolled latest changes into a version 1.0.0 release 2016-12-29 16:26:24 +01:00
Jelmer Vernooij
570af2d62e Make running in a virtualenv optional. 2016-11-28 23:34:28 +00:00
Jelmer Vernooij
8a19df2741 Drop dependency on utilkit.
utilkit is only used for one one-liner function.
2016-11-28 23:30:51 +00:00
Jelmer Vernooij
9e6d67a0ff Avoid dependency on more_itertools.
Instead, use version of unique_everseen as seen in itertools
documentation.
2016-11-28 23:25:50 +00:00
698a417bac Removed workaround for fix released upstream 2016-11-07 21:29:49 +01:00
4cdea81f16 Updated MaterializeCSS and jQuery 2016-10-31 16:11:53 +01:00
0e659a4f16 Fixes for public cards 2016-09-23 13:50:49 +02:00
2cdb0ebccb Put the tag selection in a collapsible element to prevent clutter 2016-09-14 12:50:37 +02:00
9e2bdac9ee Open in new tab/window, prevent
http://davidebove.com/blog/2016/05/05/target_blank-the-vulnerability-in-your-browser/
2016-09-13 09:53:20 +02:00
5288c9fe9c Clear error message on connection error 2016-09-05 15:21:42 +02:00
0e60f8f3cb Fix for the error catching 2016-09-05 15:18:47 +02:00
5fd8db8f43 Catch connection timeouts and such 2016-09-05 15:14:11 +02:00
0894770f12 Show url domain name along with 'no title' for items without title 2016-08-29 13:49:29 +02:00
197cb776bf Helpful texts 2016-08-26 16:54:56 +02:00
c433c2c1e8 If redirect detected, button with final uri 2016-08-26 16:45:56 +02:00
b5d1377109 Changes, todos 2016-08-19 14:13:02 +02:00
85d4466de5 Filter on bookmarks with a note 2016-08-19 14:10:21 +02:00
7609becf00 More logical order of fields, tags with tags 2016-08-19 13:41:20 +02:00
a6d6ce5c13 Show stats on notes 2016-08-17 22:15:27 +02:00
e448f69f38 Count notes 2016-08-17 16:48:06 +02:00
17c36ab8fe Note about notes 2016-08-16 21:58:49 +02:00
9a15a9946c Different note icon, was same as title 2016-08-16 21:44:31 +02:00
40b7937876 Better alignment of icons in the cards 2016-08-16 21:42:10 +02:00
38155cafaa Actually show the note in title, but truncate to be safe 2016-08-16 21:34:03 +02:00
b12974fc31 Workaround for when an existing bookmark has a null note 2016-08-16 21:29:01 +02:00
d39d793460 Support for notes 2016-08-16 21:24:40 +02:00
7c24057d35 On tag filter, also use the cached tags 2016-08-15 16:03:54 +02:00
5fde397c4a Sort reverse chronologically on tag pages too 2016-08-15 16:02:12 +02:00
62117699f8 Don't show empty mobile menu on welcome page, add Home button 2016-08-15 14:34:31 +02:00
14052148f7 Prettify the mobile nav with waves and icons 2016-08-15 14:22:35 +02:00
fe44928816 Delete link/button in card is red now 2016-08-15 14:11:23 +02:00
e5c3824034 Fix for card reveal not working in Firefox 2016-08-15 10:51:59 +02:00
d8850d33f4 Test case for card not working in Firefox 2016-08-14 20:54:07 +02:00
2117c451b2 Filter text fixes 2016-08-14 14:43:11 +02:00
9abc17d517 Star/broken status filter 2016-08-14 14:39:45 +02:00
978c34fcf3 Filter on 'star' status, 'broken' status (non-http-200-OK) 2016-08-14 14:38:20 +02:00
7fdc0706bf Changes 2016-08-14 14:06:47 +02:00
c460f713ca Stats on bookmarks without http status 200 OK 2016-08-14 13:19:26 +02:00
2de7472e27 'Delete' button in action cardback 2016-08-13 20:50:24 +02:00
a6082caf0d Undo delete bookmark. Enables unescaped rendering of message 2016-08-13 20:46:09 +02:00
ac0ac849cb Whitespace good 2016-08-13 20:34:33 +02:00
390cc0137a Cleaner stats table, now with starred and deleted too 2016-08-13 20:33:17 +02:00
2e031db2a7 Stats on total public tag pages 2016-08-13 16:23:38 +02:00
1b513e98b2 Better text 2016-08-13 16:20:00 +02:00
bb258a2a68 Better total stats, now also info about nr bookmarks 2016-08-13 16:18:51 +02:00
72ad3c0d04 Total amount of tags, number of bookmarks per tag 2016-08-13 15:35:36 +02:00
78bed962d0 Delete public tag, create link 2016-08-13 14:54:11 +02:00
c70ac6d781 Way better tag overview page, with public links 2016-08-13 14:42:15 +02:00
53b96adc79 Show pointer mousepointer on tags in add/edit bookmark 2016-08-13 14:18:23 +02:00
4e86faf2aa Updated public bookmarks with new look 2016-08-13 14:14:07 +02:00
4b17721fb0 Removed favicon from card reveal; better layout in reveal 2016-08-13 14:07:17 +02:00
870418712f Merge branch 'master' of github.com:aquatix/digimarks 2016-08-13 14:03:40 +02:00
068c6b8d94 Prevent duplicate form submission; indentation fixes 2016-08-13 13:48:54 +02:00
60734c55ce Merge branch 'master' of github.com:aquatix/digimarks 2016-08-12 08:03:40 +02:00
e5c5e5f5a9 Don't have content run out of the bottom of the card 2016-08-12 08:03:23 +02:00
16ed20bd9b Better layout of Star and Warning icons 2016-08-05 20:11:05 +02:00
5840baa657 Determine of favicon is ico, decompress gzip'ed content 2016-08-05 13:30:00 +02:00
0271a9339d Use different favicon service, 60x60 icons, cleaner cards, refresh
favicons admin endpoint
2016-08-05 12:36:10 +02:00
8a28d841e3 Merge branch 'master' of github.com:aquatix/digimarks 2016-08-05 11:54:02 +02:00
04447390fc Tweaked FAB placement 2016-08-05 11:53:41 +02:00
4ee925de45 Use referrer when url encode doesn't work because of security scope 2016-08-03 21:34:44 +02:00
271f2fa4a0 Initialise with 'url' url parameter if exists, enabling bookmarklet 2016-08-03 14:14:59 +02:00
cfbab3f98b Fix for json response of single bookmark 2016-08-02 19:30:08 +02:00
62145ecfe2 Updated public tag page card design with current cards 2016-08-02 14:47:08 +02:00
42e4021ef5 Fix for link to public tag page (server part was missing) 2016-08-02 14:35:35 +02:00
e7125c07e9 Changes 2016-08-02 14:27:01 +02:00
bcc28c6a8e Feed link 2016-08-02 14:25:52 +02:00
fab1554c70 Atom feed for public tag 2016-08-02 14:16:35 +02:00
05d636fc08 json view of public tag page 2016-08-02 14:05:17 +02:00
45 changed files with 2840 additions and 1012 deletions

4
.codacy.yaml Normal file
View File

@@ -0,0 +1,4 @@
---
exclude_paths:
- "example_config/**"
- "docs/source/**"

13
.gitignore vendored
View File

@@ -87,3 +87,16 @@ ENV/
# Rope project settings
.ropeproject
# JetBrains PyCharm/Idea
.idea
# vim
*.swp
# digimarks
static/favicons
tags
*.db_*
*.db
settings.py

View File

@@ -1,18 +1,140 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## TODO
- Sorting of bookmarks
- Sort by title
- Sort by date
- Logging of actions
- json/rss views of public tag pages
- Add new way of authentication and editing bookmark collections:
https://github.com/aquatix/digimarks/issues/8 and https://github.com/aquatix/digimarks/issues/9
- Change adding tags to use the MaterializeCSS tags: https://materializecss.com/chips.html
- Do calls to the API endpoint of an existing bookmark when editing properties
(for example to update tags, title and such, also to already suggest title)
- Look into compatibility with del.icio.us, so we can make use of existing browser integration
- Add unit tests
## [Unreleased]
### Added
- Settings through Pydantic Settings
### Changed
- Moved from Flask to FastAPI
- Moved from Peewee ORM to SQLAlchemy
### Removed
- Jinja2 templates
### Fixed
## [1.2.0] - Flask is Fine (2023-07-30)
### Added
- 'lightblue' theme
- 'black amoled' theme
- Python 3 compatibility (tested with Python 3.5 and 3.6)
- Accept 'HTTP 202' responses as 'OK'
- API: Added endpoint for 'bookmarks', returning JSON
- Top navigation items now have icons too, like the sidebar in mobile view
- Download favicons from RealFaviconGenerator: https://realfavicongenerator.net/api/download_website_favicon
- Added `/<systemkey>/findmissingfavicons` endpoint to fill in the blanks in the favicon collection
- Added fallback favicon image (semitransparent digimarks 'M' logo) for bookmarks without a favicon. No more broken images.
- Added theme support for buttons.
- Autocompletion in bookmark search field
- API: search endpoint
- Redirect endpoint for a bookmark, de-referring to its url (`/r/<userkey>/<urlhash>`)
### Changed
- Fixed theming of browser chrome in mobile browsers
- Changed link colour of 'dark' theme from blue to orange
- Modified card padding so it fits more content
- Fixed ability to select a checkbox in the add/edit bookmark form
- Made the 404 page theme aware, falls back to default (green) theme
- Fixed admin pages not working anymore due to `settings` object name clash
- On Add/Edit bookmark and encountering a 301, show a better message about automatically changing the URL with the provided button
- Switched to 1.0 (alpha 4) version of MaterializeCSS
- jQuery-b-gone: changed all jQuery code to regular JavaScript code/MaterializeCSS framework
- Fixed colour of filter text in search field for dark themes
- Unified rendering of 'private' and 'public' views of bookmark cards
- Code cleanups, readability fixes
- digimarks User Agent string to correctly identify ourselves, also preventing servers blocking 'bots'
- Text search now also finds matches in the 'note' and 'url' of a bookmark, aside from its title
- Main navigation items ('tags' and 'add bookmark') are now buttons, better visible as action items.
- Removed item limit for feeds
- Form fields are now themed
- Disabled browser autocomplete for forms, which generally interfered with editing bookmarks (e.g., tag field) and the search field,
which has its own autocomplete now
- Changed default theme to the 'freshgreen' variant
- Links are now themed in the proper colours everywhere
### Removed
- Removed dependency on jQuery
## [1.1.0] - 2017-07-22
### Added
- Show 404 page if bookmark is not found when editing
- Cache buster to force loading of the latest styling
- Theming support, default is 'green'
- Themes need an extra `theme` field in the User table
- Added 'freshgreen' and 'dark' themes
### Changed
- Make running in a virtualenv optional
- Fix for misalignment and size of hamburger icon
- Updated Python (pip) dependencies
- Updated MaterializeCSS and jQuery
### Removed
- Removed dependency on more_itertools
- Removed dependency on utilkit
## [1.0.0] - 2016-12-29
- json view of public tag pages, returns all items
- feed (rss/atom) view of public tag pages, returns latest 15
- feed link on public tag page
- Support for bookmarklets
- UI tweaks
- Redesigned cards with bigger favicon. Looks cleaner
- Different favicon service with 60x60px icons
- Prevent duplicate form submission on add/edit bookmark
- Delete bookmark from bookmark card
- Undo delete link in "Bookmark has been deleted" message
- Delete public tag page
- On tags overview page:
- Show which tags have public pages, with link
- How many bookmarks each tag has
- Statistics on:
- total tags
- number of public tag pages
- total number of bookmarks
- number of starred bookmarks
- number of bookmarks with a non-OK http status
- number of deleted bookmarks
- Filter on 'star' status, 'broken' status (non-http-200-OK)
- Bookmark can have a note now
- Note icon on card with text in title (desktop browser)
- Filter on bookmarks with a note
- Show url domain name along with 'no title' for items without title
- Catch connection timeouts and such
- Open in new tab/window, prevent
http://davidebove.com/blog/2016/05/05/target_blank-the-vulnerability-in-your-browser/
- Put the tag selection in a collapsible element to prevent clutter in edit window
- Updated MaterializeCSS and jQuery
## v0.2.0
2016-08-02
## [0.2.0] - 2016-08-02
- Favicon courtesy Freepik on flaticon.com
- Tag tags for easy adding of tags
@@ -23,9 +145,7 @@
- Option to strip parameters from url (like '?utm_source=social')
## v0.1.0
2016-07-26
## [0.1.0] - 2016-07-26
- Initial release
- Flask application with functionality to add users, add and edit bookmarks,

View File

@@ -1,7 +1,7 @@
digimarks
=========
|PyPI version| |PyPI downloads| |PyPI license| |Code health|
|PyPI version| |PyPI license| |Code health| |Codacy|
Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.
@@ -39,11 +39,35 @@ 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.
Url's are of the form https://marks.example.com/<userkey>/<action>
digimarks can also be run from the command line: ``uvicorn digimarks:app --reload``
Be sure to export/set the ``SECRETKEY`` environment variable before running, it's needed for some management URI's.
Run ``gunicorn -k uvicorn.workers.UvicornWorker`` for production. For an example of how to set up a server `see this article <https://www.slingacademy.com/article/deploying-fastapi-on-ubuntu-with-nginx-and-lets-encrypt/>`_ with configuration for nginx, uvicorn, systemd, security and such.
The RQ background worker can be run from the command line: ``rq worker --with-scheduler``
Url's are of the form https://hook.example.com/app/<appkey>/<triggerkey>
API documentation is auto-generated, and can be browsed at https://hook.example.com/docs
Bookmarklet
~~~~~~~~~~~
To easily save a link from your browser, open its bookmark manager and create a new bookmark with as url:
.. code-block:: javascript
javascript:location.href='http://marks.example.com/1234567890abcdef/add?url='+encodeURIComponent(location.href);
Creating a new user
-------------------
@@ -62,8 +86,9 @@ If you for whatever reason would lose this user key, just either look on the con
Server configuration
~~~~~~~~~~~~~~~~~~~~
* `vhost for Apache2.4`_
* `uwsgi.ini`_
* `systemd for digimarks API`_ which uses the `gunicorn config`_
* `nginx for digimarks API`_
* `more config`_
What's new?
@@ -79,18 +104,21 @@ Attributions
.. _digimarks: https://github.com/aquatix/digimarks
.. _webhook: https://en.wikipedia.org/wiki/Webhook
.. |PyPI version| image:: https://img.shields.io/pypi/v/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |PyPI downloads| image:: https://img.shields.io/pypi/dm/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |PyPI license| image:: https://img.shields.io/github/license/aquatix/digimarks.svg
:target: https://pypi.python.org/pypi/digimarks/
.. |Code health| image:: https://landscape.io/github/aquatix/digimarks/master/landscape.svg?style=flat
:target: https://landscape.io/github/aquatix/digimarks/master
:alt: Code Health
.. |Codacy| image:: https://api.codacy.com/project/badge/Grade/9a34319d917b43219a29e59e9ac75e3b
:alt: Codacy Badge
:target: https://app.codacy.com/app/aquatix/digimarks?utm_source=github.com&utm_medium=referral&utm_content=aquatix/digimarks&utm_campaign=badger
.. _hook settings: https://github.com/aquatix/digimarks/blob/master/example_config/examples.yaml
.. _vhost for Apache2.4: https://github.com/aquatix/digimarks/blob/master/example_config/apache_vhost.conf
.. _uwsgi.ini: https://github.com/aquatix/digimarks/blob/master/example_config/uwsgi.ini
.. _Changelog: https://github.com/aquatix/digimarks/blob/master/CHANGELOG.md
.. _Freepik: http://www.flaticon.com/free-icon/letter-m_2041
.. _systemd for digimarks API: https://github.com/aquatix/digimarks/blob/master/example_config/systemd/digimarks.service
.. _gunicorn config: https://github.com/aquatix/digimarks/blob/master/example_config/gunicorn_digimarks_conf.py
.. _more config: https://github.com/aquatix/digimarks/tree/master/example_config

View File

@@ -1,459 +0,0 @@
from __future__ import print_function
import datetime
import hashlib
import os
import sys
import requests
import shutil
import bs4
from more_itertools import unique_everseen
from urlparse import urlparse, urlunparse
from utilkit import datetimeutil
from flask import Flask, abort, redirect, render_template, request, url_for
from flask_peewee.db import Database
#from flask_peewee.utils import get_object_or_404
from peewee import * # noqa
try:
import settings
except ImportError:
print('Copy settings_example.py to settings.py and set the configuration to your own preferences')
sys.exit(1)
# app configuration
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__)
db = Database(app)
# set custom url for the app, for example '/bookmarks'
try:
app.config['APPLICATION_ROOT'] = settings.APPLICATION_ROOT
except AttributeError:
pass
# Cache the tags
all_tags = {}
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
class User(db.Model):
""" User account """
username = CharField()
key = CharField()
created_date = DateTimeField(default=datetime.datetime.now)
def generate_key(self):
""" Generate userkey """
self.key = os.urandom(24).encode('hex')
return self.key
class Bookmark(db.Model):
""" Bookmark instance, connected to User """
# Foreign key to User
userkey = CharField()
title = CharField(default='')
url = CharField()
#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_status = IntegerField(default=200)
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 fetch_image(self):
# url_hash = hashlib.md5(self.url).hexdigest()
# filename = 'bookmark-%s.png' % url_hash
# outfile = os.path.join(MEDIA_ROOT, filename)
# params = [PHANTOM, SCRIPT, self.url, outfile]
# exitcode = subprocess.call(params)
# if exitcode == 0:
# self.image = os.path.join(MEDIA_URL, filename)
def set_hash(self):
""" Generate hash """
self.url_hash = hashlib.md5(self.url).hexdigest()
def set_title_from_source(self):
""" Request the title by requesting the source url """
try:
result = requests.get(self.url)
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:
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 """
result = requests.head(self.url)
self.http_status = result.status_code
return self.http_status
def set_favicon(self):
""" Fetch favicon for the domain """
# http://codingclues.eu/2009/retrieve-the-favicon-for-any-url-thanks-to-google/
u = urlparse(self.url)
domain = u.netloc
filename = os.path.join(MEDIA_ROOT, 'favicons/' + domain + '.png')
# if file exists, don't re-download it
response = requests.get('http://www.google.com/s2/favicons?domain=' + domain, stream=True)
with open(filename, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
self.favicon = domain + '.png'
def set_tags(self, tags):
""" Set tags from `tags`, strip and sort them """
tags_split = tags.split(',')
tags_clean = clean_tags(tags_split)
self.tags = ','.join(tags_clean)
@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(',')
else:
return []
def to_dict(self):
result = {
'title': self.title,
'url': self.url,
'created': datetimeutil.datetime_to_string(self.created_date),
'url_hash': self.url_hash,
'tags': self.tags,
}
return result
class PublicTag(db.Model):
""" 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 = os.urandom(16).encode('hex')
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:
return all_tags[userkey]
except KeyError:
return []
@app.errorhandler(404)
def page_not_found(e):
return render_template('404.html', error=e), 404
@app.route('/')
def index():
""" Homepage, point visitors to project page """
return render_template('index.html')
@app.route('/<userkey>', methods=['GET', 'POST'])
@app.route('/<userkey>/sort/<sortmethod>', methods=['GET', 'POST'])
def bookmarks(userkey, sortmethod = None):
""" User homepage, list their bookmarks, optionally filtered and/or sorted """
#return object_list('bookmarks.html', Bookmark.select())
#user = User.select(key=userkey)
#if user:
# bookmarks = Bookmark.select(User=user)
# return render_template('bookmarks.html', bookmarks)
#else:
# abort(404)
message = request.args.get('message')
tags = get_cached_tags(userkey)
if request.method == 'POST':
filter_on = request.form['filter']
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.title.contains(filter_on),
Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
return render_template('bookmarks.html', bookmarks=bookmarks, userkey=userkey, tags=tags, filter=filter_on, message=message)
else:
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE).order_by(Bookmark.created_date.desc())
return render_template('bookmarks.html', bookmarks=bookmarks, userkey=userkey, tags=tags, message=message)
#@app.route('/<userkey>/<urlhash>')
#def viewbookmark(userkey, urlhash):
# """ Bookmark detail view """
# bookmark = Bookmark.select(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey)
# return render_template('viewbookmark.html', userkey=userkey, bookmark=bookmark)
@app.route('/<userkey>/<urlhash>/json')
def viewbookmarkjson(userkey, urlhash):
""" Serialise bookmark to json """
bookmark = Bookmark.select(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE)
return bookmark.to_dict()
@app.route('/<userkey>/<urlhash>')
@app.route('/<userkey>/<urlhash>/edit')
def editbookmark(userkey, urlhash):
""" Bookmark edit form """
# bookmark = getbyurlhash()
bookmark = Bookmark.get(Bookmark.url_hash == urlhash, Bookmark.userkey == userkey)
message = request.args.get('message')
tags = get_cached_tags(userkey)
return render_template('edit.html', action='Edit bookmark', userkey=userkey, bookmark=bookmark, message=message, formaction='edit', tags=tags)
@app.route('/<userkey>/add')
def addbookmark(userkey):
""" Bookmark add form """
bookmark = Bookmark(title='', url='', tags='')
message = request.args.get('message')
tags = get_cached_tags(userkey)
return render_template('edit.html', action='Add bookmark', userkey=userkey, bookmark=bookmark, tags=tags, message=message)
def updatebookmark(userkey, request, urlhash = None):
""" Add (no urlhash) or edit (urlhash is set) a bookmark """
title = request.form.get('title')
url = request.form.get('url')
tags = request.form.get('tags')
starred = False
if request.form.get('starred'):
starred = True
strip_params = False
if request.form.get('strip'):
strip_params = True
if url and not urlhash:
# New bookmark
bookmark, created = Bookmark.get_or_create(url=url, userkey=userkey)
if not created:
message = 'Existing bookmark, did not overwrite with new values'
return redirect(url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash, message=message))
elif url:
# Existing bookmark, get from DB
bookmark = Bookmark.get(Bookmark.userkey == userkey, Bookmark.url_hash == urlhash)
# Editing this bookmark, set modified_date to now
bookmark.modified_date = datetime.datetime.now()
else:
# No url was supplied, abort. @TODO: raise exception?
return None
bookmark.title = title
if strip_params:
url = Bookmark.strip_url_params(url)
bookmark.url = url
bookmark.starred = starred
bookmark.set_tags(tags)
bookmark.set_hash()
#bookmark.fetch_image()
if not title:
# Title was empty, automatically fetch it from the url, will also update the status code
bookmark.set_title_from_source()
else:
bookmark.set_status_code()
if bookmark.http_status == 200:
bookmark.set_favicon()
bookmark.save()
return bookmark
@app.route('/<userkey>/adding', methods=['GET', 'POST'])
#@app.route('/<userkey>/adding')
def addingbookmark(userkey):
""" Add the bookmark from form submit by /add """
tags = get_cached_tags(userkey)
if request.method == 'POST':
bookmark = updatebookmark(userkey, request)
if not bookmark:
return redirect(url_for('addbookmark', userkey=userkey, message='No url provided', tags=tags))
if type(bookmark).__name__ == 'Response':
return bookmark
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash))
return redirect(url_for('addbookmark', userkey=userkey, tags=tags))
@app.route('/<userkey>/<urlhash>/editing', methods=['GET', 'POST'])
def editingbookmark(userkey, urlhash):
""" Edit the bookmark from form submit """
if request.method == 'POST':
bookmark = updatebookmark(userkey, request, urlhash=urlhash)
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash))
return redirect(url_for('editbookmark', userkey=userkey, urlhash=urlhash))
@app.route('/<userkey>/<urlhash>/delete', methods=['GET', 'POST'])
def deletingbookmark(userkey, urlhash):
""" Delete the bookmark from form submit by <urlhash>/delete """
query = Bookmark.update(status=Bookmark.DELETED).where(Bookmark.userkey==userkey, Bookmark.url_hash==urlhash)
query.execute()
query = Bookmark.update(deleted_date = datetime.datetime.now()).where(Bookmark.userkey==userkey, Bookmark.url_hash==urlhash)
query.execute()
message = 'Bookmark deleted'
all_tags[userkey] = get_tags_for_user(userkey)
return redirect(url_for('bookmarks', userkey=userkey, message=message))
@app.route('/<userkey>/tags')
def tags(userkey):
""" Overview of all tags used by user """
tags = get_cached_tags(userkey)
return render_template('tags.html', tags=tags, userkey=userkey)
@app.route('/<userkey>/tag/<tag>')
def tag(userkey, tag):
""" Overview of all bookmarks with a certain tag """
bookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.tags.contains(tag), Bookmark.status == Bookmark.VISIBLE)
tags = get_tags_for_user(userkey)
pageheader = 'tag: ' + tag
message = request.args.get('message')
try:
publictag = PublicTag.get(PublicTag.userkey == userkey, PublicTag.tag == tag)
except PublicTag.DoesNotExist:
publictag = None
return render_template('bookmarks.html', bookmarks=bookmarks, userkey=userkey, tags=tags, tag=tag, publictag=publictag, action=pageheader, message=message)
@app.route('/pub/<tagkey>')
def publictag(tagkey):
""" Read-only overview of the bookmarks in the userkey/tag of this PublicTag """
#this_tag = get_object_or_404(PublicTag.select().where(PublicTag.tagkey == tagkey))
try:
this_tag = PublicTag.get(PublicTag.tagkey == tagkey)
bookmarks = Bookmark.select().where(Bookmark.userkey == this_tag.userkey, Bookmark.tags.contains(this_tag.tag), Bookmark.status == Bookmark.VISIBLE)
return render_template('publicbookmarks.html', bookmarks=bookmarks, tag=tag, action=this_tag.tag)
except PublicTag.DoesNotExist:
abort(404)
@app.route('/<userkey>/<tag>/makepublic', methods=['GET', 'POST'])
def addpublictag(userkey, tag):
#user = get_object_or_404(User.get(User.key == userkey))
try:
User.get(User.key == userkey)
except User.DoesNotExist:
abort(404)
try:
publictag = PublicTag.get(PublicTag.userkey == userkey, PublicTag.tag == tag)
except PublicTag.DoesNotExist:
publictag = None
if not publictag:
newpublictag = PublicTag()
newpublictag.generate_key()
newpublictag.userkey = userkey
newpublictag.tag = tag
newpublictag.save()
message = 'Public link to this tag created'
return redirect(url_for('tag', userkey=userkey, tag=tag, message=message))
else:
message = 'Public link already existed'
return redirect(url_for('tag', userkey=userkey, tag=tag, message=message))
@app.route('/<systemkey>/adduser')
def adduser(systemkey):
""" Add user endpoint, convenience """
if systemkey == settings.SYSTEMKEY:
newuser = User()
newuser.generate_key()
newuser.username = 'Nomen Nescio'
newuser.save()
all_tags[newuser.key] = []
return redirect('/' + newuser.key, code=302)
else:
abort(404)
# Initialise
# create the bookmark, user and public tag tables if they do not exist
Bookmark.create_table(True)
User.create_table(True)
PublicTag.create_table(True)
users = User.select()
print('Current user keys:')
for user in users:
all_tags[user.key] = get_tags_for_user(user.key)
print(user.key)
if __name__ == '__main__':
# run the application
app.run(port=9999, debug=True)

View File

@@ -1,4 +1,4 @@
# Virtualenv to use with the wsgi file
# Virtualenv to use with the wsgi file (optional)
VENV = '/srv/marks.example.com/venv/bin/activate_this.py'
PORT = 8086
@@ -10,6 +10,10 @@ 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:

22
pylintrc Normal file
View File

@@ -0,0 +1,22 @@
[FORMAT]
max-line-length=120
[BASIC]
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
e,
ex,
extra,
f,
fd,
fp,
logger,
Run,
q,
s,
x,
y,
_

74
pyproject.toml Normal file
View File

@@ -0,0 +1,74 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "digimarks"
version = "1.1.99"
authors = [
{name = "Michiel Scholten", email = "michiel@diginaut.net"},
]
description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags, automatic title fetching and REST API calls.'
readme = "README.rst"
requires-python = ">=3.7"
keywords = ["bookmarks", "api"]
license = {text = "Apache"}
classifiers = [
"Framework :: FastAPI",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: Apache Software License",
]
dependencies = [
"importlib-metadata; python_version<'3.8'",
"fastapi[all]",
"pydantic>2.0",
"requests",
"bs4",
"feedgen"
]
# dynamic = ["version"]
[project.scripts]
my-script = "digimarks:app"
[project.urls]
"Homepage" = "https://github.com/aquatix/digimarks"
"Bug Tracker" = "https://github.com/aquatix/digimarks/issues"
[tool.black]
line-length = 120
[tool.ruff]
exclude = [
".git",
"__pycache__",
"docs/source/conf.py",
"build",
"dist",
"example_config/gunicorn_digimarks_conf.py",
"example_config/settings.py",
]
line-length = 120
[tool.ruff.format]
# Prefer single quotes over double quotes
quote-style = "single"
[tool.ruff.lint]
ignore = ["D203", "D213"]
select = [
"C9",
"D",
"E",
"F",
"I",
"W",
]
[tool.ruff.lint.flake8-quotes]
docstring-quotes = "double"
inline-quotes = "single"
multiline-quotes = "double"
[tool.ruff.lint.mccabe]
max-complexity = 10

11
requirements-dev.in Normal file
View File

@@ -0,0 +1,11 @@
-r requirements.in
# Linting and fixing, including isort
ruff
# Test suite
pytest
# Publishing on PyPI
build
twine

223
requirements-dev.txt Normal file
View File

@@ -0,0 +1,223 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile requirements-dev.in
#
annotated-types==0.5.0
# via pydantic
anyio==3.7.1
# via
# httpcore
# starlette
# watchfiles
astroid==2.15.6
# via pylint
beautifulsoup4==4.12.2
# via bs4
black==23.7.0
# via -r requirements-dev.in
bleach==6.0.0
# via readme-renderer
bs4==0.0.1
# via -r requirements.in
build==0.10.0
# via -r requirements-dev.in
certifi==2023.7.22
# via
# httpcore
# httpx
# requests
cffi==1.15.1
# via cryptography
charset-normalizer==3.2.0
# via requests
click==8.1.6
# via
# black
# uvicorn
cryptography==41.0.2
# via secretstorage
dill==0.3.7
# via pylint
dnspython==2.4.1
# via email-validator
docutils==0.20.1
# via readme-renderer
email-validator==2.0.0.post2
# via fastapi
exceptiongroup==1.1.2
# via anyio
fastapi[all]==0.100.1
# via -r requirements.in
feedgen==0.9.0
# via -r requirements.in
greenlet==2.0.2
# via sqlalchemy
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==0.17.3
# via httpx
httptools==0.6.0
# via uvicorn
httpx==0.24.1
# via fastapi
idna==3.4
# via
# anyio
# email-validator
# httpx
# requests
importlib-metadata==6.8.0
# via
# keyring
# twine
isort==5.12.0
# via pylint
itsdangerous==2.1.2
# via fastapi
jaraco-classes==3.3.0
# via keyring
jeepney==0.8.0
# via
# keyring
# secretstorage
jinja2==3.1.2
# via fastapi
keyring==24.2.0
# via twine
lazy-object-proxy==1.9.0
# via astroid
lxml==4.9.3
# via feedgen
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.3
# via jinja2
mccabe==0.7.0
# via pylint
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.0.0
# via jaraco-classes
mypy-extensions==1.0.0
# via black
orjson==3.9.2
# via fastapi
packaging==23.1
# via
# black
# build
pathspec==0.11.2
# via black
pkginfo==1.9.6
# via twine
platformdirs==3.10.0
# via
# black
# pylint
pycparser==2.21
# via cffi
pydantic==2.1.1
# via
# fastapi
# pydantic-extra-types
# pydantic-settings
pydantic-core==2.4.0
# via pydantic
pydantic-extra-types==2.0.0
# via fastapi
pydantic-settings==2.0.2
# via fastapi
pygments==2.15.1
# via
# readme-renderer
# rich
pylint==2.17.5
# via -r requirements-dev.in
pyproject-hooks==1.0.0
# via build
python-dateutil==2.8.2
# via feedgen
python-dotenv==1.0.0
# via
# pydantic-settings
# uvicorn
python-multipart==0.0.6
# via fastapi
pyyaml==6.0.1
# via
# fastapi
# uvicorn
readme-renderer==40.0
# via twine
requests==2.31.0
# via
# -r requirements.in
# requests-toolbelt
# twine
requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
rich==13.5.0
# via twine
ruff==0.0.280
# via -r requirements-dev.in
secretstorage==3.3.3
# via keyring
six==1.16.0
# via
# bleach
# python-dateutil
sniffio==1.3.0
# via
# anyio
# httpcore
# httpx
soupsieve==2.4.1
# via beautifulsoup4
sqlalchemy==2.0.19
# via -r requirements.in
starlette==0.27.0
# via fastapi
tomli==2.0.1
# via
# black
# build
# pylint
# pyproject-hooks
tomlkit==0.12.1
# via pylint
twine==4.0.2
# via -r requirements-dev.in
typing-extensions==4.7.1
# via
# astroid
# fastapi
# pydantic
# pydantic-core
# sqlalchemy
# uvicorn
ujson==5.8.0
# via fastapi
urllib3==2.0.4
# via
# requests
# twine
uvicorn[standard]==0.23.1
# via fastapi
uvloop==0.17.0
# via uvicorn
watchfiles==0.19.0
# via uvicorn
webencodings==0.5.1
# via bleach
websockets==11.0.3
# via uvicorn
wrapt==1.15.0
# via astroid
zipp==3.16.2
# via importlib-metadata

3
requirements-server.in Normal file
View File

@@ -0,0 +1,3 @@
-r requirements.in
gunicorn

12
requirements.in Normal file
View File

@@ -0,0 +1,12 @@
# Core application
fastapi[all]
sqlmodel
# Fetch title etc from links
beautifulsoup4
# Fetch favicons
extract_favicon
# Generate (atom) feeds for tags and such
feedgen

View File

@@ -1,7 +1,121 @@
flask
peewee
flask-peewee
bs4
more_itertools
requests
utilkit
#
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile requirements.in
#
annotated-types==0.5.0
# via pydantic
anyio==3.7.1
# via
# httpcore
# starlette
# watchfiles
beautifulsoup4==4.12.2
# via bs4
bs4==0.0.1
# via -r requirements.in
certifi==2023.7.22
# via
# httpcore
# httpx
# requests
charset-normalizer==3.2.0
# via requests
click==8.1.6
# via uvicorn
dnspython==2.4.1
# via email-validator
email-validator==2.0.0.post2
# via fastapi
exceptiongroup==1.1.2
# via anyio
fastapi[all]==0.100.1
# via -r requirements.in
feedgen==0.9.0
# via -r requirements.in
greenlet==2.0.2
# via sqlalchemy
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==0.17.3
# via httpx
httptools==0.6.0
# via uvicorn
httpx==0.24.1
# via fastapi
idna==3.4
# via
# anyio
# email-validator
# httpx
# requests
itsdangerous==2.1.2
# via fastapi
jinja2==3.1.2
# via fastapi
lxml==4.9.3
# via feedgen
markupsafe==2.1.3
# via jinja2
orjson==3.9.2
# via fastapi
pydantic==2.1.1
# via
# fastapi
# pydantic-extra-types
# pydantic-settings
pydantic-core==2.4.0
# via pydantic
pydantic-extra-types==2.0.0
# via fastapi
pydantic-settings==2.0.2
# via fastapi
python-dateutil==2.8.2
# via feedgen
python-dotenv==1.0.0
# via
# pydantic-settings
# uvicorn
python-multipart==0.0.6
# via fastapi
pyyaml==6.0.1
# via
# fastapi
# uvicorn
requests==2.31.0
# via -r requirements.in
six==1.16.0
# via python-dateutil
sniffio==1.3.0
# via
# anyio
# httpcore
# httpx
soupsieve==2.4.1
# via beautifulsoup4
sqlalchemy==2.0.19
# via -r requirements.in
starlette==0.27.0
# via fastapi
typing-extensions==4.7.1
# via
# fastapi
# pydantic
# pydantic-core
# sqlalchemy
# uvicorn
ujson==5.8.0
# via fastapi
urllib3==2.0.4
# via requests
uvicorn[standard]==0.23.1
# via fastapi
uvloop==0.17.0
# via uvicorn
watchfiles==0.19.0
# via uvicorn
websockets==11.0.3
# via uvicorn

View File

@@ -1,43 +1,7 @@
"""
A setuptools based setup module.
See:
https://packaging.python.org/en/latest/distributing.html
https://github.com/pypa/sampleproject
"""
#!/usr/bin/env python
"""Install script for module installation. Compatibility stub because pyproject.toml is used."""
from setuptools import setup
# To use a consistent encoding
from codecs import open
from os import path
import setuptools
here = path.abspath(path.dirname(__file__))
# Get the long description from the relevant file
with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read()
setup(
name='digimarks', # pip install digimarks
description='Simple bookmarking service, using a SQLite database to store bookmarks, supporting tags and automatic title fetching.',
#long_description=open('README.md', 'rt').read(),
long_description=long_description,
# version
# third part for minor release
# second when api changes
# first when it becomes stable someday
version='0.2.0',
author='Michiel Scholten',
author_email='michiel@diginaut.net',
url='https://github.com/aquatix/digimarks',
license='Apache',
# as a practice no need to hard code version unless you know program wont
# work unless the specific versions are used
install_requires=['Flask', 'Peewee', 'Flask-Peewee', 'requests', 'utilkit'],
py_modules=['digimarks'],
zip_safe=True,
)
if __name__ == "__main__":
setuptools.setup()

View File

635
src/digimarks/main.py Normal file
View File

@@ -0,0 +1,635 @@
"""digimarks main module."""
import binascii
import hashlib
import logging
import os
from contextlib import asynccontextmanager
from datetime import UTC, datetime
from http import HTTPStatus
from typing import Annotated, Optional, Sequence, Type, TypeVar
from urllib.parse import urlparse, urlunparse
import bs4
import httpx
from extract_favicon import from_html
from fastapi import Depends, FastAPI, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import AnyUrl, DirectoryPath, FilePath, computed_field
from pydantic_settings import BaseSettings
from sqlmodel import AutoString, Field, Session, SQLModel, create_engine, desc, select
DIGIMARKS_USER_AGENT = 'digimarks/2.0.0-dev'
DIGIMARKS_VERSION = '2.0.0a1'
DEFAULT_THEME = 'freshgreen'
class Settings(BaseSettings):
"""Configuration needed for digimarks to find its database, favicons, API integrations."""
# outside the codebase
database_file: FilePath
favicons_dir: DirectoryPath
# inside the codebase
static_dir: DirectoryPath = 'static'
template_dir: DirectoryPath = 'templates'
media_url: str = '/static/'
system_key: str
debug: bool = False
settings = Settings()
print(settings.model_dump())
engine = create_engine(f'sqlite:///{settings.database_file}', connect_args={'check_same_thread': False})
def get_session():
"""SQLAlchemy session factory."""
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
@asynccontextmanager
async def lifespan(the_app: FastAPI):
"""Upon start, initialise an AsyncClient and assign it to an attribute named requests_client on the app object."""
the_app.requests_client = httpx.AsyncClient()
yield
await the_app.requests_client.aclose()
app = FastAPI(lifespan=lifespan)
app.mount('/static', StaticFiles(directory=settings.static_dir), name='static')
app.mount('/content/favicons', StaticFiles(directory=settings.favicons_dir), name='favicons')
templates = Jinja2Templates(directory=settings.template_dir)
logger = logging.getLogger('digimarks')
if settings.debug:
logger.setLevel(logging.DEBUG)
# CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=['*'], # Allow requests from everywhere
allow_credentials=True,
allow_methods=['*'],
allow_headers=['*'],
)
def i_filter_false(predicate, iterable):
"""Filter an iterable if predicate returns True.
i_filter_false(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_ever_seen(iterable, key=None):
"""List unique elements, preserving order. Remember all elements ever seen.
unique_ever_seen('AAAABBBCCDAABBB') --> A B C D
unique_ever_seen('ABBCcAD', str.lower) --> A B C D
"""
seen = set()
seen_add = seen.add
if key is None:
for element in i_filter_false(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: list) -> list:
"""Generate a unique list of the tags.
:param list tags_list: List with all tags
:return: deduplicated list of the tags, without leading or trailing whitespace
:rtype: list
"""
tags_res = [x.strip() for x in tags_list]
tags_res = list(unique_ever_seen(tags_res))
tags_res.sort()
if tags_res and tags_res[0] == '':
del tags_res[0]
return tags_res
def list_tags_for_bookmarks(bookmarks: list) -> list:
"""Generate a unique list of the tags from the list of bookmarks."""
tags = []
for bookmark in bookmarks:
tags += bookmark.tags_list
return clean_tags(tags)
def file_type(filename: str) -> str:
"""Try to determine the file type for the file in `filename`.
:param str filename: path to file to check
:return: zip file type
:rtype: str
"""
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)
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'
def generate_hash(input_text: str) -> str:
"""Generate a hash from string `input`, e.g., for a URL."""
return hashlib.md5(input_text.encode('utf-8')).hexdigest()
def generate_key() -> str:
"""Generate a key to be used for a user or tag."""
return str(binascii.hexlify(os.urandom(24)))
# Type var used for building custom types for the DB
T = TypeVar('T')
def build_custom_type(internal_type: Type[T]) -> Type[AutoString]:
"""Create a type that is compatible with the database.
Based on https://github.com/fastapi/sqlmodel/discussions/847
"""
class CustomType(AutoString):
def process_bind_param(self, value, dialect) -> Optional[str]:
if value is None:
return None
if isinstance(value, str):
# Test if value is valid to avoid `process_result_value` failing
try:
internal_type(value) # type: ignore[call-arg]
except ValueError as e:
raise ValueError(f'Invalid value for {internal_type.__name__}: {e}') from e
return str(value)
def process_result_value(self, value, dialect) -> Optional[T]:
if value is None:
return None
return internal_type(value) # type: ignore[call-arg]
return CustomType
def get_favicon(html_content: str, root_url: str) -> str:
"""Fetch the favicon from `html_content` using `root_url`."""
favicons = from_html(html_content, root_url=root_url, include_fallbacks=True)
for favicon in favicons:
print(favicon.url, favicon.width, favicon.height)
# TODO: save the preferred image to file and return
class User(SQLModel, table=True):
"""User account."""
__tablename__ = 'user'
id: int = Field(primary_key=True)
username: str
key: str
theme: str = Field(default=DEFAULT_THEME)
created_date: datetime
class Visibility:
"""Options for visibility of an object."""
VISIBLE = 0
DELETED = 1
class Bookmark(SQLModel, table=True):
"""Bookmark object."""
__tablename__ = 'bookmark'
id: int = Field(primary_key=True)
userkey: str = Field(foreign_key='user.key')
title: str = Field(default='')
url: AnyUrl = Field(default='', sa_type=build_custom_type(AnyUrl))
note: str = Field(default='')
# image: str = Field(default='')
url_hash: str = Field(default='')
tags: str = Field(default='')
starred: bool = Field(default=False)
favicon: str | None = Field(default=None)
http_status: int = Field(default=HTTPStatus.OK)
created_date: datetime = Field(default=datetime.now(UTC))
modified_date: datetime = Field(default=None)
deleted_date: datetime = Field(default=None)
status: int = Field(default=Visibility.VISIBLE)
@computed_field
@property
def tag_list(self) -> list:
"""The tags but as a proper list."""
if self.tags:
return self.tags.split(',')
# Not tags, return empty list instead of [''] that split returns in that case
return []
async def set_information_from_source(bookmark: Bookmark, request: Request) -> Bookmark:
"""Request the title by requesting the source url."""
logger.info('Extracting information from url %s', bookmark.url)
try:
result = await request.app.requests_client.get(bookmark.url, headers={'User-Agent': DIGIMARKS_USER_AGENT})
bookmark.http_status = result.status_code
except httpx.HTTPError as err:
# For example, 'MissingSchema: Invalid URL 'abc': No schema supplied. Perhaps you meant http://abc?'
logger.error('Exception when trying to retrieve title for %s. Error: %s', bookmark.url, str(err))
bookmark.http_status = 404
bookmark.title = ''
return bookmark
if bookmark.http_status == 200 or bookmark.http_status == 202:
html = bs4.BeautifulSoup(result.text, 'html.parser')
try:
bookmark.title = html.title.text.strip()
except AttributeError:
bookmark.title = ''
url_parts = urlparse(str(bookmark.url))
root_url = url_parts.scheme + '://' + url_parts.netloc
favicon = get_favicon(result.text, root_url)
# filename = os.path.join(settings.media_dir, 'favicons/', domain + file_extension)
# with open(filename, 'wb') as out_file:
# shutil.copyfileobj(response.raw, out_file)
# Extraction was successful
logger.info('Extracting information was successful')
return bookmark
def set_tags(bookmark: Bookmark, new_tags: str) -> None:
"""Set tags from `tags`, strip and sort them.
:param Bookmark bookmark: Bookmark to modify
:param str new_tags: New tags to sort and set.
"""
tags_split = new_tags.split(',')
tags_clean = clean_tags(tags_split)
bookmark.tags = ','.join(tags_clean)
def strip_url_params(url: str) -> str:
"""Strip URL params from URL.
:param url: URL to strip URL params from.
:return: clean URL
:rtype: str
"""
parsed = urlparse(url)
return urlunparse((parsed.scheme, parsed.netloc, parsed.path, parsed.params, '', parsed.fragment))
def update_bookmark_with_info(bookmark: Bookmark, request: Request, strip_params: bool = False):
"""Automatically update title, favicon, etc."""
if not bookmark.title:
# Title was empty, automatically fetch it from the url, will also update the status code
set_information_from_source(bookmark, request)
if strip_params:
# Strip URL parameters, e.g., tracking params
bookmark.url = AnyUrl(strip_url_params(str(bookmark.url)))
# Sort and deduplicate tags
set_tags(bookmark, bookmark.tags)
class PublicTag(SQLModel, table=True):
"""Public tag object."""
__tablename__ = 'public_tag'
id: int = Field(primary_key=True)
tagkey: str
userkey: str = Field(foreign_key='user.key')
tag: str
created_date: datetime = Field(default=datetime.now(UTC))
@app.get('/', response_class=HTMLResponse)
@app.head('/', response_class=HTMLResponse)
def index(request: Request):
"""Homepage, point visitors to project page."""
return templates.TemplateResponse(
request=request,
name='index.html',
context={'language': 'en', 'version': DIGIMARKS_VERSION, 'theme': DEFAULT_THEME},
)
@app.get('/api/v1/admin/{system_key}/users/{user_id}', response_model=User)
def get_user(session: SessionDep, system_key: str, user_id: int) -> Type[User]:
"""Show user information."""
if system_key != settings.system_key:
raise HTTPException(status_code=404)
user = session.get(User, user_id)
if not user:
raise HTTPException(status_code=404, detail='User not found')
return user
# @app.get('/admin/{system_key}/users/', response_model=list[User])
@app.get('/api/v1/admin/{system_key}/users/')
def list_users(
session: SessionDep,
system_key: str,
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100,
) -> Sequence[User]:
"""List all users in the database.
:param SessionDep session:
:param str system_key: secrit key
:param int offset: [Optional] offset of pagination
:param int limit: [Optional] limits the number of users to return, defaults to 100
:return: list of users in the system
:rtype: list[User]
"""
if system_key != settings.system_key:
raise HTTPException(status_code=404)
users = session.exec(select(User).offset(offset).limit(limit)).all()
return users
@app.get('/api/v1/{user_key}/bookmarks/')
def list_bookmarks(
session: SessionDep,
user_key: str,
offset: int = 0,
limit: Annotated[int, Query(le=10000)] = 100,
) -> list[Bookmark]:
"""List all bookmarks in the database. By default 100 items are returned."""
bookmarks = session.exec(
select(Bookmark)
.where(Bookmark.userkey == user_key, Bookmark.status != Visibility.DELETED)
.offset(offset)
.limit(limit)
).all()
return bookmarks
@app.get('/api/v1/{user_key}/bookmarks/{url_hash}')
def get_bookmark(
session: SessionDep,
user_key: str,
url_hash: str,
) -> Bookmark:
"""Show bookmark details."""
bookmark = session.exec(
select(Bookmark).where(
Bookmark.userkey == user_key, Bookmark.url_hash == url_hash, Bookmark.status != Visibility.DELETED
)
).first()
# bookmark = session.get(Bookmark, {'url_hash': url_hash, 'userkey': user_key})
return bookmark
@app.post('/api/v1/{user_key}/autocomplete_bookmark/', response_model=Bookmark)
def autocomplete_bookmark(
session: SessionDep,
request: Request,
user_key: str,
bookmark: Bookmark,
strip_params: bool = False,
):
"""Autofill some fields for this (new) bookmark for user `user_key`."""
bookmark.userkey = user_key
# Auto-fill title, fix tags etc.
update_bookmark_with_info(bookmark, request, strip_params)
url_hash = generate_hash(str(bookmark.url))
bookmark_db = session.exec(
select(Bookmark).where(
Bookmark.userkey == user_key, Bookmark.url_hash == url_hash, Bookmark.status != Visibility.DELETED
)
).first()
if bookmark_db:
# Bookmark with this URL already exists, provide the hash so the frontend can look it up and the user can
# merge them if so wanted
bookmark.url_hash = url_hash
return bookmark
@app.post('/api/v1/{user_key}/bookmarks/', response_model=Bookmark)
def add_bookmark(
session: SessionDep,
request: Request,
user_key: str,
bookmark: Bookmark,
strip_params: bool = False,
):
"""Add new bookmark for user `user_key`."""
bookmark.userkey = user_key
# Auto-fill title, fix tags etc.
update_bookmark_with_info(bookmark, request, strip_params)
bookmark.url_hash = generate_hash(str(bookmark.url))
session.add(bookmark)
session.commit()
session.refresh(bookmark)
return bookmark
@app.patch('/api/v1/{user_key}/bookmarks/{url_hash}', response_model=Bookmark)
def update_bookmark(
session: SessionDep,
request: Request,
user_key: str,
bookmark: Bookmark,
url_hash: str,
strip_params: bool = False,
):
"""Update existing bookmark `bookmark_key` for user `user_key`."""
bookmark_db = session.exec(
select(Bookmark).where(
Bookmark.userkey == user_key, Bookmark.url_hash == url_hash, Bookmark.status != Visibility.DELETED
)
).first()
if not bookmark_db:
raise HTTPException(status_code=404, detail='Bookmark not found')
bookmark.modified_date = datetime.now(UTC)
# 'patch' endpoint, which means that you can send only the data that you want to update, leaving the rest intact
bookmark_data = bookmark.model_dump(exclude_unset=True)
# Merge the changed fields into the existing object
bookmark_db.sqlmodel_update(bookmark_data)
# Autofill title, fix tags, etc. where (still) needed
update_bookmark_with_info(bookmark, request, strip_params)
session.add(bookmark_db)
session.commit()
session.refresh(bookmark_db)
return bookmark_db
@app.delete('/api/v1/{user_key}/bookmarks/{url_hash}', response_model=Bookmark)
def delete_bookmark(
session: SessionDep,
user_key: str,
url_hash: str,
):
"""(Soft)Delete bookmark `bookmark_key` for user `user_key`."""
bookmark = session.get(Bookmark, {'url_hash': url_hash, 'userkey': user_key})
if not bookmark:
raise HTTPException(status_code=404, detail='Bookmark not found')
bookmark.deleted_date = datetime.now(UTC)
bookmark.status = Visibility.DELETED
session.add(bookmark)
session.commit()
return {'ok': True}
@app.get('/api/v1/{user_key}/latest_changes/')
def bookmarks_changed_since(
session: SessionDep,
user_key: str,
):
"""Last update on server, so the (browser) client knows whether to fetch an update."""
latest_modified_bookmark = session.exec(
select(Bookmark)
.where(Bookmark.userkey == user_key, Bookmark.status != Visibility.DELETED)
.order_by(desc(Bookmark.modified_date))
).first()
latest_created_bookmark = session.exec(
select(Bookmark)
.where(Bookmark.userkey == user_key, Bookmark.status != Visibility.DELETED)
.order_by(desc(Bookmark.created_date))
).first()
latest_modification = max(latest_modified_bookmark.modified_date, latest_created_bookmark.created_date)
return {
'current_time': datetime.now(UTC),
'latest_change': latest_modified_bookmark.modified_date,
'latest_created': latest_created_bookmark.created_date,
'latest_modification': latest_modification,
}
@app.get('/api/v1/{user_key}/tags/')
def list_tags_for_user(
session: SessionDep,
user_key: str,
) -> list[str]:
"""List all tags in use by the user."""
bookmarks = session.exec(
select(Bookmark).where(Bookmark.userkey == user_key, Bookmark.status != Visibility.DELETED)
).all()
tags = []
for bookmark in bookmarks:
tags += bookmark.tag_list
return clean_tags(tags)
@app.get('/api/v1/{user_key}/tags/{tag_key}')
def list_tags_for_user(
session: SessionDep,
user_key: str,
) -> list[str]:
"""List all tags in use by the user."""
bookmarks = session.exec(select(Bookmark).where(Bookmark.userkey == user_key)).all()
return list_tags_for_bookmarks(bookmarks)
@app.get('/{user_key}', response_class=HTMLResponse)
def page_user_landing(
session: SessionDep,
request: Request,
user_key: str,
):
"""HTML page with the main view for the user."""
user = session.exec(select(User).where(User.key == user_key)).first()
if not user:
raise HTTPException(status_code=404, detail='User not found')
language = 'en'
return templates.TemplateResponse(
request=request,
name='user_index.html',
context={'language': language, 'version': DIGIMARKS_VERSION, 'user_key': user_key},
)
# def tags_page(userkey):
# """Overview of all tags used by user"""
# tags = get_cached_tags(userkey)
# alltags = []
# for tag in tags:
# try:
# publictag = PublicTag.get(PublicTag.userkey == userkey, PublicTag.tag == tag)
# except PublicTag.DoesNotExist:
# publictag = None
#
# total = (
# Bookmark.select()
# .where(Bookmark.userkey == userkey, Bookmark.tags.contains(tag), Bookmark.status == Bookmark.VISIBLE)
# .count()
# )
# alltags.append({'tag': tag, 'publictag': publictag, 'total': total})
# totaltags = len(alltags)
# totalbookmarks = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.VISIBLE).count()
# totalpublic = PublicTag.select().where(PublicTag.userkey == userkey).count()
# totalstarred = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.starred).count()
# totaldeleted = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.status == Bookmark.DELETED).count()
# totalnotes = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.note != '').count()
# totalhttperrorstatus = Bookmark.select().where(Bookmark.userkey == userkey, Bookmark.http_status != 200).count()
# theme = get_theme(userkey)
# return render_template(
# 'tags.html',
# tags=alltags,
# totaltags=totaltags,
# totalpublic=totalpublic,
# totalbookmarks=totalbookmarks,
# totaldeleted=totaldeleted,
# totalstarred=totalstarred,
# totalhttperrorstatus=totalhttperrorstatus,
# totalnotes=totalnotes,
# userkey=userkey,
# theme=theme,
# )

View File

@@ -0,0 +1,25 @@
/**
* digimarks styling
*
* Overrides on and additions to the digui styling
*/
/* Star, error, note etc */
.card .statuses {
display: flex;
flex-direction: column;
}
.star {
color: #ffeb3b;
}
.thumbnail {
/*width: 80px;*/
width: 66;
}
.thumbnail img {
/*width: 72px;*/
width: 60px;
}

View File

@@ -0,0 +1,478 @@
/**
* digui structure and theming
* v0.0.2
*
* Created by: Michiel Scholten
* Source: https://github.com/aquatix/digui
*/
/** Colours and themes */
:root {
--padding: .5rem;
color-scheme: only light;
/* Default is nebula-light */
--font-family: sans-serif;
--background-color: #fff;
--background-color-secondary: #ccc;
--button-color: #eee;
--button-text: var(--text-color);
--text-color: #121416d8;
--text-color-secondary: #121416d8;
--text-color-muted: #d5d9d9;
--link-color: #543fd7;
--nav-background-color: #FFF;
--nav-color: var(--text-color);
--border-color: #d5d9d9;
--border-width: 1px;
--border-radius: 8px;
--shadow-color: rgba(213, 217, 217, .5);
--global-theme-toggle-content: ' 🌞';
/* E.g., an active button */
--color-highlight: #fb8c00;
/* Generic colors */
/*--color-danger: #e03131;*/
--color-danger: var(--color-red);
--color-warning: var(--color-yellow);
--color-error: var(--color-danger);
/*--color-ok: #31e031;*/
--color-ok: var(--color-green);
/* Argonaut colours */
--color-black: #000000;
--color-red: #FF000F;
--color-green: #8CE10B;
--color-yellow: #FFB900;
--color-blue: #008DF8;
--color-purple: #6D43A6;
--color-cyan: #00D8EB;
--color-white: #FFFFFF;
}
html[data-theme='nebula'] {
/* Default theme, see :root element */
}
html[data-theme='nebula-dark'] {
color-scheme: dark;
--background-color: #29292c;
--background-color-secondary: #29292c;
--button-color: #29292c;
--button-text: var(--text-color);
--text-color: #F7F8F8;
--text-color-secondary: #ddd;
--text-color-muted: #F7F8F8;
--link-color: #ffe7a3;
--color-highlight: #e03131;
--nav-background-color: #FF9800;
--nav-color: var(--text-color);
--border-color: #333;
--border-width: 1px;
--border-radius: 8px;
--shadow-color: rgba(3, 3, 3, .5);
--global-theme-toggle-content: ' 🌝';
}
html[data-theme='bbs'] {
--font-family: monospace;
--background-color: #FFF;
--background-color-secondary: #ccc;
--button-color: #FFFFFF;
--button-text: var(--text-color);
--text-color: #000;
--text-color-secondary: #000;
--text-color-muted: #000;
--link-color: #543fd7;
--color-highlight: #e03131;
/*--nav-background-color: #ccc;*/
/*--nav-color: var(--text-color);*/
--border-color: #333;
--border-width: 2px;
--border-radius: 0;
--global-theme-toggle-content: ' 🖥️';
}
html[data-theme='silo'] {
--font-family: monospace;
/*--background-color: #003eaa;*/
--background-color: #1d212c;
--background-color-secondary: var(--color-highlight);
--button-color: #FFFFFF;
--button-text: var(--text-color);
--text-color: #FFF;
--text-color-secondary: #29292c;
--text-color-muted: #FFF;
--link-color: #FF9800;
--color-highlight: #23B0FF;
/*--nav-background-color: #003eaa;*/
/*--nav-background-color: #23B0FF;*/
/*--nav-color: var(--text-color);*/
--nav-background-color: var(--background-color);
--border-color: #23B0FF;
/*--border-color: #003eaa;*/
--border-width: 2px;
--border-radius: 0;
--global-theme-toggle-content: ' ⌨️';
}
/* AlpineJS blip-preventer */
[x-cloak] {
display: none !important;
}
/** Main structure */
body {
background: var(--background-color);
color: var(--text-color);
height: 125vh;
font-family: var(--font-family), sans-serif;
margin-top: 3rem;
/*padding: 30px;*/
}
main {
color: var(--text-color);
padding-top: .5em;
}
/* Navigation */
header {
background-color: var(--nav-background-color);
position: fixed;
top: 0;
left: 0;
right: 0;
height: 3rem;
display: flex;
align-items: center;
}
[data-theme='nebula'] header,
[data-theme='nebula-dark'] header {
/*box-shadow: 0 0 5px 0 rgba(213, 217, 217, .5);*/
/*box-shadow: 0 0 5px 0 #999;*/
box-shadow: 0 0 5px var(--shadow-color);
}
header * {
display: inline;
}
header li {
/*margin: 10px;*/
}
header li a {
color: black;
text-decoration: none;
}
header li h1 {
font-weight: bold;
font-size: 1.2rem;
vertical-align: middle;
margin-right: 3rem;
}
[data-theme='silo'] header nav::after {
content: '';
background: repeating-linear-gradient(90deg, #23B0FF, #23B0FF 2px, transparent 0, transparent 10px);
display: block;
width: 100%;
right: 10px;
}
[data-theme='silo'] header {
border-bottom: 3px dotted #23B0FF;
}
/** Generic elements */
/* https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/Heading_Elements#specifying_a_uniform_font_size_for_h1 */
h1 {
margin-block: 0.67em;
font-size: 2em;
}
a, a:hover, a:visited, a:active, a.button, a.button:hover, a.button:active, a.button:visited {
text-decoration: none;
}
a {
color: var(--link-color);
}
a:hover {
text-decoration: underline;
filter: brightness(80%);
}
ol li::marker, ul li::marker {
color: var(--text-color-muted);
}
/* Active element, e.g. a button */
.active {
background-color: var(--color-highlight);
color: var(--text-color);
}
/* Special button */
.theme-toggle::after {
content: var(--global-theme-toggle-content);
}
/* Buttons */
button, .button, input, select, textarea {
box-sizing: border-box;
color: var(--text-color-secondary);
background-color: var(--button-color);
cursor: pointer;
display: inline-block;
font-size: 13px;
/*line-height: 29px;*/
/*padding: 0 10px 0 11px;*/
position: relative;
text-align: left;
text-decoration: none;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
vertical-align: middle;
}
button, .button, input, select, textarea, table {
border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
padding: .5rem .5rem;
}
button:hover, .button:hover {
/*background-color: #f7fafa;*/
/*background-color: #d57803;*/
background-color: var(--color-highlight);
filter: brightness(80%);
}
button:focus, .button:focus {
/*border-color: #008296;*/
/*box-shadow: rgba(213, 217, 217, .5) 0 2px 5px 0;*/
outline: 0;
/*border-color: #d57803;*/
}
.btn-dangerous {
background: var(--color-danger);
}
.btn-dangerous:hover {
background: var(--color-danger);
filter: brightness(80%);
}
.btn-warning {
background-color: var(--color-warning);
}
.btn-warning:hover {
background-color: var(--color-warning);
filter: brightness(80%);
}
.btn-ok {
background: var(--color-ok);
}
.btn-ok:hover {
background: var(--color-ok);
filter: brightness(80%);
}
/* Table */
th {
text-align: left;
}
th, td {
padding: 0 0.3rem;
}
/* Cards */
.cards {
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
}
.card {
display: inline-grid;
border-radius: var(--border-radius);
border: var(--border-width) solid var(--border-color);
/*margin: 1em;*/
/*padding: 1em;*/
}
[data-theme='nebula'] :modal,
[data-theme='nebula'] .card,
[data-theme='nebula'] button,
[data-theme='nebula'] .button,
[data-theme='nebula'] input,
[data-theme='nebula'] select,
[data-theme='nebula'] textarea,
[data-theme='nebula'] table,
[data-theme='nebula-dark'] :modal,
[data-theme='nebula-dark'] .card,
[data-theme='nebula-dark'] button,
[data-theme='nebula-dark'] .button,
[data-theme='nebula-dark'] input,
[data-theme='nebula-dark'] select,
[data-theme='nebula-dark'] textarea,
[data-theme='nebula-dark'] table {
box-shadow: var(--shadow-color) 0 2px 5px 0;
}
.card .card-header {
}
.card-body {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
gap: 1rem;
padding: 1em;
}
.card-body > * {
/*padding-left: 1em;*/
}
.card .card-image {
width: 100%;
}
.card .card-image img {
width: 100%;
border-radius: var(--border-radius);
}
.card .card-thumb {
width: 72px;
/*min-width: 60px;*/
/*max-width: 100px;*/
/*position: relative;*/
/*box-sizing: inherit;*/
}
.card .card-thumb img {
width: 72px;
}
.card .card-action {
padding: .5em;
}
.card .meta {
filter: brightness(80%);
color: var(--text-color);
}
.card .card-footer {
display: flex;
flex-direction: row-reverse;
gap: 1rem;
align-items: center;
padding: .3em;
}
.card-footer h1, .card-footer h2, .card-footer h3, .card-footer h4, .card-footer h5, .card-footer h6 {
margin: 0;
}
/*
.card button {
border: none;
background: none;
}
*/
/* Tags/chips */
.chip {
font-size: .8rem;
border-radius: var(--border-radius);
background-color: var(--background-color-secondary);
color: var(--text-color-secondary);
/*color: var(--text-color);*/
padding: .2rem .5rem;
margin-left: .5rem;
}
.chip .button {
border-radius: var(--border-radius);
}
/* Status */
.error {
color: var(--color-error);
}
/** Modal, e.g. for showing info or filling in a form; on top of the other content */
dialog:modal {
color: var(--text-color);
background-color: var(--background-color);
/*background-color: var(--nav-background-color);*/
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
width: 90%;
/*height: 90%;*/
overflow: auto;
}
/* The umwelt of the modal, on top of the regular content */
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
/** Footer */
footer {
/*background-color: var(--secondary-background-color);*/
margin-top: 1rem;
padding: 2rem 1rem;
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
}
footer .column {
display: inline-grid;
}
footer h2 {
text-align: left;
margin-bottom: 0;
}

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

View File

View File

@@ -0,0 +1,225 @@
document.addEventListener('alpine:init', () => {
Alpine.store('digimarks', {
/** Main digimarks application, state etc */
userKey: -1,
/* cache consists of cache[userKey] = {'bookmarks': [], 'tags': [], ??} */
cache: Alpine.$persist({}).as('cache'),
bookmarks: [],
/* nebula (drop-shadows), bbs (monospace, right lines), silo (like bbs but dark) ?? */
themes: ['nebula', 'nebula-dark', 'bbs', 'silo'],
theme: Alpine.$persist('nebula').as('theme'),
showBookmarks: Alpine.$persist(true).as('showBookmarks'),
showBookmarksList: Alpine.$persist(true).as('showBookmarksList'),
showBookmarksCards: Alpine.$persist(false).as('showBookmarksCards'),
showTags: Alpine.$persist(false).as('showTags'),
/* Bookmark that is being edited, used to fill the form, etc. */
bookmarkToEdit: Alpine.$persist(null).as('bookmarkToEdit'),
/* Loading indicator */
loading: false,
/* Search filter */
search: '',
/* Show bookmarks with this tag/these tags */
tagsFilter: [],
/* Hide bookmarks with these tags */
tagsToHide: Alpine.$persist([]).as('tags_to_hide'),
/* Sort on ~ */
sortTitleAsc: Alpine.$persist(false).as('sortTitleAsc'),
sortTitleDesc: Alpine.$persist(false).as('sortTitleDesc'),
sortCreatedAsc: Alpine.$persist(false).as('sortCreatedAsc'),
sortCreatedDesc: Alpine.$persist(false).as('sortCreatedDesc'),
async init() {
/** Initialise the application after loading */
document.documentElement.setAttribute('data-theme', this.theme);
console.log('Set theme', this.theme);
/* Bookmarks are refreshed through the getBookmarks() call in the HTML page */
/* await this.getBookmarks(); */
setInterval(() => {
// Update counter to next game (midnight UTC, fetched from API) every second
// this.countDownTimer();
}, 1000);
},
async loopToNextTheme() {
/* Loop through themes */
let currentThemeIndex = this.themes.indexOf(this.theme);
if (currentThemeIndex + 1 >= this.themes.length) {
currentThemeIndex = 0
} else {
currentThemeIndex++;
}
this.theme = this.themes[currentThemeIndex];
console.log('Switching to theme', this.theme)
document.documentElement.setAttribute('data-theme', this.theme);
/* Optionally, change the theme CSS file too */
// document.getElementById('theme-link').setAttribute('href', 'digui-theme-' + this.theme + '.css');
},
async loadCache() {
/* Load bookmarks and tags from cache */
if (this.userKey in this.cache) {
console.log('Loading bookmarks from cache for user "' + this.userKey + '"');
this.filterBookmarksByTags();
}
},
async getBookmarks() {
/** Get the bookmarks from the backend */
this.loading = true;
if (!(this.userKey in this.cache)) {
/* There is no cache for this userKey yet, create on */
console.log('Creating cache for user "' + this.userKey + '"');
this.cache[this.userKey] = {'bookmarks': [], 'latest_changes': {}};
}
let latestStatusResponse = await fetch('/api/v1/' + this.userKey + '/latest_changes/');
let latestStatusResult = await latestStatusResponse.json();
let shouldFetch = false;
let latestModificationInCache = this.cache[this.userKey].latest_changes.latest_modification || "0000-00-00";
shouldFetch = latestStatusResult.latest_modification > latestModificationInCache;
this.cache[this.userKey].latest_changes = latestStatusResult;
if (!shouldFetch) {
console.log('Cache is up-to-date');
this.loading = false;
return;
}
console.log('Fetching latest bookmarks from backend for user "' + this.userKey + '"...');
/* At the moment, request 'a lot' bookmarks; likely all of them in one go; paging tbd if needed */
let response = await fetch('/api/v1/' + this.userKey + '/bookmarks/?limit=10000');
/* Cache the bookmarks to Local Storage */
this.cache[this.userKey]['bookmarks'] = await response.json();
let tagsResponse = await fetch('/api/v1/' + this.userKey + '/tags/');
this.cache[this.userKey]['tags'] = await tagsResponse.json();
/* Filter bookmarks by (blacklisted) tags */
await this.filterBookmarksByTags();
this.loading = false;
},
hasTag(tagList, filterList) {
/* Looks for the items in filterList and returns True when one appears on the tagList */
if (tagList === undefined) {
return false;
}
for (let tag in filterList) {
if (tagList.includes(tag))
return true;
}
return false;
},
filterBookmarksByTags() {
/* Filter away bookmarks with a tag on the 'blacklist' */
/* First make a shallow copy of all bookmarks */
let prefilteredBookmarks = [...this.cache[this.userKey]['bookmarks']] || [];
if (this.tagsToHide.length > 0) {
console.log('Filtering away bookmarks containing blacklisted tags');
this.bookmarks = prefilteredBookmarks.filter(
i => !this.hasTag(i.tag_list, this.tagsToHide)
)
} else {
this.bookmarks = prefilteredBookmarks;
}
this.sortBookmarks();
},
get filteredBookmarks() {
/* Get the bookmarks, optionally filtered by search text or tag black-/whitelists */
/* Use 'bookmarks' and not the cache, as it can already be pre-filtered */
if (this.search === '') {
/* No need to filter, quickly return the set */
return this.bookmarks;
}
return this.bookmarks.filter(
i => i.title.match(new RegExp(this.search, "i"))
)
},
get filteredTags() {
/* Search in the list of all tags */
return this.cache[this.userKey].tags.filter(
i => i.match(new RegExp(this.search, "i"))
)
},
sortBookmarks() {
/* Sort the bookmarks according to the setting */
if (this.sortTitleAsc) {
this.bookmarks.sort((a, b) => a.title.localeCompare(b.title));
} else if (this.sortTitleDesc) {
this.bookmarks.sort((a, b) => b.title.localeCompare(a.title));
} else if (this.sortCreatedAsc) {
this.bookmarks.sort((a, b) => a.created_date.localeCompare(b.created_date));
} else if (this.sortCreatedDesc) {
this.bookmarks.sort((a, b) => b.created_date.localeCompare(a.created_date));
}
},
async sortAlphabetically(order = 'asc') {
/* Sort the bookmarks (reverse) alphabetically, based on 'asc' or 'desc' */
this.loading = true;
this.sortCreatedAsc = false;
this.sortCreatedDesc = false;
this.sortTitleAsc = false;
this.sortTitleDesc = false;
if (order === 'desc') {
this.sortTitleDesc = true;
} else {
this.sortTitleAsc = true;
}
this.sortBookmarks();
this.loading = false;
},
async sortCreated(order = 'asc') {
/* Sort the bookmarks (reverse) chronologically, based on 'asc' or 'desc' */
this.loading = true;
this.sortCreatedAsc = false;
this.sortCreatedDesc = false;
this.sortTitleAsc = false;
this.sortTitleDesc = false;
if (order === 'desc') {
this.sortCreatedDesc = true;
} else {
this.sortCreatedAsc = true;
}
this.sortBookmarks();
this.loading = false;
},
async toggleTagPage() {
/* Show or hide the tag page instead of the bookmarks */
this.showBookmarks = !this.showBookmarks;
this.showTags = !this.showBookmarks;
},
async toggleListOrGrid() {
/* Toggle between 'list' or 'grid' (cards) view */
this.showBookmarksList = !this.showBookmarksList;
this.showBookmarksCards = !this.showBookmarksList;
},
async startAddingBookmark() {
/* Open 'add bookmark' page */
console.log('Start adding bookmark');
this.bookmarkToEdit = {
'url': ''
}
// this.show_bookmark_details = true;
const editFormDialog = document.getElementById("editFormDialog");
editFormDialog.showModal();
},
async saveBookmark() {
console.log('Saving bookmark');
// this.show_bookmark_details = false;
},
async addBookmark() {
/* Post new bookmark to the backend */
//
}
})
});

View File

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

View File

@@ -0,0 +1,33 @@
<!doctype html>
<html lang="{{ language }}">
<head>
<title>{% block title %}{% endblock %} - digimarks</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="/static/css/digui.css?v={{ version }}">
<link rel="stylesheet" href="/static/css/digimarks.css?v={{ version }}">
<link id="favicon" rel="icon" type="image/x-icon" href="/static/images/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/static/images/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/images/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/images/favicon-16x16.png">
<link rel="manifest" href="/static/images/site.webmanifest">
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-clipboard@2.x.x/dist/alpine-clipboard.js"
defer></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body>
{% block page_content %}
<div id="container" x-data="">
</div>
{% endblock %}
<!-- Scripts -->
<script src="/static/js/digimarks.js?v={{ version }}"></script>
{% block extrajs %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,127 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block page_header %}{{ action }}{% endblock %}
{% block page_content %}
{% 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

@@ -0,0 +1,65 @@
<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

@@ -0,0 +1,175 @@
{% 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

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}digimarks{% endblock %}
{% block page_header %}digimarks{% endblock %}
{% block page_content %}
<article>
<header>
<nav class="menu">
<ul>
<li><h1>digimarks</h1></li>
<li>
<a class="button" href="https://github.com/aquatix/digimarks">digimarks project page</a>
</li>
</ul>
</nav>
</header>
<main>
<h1>Welcome to digimarks, your online bookmarking and notes tool</h1>
<p>Please visit your personal url, or <a href="https://github.com/aquatix/digimarks">see the digimarks
project page</a>.</p>
<p>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.#}
</p>
</main>
</article>
{% endblock %}

View File

@@ -0,0 +1,62 @@
<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

@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% if not action %}
{% set action = 'Bookmarks' %}
{% endif %}
{% block title %}{{ action }}{% endblock %}
{% block page_header %}{{ action }}{% endblock %}
{% block page_content %}
{% 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

@@ -0,0 +1,16 @@
<!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

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Tags{% endblock %}
{% block page_header %}Tags{% endblock %}
{% block page_content %}
<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 %}

View File

@@ -0,0 +1,190 @@
{% extends "base.html" %}
{% block title %}Bookmarks{% endblock %}
{% block page_header %}Bookmarks{% endblock %}
{% block page_content %}
<article
x-init="$store.digimarks.userKey = '{{ user_key }}'; $store.digimarks.loadCache(); $store.digimarks.getBookmarks()"
x-data="">
<header>
<nav class="menu">
<ul>
<li><h1>digimarks</h1></li>
<li>
<button x-data @click="$store.digimarks.toggleTagPage()"
:class="$store.digimarks.showTags && 'active'">tags
</button>
</li>
<li>
<button @click="$store.digimarks.startAddingBookmark()">add bookmark</button>
</li>
<li>
<button @click="$store.digimarks.loopToNextTheme()" class="theme-toggle">theme</button>
</li>
<li><input x-model="$store.digimarks.search" placeholder="Search/filter..."></li>
<li x-show="$store.digimarks.loading"><i class="fa-solid fa-rotate-right fa-spin"></i></li>
</ul>
</nav>
</header>
<main>
<section x-cloak x-show="$store.digimarks.showBookmarks" x-transition.opacity>
<h1 x-bind:title="$store.digimarks.userKey">Bookmarks</h1>
<p>
<button @click="$store.digimarks.sortAlphabetically()"
:class="$store.digimarks.sortTitleAsc && 'active'">a-z &darr;
</button>
<button @click="$store.digimarks.sortAlphabetically('desc')"
:class="$store.digimarks.sortTitleDesc && 'active'">z-a &uarr;
</button>
<button @click="$store.digimarks.sortCreated()"
:class="$store.digimarks.sortCreatedAsc && 'active'">date &darr;
</button>
<button @click="$store.digimarks.sortCreated('desc')"
:class="$store.digimarks.sortCreatedDesc && 'active'">date &uarr;
</button>
<button @click="$store.digimarks.toggleListOrGrid()"
:class="$store.digimarks.showBookmarksCards && 'active'">list or grid
</button>
</p>
<table x-cloak x-show="$store.digimarks.showBookmarksList">
<thead>
<tr>
<th colspan="2">&nbsp;</th>
<th>Title</th>
<th>Note</th>
<th>Tags</th>
</tr>
</thead>
<tbody>
<template x-for="bookmark in $store.digimarks.filteredBookmarks" :key="bookmark.id">
<tr>
<td class="thumbnail">
<div class="card-thumb" x-show="bookmark.favicon"><img
x-bind:src="'/content/favicons/' + bookmark.favicon"></div>
</td>
<td>
<div x-show="bookmark.starred" class="star"><i class="fa-fw fa-solid fa-star"></i>
</div>
<div x-show="bookmark.http_status !== 200 && bookmark.http_status !== 304"
class="error"><i
class="fa-fw fa-solid fa-triangle-exclamation"></i>
</div>
</td>
<td><a x-text="bookmark.title" x-bind:href="bookmark.url" target="_blank"></a></td>
<td x-text="bookmark.note"></td>
<td>
<template x-for="tag in bookmark.tag_list">
<span x-text="tag" class="chip"></span>
</template>
</td>
</tr>
</template>
</tbody>
</table>
{#
<ul x-cloak x-show="$store.digimarks.show_bookmarks_list">
<template x-for="bookmark in $store.digimarks.filteredBookmarks" :key="bookmark.id">
<li><a x-text="bookmark.title" x-bind:href="bookmark.url" target="_blank"></a></li>
</template>
</ul>
#}
<section x-cloak x-show="$store.digimarks.showBookmarksCards" class="cards">
<template x-for="bookmark in $store.digimarks.filteredBookmarks" :key="bookmark.id">
<div class="card">
<div class="card-body">
<div class="card-thumb" x-show="bookmark.favicon"><img
x-bind:src="'/content/favicons/' + bookmark.favicon"></div>
<div class="statuses">
<div x-show="bookmark.starred" class="star"><i class="fa-fw fa-solid fa-star"></i>
</div>
<div x-show="bookmark.http_status !== 200 && bookmark.http_status !== 304"
class="error"><i
class="fa-fw fa-solid fa-triangle-exclamation"></i>
</div>
<div x-show="bookmark.note"><i class="fa-fw fa-regular fa-note-sticky"></i></div>
</div>
<div><a x-text="bookmark.title" x-bind:href="bookmark.url" target="_blank"></a></div>
</div>
<div class="card-footer">
<button title="show actions"><i class="fa-solid fa-square-caret-down"></i></button>
<div class="meta">
<template x-for="tag in bookmark.tag_list">
<span x-text="tag" class="chip"></span>
</template>
</div>
{# <div x-text="bookmark.created_date" class="meta"></div>#}
</div>
</div>
</template>
</section>
</section>
<section x-cloak x-show="$store.digimarks.showTags" x-transition.opacity>
<h1>Tags</h1>
<table>
<thead>
<tr>
<th>Tag</th>
<th>Public link</th>
<th>Number of bookmarks</th>
</tr>
</thead>
<tbody>
<template x-for="tag in $store.digimarks.filteredTags" :key="tag">
<tr>
<td x-text="tag"></td>
<td></td>
<td></td>
</tr>
</template>
</tbody>
</table>
</section>
<dialog x-cloak id="editFormDialog"
x-transition:enter="modal-enter"
x-transition:enter-start="modal-enter"
x-transition:enter-end="modal-enter-active"
x-transition:leave="modal-leave-active"
x-transition:leave-start="modal-enter-active"
x-transition:leave-end="modal-enter">
<h1>Add/Edit bookmark</h1>
{#
<div class="card-panel {{ theme.ERRORMESSAGE_BACKGROUND }}">
<span class="error">
{% 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>
#}
<form method="dialog">
<input type="text" name="">
<p>
<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>
</p>
<div>
<button value="cancel">Cancel</button>
<button @click="$store.digimarks.saveBookmark()">Save</button>
</div>
</form>
</dialog>
</main>
</article>
{% endblock %}

View File

@@ -1,59 +0,0 @@
/* label color */
.input-field label {
color: #000;
}
/* label focus color */
.input-field input[type=text]:focus + label {
color: #000;
}
/* label underline focus color */
.input-field input[type=text]:focus {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* valid color */
.input-field input[type=text].valid {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* invalid color */
.input-field input[type=text].invalid {
border-bottom: 1px solid #000;
box-shadow: 0 1px 0 0 #000;
}
/* icon prefix focus color */
.input-field .prefix.active {
color: #000;
}
/* Card title anchor colour */
.white-text .card-title a,
.white-text a
{
color: #FFF;
}
.chip a,
.white-text .chip a
{
color: #1b5e20; /* green darken-4 */
}
.card.tiny
{
height: 140px;
}
.card .card-reveal .digimark-card-header,
.card .digimark-card-header.activator
{
cursor: pointer;
/*display: block;*/
}
.card .digimark-card-content
{
padding-top: 10px;
}

View File

@@ -1,7 +0,0 @@
(function($){
$(function(){
$('.button-collapse').sideNav();
}); // end of document ready
})(jQuery); // end of jQuery name space

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,62 +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="#2e7d32" />
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#2e7d32">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<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/0.97.7/css/materialize.min.css" type="text/css" rel="stylesheet" media="screen,projection"/>
<link href="{{ url_for('static', filename='css/digimarks.css') }}" type="text/css" rel="stylesheet" media="screen,projection"/>
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
</head>
<body class="grey lighten-4">
<nav class="green darken-3" role="navigation">
<div class="nav-wrapper container"><a id="logo-container" href="{% if userkey %}{{ url_for('bookmarks', userkey=userkey) }}{% 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', userkey=userkey) }}">Tags</a></li>
<li><a href="{{ url_for('addbookmark', userkey=userkey) }}">Add bookmark</a></li>
{% endif %}
</ul>
<ul id="nav-mobile" class="side-nav">
{% if userkey %}
<li><a href="{{ url_for('tags', userkey=userkey) }}">Tags</a></li>
<li><a href="{{ url_for('addbookmark', userkey=userkey) }}">Add bookmark</a></li>
{% endif %}
</ul>
<a href="#" data-activates="nav-mobile" class="button-collapse"><i class="material-icons">menu</i></a>
</div>
</nav>
<div class="section no-pad-bot" id="index-banner">
<div class="container">
<div class="header grey-text lighten-5">
<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/0.97.7/js/materialize.min.js"></script>
<script src="{{ url_for('static', filename='js/init.js') }}"></script>
</body>
</html>

View File

@@ -1,127 +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', tagkey=publictag.tagkey) }}">Public link</a></div>
</div>
{% endif %}
{% 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">
<form action="{{ url_for('bookmarks', userkey=userkey) }}" method="POST">
<div class="input-field col l10 m10 s8">
<input placeholder="filter" type="text" name="filter" id="filter" value="{{ filter }}" class="validate" />
</div>
<div class="input-field col l2 m2 s4">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="submit">Filter</button></p>
</div>
</form>
</div>
{% if tags %}
<div class="row">
<p>
{% for tag in tags %}
<div class="chip">
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</p>
</div>
{% endif %}
<div class="row">
{% for bookmark in bookmarks %}
<div class="col s12 m6 l4">
{#
<div class="thumbnail">
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}">
<img style="width:450px;" src="{{ bookmark.image }}" />
</a>
<p><a href="{{ bookmark.url }}">{{ bookmark.url|urlize(25) }}</a></p>
<p>{{ bookmark.created_date.strftime("%m/%d/%Y %H:%M") }}</p>
</div>
#}
<div class="card tiny green darken-3">
<div class="card-content white-text">
<span class="digimark-card-header activator">
{% for tag in bookmark.tags_list %}
<div class="chip">
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
<i class="material-icons right">more_vert</i>
</span>
<div class="valign-wrapper digimark-card-content">
<div class="valign">
{% if bookmark.http_status != 200 %}
<i class="tiny material-icons" title="HTTP status {{ bookmark.http_status }}">report_problem</i>
{% endif %}
{% if bookmark.favicon %}
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" />&nbsp;
{% endif %}
<a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer">
{% if bookmark.starred == True %}
<i class="tiny material-icons yellow-text">star</i>
{% endif %}
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
[ no title ]
{% endif %}
</a></div>
</div>
</div>
<div class="card-reveal green darken-3">
<span class="card-title white-text valign-wrapper">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
<div class="white-text valign">
{% if bookmark.favicon %}
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" />&nbsp;&nbsp;&nbsp;
{% endif %}
<a href="{{ url_for('editbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" style="padding: 3px"><i class="tiny material-icons">mode_edit</i> EDIT</a>
</div>
</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>
<div class="fixed-action-btn" style="bottom: 45px; right: 24px;">
<a class="btn-floating btn-large red" href="{{ url_for('addbookmark', userkey=userkey) }}">
<i class="large material-icons">add</i>
</a>
</div>
{% endblock %}

View File

@@ -1,133 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ action }}{% endblock %}
{% block pageheader %}{{ action }}{% endblock %}
{% block pagecontent %}
{% if bookmark.http_status != 200 %}
<div class="row">
<div class="col s12">
<div class="card-panel red darken-1">
<span class="white-text">
{% if bookmark.http_status == 404 %}
<i class="material-icons">report_problem</i>&nbsp;&nbsp;URL not found (404), broken/outdated link?
{% 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 orange lighten-2">
<span class="white-text">
{{ message }}
</span>
</div>
</div>
</div>
{% endif %}
{% if formaction and formaction == 'edit' %}
<form action="{{ url_for('editingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
{% else %}
<form action="{{ url_for('addingbookmark', userkey=userkey) }}" method="POST">
{% endif %}
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">description</i>
<input placeholder="title" type="text" name="title" id="title" value="{{ bookmark.title }}" class="validate" />
<label for="title">Title</label>
</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 }}" class="validate" />
<label for="url">URL</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 }}" class="validate" />
<label for="tags">Tags</label>
</div>
</div>
{% if tags %}
<div class="row">
<div class="col s12">
{% for tag in tags %}
<div class="chip" id="tag_{{ tag }}">
{{ tag }}
</div>
{% endfor %}
</div>
{% for tag in tags %}
<script type="text/javascript">
$(function () {
$('#tag_{{ tag }}').on('click', function () {
var text = $('#tags');
text.val(text.val() + ', {{ tag }}');
});
});
</script>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="input-field col s12">
{#<i class="material-icons prefix">star</i>#}
<input type="checkbox" name="starred" id="starred" {% if bookmark.starred == True %}checked{% endif %} />
<label for="starred">Starred</label>
</div>
<div class="input-field col s12">
<input type="checkbox" name="strip" id="strip" />
<label for="strip">Strip parameters from url (like <em>?utm_source=social</em> - can break the link!)</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">Save <i class="material-icons right">send</i></button></p>
</div>
</form>
{% if bookmark.url_hash %}
<div class="input-field col l2 m3 s4">
<form action="{{ url_for('deletingbookmark', userkey=userkey, urlhash=bookmark.url_hash) }}" method="POST">
<p class="left-align"><button class="btn waves-effect waves-light" type="submit" name="delete">Delete <i class="material-icons right">delete</i></button></p>
</form>
</div>
{% endif %}
</div>
{% 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,56 +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">
{% for bookmark in bookmarks %}
<div class="col s12 m6 l4">
<div class="card green darken-3">
<div class="card-content white-text">
<span class="card-title activator"><i class="material-icons right">more_vert</i></span>
{% if bookmark.http_status != 200 %}
<i class="material-icons" title="HTTP status {{ bookmark.http_status }}">report_problem</i>
{% endif %}
{% if bookmark.favicon %}
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" />&nbsp;&nbsp;&nbsp;
{% endif %}
<p><a href="{{ bookmark.url }}" title="{{ bookmark.url }}" rel="noreferrer">
{% if bookmark.starred == True %}
<i class="material-icons">star</i>
{% endif %}
{% if bookmark.title %}
{{ bookmark.title }}
{% else %}
[ no title ]
{% endif %}
</a></p>
</div>
<div class="card-reveal green darken-3">
<span class="card-title white-text">Added @ {{ bookmark.created_date.strftime('%Y-%m-%d %H:%M') }}<i class="material-icons right">close</i></span>
<div class="white-text">
{% if bookmark.favicon %}
<img src="{{ url_for('static', filename='favicons/' + bookmark.favicon) }}" />&nbsp;&nbsp;&nbsp;
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -1,14 +0,0 @@
{% extends "base.html" %}
{% block title %}Tags{% endblock %}
{% block pageheader %}Tags{% endblock %}
{% block pagecontent %}
<div class="row">
{% for tag in tags %}
<div class="chip">
<a href="{{ url_for('tag', userkey=userkey, tag=tag) }}">{{ tag }}</a>
</div>
{% endfor %}
</div>
{% endblock %}

21
tox.ini Normal file
View File

@@ -0,0 +1,21 @@
[flake8]
ignore = D203, W503
exclude =
.git,
__pycache__,
docs/source/conf.py,
build,
dist,
example_config/gunicorn_webhaak_conf.py,
example_config/rq_settings.example.py,
example_config/settings.py,
max-line-length = 120
max-complexity = 10
[pycodestyle]
max_line_length = 120
ignore = E501, W503
[isort]
line_length = 120
multi_line_output = 3

10
wsgi.py
View File

@@ -1,10 +0,0 @@
# Activate virtualenv
import settings
activate_this = settings.VENV
execfile(activate_this, dict(__file__=activate_this))
from digimarks import app as application
if __name__ == "__main__":
# application is ran standalone
application.run(debug=settings.DEBUG)