19 Commits

Author SHA1 Message Date
Archos a863ae498d feat: filtrování účtů podle instance 2026-05-15 10:05:54 +02:00
Archos 00768bd887 fix: odebrán vhsky z manual_accounts.csv, nyní v adresáři instancí 2026-05-03 14:10:07 +02:00
Archos 5ba472f87a feat: podpora GTS instancí s GTS_TOKEN, přidán arch@gts.arch-linux.cz 2026-05-03 08:03:02 +02:00
Archos 7d115e8b43 fix: odebrán politiq@mastodon.arch-linux.cz, instance vypnuta 2026-05-03 06:46:43 +02:00
Archos 6a69b2c4ba fix: archlinux@mamutovo.cz → arch@arch-linux.cz 2026-05-03 06:41:32 +02:00
Archos b958d6616b feat: přidán vhsky@mamutovo.cz 2026-04-27 20:38:06 +02:00
Archos 430cbb6c10 feat: přidána kartička mxchat.cz 2026-04-25 20:14:47 +02:00
Archos 7754449b8b fix: patička – #pomoc nahrazen #tip_mastodon 2026-04-16 21:36:44 +02:00
Archos a530ebae35 Merge branch 'dev'
Merge branch 'dev' - v1.2.0

- CATEGORIES rozšířeny: fediverse, cestovani, priroda, jidlo
- Limit featured_tags zvýšen na 6, zobrazení na 4
- Klikací hashtagy u kartiček – filtrování na stránce
- Odkaz na about.mamutovo.cz v hero sekci start.html
- Přidáni noví účty: mirek@rodina-sucha.cz, michal@vltava.cloud,
  sesivany@social.vivaldi.net, jimmac@mastodon.social,
  Oskar456@mastodon.social, jmlich@fosstodon.org,
  ronny@metalhead.club, vancura@mastodon.design
2026-04-13 19:16:20 +02:00
Archos 689fd437f6 feat: klikací hashtagy u kartiček – filtrování na stránce 2026-04-12 21:19:24 +02:00
Archos 3cc80eaef9 feat: přidán odkaz na about.mamutovo.cz v hero sekci 2026-04-12 21:00:25 +02:00
Archos 662b462192 feat: přidán vancura@mastodon.design, opraven sesivany na social.vivaldi.net 2026-04-12 10:35:20 +02:00
Archos 1f5bf3fc02 V manual_accounts.csv opraveno: sesivany@vivaldi.net
za:
sesivany@social.vivaldi.ne
2026-04-12 09:49:59 +02:00
Archos faf2ae3c5e feat: přidáni jimmac@mastodon.social
Oskar456@mastodon.social
jmlich@fosstodon.org
ronny@metalhead.club
2026-04-12 09:20:24 +02:00
Archos 3460e45a37 feat: přidáni michal@vltava.cloud a sesivany@vivaldi.net 2026-04-12 08:54:50 +02:00
Archos 55359c25c3 feat: CATEGORIES rozšířeny, limit tagů na 6, zobrazení na 4, přidán mirek@rodina-sucha.cz 2026-04-12 08:16:33 +02:00
Archos fe1ad16e50 fix: Mast nahrazen Tuba v kategorii Počítač 2026-04-11 13:00:37 +02:00
Archos 83992f6112 feat: kategorie z featured_tags, přidány sport a politika 2026-04-11 12:43:11 +02:00
Archos 1944c56d9c feat: hashtagy z featured_tags místo bio parsování 2026-04-11 12:22:15 +02:00
7 changed files with 152 additions and 31 deletions
+59 -4
View File
@@ -384,6 +384,15 @@ select:focus { border-color: var(--accent); }
border: 1px solid rgba(0,200,150,0.15); border: 1px solid rgba(0,200,150,0.15);
border-radius: 999px; border-radius: 999px;
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
cursor: pointer;
position: relative;
z-index: 1;
transition: background 0.15s, border-color 0.15s;
}
.card-tag:hover {
background: rgba(0,200,150,0.22);
border-color: var(--accent);
} }
.card-stats { .card-stats {
@@ -582,6 +591,9 @@ select:focus { border-color: var(--accent); }
<select id="categoryFilter"> <select id="categoryFilter">
<option value="">Všechny kategorie</option> <option value="">Všechny kategorie</option>
</select> </select>
<select id="instanceFilter">
<option value="">Všechny instance</option>
</select>
<select id="sortFilter"> <select id="sortFilter">
<option value="score">Nejvyšší skóre</option> <option value="score">Nejvyšší skóre</option>
<option value="followers" selected>Nejvíce sledujících</option> <option value="followers" selected>Nejvíce sledujících</option>
@@ -731,22 +743,47 @@ let viewMode = 'grid'; // 'grid' | 'list'
// ──────────────────────────────── // ────────────────────────────────
// FILTER + SORT // FILTER + SORT
// ──────────────────────────────── // ────────────────────────────────
function filterByTag(tag) {
const wrap = document.getElementById('filterTags');
const existing = wrap.querySelector(`.ftag[data-tag="${CSS.escape(tag)}"]`);
if (existing) {
wrap.querySelectorAll('.ftag').forEach(b => b.classList.remove('active'));
existing.classList.add('active');
} else {
const btn = document.createElement('button');
btn.className = 'ftag active';
btn.dataset.tag = tag;
btn.textContent = '#' + tag;
btn.addEventListener('click', () => {
wrap.querySelectorAll('.ftag').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyFilters();
});
wrap.querySelectorAll('.ftag').forEach(b => b.classList.remove('active'));
wrap.appendChild(btn);
}
applyFilters();
}
function applyFilters() { function applyFilters() {
const q = document.getElementById('searchInput').value.toLowerCase().trim(); const q = document.getElementById('searchInput').value.toLowerCase().trim();
const cat = document.getElementById('categoryFilter').value; const cat = document.getElementById('categoryFilter').value;
const inst = document.getElementById('instanceFilter').value;
const sort = document.getElementById('sortFilter').value; const sort = document.getElementById('sortFilter').value;
const activeTag = document.querySelector('.ftag.active')?.dataset.tag || ''; const activeTag = document.querySelector('.ftag.active')?.dataset.tag || '';
filtered = allAccounts.filter(a => { filtered = allAccounts.filter(a => {
const handle = a.acct || a.handle || '';
const matchQ = !q || const matchQ = !q ||
a.name.toLowerCase().includes(q) || a.name.toLowerCase().includes(q) ||
(a.bio || '').toLowerCase().includes(q) || (a.bio || '').toLowerCase().includes(q) ||
(a.handle || '').toLowerCase().includes(q) || handle.toLowerCase().includes(q) ||
(a.tags || []).some(t => t.toLowerCase().includes(q)); (a.tags || []).some(t => t.toLowerCase().includes(q));
const matchCat = !cat || a.category === cat; const matchCat = !cat || a.category === cat;
const matchInst = !inst || handle.split('@').pop() === inst;
const matchTag = !activeTag || const matchTag = !activeTag ||
(a.tags || []).some(t => t.toLowerCase().includes(activeTag.toLowerCase())); (a.tags || []).some(t => t.toLowerCase().includes(activeTag.toLowerCase()));
return matchQ && matchCat && matchTag; return matchQ && matchCat && matchInst && matchTag;
}); });
filtered.sort((a, b) => { filtered.sort((a, b) => {
@@ -792,8 +829,8 @@ function avatarFallback(name) {
function cardHTML(a, idx) { function cardHTML(a, idx) {
const av = a.avatar || avatarFallback(a.name); const av = a.avatar || avatarFallback(a.name);
const tags = (a.tags || []).slice(0, 3).map(t => const tags = (a.tags || []).slice(0, 4).map(t =>
`<span class="card-tag">#${t}</span>`).join(''); `<span class="card-tag" onclick="event.stopPropagation();filterByTag('${t.replace(/'/g, "\\'")}')">#${t}</span>`).join('');
if (viewMode === 'list') { if (viewMode === 'list') {
return ` return `
@@ -873,6 +910,7 @@ function downloadCSV(content, filename) {
// ──────────────────────────────── // ────────────────────────────────
document.getElementById('searchInput').addEventListener('input', applyFilters); document.getElementById('searchInput').addEventListener('input', applyFilters);
document.getElementById('categoryFilter').addEventListener('change', applyFilters); document.getElementById('categoryFilter').addEventListener('change', applyFilters);
document.getElementById('instanceFilter').addEventListener('change', applyFilters);
document.getElementById('sortFilter').addEventListener('change', applyFilters); document.getElementById('sortFilter').addEventListener('change', applyFilters);
document.querySelectorAll('.ftag').forEach(btn => { document.querySelectorAll('.ftag').forEach(btn => {
@@ -932,6 +970,23 @@ function buildDynamicUI() {
sel.appendChild(opt); sel.appendChild(opt);
}); });
// --- Instance select ---
const instanceCount = {};
allAccounts.forEach(a => {
const handle = a.acct || a.handle || '';
const instance = handle.includes('@') ? handle.split('@').pop() : '';
if (instance) instanceCount[instance] = (instanceCount[instance] || 0) + 1;
});
const instances = Object.keys(instanceCount).sort((a, b) => a.localeCompare(b, 'cs'));
const instSel = document.getElementById('instanceFilter');
instSel.innerHTML = '<option value="">Všechny instance</option>';
instances.forEach(instance => {
const opt = document.createElement('option');
opt.value = instance;
opt.textContent = `${instance} (${instanceCount[instance]})`;
instSel.appendChild(opt);
});
// --- Hashtag tlačítka top 8 nejčastějších tagů --- // --- Hashtag tlačítka top 8 nejčastějších tagů ---
const tagCount = {}; const tagCount = {};
allAccounts.forEach(a => (a.tags || []).forEach(t => { allAccounts.forEach(a => (a.tags || []).forEach(t => {
+5 -4
View File
@@ -451,14 +451,15 @@ h1 span { color: var(--accent); }
<!-- POČÍTAČ --> <!-- POČÍTAČ -->
<div class="card" data-platforms="pocitac"> <div class="card" data-platforms="pocitac">
<div class="app-logo"><img src="img/apps/mast.webp" alt="Mast"></div> <div class="app-logo"><img src="img/apps/tuba.svg" alt="Tuba"></div>
<div class="card-body"> <div class="card-body">
<div class="card-name">Mast</div> <div class="card-name">Tuba</div>
<div class="card-tags"> <div class="card-tags">
<span class="tag tag-platform">Počítač</span> <span class="tag tag-platform">Počítač</span>
<span class="tag tag-paid">Placené</span> <span class="tag tag-free">Zdarma</span>
<span class="tag tag-open">Otevřená</span>
</div> </div>
<a href="https://apps.apple.com/app/mast-for-mastodon/id1437429129" class="btn-dl" target="_blank" rel="noopener">Stáhnout →</a> <a href="https://tuba.geopjr.dev" class="btn-dl" target="_blank" rel="noopener">Stáhnout →</a>
</div> </div>
</div> </div>
+6
View File
@@ -218,6 +218,12 @@
<p>Hashtagy jsou na Mastodonu důležité slouží k objevování obsahu. Přidávej je na konec tootů. Můžeš sledovat i samotný hashtag a jeho příspěvky se ti budou zobrazovat v timeline. Příklady: <code>#cesky</code> <code>#fotografie</code> <code>#linux</code></p> <p>Hashtagy jsou na Mastodonu důležité slouží k objevování obsahu. Přidávej je na konec tootů. Můžeš sledovat i samotný hashtag a jeho příspěvky se ti budou zobrazovat v timeline. Příklady: <code>#cesky</code> <code>#fotografie</code> <code>#linux</code></p>
</div> </div>
<div class="section">
<h2>Zvýrazněné hashtagy</h2>
<p>Nastav si zvýrazněné hashtagy zobrazí se na tvém profilu a v seznamu CZ/SK účtů na <a href="https://fedi.mamutovo.cz">fedi.mamutovo.cz</a>.</p>
<p style="margin-top:0.5rem;">Jak na to: <code>Předvolby → Profil → Zvýrazněné hashtagy</code></p>
</div>
<div class="section"> <div class="section">
<h2>Užitečné odkazy</h2> <h2>Užitečné odkazy</h2>
<ul> <ul>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 51 KiB

+8 -2
View File
@@ -55,7 +55,6 @@ amarok@mastodonczech.cz,true,false,
anlexcz@witter.cz,true,false, anlexcz@witter.cz,true,false,
lacertacz@mastodonczech.cz,true,false, lacertacz@mastodonczech.cz,true,false,
srandista@mastodonczech.cz,true,false, srandista@mastodonczech.cz,true,false,
politiq@mastodon.arch-linux.cz,true,false,
aikencz@f.cz,true,false, aikencz@f.cz,true,false,
sandruska93@mas.to,true,false, sandruska93@mas.to,true,false,
karelcapek@mastodonczech.cz,true,false, karelcapek@mastodonczech.cz,true,false,
@@ -97,7 +96,7 @@ nacelnik01@mamutovo.cz,true,false,
sledge@mastodonczech.cz,true,false, sledge@mastodonczech.cz,true,false,
tensob_@mastodonczech.cz,true,false tensob_@mastodonczech.cz,true,false
archos@mamutovo.cz,true,false, archos@mamutovo.cz,true,false,
archlinux@mamutovo.cz,true,false, arch@gts.arch-linux.cz,true,false,
electric@vaclavpasek.cz,true,false, electric@vaclavpasek.cz,true,false,
adamhavelka@mastodon.prorocketeers.com,true,false, adamhavelka@mastodon.prorocketeers.com,true,false,
Kipe@mastodon.social,true,false, Kipe@mastodon.social,true,false,
@@ -105,3 +104,10 @@ zpravobot@zpravobot.news,true,false,
ozzelot@mstdn.social,true,false, ozzelot@mstdn.social,true,false,
j4n3z@mastodon.social,true,false, j4n3z@mastodon.social,true,false,
prahou@merveilles.town,true,false, prahou@merveilles.town,true,false,
mirek@rodina-sucha.cz,true,false,
sesivany@social.vivaldi.net,true,false,
jimmac@mastodon.social,true,false,
Oskar456@mastodon.social,true,false,
jmlich@fosstodon.org,true,false,
ronny@metalhead.club,true,false,
vancura@mastodon.design,true,false,
1 medvidekpu@mastodon.social true false
55 anlexcz@witter.cz true false
56 lacertacz@mastodonczech.cz true false
57 srandista@mastodonczech.cz true false
politiq@mastodon.arch-linux.cz true false
58 aikencz@f.cz true false
59 sandruska93@mas.to true false
60 karelcapek@mastodonczech.cz true false
96 sledge@mastodonczech.cz true false
97 tensob_@mastodonczech.cz true false
98 archos@mamutovo.cz true false
99 archlinux@mamutovo.cz arch@gts.arch-linux.cz true false
100 electric@vaclavpasek.cz true false
101 adamhavelka@mastodon.prorocketeers.com true false
102 Kipe@mastodon.social true false
104 ozzelot@mstdn.social true false
105 j4n3z@mastodon.social true false
106 prahou@merveilles.town true false
107 mirek@rodina-sucha.cz true false
108 sesivany@social.vivaldi.net true false
109 jimmac@mastodon.social true false
110 Oskar456@mastodon.social true false
111 jmlich@fosstodon.org true false
112 ronny@metalhead.club true false
113 vancura@mastodon.design true false
+54 -17
View File
@@ -50,9 +50,19 @@ _TOKENS = _load_tokens()
MASTODON_TOKEN = _TOKENS.get("MASTODON_TOKEN") MASTODON_TOKEN = _TOKENS.get("MASTODON_TOKEN")
GTS_TOKEN = _TOKENS.get("GTS_TOKEN") GTS_TOKEN = _TOKENS.get("GTS_TOKEN")
_gts_cache: dict[str, bool] = {}
def _is_gts(instance: str) -> bool:
if instance in _gts_cache:
return _gts_cache[instance]
info = api_get(f"https://{instance}/api/v1/instance")
version = (info or {}).get("version", "") if isinstance(info, dict) else ""
result = "git" in version or version.startswith("0.")
_gts_cache[instance] = result
return result
def _token_for(instance: str) -> str | None: def _token_for(instance: str) -> str | None:
"""Vrátí GTS_TOKEN pro GoToSocial instance (obsahují 'gts.' v doméně), jinak MASTODON_TOKEN.""" if GTS_TOKEN and _is_gts(instance):
if GTS_TOKEN and "gts." in instance:
return GTS_TOKEN return GTS_TOKEN
return MASTODON_TOKEN return MASTODON_TOKEN
@@ -167,6 +177,11 @@ def load_manual_accounts(seen_handles=None):
url = f"https://{instance}/api/v1/accounts/lookup?acct={urllib.parse.quote(handle_part)}" url = f"https://{instance}/api/v1/accounts/lookup?acct={urllib.parse.quote(handle_part)}"
token = _token_for(instance) token = _token_for(instance)
acc = api_get(url, token=token) acc = api_get(url, token=token)
if not acc or not isinstance(acc, dict):
log.debug(f" {instance}: is_gts={_is_gts(instance)}, gts_token={GTS_TOKEN is not None}")
if GTS_TOKEN and _is_gts(instance):
log.debug(f" {handle}: zkouším GTS_TOKEN")
acc = api_get(url, token=GTS_TOKEN)
if not acc or not isinstance(acc, dict): if not acc or not isinstance(acc, dict):
log.warning(f" {handle}: lookup selhal") log.warning(f" {handle}: lookup selhal")
continue continue
@@ -216,15 +231,28 @@ def score(acc):
# ── KATEGORIE ───────────────────────────────── # ── KATEGORIE ─────────────────────────────────
CATEGORIES = { CATEGORIES = {
"tech": ["linux", "python", "programov", "software", "opensource", "developer", "sysadmin", "git"], "tech": ["linux", "python", "programov", "software", "opensource", "developer", "sysadmin", "git", "foss", "selfhosted", "homelab", "arch"],
"foto": ["fotografi", "foto", "photograph", "objektiv", "kamera"], "foto": ["fotografi", "foto", "photograph", "objektiv", "kamera"],
"veda": ["věda", "fyzika", "biologi", "astronom", "výzkum", "science", "matematik"], "veda": ["věda", "fyzika", "biologi", "astronom", "výzkum", "science", "matematik"],
"kultura": ["knihy", "literatura", "film", "hudba", "divadlo", "umění"], "kultura": ["knihy", "literatura", "film", "hudba", "divadlo", "umění"],
"gaming": ["gaming", "hry", "videohry", "steam", "gamer"], "gaming": ["gaming", "hry", "videohry", "steam", "gamer"],
"zpravy": ["novinář", "zprávy", "politik", "média", "journalist"], "zpravy": ["novinář", "zprávy", "politik", "média", "journalist", "zpravy", "news", "aktualne"],
"sport": ["sport", "fotbal", "hokej", "cycling", "running", "fitness", "tenis", "atletika", "cyklistika", "kolo", "beh", "plavani", "turistika"],
"politika": ["politika", "politics", "czech", "democracy", "volby", "eu"],
"fediverse": ["fediverse", "mastodon", "activitypub", "mamutovo"],
"cestovani": ["cestovani", "cestování", "travel", "dovolena"],
"priroda": ["příroda", "priroda", "les", "hory", "zahrada"],
"jidlo": ["jídlo", "jidlo", "vareni", "vaření", "recept", "food"],
} }
def categorize(acc): def categorize(acc):
# Primárně matchuj featured_tags proti CATEGORIES
for tag in acc.get("_featured_tags", []):
tag_lower = tag.lower()
for cat, kws in CATEGORIES.items():
if any(kw in tag_lower for kw in kws):
return cat
# Fallback: bio text + display_name
text = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").lower() text = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").lower()
text += " " + (acc.get("display_name", "") or "").lower() text += " " + (acc.get("display_name", "") or "").lower()
for cat, kws in CATEGORIES.items(): for cat, kws in CATEGORIES.items():
@@ -232,14 +260,23 @@ def categorize(acc):
return cat return cat
return "ostatni" return "ostatni"
def extract_tags(acc): def fetch_featured_tags(acc):
text = re.sub(r"<[^>]+>", " ", acc.get("note", "") or "").lower() if "_featured_tags" in acc:
found = [] return acc["_featured_tags"]
for kws in CATEGORIES.values(): account_id = acc.get("id")
for kw in kws: instance = acc.get("_source_instance", "")
if kw in text and kw not in found and len(kw) > 3: if not account_id or not instance:
found.append(kw.strip()) acc["_featured_tags"] = []
return found[:4] return []
url = f"https://{instance}/api/v1/accounts/{account_id}/featured_tags"
token = _token_for(instance)
data = api_get(url, token=token)
if not data or not isinstance(data, list):
acc["_featured_tags"] = []
return []
tags = [t["name"] for t in data if isinstance(t, dict) and t.get("name")][:6]
acc["_featured_tags"] = tags
return tags
# ── VÝSTUP ──────────────────────────────────── # ── VÝSTUP ────────────────────────────────────
def _to_output(acc): def _to_output(acc):
@@ -253,7 +290,7 @@ def _to_output(acc):
"followers": acc.get("followers_count", 0), "followers": acc.get("followers_count", 0),
"statuses": acc.get("statuses_count", 0), "statuses": acc.get("statuses_count", 0),
"score": score(acc), "score": score(acc),
"tags": extract_tags(acc), "tags": fetch_featured_tags(acc),
"category": categorize(acc), "category": categorize(acc),
"last_active": acc.get("last_status_at", ""), "last_active": acc.get("last_status_at", ""),
"url": acc.get("url", ""), "url": acc.get("url", ""),
+19 -4
View File
@@ -76,10 +76,20 @@
.tagline strong { color: var(--text); } .tagline strong { color: var(--text); }
.about-link {
color: var(--accent);
text-decoration: none;
font-size: 0.92rem;
opacity: 0.75;
transition: opacity 0.15s;
}
.about-link:hover { opacity: 1; text-decoration: underline; }
/* --- CARDS GRID --- */ /* --- CARDS GRID --- */
.cards { .cards {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
gap: 1rem; gap: 1rem;
} }
@@ -378,7 +388,8 @@
<h1>🦣 Mamutovo za 3 minuty.</h1> <h1>🦣 Mamutovo za 3 minuty.</h1>
<p class="tagline"> <p class="tagline">
Žádné algoritmy. Žádné reklamy. <strong>Patří komunitě.</strong><br> Žádné algoritmy. Žádné reklamy. <strong>Patří komunitě.</strong><br>
Tyhle 4 kroky ti zaplní feed a pomůžou udělat první post. Tyhle 4 kroky ti zaplní feed a pomůžou udělat první post.<br>
<a href="https://about.mamutovo.cz" target="_blank" class="about-link">Co je Mamutovo.cz?</a>
</p> </p>
</div> </div>
@@ -460,12 +471,16 @@ Rád/a poznám nové lidi 🙂 #Představení #novacek #cesky
<p>Doporučené appky pro Android a iOS.</p> <p>Doporučené appky pro Android a iOS.</p>
<a href="apps.html" class="btn btn-secondary">Zobrazit →</a> <a href="apps.html" class="btn btn-secondary">Zobrazit →</a>
</div> </div>
<div class="card">
<h3>💬 Chat v reálném čase</h3>
<p>Mastodon je na příspěvky, Matrix na chat. mxchat.cz je český Matrix server.</p>
<a href="https://web.mxchat.cz" class="btn btn-secondary" target="_blank">Otevřít →</a>
</div>
</div> </div>
</div> </div>
<div class="footer-note"> <div class="footer-note">
Zasekl/a ses? Napiš na <a href="https://mamutovo.cz/@archos">@archos@mamutovo.cz</a> nebo Tipy a triky: <a href="https://mamutovo.cz/tags/tip_mastodon">#tip_mastodon</a> · Potřebuješ pomoc? Napiš <a href="https://mamutovo.cz/@archos">@archos@mamutovo.cz</a><br>
se zeptej v <a href="https://mamutovo.cz/tags/pomoc">#pomoc</a>.<br>
Tato stránka je open source: <a href="https://git.arch-linux.cz/Mamutovo/fedi_start">Gitea</a> · Tato stránka je open source: <a href="https://git.arch-linux.cz/Mamutovo/fedi_start">Gitea</a> ·
<a href="https://oscloud.cz">oscloud.cz</a> <a href="https://oscloud.cz">oscloud.cz</a>
</div> </div>