Merge improvement/quick-wins-1: channel YT links + badge checks, playlist input on top, stats bars
This commit is contained in:
commit
dfaa1551dc
6 changed files with 64 additions and 22 deletions
|
|
@ -4,6 +4,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
ArrowUp,
|
ArrowUp,
|
||||||
|
Check,
|
||||||
|
ExternalLink,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
History,
|
History,
|
||||||
|
|
@ -361,10 +363,11 @@ function SyncBadge({ ok, label, hint }: { ok: boolean; label: string; hint?: str
|
||||||
return (
|
return (
|
||||||
<Tooltip hint={hint ?? ""}>
|
<Tooltip hint={hint ?? ""}>
|
||||||
<span
|
<span
|
||||||
className={`text-[10px] px-1.5 py-0.5 rounded-full border ${
|
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full border ${
|
||||||
ok ? "border-accent/40 text-accent" : "border-border text-muted"
|
ok ? "border-accent/40 text-accent" : "border-border text-muted"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
{ok && <Check className="w-3 h-3" />}
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
@ -393,6 +396,9 @@ function ChannelRow({
|
||||||
onToggleTag: (tagId: number) => void;
|
onToggleTag: (tagId: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const ytUrl = c.handle
|
||||||
|
? `https://www.youtube.com/@${c.handle.replace(/^@/, "")}`
|
||||||
|
: `https://www.youtube.com/channel/${c.id}`;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`glass-card glass-hover flex items-center gap-3 p-2.5 rounded-xl transition ${
|
className={`glass-card glass-hover flex items-center gap-3 p-2.5 rounded-xl transition ${
|
||||||
|
|
@ -418,9 +424,36 @@ function ChannelRow({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<button onClick={onView} className="text-sm font-semibold truncate hover:text-accent block max-w-full text-left">
|
<div className="flex items-center gap-1 max-w-full">
|
||||||
{c.title ?? c.id}
|
<button
|
||||||
</button>
|
onClick={onView}
|
||||||
|
onAuxClick={(e) => {
|
||||||
|
// Middle-click opens the channel's YouTube page in a new tab; left-click
|
||||||
|
// keeps opening the in-app channel detail.
|
||||||
|
if (e.button === 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(ytUrl, "_blank", "noopener,noreferrer");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseDown={(e) => {
|
||||||
|
if (e.button === 1) e.preventDefault(); // suppress middle-click autoscroll
|
||||||
|
}}
|
||||||
|
className="text-sm font-semibold truncate hover:text-accent text-left min-w-0"
|
||||||
|
>
|
||||||
|
{c.title ?? c.id}
|
||||||
|
</button>
|
||||||
|
<Tooltip hint={t("channels.row.openOnYouTube")}>
|
||||||
|
<a
|
||||||
|
href={ytUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-muted hover:text-accent shrink-0"
|
||||||
|
aria-label={t("channels.row.openOnYouTube")}
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-3.5 h-3.5" />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[11px] text-muted">
|
<div className="flex items-center gap-2 text-[11px] text-muted">
|
||||||
<span>{t("channels.row.stored", { count: c.stored_videos, formatted: c.stored_videos.toLocaleString() })}</span>
|
<span>{t("channels.row.stored", { count: c.stored_videos, formatted: c.stored_videos.toLocaleString() })}</span>
|
||||||
{c.subscriber_count != null && <span>· {t("channels.row.subs", { count: c.subscriber_count, formatted: c.subscriber_count.toLocaleString() })}</span>}
|
{c.subscriber_count != null && <span>· {t("channels.row.subs", { count: c.subscriber_count, formatted: c.subscriber_count.toLocaleString() })}</span>}
|
||||||
|
|
|
||||||
|
|
@ -580,6 +580,21 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (newName.trim()) createMut.mutate(newName.trim());
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 mb-3 pb-3 border-b border-border"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 text-muted shrink-0" />
|
||||||
|
<input
|
||||||
|
value={newName}
|
||||||
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
|
placeholder={t("playlists.newPlaylist")}
|
||||||
|
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
{playlists.length === 0 && !listQuery.isLoading && (
|
{playlists.length === 0 && !listQuery.isLoading && (
|
||||||
<div className="text-xs text-muted px-1 py-2">{t("playlists.noneYet")}</div>
|
<div className="text-xs text-muted px-1 py-2">{t("playlists.noneYet")}</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -618,21 +633,6 @@ export default function Playlists({ canWrite }: { canWrite: boolean }) {
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (newName.trim()) createMut.mutate(newName.trim());
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-1.5 mt-3 pt-3 border-t border-border"
|
|
||||||
>
|
|
||||||
<Plus className="w-4 h-4 text-muted shrink-0" />
|
|
||||||
<input
|
|
||||||
value={newName}
|
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
|
||||||
placeholder={t("playlists.newPlaylist")}
|
|
||||||
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="flex-1 min-w-0 overflow-y-auto p-4">
|
<main className="flex-1 min-w-0 overflow-y-auto p-4">
|
||||||
|
|
|
||||||
|
|
@ -63,15 +63,21 @@ export default function Stats() {
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-end gap-0.5 h-24">
|
<div className="flex items-end gap-0.5 h-24">
|
||||||
{data.daily.map((d) => (
|
{data.daily.map((d) => (
|
||||||
|
// Each day gets a full-height track so the column is always visible
|
||||||
|
// (even on near-zero days); the accent fill renders the value on top.
|
||||||
<div
|
<div
|
||||||
key={d.day}
|
key={d.day}
|
||||||
className="flex-1 bg-accent/70 hover:bg-accent rounded-t transition"
|
className="flex-1 h-full flex items-end bg-card/50 rounded-t"
|
||||||
style={{ height: `${Math.max(2, (d.total / maxDaily) * 100)}%` }}
|
|
||||||
title={t("stats.dailyTooltip", {
|
title={t("stats.dailyTooltip", {
|
||||||
day: d.day,
|
day: d.day,
|
||||||
units: d.total.toLocaleString(),
|
units: d.total.toLocaleString(),
|
||||||
})}
|
})}
|
||||||
/>
|
>
|
||||||
|
<div
|
||||||
|
className="w-full bg-accent hover:opacity-80 rounded-t transition"
|
||||||
|
style={{ height: `${Math.max(3, (d.total / maxDaily) * 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"row": {
|
"row": {
|
||||||
"stored": "{{formatted}} gespeichert",
|
"stored": "{{formatted}} gespeichert",
|
||||||
"subs": "{{formatted}} Abonnenten",
|
"subs": "{{formatted}} Abonnenten",
|
||||||
|
"openOnYouTube": "Auf YouTube öffnen",
|
||||||
"priorityHint": "Deine Einstufung für diesen Kanal. Sortiere den Feed nach „Kanalpriorität“, um Kanäle mit höherer Priorität nach oben zu bringen.",
|
"priorityHint": "Deine Einstufung für diesen Kanal. Sortiere den Feed nach „Kanalpriorität“, um Kanäle mit höherer Priorität nach oben zu bringen.",
|
||||||
"raisePriority": "Priorität erhöhen",
|
"raisePriority": "Priorität erhöhen",
|
||||||
"lowerPriority": "Priorität senken",
|
"lowerPriority": "Priorität senken",
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"row": {
|
"row": {
|
||||||
"stored": "{{formatted}} stored",
|
"stored": "{{formatted}} stored",
|
||||||
"subs": "{{formatted}} subs",
|
"subs": "{{formatted}} subs",
|
||||||
|
"openOnYouTube": "Open on YouTube",
|
||||||
"priorityHint": "Your ranking for this channel. Sort the feed by “Channel priority” to bring higher-priority channels to the top.",
|
"priorityHint": "Your ranking for this channel. Sort the feed by “Channel priority” to bring higher-priority channels to the top.",
|
||||||
"raisePriority": "Raise priority",
|
"raisePriority": "Raise priority",
|
||||||
"lowerPriority": "Lower priority",
|
"lowerPriority": "Lower priority",
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@
|
||||||
"row": {
|
"row": {
|
||||||
"stored": "{{formatted}} tárolt",
|
"stored": "{{formatted}} tárolt",
|
||||||
"subs": "{{formatted}} feliratkozó",
|
"subs": "{{formatted}} feliratkozó",
|
||||||
|
"openOnYouTube": "Megnyitás a YouTube-on",
|
||||||
"priorityHint": "A te rangsorod ehhez a csatornához. Rendezd a hírfolyamot „Csatorna prioritás” szerint, hogy a magasabb prioritású csatornák felülre kerüljenek.",
|
"priorityHint": "A te rangsorod ehhez a csatornához. Rendezd a hírfolyamot „Csatorna prioritás” szerint, hogy a magasabb prioritású csatornák felülre kerüljenek.",
|
||||||
"raisePriority": "Prioritás növelése",
|
"raisePriority": "Prioritás növelése",
|
||||||
"lowerPriority": "Prioritás csökkentése",
|
"lowerPriority": "Prioritás csökkentése",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue