Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c748241291 | ||
|
|
95accdf436 | ||
|
|
9deb28330a | ||
|
|
2e5610555e |
@@ -5,7 +5,7 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 1804 1712" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-669.006,-255.89)">
|
||||
<g transform="matrix(0.972876,0,0,1.26057,-24.1588,-587.485)">
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(88,91,112);"/>
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(24, 24, 37);"/>
|
||||
</g>
|
||||
<g transform="matrix(5.14628,0,0,6.12686,41.2191,-8673.98)">
|
||||
<path d="M141.95,1644.94L141.95,1633.94C157.554,1633.4 169.724,1631.04 178.462,1626.86C187.2,1622.69 193.344,1615.8 196.894,1606.21C200.444,1596.61 202.218,1583.47 202.218,1566.77C202.218,1554.44 201.146,1544.16 199,1535.93C196.855,1527.7 193.285,1521.25 188.292,1516.57C185.016,1513.6 181.154,1511.26 176.707,1509.54C172.26,1507.83 167.15,1506.62 161.376,1505.92C155.603,1505.21 149.128,1504.86 141.95,1504.86L141.95,1493.86L384.076,1493.86L384.076,1504.86C373.388,1504.86 364.221,1505.7 356.575,1507.38C348.929,1509.06 342.708,1512.12 337.909,1516.57C333.111,1521.01 329.581,1527.27 327.319,1535.35C325.056,1543.42 323.925,1553.9 323.925,1566.77L323.925,1633.36C339.06,1633.36 353.532,1631.08 367.341,1626.51C381.15,1621.95 393.399,1616 404.087,1608.66C414.776,1601.41 423.299,1593.3 429.657,1584.32C436.016,1575.35 439.546,1566.61 440.248,1558.11L452.536,1558.11C452.536,1562.4 452.419,1568.35 452.185,1575.96C451.951,1583.56 451.6,1591.85 451.132,1600.82C450.586,1609.95 449.942,1618.34 449.201,1625.99C448.46,1633.63 447.582,1639.95 446.568,1644.94L141.95,1644.94Z" style="fill:rgb(245,224,220);fill-rule:nonzero;"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -225,7 +225,7 @@ For a real deployment, start with the docker-compose-example.yaml file. replace
|
||||
|
||||
## Mastodon Configuration
|
||||
|
||||
After logging in, open <http://localhost:8000/profile>, enter the Mastodon instance, and select **Connect Mastodon**. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are `read:accounts` and `write:statuses`.
|
||||
After logging in, open <http://localhost:8000/profile>, enter the Mastodon instance, and select **Connect Mastodon**. LinkLog registers an OAuth application on that instance, opens Mastodon authorization, and stores the returned per-user access token after the callback. The requested scopes are `read:accounts` and `write:statuses`. Posts use the configured prefix followed directly by the title, an optional comment, a `from: URL` line when a title exists, and tags on the final line, with blank lines between sections.
|
||||
|
||||
- **Instance**: hostname or URL such as `mastodon.social` or `https://mastodon.social`
|
||||
- **Post prefix**: text placed immediately before the link; defaults to `From my #LinkLog: `
|
||||
|
||||
@@ -628,3 +628,45 @@ Changed Mastodon status publishing to form-encoded API requests, added safe conf
|
||||
|
||||
### Assistant outcome
|
||||
Removed the duplicate `TRAEFIK_HOST` configuration, changed both Traefik router rules to use `LINKLOG_PUBLIC_URL`, standardized the value as a host name, and normalized scheme-less values to valid HTTP/HTTPS backend callback URLs. Compose validation, URL normalization, and the complete Dockerized backend suite passed with 22 tests.
|
||||
|
||||
### User
|
||||
Format Mastodon posts as `From my #LinkLog:`, the web page title, an optional comment, and `from: URL`.
|
||||
|
||||
### Assistant outcome
|
||||
Updated the Mastodon formatter to emit the configured prefix, title, optional comment, and source URL as blank-line-separated blocks. Tags remain on the source URL line. The focused posting test and complete Dockerized backend suite passed with 22 tests, and the running container is healthy on port 5469.
|
||||
|
||||
### User
|
||||
Only print the `from` line if there is no title. Put the title directly behind the colon, and put all tags on the last line.
|
||||
|
||||
### Assistant outcome
|
||||
Updated Mastodon formatting to place the title directly after `From my #LinkLog:`, omit the `from:` line when no title is available, and place tags on a separate final line. Added no-title regression coverage. The focused tests passed and the complete Dockerized backend suite passed with 23 tests.
|
||||
|
||||
### User
|
||||
Do not show edit buttons on the home page. On `/<user>/`, show the edit button on the right and add a delete button.
|
||||
|
||||
### Assistant outcome
|
||||
Added an owner-scoped `DELETE /api/links/{link_id}` endpoint. Feed edit/delete controls now render only on user-specific pages, never on the home feed, and are right-aligned in an entry action group. Added owner-only deletion regression coverage and updated the feed cache key. The complete Dockerized backend suite passed with 23 tests, and the running container is healthy.
|
||||
|
||||
### User
|
||||
Put the buttons on the right-hand side of the link item, make them smaller, and keep the same color scheme.
|
||||
|
||||
### Assistant outcome
|
||||
Changed the entry action group to float at the right edge of user-specific link items, allowing content to wrap around it. Reduced Edit and Delete button padding, radius, and font size while preserving Catppuccin surface and danger colors.
|
||||
|
||||
### User
|
||||
Put the Mastodon button on the lower-right corner of the log entry.
|
||||
|
||||
### Assistant outcome
|
||||
Split the Mastodon control from the Edit/Delete action group and positioned it at the lower-right corner of user-specific link entries. Reserved bottom space in each entry to prevent overlap.
|
||||
|
||||
### User
|
||||
Change the Mastodon button location to immediately below the Edit and Delete buttons.
|
||||
|
||||
### Assistant outcome
|
||||
Changed the Mastodon button from absolute lower-right positioning to normal right-floated flow with `clear: right`, placing it directly beneath the Edit/Delete row while preserving the existing logo and posted-state styling.
|
||||
|
||||
### User
|
||||
When an authorized user is on their `/<user>/` page, show a button to post on Mastodon. After a message has been posted, keep the button functional but change its color and use the Mastodon logo inside it.
|
||||
|
||||
### Assistant outcome
|
||||
Added persisted Mastodon post state, an owner-scoped repost endpoint, and an owner-only Mastodon button on user-specific pages. The button includes a bundled Mastodon logo, says `Post to Mastodon` before posting, changes to the posted teal state and `Post again` afterward, and remains functional for reposting. Home-page entries remain free of controls. The complete Dockerized backend suite passed with 23 tests and the running container is healthy.
|
||||
|
||||
@@ -115,6 +115,13 @@
|
||||
109. use VIVE that is in the current directory
|
||||
110. Posting to mastodon seems to fail, try to fix, add debug logging
|
||||
111. LINKLOG_PUBLIC_URL and TRAEFIK_HOST are the same and can be merged. (use LINKLOG_PUBLIC_URL), fix docker-compose to use said variable
|
||||
112. Format mastodon posts like From my #LinkLog: Title, optional comment, and from: URL
|
||||
113. Only print the 'from' line if there is no title. Put the title directly behind the colon, and put all tags on the last line.
|
||||
114. In the frontend do not show edit buttons on the home page even when a user is logged in. On the /<user>/ page show the edit button on the right of the entry. Also add a delete button.
|
||||
115. Put the buttons on the right hand side of the link item, make the buttons smaller and same color scheme
|
||||
116. Put the mastodon button on the lower right corner of the log entry
|
||||
117. Change that location to be immediately below the edit and delete button
|
||||
116. When the user is authorized and on its /<user>/ page show a Mastodon post button with logo; after posting keep it functional but change its color.
|
||||
|
||||
## Future entries
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ from fastapi import APIRouter, Header, HTTPException, status
|
||||
import logging
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.services.link_service import create_link, list_public_links, list_tags, update_link
|
||||
from backend.app.services.link_service import create_link, delete_link, get_link_tags, list_public_links, list_tags, mark_mastodon_posted, update_link
|
||||
from backend.app.database import get_connection
|
||||
from backend.app.services.plugin_manager import plugin_manager
|
||||
from backend.app.services.token_service import validate_token
|
||||
|
||||
@@ -47,6 +48,9 @@ def create_link_endpoint(payload: LinkCreate, authorization: str | None = Header
|
||||
except ValueError as error:
|
||||
raise HTTPException(status_code=422, detail=str(error)) from error
|
||||
plugin_results = plugin_manager.dispatch({'type': 'link_created', **record})
|
||||
mastodon_result = next((result for result in plugin_results if result.get('plugin') == 'mastodon'), None)
|
||||
if mastodon_result and mastodon_result.get('status') == 'posted':
|
||||
mark_mastodon_posted(record['id'], info['user_id'], mastodon_result.get('post_id'))
|
||||
if any(result.get('status') == 'failed' for result in plugin_results):
|
||||
logger.warning('One or more plugins failed for link_id=%s results=%s', record['id'], plugin_results)
|
||||
return record
|
||||
@@ -73,6 +77,47 @@ def update_link_endpoint(
|
||||
return record
|
||||
|
||||
|
||||
@router.delete('/links/{link_id}')
|
||||
def delete_link_endpoint(
|
||||
link_id: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not authorization or not authorization.startswith('Bearer '):
|
||||
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
||||
info = validate_token(authorization.replace('Bearer ', '', 1))
|
||||
if info is None:
|
||||
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
||||
if not delete_link(link_id, info['user_id']):
|
||||
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
||||
return {'status': 'deleted', 'id': link_id}
|
||||
|
||||
|
||||
@router.post('/links/{link_id}/mastodon')
|
||||
def post_link_to_mastodon(
|
||||
link_id: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not authorization or not authorization.startswith('Bearer '):
|
||||
raise HTTPException(status_code=401, detail='Missing or invalid Authorization header')
|
||||
info = validate_token(authorization.replace('Bearer ', '', 1))
|
||||
if info is None:
|
||||
raise HTTPException(status_code=401, detail='Token expired or invalid')
|
||||
with get_connection() as conn:
|
||||
row = conn.execute('SELECT * FROM links WHERE id = ? AND user_id = ?', (link_id, info['user_id'])).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail='Link not found or not owned by user')
|
||||
event = dict(row)
|
||||
with get_connection() as conn:
|
||||
event['tags'] = get_link_tags(conn, link_id)
|
||||
result = plugin_manager.post_to_mastodon({'type': 'link_created', **event})
|
||||
if result.get('status') == 'posted':
|
||||
mark_mastodon_posted(link_id, info['user_id'], result.get('post_id'))
|
||||
return {'status': 'posted', 'post_id': result.get('post_id')}
|
||||
if result.get('status') == 'skipped':
|
||||
raise HTTPException(status_code=409, detail='Mastodon is not enabled or configured')
|
||||
raise HTTPException(status_code=502, detail=result.get('reason', 'Mastodon post failed'))
|
||||
|
||||
|
||||
@router.get('/links')
|
||||
def list_links():
|
||||
return list_public_links()
|
||||
|
||||
@@ -43,6 +43,7 @@ def public_feed(
|
||||
'is_owner': item['user_id'] == current_user_id,
|
||||
'can_edit': item['user_id'] == current_user_id,
|
||||
'created_at': item['created_at'],
|
||||
'mastodon_posted': bool(item['mastodon_posted']),
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
|
||||
@@ -151,6 +151,11 @@ CREATE TABLE IF NOT EXISTS mastodon_oauth_states (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_mastodon_oauth_states_state_hash
|
||||
ON mastodon_oauth_states(state_hash);
|
||||
'''),
|
||||
(9, '''
|
||||
ALTER TABLE links ADD COLUMN mastodon_posted INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE links ADD COLUMN mastodon_post_id TEXT;
|
||||
ALTER TABLE links ADD COLUMN mastodon_posted_at TEXT;
|
||||
'''),
|
||||
]
|
||||
|
||||
|
||||
@@ -157,6 +157,28 @@ def update_link(
|
||||
return record
|
||||
|
||||
|
||||
def delete_link(link_id: str, user_id: str) -> bool:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
'DELETE FROM links WHERE id = ? AND user_id = ?',
|
||||
(link_id, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def mark_mastodon_posted(link_id: str, user_id: str, post_id: str | None) -> bool:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
'''UPDATE links
|
||||
SET mastodon_posted = 1, mastodon_post_id = ?, mastodon_posted_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND user_id = ?''',
|
||||
(post_id, link_id, user_id),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
|
||||
|
||||
def list_public_users():
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
|
||||
@@ -59,18 +59,19 @@ class MastodonPlugin(BasePlugin):
|
||||
)
|
||||
return {'status': 'skipped', 'plugin': self.name, 'reason': 'not_configured'}
|
||||
|
||||
status_parts = [event.get('title') or event.get('url', '')]
|
||||
if event.get('comment'):
|
||||
status_parts.append(event['comment'])
|
||||
post_prefix = config.get('post_prefix')
|
||||
if post_prefix is None and config.get('hashtag'):
|
||||
post_prefix = f'#{str(config["hashtag"]).strip().lstrip("#")} '
|
||||
post_prefix = str(post_prefix if post_prefix is not None else DEFAULT_POST_PREFIX)
|
||||
status = f'{post_prefix}{event.get("url", "")}'.strip()
|
||||
post_prefix = str(post_prefix if post_prefix is not None else DEFAULT_POST_PREFIX).strip()
|
||||
title = str(event.get('title') or '').strip()
|
||||
status_parts = [f'{post_prefix} {title}'.strip()]
|
||||
if event.get('comment'):
|
||||
status_parts.append(event['comment'])
|
||||
if title:
|
||||
status_parts.append(f'from: {event.get("url", "")}')
|
||||
if event.get('tags'):
|
||||
status = f'{status} {" ".join(event["tags"])}'
|
||||
status_parts.append(status)
|
||||
post_body = '\n'.join(status_parts)
|
||||
status_parts.append(' '.join(event['tags']))
|
||||
post_body = '\n\n'.join(status_parts)
|
||||
endpoint = f'{instance}/api/v1/statuses'
|
||||
logger.debug(
|
||||
'Posting link to Mastodon: endpoint=%s user_id=%s event_id=%s body_length=%d',
|
||||
@@ -149,5 +150,12 @@ class PluginManager:
|
||||
logger.debug('Plugin dispatch result: plugin=%s event_id=%s result=%s', plugin.name, event.get('id'), result)
|
||||
return results
|
||||
|
||||
def post_to_mastodon(self, event):
|
||||
self.refresh_from_db()
|
||||
plugin = next(plugin for plugin in self.plugins if plugin.name == 'mastodon')
|
||||
if not plugin.enabled:
|
||||
return {'status': 'skipped', 'plugin': 'mastodon', 'reason': 'disabled'}
|
||||
return plugin.handle_event(event)
|
||||
|
||||
|
||||
plugin_manager = PluginManager()
|
||||
|
||||
@@ -6,7 +6,7 @@ import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import parse_qs
|
||||
from uuid import uuid4
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
@@ -334,6 +334,11 @@ def test_only_link_owner_can_edit_link():
|
||||
})
|
||||
assert denied.status_code == 404
|
||||
|
||||
deleted = client.delete(f'/api/links/{link_id}', headers=owner_headers)
|
||||
assert deleted.status_code == 200
|
||||
assert client.delete(f'/api/links/{link_id}', headers=owner_headers).status_code == 404
|
||||
assert client.delete(f'/api/links/{link_id}', headers=login_headers('bob')).status_code == 404
|
||||
|
||||
|
||||
def test_logout_revokes_token_and_admin_can_list_plugins():
|
||||
headers = login_headers()
|
||||
@@ -396,7 +401,9 @@ def test_public_and_admin_pages_render_html():
|
||||
assert '<a id="auth-username" class="user-name" href="/">' in admin_page
|
||||
assert 'admin.js?v=5' in admin_page
|
||||
feed_script = client.get('/static/feed.js?v=7').text
|
||||
assert 'if (item.is_owner)' in feed_script
|
||||
assert 'if (item.is_owner && !showIdentity)' in feed_script
|
||||
assert 'deleteEntry(item, deleteButton)' in feed_script
|
||||
assert 'postToMastodon(item, mastodonButton)' in feed_script
|
||||
assert 'tag.toLowerCase() === pref.tag.toLowerCase()' in feed_script
|
||||
assert 'return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`' in feed_script
|
||||
assert 'edit-tag-options' in feed_script
|
||||
@@ -445,13 +452,36 @@ def test_link_submission_posts_to_enabled_mastodon_plugin():
|
||||
assert received['path'] == '/api/v1/statuses'
|
||||
assert received['authorization'] == 'Bearer test-token'
|
||||
assert received['content_type'] == 'application/x-www-form-urlencoded'
|
||||
assert received['body'] == {'status': ['A useful page\nWorth sharing\nFrom my #LinkLog: https://example.com/useful #python #web']}
|
||||
assert received['body'] == {'status': ['From my #LinkLog: A useful page\n\nWorth sharing\n\nfrom: https://example.com/useful\n\n#python #web']}
|
||||
posted_item = next(item for item in client.get('/api/public/feed/alice', headers=headers).json() if item['id'] == response.json()['id'])
|
||||
assert posted_item['mastodon_posted'] is True
|
||||
finally:
|
||||
server.shutdown()
|
||||
thread.join()
|
||||
server.server_close()
|
||||
|
||||
|
||||
def test_mastodon_post_without_title_omits_source_line():
|
||||
from backend.app.services.plugin_manager import MastodonPlugin
|
||||
|
||||
response = MagicMock()
|
||||
response.__enter__.return_value.read.return_value = b'{"id":"status-2"}'
|
||||
plugin = MastodonPlugin()
|
||||
plugin.initialize({'instance': 'https://mastodon.example', 'access_token': 'test-token'})
|
||||
|
||||
with patch('backend.app.services.plugin_manager.urlopen', return_value=response) as open_url:
|
||||
result = plugin.handle_event({
|
||||
'url': 'https://example.com/useful',
|
||||
'title': '',
|
||||
'comment': 'A comment',
|
||||
'tags': ['#tag'],
|
||||
})
|
||||
|
||||
body = parse_qs(open_url.call_args.args[0].data.decode())['status'][0]
|
||||
assert result['status'] == 'posted'
|
||||
assert body == 'From my #LinkLog:\n\nA comment\n\n#tag'
|
||||
|
||||
|
||||
def test_plugin_config_can_be_saved_for_mastodon():
|
||||
headers = login_headers()
|
||||
assert client.put('/api/user/plugins/mastodon', headers=headers, json={
|
||||
|
||||
@@ -10,7 +10,7 @@ def test_database_migrations_are_versioned_and_idempotent():
|
||||
connection = sqlite3.connect(':memory:')
|
||||
|
||||
apply_migrations(connection)
|
||||
assert get_schema_version(connection) == 8
|
||||
assert get_schema_version(connection) == 9
|
||||
tables = {
|
||||
row[0]
|
||||
for row in connection.execute(
|
||||
@@ -27,6 +27,6 @@ def test_database_migrations_are_versioned_and_idempotent():
|
||||
assert set(DEFAULT_TAGS) <= seeded_tags
|
||||
|
||||
apply_migrations(connection)
|
||||
assert get_schema_version(connection) == 8
|
||||
assert get_schema_version(connection) == 9
|
||||
|
||||
connection.close()
|
||||
@@ -117,13 +117,30 @@ function renderFeed(items, showIdentity = true) {
|
||||
meta.className = 'meta';
|
||||
meta.textContent = formatDate(item.created_at);
|
||||
|
||||
if (item.is_owner) {
|
||||
if (item.is_owner && !showIdentity) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'entry-actions';
|
||||
const editButton = document.createElement('button');
|
||||
editButton.type = 'button';
|
||||
editButton.className = 'edit-button';
|
||||
editButton.textContent = 'Edit';
|
||||
editButton.addEventListener('click', () => showEditForm(article, item));
|
||||
article.appendChild(editButton);
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.type = 'button';
|
||||
deleteButton.className = 'delete-button';
|
||||
deleteButton.textContent = 'Delete';
|
||||
deleteButton.addEventListener('click', () => deleteEntry(item, deleteButton));
|
||||
const mastodonButton = document.createElement('button');
|
||||
mastodonButton.type = 'button';
|
||||
mastodonButton.className = `mastodon-button${item.mastodon_posted ? ' posted' : ''}`;
|
||||
const mastodonLogo = document.createElement('img');
|
||||
mastodonLogo.src = '/static/mastodon.svg';
|
||||
mastodonLogo.alt = '';
|
||||
mastodonButton.append(mastodonLogo, document.createTextNode(item.mastodon_posted ? 'Post again' : 'Post to Mastodon'));
|
||||
mastodonButton.addEventListener('click', () => postToMastodon(item, mastodonButton));
|
||||
actions.append(editButton, deleteButton);
|
||||
article.appendChild(actions);
|
||||
article.appendChild(mastodonButton);
|
||||
}
|
||||
|
||||
if (showIdentity) {
|
||||
@@ -144,6 +161,33 @@ function renderFeed(items, showIdentity = true) {
|
||||
});
|
||||
}
|
||||
|
||||
async function postToMastodon(item, button) {
|
||||
button.disabled = true;
|
||||
const response = await fetch(`/api/links/${encodeURIComponent(item.id)}/mastodon`, {
|
||||
method: 'POST',
|
||||
headers: {Authorization: `Bearer ${accessToken}`},
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadFeed();
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteEntry(item, button) {
|
||||
if (!window.confirm(`Delete this link?`)) return;
|
||||
button.disabled = true;
|
||||
const response = await fetch(`/api/links/${encodeURIComponent(item.id)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {Authorization: `Bearer ${accessToken}`},
|
||||
});
|
||||
if (response.ok) {
|
||||
await loadFeed();
|
||||
} else {
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showEditForm(article, item) {
|
||||
if (article.querySelector('.edit-form')) return;
|
||||
const form = document.createElement('form');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 1804 1712" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-669.006,-255.89)">
|
||||
<g transform="matrix(0.972876,0,0,1.26057,-24.1588,-587.485)">
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(88,91,112);"/>
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(24, 24, 37);"/>
|
||||
</g>
|
||||
<g transform="matrix(5.14628,0,0,6.12686,41.2191,-8673.98)">
|
||||
<path d="M141.95,1644.94L141.95,1633.94C157.554,1633.4 169.724,1631.04 178.462,1626.86C187.2,1622.69 193.344,1615.8 196.894,1606.21C200.444,1596.61 202.218,1583.47 202.218,1566.77C202.218,1554.44 201.146,1544.16 199,1535.93C196.855,1527.7 193.285,1521.25 188.292,1516.57C185.016,1513.6 181.154,1511.26 176.707,1509.54C172.26,1507.83 167.15,1506.62 161.376,1505.92C155.603,1505.21 149.128,1504.86 141.95,1504.86L141.95,1493.86L384.076,1493.86L384.076,1504.86C373.388,1504.86 364.221,1505.7 356.575,1507.38C348.929,1509.06 342.708,1512.12 337.909,1516.57C333.111,1521.01 329.581,1527.27 327.319,1535.35C325.056,1543.42 323.925,1553.9 323.925,1566.77L323.925,1633.36C339.06,1633.36 353.532,1631.08 367.341,1626.51C381.15,1621.95 393.399,1616 404.087,1608.66C414.776,1601.41 423.299,1593.3 429.657,1584.32C436.016,1575.35 439.546,1566.61 440.248,1558.11L452.536,1558.11C452.536,1562.4 452.419,1568.35 452.185,1575.96C451.951,1583.56 451.6,1591.85 451.132,1600.82C450.586,1609.95 449.942,1618.34 449.201,1625.99C448.46,1633.63 447.582,1639.95 446.568,1644.94L141.95,1644.94Z" style="fill:rgb(245,224,220);fill-rule:nonzero;"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Mastodon">
|
||||
<path fill="currentColor" d="M21.6 8.2c0-4.1-2.7-5.3-2.7-5.3C17.5 2.3 14.8 2 12 2h-.1c-2.8 0-5.5.3-6.9.9 0 0-2.7 1.2-2.7 5.3 0 .9 0 2 0 3.1 0 4.2.3 8.3 1.8 9.4 1.7 1.3 4.1 1.6 6.1 1.7 1.9.1 3.6-.4 3.6-.4l-.1-1.8s-1.6.5-3.5.4c-1.8-.1-3.6-.2-3.9-2.1-.1-.5-.1-1-.1-1.5 0 0 1.7.4 3.9.5 1.3.1 2.6.1 3.9-.1 2.5-.3 4.7-.8 4.7-.8v-1.8c0-1.4 0-2.8 0-3.1 0-4.1-2.7-5.3-2.7-5.3-1.4-.6-3.8-.9-6-.9h-.1c-2.2 0-4.6.3-6 .9 0 0-2.7 1.2-2.7 5.3v.1h3.3v-.1c0-1.7.7-2.1.7-2.1.4-.2 1.1-.3 1.8-.3.7 0 1.4.2 1.8.4.7.4 1.1 1.2 1.1 2.3v.5c-1.1-.3-2.3-.4-3.5-.4-2.2 0-3.3.8-3.3.8v3.2h3.3v-1.5c.4-.2 1.1-.3 1.9-.3.8 0 1.5.1 1.9.3v1.5h3.3V9.5c0-1.1.4-1.9 1.1-2.3.4-.2 1.1-.4 1.8-.4.7 0 1.4.1 1.8.3 0 0 .7.4.7 2.1v.1h3.3V8.2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 803 B |
@@ -52,13 +52,14 @@ body::selection {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
|
||||
.site-header {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
padding: 56px 0 48px;
|
||||
padding: 6px 0 8px;
|
||||
background:
|
||||
linear-gradient(115deg, rgba(203, 166, 247, 0.18), transparent 45%),
|
||||
var(--mantle);
|
||||
linear-gradient(115deg, var(--mantle), transparent 45%),
|
||||
rgba(203, 166, 247, 0.18);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -420,11 +421,56 @@ button:disabled {
|
||||
|
||||
.edit-button {
|
||||
min-width: 0;
|
||||
margin-top: 14px;
|
||||
padding: 8px 12px;
|
||||
padding: 5px 9px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-1);
|
||||
border-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.entry-actions {
|
||||
display: flex;
|
||||
float: right;
|
||||
gap: 8px;
|
||||
margin: 0 0 8px 16px;
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
min-width: 0;
|
||||
padding: 5px 9px;
|
||||
border-radius: 6px;
|
||||
border-color: rgba(243, 139, 168, 0.45);
|
||||
background: transparent;
|
||||
color: var(--red);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.mastodon-button {
|
||||
clear: right;
|
||||
float: right;
|
||||
margin: 0 0 8px 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 5px 9px;
|
||||
border-radius: 6px;
|
||||
background: var(--surface-1);
|
||||
border-color: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.mastodon-button.posted {
|
||||
background: var(--teal);
|
||||
border-color: var(--teal);
|
||||
color: var(--crust);
|
||||
}
|
||||
|
||||
.mastodon-button img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
@@ -475,7 +521,7 @@ button:disabled {
|
||||
}
|
||||
|
||||
.link-item {
|
||||
padding: 22px;
|
||||
padding: 11px;
|
||||
background: rgba(49, 50, 68, 0.84);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
|
||||
@@ -79,6 +79,6 @@
|
||||
|
||||
<script src="/static/auth-header.js?v=3"></script>
|
||||
<script src="/static/logout.js?v=3"></script>
|
||||
<script src="/static/feed.js?v=7"></script>
|
||||
<script src="/static/feed.js?v=9"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
After Width: | Height: | Size: 320 KiB |
|
Before Width: | Height: | Size: 950 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 13 KiB |
@@ -5,7 +5,7 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 1804 1712" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,-669.006,-255.89)">
|
||||
<g transform="matrix(0.972876,0,0,1.26057,-24.1588,-587.485)">
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(88,91,112);"/>
|
||||
<rect x="712.491" y="669.041" width="1854.11" height="1357.72" style="fill:rgb(24, 24, 37);"/>
|
||||
</g>
|
||||
<g transform="matrix(5.14628,0,0,6.12686,41.2191,-8673.98)">
|
||||
<path d="M141.95,1644.94L141.95,1633.94C157.554,1633.4 169.724,1631.04 178.462,1626.86C187.2,1622.69 193.344,1615.8 196.894,1606.21C200.444,1596.61 202.218,1583.47 202.218,1566.77C202.218,1554.44 201.146,1544.16 199,1535.93C196.855,1527.7 193.285,1521.25 188.292,1516.57C185.016,1513.6 181.154,1511.26 176.707,1509.54C172.26,1507.83 167.15,1506.62 161.376,1505.92C155.603,1505.21 149.128,1504.86 141.95,1504.86L141.95,1493.86L384.076,1493.86L384.076,1504.86C373.388,1504.86 364.221,1505.7 356.575,1507.38C348.929,1509.06 342.708,1512.12 337.909,1516.57C333.111,1521.01 329.581,1527.27 327.319,1535.35C325.056,1543.42 323.925,1553.9 323.925,1566.77L323.925,1633.36C339.06,1633.36 353.532,1631.08 367.341,1626.51C381.15,1621.95 393.399,1616 404.087,1608.66C414.776,1601.41 423.299,1593.3 429.657,1584.32C436.016,1575.35 439.546,1566.61 440.248,1558.11L452.536,1558.11C452.536,1562.4 452.419,1568.35 452.185,1575.96C451.951,1583.56 451.6,1591.85 451.132,1600.82C450.586,1609.95 449.942,1618.34 449.201,1625.99C448.46,1633.63 447.582,1639.95 446.568,1644.94L141.95,1644.94Z" style="fill:rgb(245,224,220);fill-rule:nonzero;"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |