Compare commits
9 Commits
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 50f325c710 | |
Tim Van Baak | 759cbb0346 | |
Tim Van Baak | ecb1afb634 | |
Tim Van Baak | b71a8d2a59 | |
Tim Van Baak | 7e0a95317c | |
Tim Van Baak | 0ae5a77844 | |
Tim Van Baak | a4adbff8f6 | |
Tim Van Baak | b424490003 | |
Tim Van Baak | f86db8753e |
|
@ -38,16 +38,16 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1717179513,
|
"lastModified": 1685566663,
|
||||||
"narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
|
"narHash": "sha256-btHN1czJ6rzteeCuE/PNrdssqYD2nIA4w48miQAFloM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
|
"rev": "4ecab3273592f27479a583fb6d975d4aba3486fe",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "24.05",
|
"ref": "23.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|
12
flake.nix
12
flake.nix
|
@ -2,7 +2,7 @@
|
||||||
description = "A personal feed aggregator";
|
description = "A personal feed aggregator";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
nixpkgs.url = "github:NixOS/nixpkgs/23.05";
|
||||||
# Included to support default.nix and shell.nix
|
# Included to support default.nix and shell.nix
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
url = "github:edolstra/flake-compat";
|
url = "github:edolstra/flake-compat";
|
||||||
|
@ -43,6 +43,16 @@
|
||||||
PS1="(develop) $PS1"
|
PS1="(develop) $PS1"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
client = let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in pkgs.mkShell {
|
||||||
|
packages = [
|
||||||
|
pkgs.nodejs_18
|
||||||
|
];
|
||||||
|
shellHook = ''
|
||||||
|
PS1="(client) $PS1"
|
||||||
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
overlays.default = final: prev: {
|
overlays.default = final: prev: {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "intake-client",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-scripts": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build"
|
||||||
|
},
|
||||||
|
"proxy": "http://localhost:5000",
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="icon" type="image/png" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAzAAAAADVpr6lAAAAOElEQVQ4y2NgoBAwMjAwMNQzMPwnR3MjAwMjC4zjS6LmzVCaBV2A7mDgw4CJUi8MvAGj0TgYwgAADKMLO3k0eaQAAAAASUVORK5CYII=">
|
||||||
|
<style>
|
||||||
|
main {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
border: 1px solid black; border-radius: 6px;
|
||||||
|
padding: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.item-title {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
.item-button {
|
||||||
|
font-size: 1em;
|
||||||
|
float:right;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.item-link {
|
||||||
|
text-decoration: none;
|
||||||
|
float:right;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.item-info {
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
article img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
button, summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
summary:focus {
|
||||||
|
outline: 1px dotted gray;
|
||||||
|
}
|
||||||
|
.strikethru span, .strikethru p {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
.fade span, .fade p {
|
||||||
|
color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
table.feed-control td {
|
||||||
|
font-family: monospace; padding: 5px 10px;
|
||||||
|
}
|
||||||
|
article.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=checkbox]{
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 3.8em;
|
||||||
|
padding-inline: 1em 0;
|
||||||
|
height: 1.2em;
|
||||||
|
background: grey;
|
||||||
|
display: inline-block;
|
||||||
|
border-radius: 0.6em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0.1em;
|
||||||
|
left: 0.1em;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
transition: 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + label {
|
||||||
|
background: #cc0000;
|
||||||
|
padding-inline: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:checked + label:after {
|
||||||
|
left: calc(100% - 0.1em);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
label:active:after {
|
||||||
|
width: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div id="root"></div>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { StrictMode, useState, useEffect } from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
|
||||||
|
function Item({ item }) {
|
||||||
|
|
||||||
|
function deactivate(source, itemid) {
|
||||||
|
console.log(`deactivate(${source}, ${itemid})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function punt(source, itemid) {
|
||||||
|
console.log(`punt(${source}, ${itemid})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAction(source, itemid, action) {
|
||||||
|
console.log(`doAction(${source}, ${itemid}, ${action})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
let classNames = !item.hidden ? "" : item.active ? "fade" : "strikethru fade";
|
||||||
|
return (
|
||||||
|
<article className={classNames} id={`${item.source}-${item.id}`}>
|
||||||
|
<button
|
||||||
|
className="item-button"
|
||||||
|
title="Deactivate"
|
||||||
|
onClick={() => deactivate(item.source, item.id)}>✕</button>
|
||||||
|
<button
|
||||||
|
className="item-button"
|
||||||
|
title="Punt to tomorrow"
|
||||||
|
onClick={() => punt(item.source, item.id)}>↷</button>
|
||||||
|
{item.link && <a className="item-link" href="#" target="_blank">⇗</a>}
|
||||||
|
{/* The item title is a clickable <details> if there is body content */}
|
||||||
|
{(item.body || item.actions) ? (
|
||||||
|
<details open>
|
||||||
|
<summary><span className="item-title">{item.title || item.id}</span></summary>
|
||||||
|
{item.body && (
|
||||||
|
<p dangerouslySetInnerHTML={{ __html: item.body }}></p>
|
||||||
|
)}
|
||||||
|
{item.actions && item.actions.map((action) => {
|
||||||
|
return (
|
||||||
|
<p key={`${item.source}-${item.id}-action-${action}`}>
|
||||||
|
<button onClick={() => doAction(item.source, item.id, action)}>
|
||||||
|
{action}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</details>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="item-title">{item.title || item.id}</span>
|
||||||
|
<br/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Author/time footer */}
|
||||||
|
{(item.author || item.time) && (
|
||||||
|
<>
|
||||||
|
<span className="item-info">{item.author} {item.time}</span><br/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Source/id/created footer */}
|
||||||
|
{(item.source || item.id || item.created) && (
|
||||||
|
<span className="item-info" title="Tags: TODO">
|
||||||
|
{item.source} {item.id} {item.created}
|
||||||
|
{item.ttl && "T"} {item.ttd && "D"} {item.tts && "S"}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [showHidden, setShowHidden] = useState(false);
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`http://localhost:5000/api/v1/items?hidden=${showHidden}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(newItems => setItems(newItems.items))
|
||||||
|
.catch(error => console.log(error));
|
||||||
|
}, [showHidden]);
|
||||||
|
|
||||||
|
// Button actions
|
||||||
|
function bulkDeactivate(items) {
|
||||||
|
if (confirm(`Deactivate ${items.length} items?`)) {
|
||||||
|
console.log(`bulkDeactivate(${items})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<article className="center">
|
||||||
|
<span className="item-title">
|
||||||
|
<a href="{{url_for('root')}}">Home</a>
|
||||||
|
<input
|
||||||
|
id="showHidden"
|
||||||
|
type="checkbox"
|
||||||
|
checked={showHidden}
|
||||||
|
onChange={(e) => setShowHidden(e.target.checked) }
|
||||||
|
className="toggle-checkbox"/>
|
||||||
|
<label htmlFor="showHidden">{showHidden ? "All" : "Active"}</label>
|
||||||
|
{/* {% if item_count > items|length -%} */}
|
||||||
|
{/* [<a {% if page_num is greaterthan(0) -%} href="{{ set_query(page=page_num - 1) }}" {%- endif %}>Prev</a> */}
|
||||||
|
{/* | */}
|
||||||
|
{/* <a {% if ((page_num + 1) * page_count) is lessthan(item_count) -%} href="{{ set_query(page=page_num + 1) }}" {%- endif %}>Next</a>] */}
|
||||||
|
{/* {%- endif %} */}
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
|
||||||
|
{items.map((item) => {
|
||||||
|
return <Item item={item} key={item.id}/>;
|
||||||
|
})}
|
||||||
|
|
||||||
|
{items.length == 0 && (
|
||||||
|
<article className="center">
|
||||||
|
<span className="item-title">Feed is empty</span>
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* {% if item_count > items|length %} */}
|
||||||
|
<article className="center">
|
||||||
|
<span className="item-title">
|
||||||
|
{/* <a {% if page_num is greaterthan(0) -%} href="{{ set_query(page=page_num - 1) }}" {%- endif %}>Prev</a> */}
|
||||||
|
|
|
||||||
|
{/* <a {% if ((page_num + 1) * page_count) is lessthan(item_count) -%} href="{{ set_query(page=page_num + 1) }}" {%- endif %}>Next</a> */}
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
{/* {% endif %} */}
|
||||||
|
|
||||||
|
<article className="center">
|
||||||
|
<button onClick={() => bulkDeactivate(items)}>Deactivate All</button>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
|
root.render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
119
intake/app.py
119
intake/app.py
|
@ -253,7 +253,7 @@ def action(source_name, item_id, action):
|
||||||
data_path: Path = current_app.config["INTAKE_DATA"]
|
data_path: Path = current_app.config["INTAKE_DATA"]
|
||||||
source = LocalSource(data_path, source_name)
|
source = LocalSource(data_path, source_name)
|
||||||
item = execute_action(source, item_id, action)
|
item = execute_action(source, item_id, action)
|
||||||
return jsonify(item._item)
|
return jsonify(item)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/edit/source/<string:name>", methods=["GET", "POST"])
|
@app.route("/edit/source/<string:name>", methods=["GET", "POST"])
|
||||||
|
@ -415,3 +415,120 @@ def _get_ttx_for_date(dt: datetime) -> int:
|
||||||
def wsgi():
|
def wsgi():
|
||||||
app.config["INTAKE_DATA"] = intake_data_dir()
|
app.config["INTAKE_DATA"] = intake_data_dir()
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Experimental API endpoints for the React frontend
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
def to_api_model(item: Item) -> dict:
|
||||||
|
"""
|
||||||
|
Convert an item to a JSON representation for the frontend.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"source": item.source.source_name,
|
||||||
|
"id": item["id"],
|
||||||
|
"created": item["created"],
|
||||||
|
"active": item["active"],
|
||||||
|
}
|
||||||
|
for unchanged_field in (
|
||||||
|
"title",
|
||||||
|
"author",
|
||||||
|
"body",
|
||||||
|
"link",
|
||||||
|
"time",
|
||||||
|
"tags",
|
||||||
|
"tts",
|
||||||
|
"ttl",
|
||||||
|
"ttd",
|
||||||
|
):
|
||||||
|
if val := item.get(unchanged_field):
|
||||||
|
result[unchanged_field] = val
|
||||||
|
if actions := item.get("action"):
|
||||||
|
result["actions"] = list(actions.keys())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/items")
|
||||||
|
# @auth_check
|
||||||
|
def items():
|
||||||
|
"""
|
||||||
|
Get multiple items according to a filter.
|
||||||
|
Supported filters:
|
||||||
|
- &channel=<channel>
|
||||||
|
- &source=<source>
|
||||||
|
- &hidden=<true|false>
|
||||||
|
- TODO &tags=<+tag,-tag,...>
|
||||||
|
- &count= and &page=
|
||||||
|
|
||||||
|
Returns a JSON response with
|
||||||
|
- count: number of items
|
||||||
|
- items: items as JSON
|
||||||
|
- prev: if there are previous pages, the previous page number
|
||||||
|
- next: if there are further pages, the next page number
|
||||||
|
"""
|
||||||
|
data_path: Path = current_app.config["INTAKE_DATA"]
|
||||||
|
|
||||||
|
# &channels and &sources may not both be specified
|
||||||
|
filter_channel = request.args.get("channel")
|
||||||
|
filter_source = request.args.get("source")
|
||||||
|
if filter_channel and filter_source:
|
||||||
|
response = jsonify({"error": "One of channel and source may be specified"})
|
||||||
|
response.status_code = 400
|
||||||
|
return response
|
||||||
|
|
||||||
|
source_names = []
|
||||||
|
|
||||||
|
# If the channel was specified, load the channel defs to get the sources
|
||||||
|
if filter_channel:
|
||||||
|
channels_config_path = data_path / "channels.json"
|
||||||
|
if not channels_config_path.exists():
|
||||||
|
abort(404)
|
||||||
|
channels = json.loads(channels_config_path.read_text(encoding="utf8"))
|
||||||
|
if filter_channel not in channels:
|
||||||
|
abort(404)
|
||||||
|
source_names = channels[filter_channel]
|
||||||
|
|
||||||
|
# If a source was specified, use that source
|
||||||
|
elif filter_source:
|
||||||
|
source_names = [filter_source]
|
||||||
|
|
||||||
|
# If neither was specified, use all sources
|
||||||
|
else:
|
||||||
|
source_names = [
|
||||||
|
child.name
|
||||||
|
for child in data_path.iterdir()
|
||||||
|
if (child / "intake.json").exists()
|
||||||
|
]
|
||||||
|
|
||||||
|
sources = [LocalSource(data_path, name) for name in source_names]
|
||||||
|
|
||||||
|
# Get the items, applying the hidden filter
|
||||||
|
show_hidden = request.args.get("hidden") == "true"
|
||||||
|
all_items = sorted(
|
||||||
|
[
|
||||||
|
item
|
||||||
|
for source in sources
|
||||||
|
for item in source.get_all_items()
|
||||||
|
if not item.is_hidden or show_hidden
|
||||||
|
],
|
||||||
|
key=item_sort_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply paging filters
|
||||||
|
count = int(request.args.get("count", "100"))
|
||||||
|
page = int(request.args.get("page", "0"))
|
||||||
|
paged_items = all_items[count * page : count * page + count]
|
||||||
|
|
||||||
|
# Return the result set
|
||||||
|
response_params = {
|
||||||
|
"count": len(paged_items),
|
||||||
|
"items": list(map(to_api_model, paged_items)),
|
||||||
|
}
|
||||||
|
if page > 0:
|
||||||
|
response_params["prev"] = page - 1
|
||||||
|
if (count * page + count) < len(all_items):
|
||||||
|
response_params["next"] = page + 1
|
||||||
|
response = jsonify(response_params)
|
||||||
|
return response
|
||||||
|
|
|
@ -172,7 +172,7 @@ def cmd_action(cmd_args):
|
||||||
source = LocalSource(data_path, args.source)
|
source = LocalSource(data_path, args.source)
|
||||||
try:
|
try:
|
||||||
item = execute_action(source, args.item, args.action, 5)
|
item = execute_action(source, args.item, args.action, 5)
|
||||||
print("Item:", item._item, file=sys.stderr)
|
print("Item:", item, file=sys.stderr)
|
||||||
except InvalidConfigException as ex:
|
except InvalidConfigException as ex:
|
||||||
print("Could not fetch", args.source, file=sys.stderr)
|
print("Could not fetch", args.source, file=sys.stderr)
|
||||||
print(ex, file=sys.stderr)
|
print(ex, file=sys.stderr)
|
||||||
|
|
|
@ -53,7 +53,6 @@ def update_crontab_entries(data_path: Path):
|
||||||
section_found = False
|
section_found = False
|
||||||
in_section = False
|
in_section = False
|
||||||
for i in range(len(crontab_lines)):
|
for i in range(len(crontab_lines)):
|
||||||
|
|
||||||
if not section_found and crontab_lines[i] == INTAKE_CRON_BEGIN:
|
if not section_found and crontab_lines[i] == INTAKE_CRON_BEGIN:
|
||||||
section_found = True
|
section_found = True
|
||||||
in_section = True
|
in_section = True
|
||||||
|
|
|
@ -35,7 +35,6 @@ article {
|
||||||
}
|
}
|
||||||
article img {
|
article img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
article textarea {
|
article textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -35,7 +35,6 @@ article {
|
||||||
}
|
}
|
||||||
article img {
|
article img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
button, summary {
|
button, summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -19,7 +19,6 @@ article {
|
||||||
}
|
}
|
||||||
article img {
|
article img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
|
||||||
}
|
}
|
||||||
button, summary {
|
button, summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -54,7 +53,7 @@ summary:focus {
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
<details>
|
<details open>
|
||||||
<summary><span class="item-title">Sources</span></summary>
|
<summary><span class="item-title">Sources</span></summary>
|
||||||
{% if not sources %}
|
{% if not sources %}
|
||||||
<p>No sources found.</p>
|
<p>No sources found.</p>
|
||||||
|
|
|
@ -57,7 +57,7 @@ in {
|
||||||
let
|
let
|
||||||
# Define the intake package and a python environment to run it from
|
# Define the intake package and a python environment to run it from
|
||||||
intake = intakeCfg.package;
|
intake = intakeCfg.package;
|
||||||
pythonEnv = pkgs.python3.withPackages (pypkgs: [ intake ]);
|
pythonEnv = pkgs.python38.withPackages (pypkgs: [ intake ]);
|
||||||
|
|
||||||
# Assign each user an internal port for their personal intake instance
|
# Assign each user an internal port for their personal intake instance
|
||||||
enabledUsers = filterAttrs (userName: userCfg: userCfg.enable) intakeCfg.users;
|
enabledUsers = filterAttrs (userName: userCfg: userCfg.enable) intakeCfg.users;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "intake"
|
name = "intake"
|
||||||
version = "1.1.0"
|
version = "1.0.4"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
intake = "intake.cli:main"
|
intake = "intake.cli:main"
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse, json, sys, time
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("action")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("args:", args, file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
def item(i):
|
||||||
|
print(json.dumps(i))
|
||||||
|
|
||||||
|
if args.action == "fetch":
|
||||||
|
item({
|
||||||
|
"id": "item1-only-id",
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item2-title",
|
||||||
|
"title": "This item has a title",
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item3-body",
|
||||||
|
"title": "This item has a title and body",
|
||||||
|
"body": "<p>Hello, intake! This is an <b>item</b> body.</p>",
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item4-action",
|
||||||
|
"title": "This item has an action but no body",
|
||||||
|
"action": {
|
||||||
|
"action1": None
|
||||||
|
},
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item5-actions",
|
||||||
|
"title": "This item has two actions and a body",
|
||||||
|
"body": "<p>This is body text.</p>",
|
||||||
|
"action": {
|
||||||
|
"action1": None,
|
||||||
|
"action2": None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item6-footer",
|
||||||
|
"title": "No body text but all footer fields",
|
||||||
|
"author": "Authorname",
|
||||||
|
"time": int(time.time()),
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item7-footer",
|
||||||
|
"title": "Body text and all footer fields",
|
||||||
|
"author": "Authorname",
|
||||||
|
"time": int(time.time()),
|
||||||
|
"body": "<p>This is body text.</p>",
|
||||||
|
})
|
||||||
|
item({
|
||||||
|
"id": "item8-link",
|
||||||
|
"title": "Item with a link",
|
||||||
|
"link": "#",
|
||||||
|
})
|
||||||
|
|
||||||
|
if args.action == "action1":
|
||||||
|
item = sys.stdin.readline()
|
||||||
|
item = json.loads(item)
|
||||||
|
print(json.dumps(item))
|
||||||
|
|
||||||
|
if args.action == "action2":
|
||||||
|
item = sys.stdin.readline()
|
||||||
|
item = json.loads(item)
|
||||||
|
print(json.dumps(item))
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"action": {
|
||||||
|
"fetch": {
|
||||||
|
"exe": "./command.py",
|
||||||
|
"args": [
|
||||||
|
"fetch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action1": {
|
||||||
|
"exe": "./command.py",
|
||||||
|
"args": [
|
||||||
|
"action1"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action2": {
|
||||||
|
"exe": "./command.py",
|
||||||
|
"args": [
|
||||||
|
"action2"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue