Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions MODS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Fluent Reader Mods

Fork local basado en `FluentFlame/fluentflame-reader` (`2.2.0-dev.3`).

Rama: `mods/local-ui`.

## Cambios aplicados

### 1. Fechas futuras
- `src/scripts/models/item.ts`: si un feed publica un ítem con fecha futura, se usa la fecha de descarga (`fetchedDate`) en lugar de dejarlo clavado arriba para siempre.
- `src/components/utils/time.tsx`: una fecha futura nunca se muestra como “now”; se muestra su fecha real.
- `src/scripts/db.ts` (versión 11): migración que corrige los ítems con fecha futura que ya estaban guardados.

Esto corrige el caso de “Visual Studio Code 1.144 (Insiders)”: el feed trae `2026-10-28`, y la app lo mostraba como “now” y siempre primero.

### 2. Deduplicación por enlace
- `src/scripts/db.ts`: nueva versión de Dexie (10) con índice `[source+link]`.
- `src/scripts/models/source.ts`: los ítems se deduplican por `source + link` cuando existe enlace; si no, se mantiene el criterio anterior (fecha + fuente + título).

Esto evita que feeds sin `pubDate` (o con fecha futura) inserten copias en cada refresco.

### 3. Concurrencia de refresco
- `src/scripts/models/item.ts`: máximo **24 feeds simultáneos** al refrescar, en lugar de lanzar todos a la vez.

Evita saturar la red y que un feed lento bloquee el refresco completo.

### 4. Diseño de tarjetas
- `resources/styles/mods.css` (nuevo) e importado al final de `resources/index.css`.
- Incluye el rediseño de tarjetas: 264×280, radio 14px, portada 16:9, degradado, hover, snippet visible, estados leídos y tarjetas sin portada.

### 5. YouTube
- `src/scripts/youtube.ts`: extracción de IDs y URL de miniatura/embed.
- `src/scripts/models/item.ts`: si el feed no trae miniatura y el enlace es de YouTube, se usa `i.ytimg.com`.
- `src/components/cards/default-card.tsx`: clase `yt-card` y badge ▶.
- `src/components/article.tsx`: para YouTube carga el reproductor iframe en lugar de la página completa.
- `src/main/youtube-server.ts`: servidor local en `127.0.0.1:39217` que sirve el iframe (YouTube rechaza el embed desde `file://` con error 153).
- `src/electron.ts`: arranca el servidor y fija cookies de consentimiento de YouTube.

## Pendiente / siguientes mods
- Migración opcional para limpiar duplicados antiguos ya existentes.
- Revisar el resto de feeds sin fecha para no depender de aislamientos manuales.

## Build
```bash
npm ci
npm run build
```

La instalación original en `C:\Program Files\Fluentflame Reader` no se toca.
1 change: 1 addition & 0 deletions resources/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
@import "styles/feeds.css";
@import "styles/cards.css";
@import "styles/dark.css";
@import "styles/mods.css";
220 changes: 220 additions & 0 deletions resources/styles/mods.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/* Fluentflame Reader — card view refresh (preview) */
.default-card {
--card-radius: 14px;
box-sizing: border-box;
width: 264px;
height: 280px;
margin: 12px 8px;
border-radius: var(--card-radius);
background: var(--white);
border: 1px solid color-mix(in srgb, var(--neutralDarker) 10%, transparent);
box-shadow: 0 1px 2px rgba(0, 0, 0, .06), 0 10px 28px rgba(0, 0, 0, .10);
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.default-card:hover,
.ms-Fabric--isFocusVisible .default-card:focus {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--neutralDarker) 18%, transparent);
box-shadow: 0 2px 4px rgba(0, 0, 0, .08), 0 18px 44px rgba(0, 0, 0, .16);
}
.default-card:active {
transform: translateY(-1px) scale(.995);
box-shadow: 0 1px 2px rgba(0, 0, 0, .08), 0 8px 20px rgba(0, 0, 0, .12);
}

.default-card img.head,
.default-card canvas.head,
.default-card video.head {
height: 148px;
transition: transform .35s cubic-bezier(.2, .7, .2, 1);
}
.default-card:hover img.head,
.default-card:hover canvas.head,
.default-card:hover video.head {
transform: scale(1.045);
}
.default-card:has(.head)::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 148px;
background: linear-gradient(to bottom, transparent 55%, rgba(0, 0, 0, .30));
pointer-events: none;
z-index: 1;
}
.default-card.transform:hover img.head,
.default-card.transform:hover canvas.head,
.default-card.transform:hover video.head,
.ms-Fabric--isFocusVisible .default-card.transform:focus img.head,
.ms-Fabric--isFocusVisible .default-card.transform:focus canvas.head,
.ms-Fabric--isFocusVisible .default-card.transform:focus video.head {
transform: scale(1.045);
}
.default-card.transform:hover p,
.default-card.transform:hover h3,
.ms-Fabric--isFocusVisible .default-card.transform:focus p,
.ms-Fabric--isFocusVisible .default-card.transform:focus h3 {
transform: none;
}

.default-card .info {
margin: 8px 14px 0;
line-height: 16px;
align-items: center;
}
.default-card .info img {
width: 16px;
height: 16px;
margin-right: 7px;
border-radius: 4px;
object-fit: cover;
}
.default-card .info span.name {
font-size: 12px;
font-weight: 600;
letter-spacing: .01em;
color: var(--neutralSecondary);
}
.default-card .info span.time {
font-size: 11px;
color: var(--neutralTertiaryAlt);
white-space: nowrap;
}
.default-card .read-indicator::after {
width: 7px;
height: 7px;
margin: 4.5px 5px;
border-radius: 50%;
background: #ffaa44;
box-shadow: 0 0 0 3px rgba(255, 170, 68, .16);
}

.default-card h3.title {
margin: 6px 14px 0;
font-size: 16px;
line-height: 22px;
font-weight: 700;
letter-spacing: -.01em;
-webkit-line-clamp: 2;
}
.default-card.read h3.title { font-weight: 600; }

.default-card p.snippet {
margin: 4px 14px 0;
font-size: 13px;
line-height: 18px;
color: var(--neutralSecondary);
-webkit-line-clamp: 2;
transform: none;
opacity: 1;
}
.default-card p.snippet.show { transform: none; }

.default-card.read { color: var(--neutralSecondary); }
.default-card.read img.head,
.default-card.read canvas.head,
.default-card.read video.head {
opacity: .72;
filter: saturate(.75);
}

.default-card:not(:has(.head)) {
display: flex;
flex-direction: column;
justify-content: center;
padding: 18px 0;
background:
radial-gradient(130% 110% at 0% 0%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 62%),
radial-gradient(100% 90% at 100% 100%, color-mix(in srgb, #8764b8 14%, transparent), transparent 58%),
var(--white);
}
.default-card:not(:has(.head)) .bg { display: none; }
.default-card:not(:has(.head))::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 3px;
background: linear-gradient(90deg, var(--primary), color-mix(in srgb, var(--primary) 35%, #8764b8));
pointer-events: none;
z-index: 2;
}
.default-card:not(:has(.head)) .info img {
width: 26px;
height: 26px;
border-radius: 7px;
margin-right: 10px;
}
.default-card:not(:has(.head)) .info { margin-top: 0; }
.default-card:not(:has(.head)) h3.title { -webkit-line-clamp: 4; font-size: 17px; line-height: 23px; }
.default-card:not(:has(.head)) p.snippet { -webkit-line-clamp: 3; }

.ms-Fabric--isFocusVisible .card:focus::after {
border-radius: 10px;
top: 3px;
left: 3px;
width: calc(100% - 8px);
height: calc(100% - 8px);
}

/* YouTube cards: real thumbnail + play badge */
.default-card.ff-yt { background: var(--white); }
.ff-yt-head {
position: relative;
height: 148px;
overflow: hidden;
background: #000;
}
.ff-yt-head img.head {
display: block;
width: 100%;
height: 148px;
object-fit: cover;
}
.ff-yt-play {
position: absolute;
z-index: 3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 0, 0, .62);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
line-height: 1;
padding-left: 3px;
box-sizing: border-box;
pointer-events: none;
backdrop-filter: blur(4px);
box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}
.default-card.ff-yt .info { margin-top: 8px; }
.default-card.ff-yt.ff-yt-no-snippet h3.title { -webkit-line-clamp: 3; }

/* YouTube card play badge */
.default-card .yt-play {
position: absolute;
z-index: 3;
top: 74px;
left: 50%;
transform: translate(-50%, -50%);
width: 44px;
height: 44px;
border-radius: 50%;
background: rgba(0, 0, 0, .62);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
line-height: 1;
padding-left: 3px;
box-sizing: border-box;
pointer-events: none;
backdrop-filter: blur(4px);
box-shadow: 0 2px 10px rgba(0, 0, 0, .35);
}
19 changes: 13 additions & 6 deletions src/components/article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from "react";
import intl from "react-intl-universal";
import { renderToString } from "react-dom/server";
import { RSSItem } from "../scripts/models/item";
import { getYouTubeId, youtubeEmbedPath } from "../scripts/youtube";
import {
Stack,
CommandBarButton,
Expand Down Expand Up @@ -500,6 +501,12 @@ class Article extends React.Component<ArticleProps, ArticleState> {
this.toggleWebpage,
this.moreMenuProps,
);
const youtubeId = getYouTubeId(this.props.item.link);
const articleSrc = youtubeId
? youtubeEmbedPath(youtubeId)
: this.state.loadWebpage
? this.props.item.link
: this.articleView();
return (
<FocusZone className="article">
{commandBar}
Expand All @@ -512,15 +519,15 @@ class Article extends React.Component<ArticleProps, ArticleState> {
(this.state.loadWebpage ? "_" : "") +
(this.state.loadFull ? "__" : "")
}
src={
this.state.loadWebpage
? this.props.item.link
: this.articleView()
}
src={articleSrc}
allowpopups={"true" as unknown as boolean}
webpreferences="contextIsolation,disableDialogs,autoplayPolicy=document-user-activation-required"
partition={
this.state.loadWebpage ? "sandbox" : undefined
youtubeId
? undefined
: this.state.loadWebpage
? "sandbox"
: undefined
}
/>
)}
Expand Down
4 changes: 4 additions & 0 deletions src/components/cards/default-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Highlights from "./highlights";
import { SourceTextDirection } from "../../scripts/models/source";
import { RSSItem } from "../../scripts/models/item";
import CardThumbnail from "./thumbnail";
import { getYouTubeId } from "../../scripts/youtube";

const className = (props: Card.Props) => {
let cn = ["card", "default-card"];
Expand All @@ -18,6 +19,7 @@ const className = (props: Card.Props) => {
cn.push("transform");
if (props.item.hidden) cn.push("hidden");
if (props.source.textDir === SourceTextDirection.RTL) cn.push("rtl");
if (getYouTubeId(props.item.link)) cn.push("yt-card");
return cn.join(" ");
};

Expand All @@ -29,6 +31,7 @@ function DefaultCard(props: Card.Props): React.JSX.Element {
const title = RSSItem.getTitle(props.item);
const hasThumbnail =
props.item.thumbnails?.length > 0 || props.item.thumb != null;
const youtubeId = getYouTubeId(props.item.link);
return (
<div
className={className(props)}
Expand All @@ -43,6 +46,7 @@ function DefaultCard(props: Card.Props): React.JSX.Element {
width={HEADER_IMG_WIDTH * RESCALE_FACTOR}
height={HEADER_IMG_HEIGHT * RESCALE_FACTOR}
/>
{youtubeId && <span className="yt-play">▶</span>}
<CardInfo source={props.source} item={props.item} />
<h3 className="title">
<Highlights text={title} filter={props.filter} title />
Expand Down
6 changes: 4 additions & 2 deletions src/components/utils/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ class Time extends React.Component<TimeProps> {
}

displayTime(past: Date, now: Date): string {
// difference in seconds
// difference in minutes
let diff = (now.getTime() - past.getTime()) / 60000;
if (diff < 1) return intl.get("time.now");
// Never render a future date as "now"; show its actual date instead.
if (diff < 0) return past.toLocaleDateString();
else if (diff < 1) return intl.get("time.now");
else if (diff < 60) return Math.floor(diff) + intl.get("time.m");
else if (diff < 1440) return Math.floor(diff / 60) + intl.get("time.h");
else return Math.floor(diff / 1440) + intl.get("time.d");
Expand Down
Loading