Merge branch 'edge' into feature/modernize-code-to-php74

This commit is contained in:
Luc SANCHEZ 2024-04-30 14:51:45 +02:00 committed by GitHub
commit 8a13a378df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 620 additions and 330 deletions

View File

@ -21,7 +21,7 @@ Example for Linux Debian / Ubuntu:
```sh
# Install default Docker Compose and automatically the corresponding version of Docker
apt install docker-compose
apt install docker-compose-v2
```
## Quick run
@ -194,6 +194,8 @@ docker run -d --restart unless-stopped --log-opt max-size=10m \
In the FreshRSS setup, you will then specify the name of the container (`freshrss-db`) as the host for the database.
See also the section [Docker Compose with PostgreSQL](#docker-compose-with-postgresql) below.
### [MySQL](https://hub.docker.com/_/mysql/) or [MariaDB](https://hub.docker.com/_/mariadb)
```sh
@ -285,13 +287,13 @@ See [`docker-compose.yml`](./freshrss/docker-compose.yml)
```sh
cd ./FreshRSS/Docker/freshrss/
# Update
docker-compose pull
docker compose pull
# Run
docker-compose -f docker-compose.yml -f docker-compose-local.yml up -d --remove-orphans
docker compose -f docker-compose.yml -f docker-compose-local.yml up -d --remove-orphans
# Logs
docker-compose logs -f --timestamps
docker compose logs -f --timestamps
# Stop
docker-compose down --remove-orphans
docker compose down --remove-orphans
```
Detailed (partial) example of Docker Compose for FreshRSS:
@ -378,13 +380,15 @@ See [`docker-compose-db.yml`](./freshrss/docker-compose-db.yml)
```sh
cd ./FreshRSS/Docker/freshrss/
# Update
docker-compose -f docker-compose.yml -f docker-compose-db.yml pull
docker compose -f docker-compose.yml -f docker-compose-db.yml pull
# Run
docker-compose -f docker-compose.yml -f docker-compose-db.yml -f docker-compose-local.yml up -d --remove-orphans
docker compose -f docker-compose.yml -f docker-compose-db.yml -f docker-compose-local.yml up -d --remove-orphans
# Logs
docker-compose -f docker-compose.yml -f docker-compose-db.yml logs -f --timestamps
docker compose -f docker-compose.yml -f docker-compose-db.yml logs -f --timestamps
```
See also the section [Migrate database](#migrate-database) below to upgrade to a major PostgreSQL version with Docker Compose.
### Docker Compose for development
Use the local (git) FreshRSS source code instead of the one inside the Docker container,
@ -396,11 +400,11 @@ See [`docker-compose-development.yml`](./freshrss/docker-compose-development.yml
cd ./FreshRSS/Docker/freshrss/
# Update
git pull --ff-only --prune
docker-compose pull
docker compose pull
# Run
docker-compose -f docker-compose-development.yml -f docker-compose.yml -f docker-compose-local.yml up --remove-orphans
docker compose -f docker-compose-development.yml -f docker-compose.yml -f docker-compose-local.yml up --remove-orphans
# Stop with [Control]+[C] and purge
docker-compose down --remove-orphans --volumes
docker compose down --remove-orphans --volumes
```
> You can combine it with `-f docker-compose-db.yml` to spin a PostgreSQL database.
@ -446,13 +450,13 @@ See [`docker-compose-proxy.yml`](./freshrss/docker-compose-proxy.yml)
```sh
cd ./FreshRSS/Docker/freshrss/
# Update
docker-compose -f docker-compose.yml -f docker-compose-proxy.yml pull
docker compose -f docker-compose.yml -f docker-compose-proxy.yml pull
# Run
docker-compose -f docker-compose.yml -f docker-compose-proxy.yml up -d --remove-orphans
docker compose -f docker-compose.yml -f docker-compose-proxy.yml up -d --remove-orphans
# Logs
docker-compose -f docker-compose.yml -f docker-compose-proxy.yml logs -f --timestamps
docker compose -f docker-compose.yml -f docker-compose-proxy.yml logs -f --timestamps
# Stop
docker-compose -f docker-compose.yml -f docker-compose-proxy.yml down --remove-orphans
docker compose -f docker-compose.yml -f docker-compose-proxy.yml down --remove-orphans
```
> You can combine it with `-f docker-compose-db.yml` to spin a PostgreSQL database.
@ -650,3 +654,46 @@ docker run -d --restart unless-stopped --log-opt max-size=10m \
--name freshrss_cron freshrss/freshrss:alpine \
crond -f -d 6
```
## Migrate database
Our [CLI](../cli/README.md) offers commands to back-up and migrate user databases,
with `cli/db-backup.php` and `cli/db-restore.php` in particular.
Here is an example (assuming our [Docker Compose example](#docker-compose-with-postgresql))
intended for migrating to a newer major version of PostgreSQL,
but which can also be used to migrate between other databases (e.g. MySQL to PostgreSQL).
```sh
# Stop FreshRSS container (Web server + cron) during maintenance
docker compose down freshrss
# Optional additional pre-upgrade back-up using PostgreSQL own mechanism
docker compose -f docker-compose-db.yml \
exec freshrss-db pg_dump -U freshrss freshrss | gzip -9 > freshrss-postgres-backup.sql.gz
# ------↑ Name of your PostgreSQL Docker container
# -----------------------------↑ Name of your PostgreSQL user for FreshRSS
# --------------------------------------↑ Name of your PostgreSQL database for FreshRSS
# Back-up all users respective tables to SQLite files
docker compose -f docker-compose.yml -f docker-compose-db.yml \
run --rm freshrss cli/db-backup.php
# Remove old database (PostgreSQL) container and its data volume
docker compose -f docker-compose-db.yml \
down --volumes freshrss-db
# Edit your Compose file to use new database (e.g. newest postgres:xx)
nano docker-compose-db.yml
# Start new database (PostgreSQL) container and its new empty data volume
docker compose -f docker-compose.yml -f docker-compose-db.yml \
up -d freshrss-db
# Restore all users respective tables from SQLite files
docker compose -f docker-compose.yml -f docker-compose-db.yml \
run --rm freshrss cli/db-restore.php --delete-backup
# Restart a new FreshRSS container after maintenance
docker compose -f docker-compose.yml -f docker-compose-db.yml up -d freshrss
```

View File

@ -259,7 +259,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if (!empty($xPathSettings)) {
$attributes['xpath'] = $xPathSettings;
}
} elseif ($feed_kind === FreshRSS_Feed::KIND_JSON_DOTPATH) {
} elseif ($feed_kind === FreshRSS_Feed::KIND_JSON_DOTNOTATION) {
$jsonSettings = [];
if (Minz_Request::paramString('jsonFeedTitle') !== '') {
$jsonSettings['feedTitle'] = Minz_Request::paramString('jsonFeedTitle', true);
@ -295,7 +295,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$jsonSettings['itemUid'] = Minz_Request::paramString('jsonItemUid', true);
}
if (!empty($jsonSettings)) {
$attributes['json_dotpath'] = $jsonSettings;
$attributes['json_dotnotation'] = $jsonSettings;
}
}
@ -495,10 +495,10 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('XML+XPath parsing failed for [' . $feed->url(false) . ']');
}
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH) {
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION) {
$simplePie = $feed->loadJson();
if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('JSON dotpath parsing failed for [' . $feed->url(false) . ']');
throw new FreshRSS_Feed_Exception('JSON dot notation parsing failed for [' . $feed->url(false) . ']');
}
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSONFEED) {
$simplePie = $feed->loadJson();
@ -571,6 +571,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$existingHash = $existingHashForGuids[$entry->guid()];
if (strcasecmp($existingHash, $entry->hash()) !== 0) {
//This entry already exists but has been updated
$entry->_isUpdated(true);
//Minz_Log::debug('Entry with GUID `' . $entry->guid() . '` updated in feed ' . $feed->url(false) .
//', old hash ' . $existingHash . ', new hash ' . $entry->hash());
$entry->_isFavorite(null); // Do not change favourite state
@ -585,6 +586,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
continue;
}
$entry->applyFilterActions($titlesAsRead);
if ($readWhenSameTitleInFeed > 0) {
$titlesAsRead[$entry->title()] = true;
}
if (!$entry->isRead()) {
$needFeedCacheRefresh = true; //Maybe
$nbMarkedUnread++;
@ -599,6 +605,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$entryDAO->updateEntry($entry->toArray());
}
} else {
$entry->_isUpdated(false);
$id = uTimeString();
$entry->_id($id);

View File

@ -238,7 +238,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$xPathSettings['itemUid'] = Minz_Request::paramString('xPathItemUid', true);
if (!empty($xPathSettings))
$feed->_attribute('xpath', $xPathSettings);
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH) {
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION) {
$jsonSettings = [];
if (Minz_Request::paramString('jsonFeedTitle') !== '') {
$jsonSettings['feedTitle'] = Minz_Request::paramString('jsonFeedTitle', true);
@ -274,7 +274,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$jsonSettings['itemUid'] = Minz_Request::paramString('jsonItemUid', true);
}
if (!empty($jsonSettings)) {
$feed->_attribute('json_dotpath', $jsonSettings);
$feed->_attribute('json_dotnotation', $jsonSettings);
}
}

View File

@ -48,6 +48,18 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
}
}
public function exits(): bool {
$sql = 'SELECT * FROM `_entry` LIMIT 1';
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
if ($res !== false) {
return true;
}
}
return false;
}
public function tablesAreCorrect(): bool {
$res = $this->fetchAssoc('SHOW TABLES');
if ($res == null) {
@ -242,6 +254,7 @@ SQL;
}
$error = '';
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$userDAO = FreshRSS_Factory::createUserDao();
$catDAO = FreshRSS_Factory::createCategoryDao();
$feedDAO = FreshRSS_Factory::createFeedDao();
@ -259,15 +272,18 @@ SQL;
$error = 'Error: SQLite import file is not readable: ' . $filename;
} elseif ($clearFirst) {
$userDAO->deleteUser();
$userDAO = FreshRSS_Factory::createUserDao();
if ($this->pdo->dbType() === 'sqlite') {
//We cannot just delete the .sqlite file otherwise PDO gets buggy.
//SQLite is the only one with database-level optimization, instead of at table level.
$this->optimize();
}
} else {
$nbEntries = $entryDAO->countUnreadRead();
if (!empty($nbEntries['all'])) {
$error = 'Error: Destination database already contains some entries!';
if ($databaseDAO->exits()) {
$nbEntries = $entryDAO->countUnreadRead();
if (isset($nbEntries['all']) && $nbEntries['all'] > 0) {
$error = 'Error: Destination database already contains some entries!';
}
}
}
break;

View File

@ -24,6 +24,7 @@ class FreshRSS_Entry extends Minz_Model {
private string $hash = '';
private ?bool $is_read;
private ?bool $is_favorite;
private bool $is_updated = false;
private int $feedId;
private ?FreshRSS_Feed $feed;
/** @var array<string> */
@ -394,6 +395,18 @@ HTML;
return $this->is_favorite;
}
/**
* Returns whether the entry has been modified since it was inserted in database.
* @returns bool `true` if the entry already existed (and has been modified), `false` if the entry is new (or unmodified).
*/
public function isUpdated(): ?bool {
return $this->is_updated;
}
public function _isUpdated(bool $value): void {
$this->is_updated = $value;
}
public function feed(): ?FreshRSS_Feed {
if ($this->feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();

View File

@ -31,7 +31,7 @@ class FreshRSS_Feed extends Minz_Model {
public const KIND_JSON_XPATH = 20;
public const KIND_JSONFEED = 25;
public const KIND_JSON_DOTPATH = 30;
public const KIND_JSON_DOTNOTATION = 30;
public const PRIORITY_IMPORTANT = 20;
public const PRIORITY_MAIN_STREAM = 10;
@ -621,7 +621,7 @@ class FreshRSS_Feed extends Minz_Model {
}
/** @return array<string,string> */
private function dotPathsForStandardJsonFeed(): array {
private function dotNotationForStandardJsonFeed(): array {
return [
'feedTitle' => 'title',
'item' => 'items',
@ -662,11 +662,11 @@ class FreshRSS_Feed extends Minz_Model {
return null;
}
/** @var array<string,string> $json_dotpath */
$json_dotpath = $this->attributeArray('json_dotpath') ?? [];
$dotPaths = $this->kind() === FreshRSS_Feed::KIND_JSONFEED ? $this->dotPathsForStandardJsonFeed() : $json_dotpath;
/** @var array<string,string> $json_dotnotation */
$json_dotnotation = $this->attributeArray('json_dotnotation') ?? [];
$dotnotations = $this->kind() === FreshRSS_Feed::KIND_JSONFEED ? $this->dotNotationForStandardJsonFeed() : $json_dotnotation;
$feedContent = FreshRSS_dotNotation_Util::convertJsonToRss($jf, $feedSourceUrl, $dotPaths, $this->name());
$feedContent = FreshRSS_dotNotation_Util::convertJsonToRss($jf, $feedSourceUrl, $dotnotations, $this->name());
if ($feedContent == null) {
return null;
}

View File

@ -133,10 +133,16 @@ trait FreshRSS_FilterActionsTrait {
}
break;
case 'star':
$entry->_isFavorite(true);
if (!$entry->isUpdated()) {
// Do not apply to updated articles, to avoid overruling a user manual action
$entry->_isFavorite(true);
}
break;
case 'label':
$applyLabel = true;
if (!$entry->isUpdated()) {
// Do not apply to updated articles, to avoid overruling a user manual action
$applyLabel = true;
}
break;
}
}

View File

@ -20,7 +20,8 @@ class FreshRSS_Export_Service {
public const TYPE_HTML_XPATH = 'HTML+XPath';
public const TYPE_XML_XPATH = 'XML+XPath';
public const TYPE_RSS_ATOM = 'rss';
public const TYPE_JSON_DOTPATH = 'JSON+DotPath';
public const TYPE_JSON_DOTPATH = 'JSON+DotPath'; // Legacy 1.24.0-dev
public const TYPE_JSON_DOTNOTATION = 'JSON+DotNotation';
public const TYPE_JSONFEED = 'JSONFeed';
/**
@ -153,7 +154,7 @@ class FreshRSS_Export_Service {
$zip_filename = 'freshrss_' . $this->username . '_' . $day . '_export.zip';
// From https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly
$zip_file = tempnam('/tmp', 'zip');
$zip_file = tempnam(TMP_PATH, 'zip');
if ($zip_file == false) {
return [$zip_filename, false];
}

View File

@ -161,8 +161,9 @@ class FreshRSS_Import_Service {
case strtolower(FreshRSS_Export_Service::TYPE_XML_XPATH):
$feed->_kind(FreshRSS_Feed::KIND_XML_XPATH);
break;
case strtolower(FreshRSS_Export_Service::TYPE_JSON_DOTNOTATION):
case strtolower(FreshRSS_Export_Service::TYPE_JSON_DOTPATH):
$feed->_kind(FreshRSS_Feed::KIND_JSON_DOTPATH);
$feed->_kind(FreshRSS_Feed::KIND_JSON_DOTNOTATION);
break;
case strtolower(FreshRSS_Export_Service::TYPE_JSONFEED):
$feed->_kind(FreshRSS_Feed::KIND_JSONFEED);
@ -254,7 +255,7 @@ class FreshRSS_Import_Service {
$jsonSettings['itemUid'] = $feed_elt['frss:jsonItemUid'];
}
if (!empty($jsonSettings)) {
$feed->_attribute('json_dotpath', $jsonSettings);
$feed->_attribute('json_dotnotation', $jsonSettings);
}
$curl_params = [];

View File

@ -97,11 +97,11 @@ final class FreshRSS_dotNotation_Util
*
* @param array<string> $jf json feed
* @param string $feedSourceUrl the source URL for the feed
* @param array<string,string> $dotPaths dot paths to map JSON into RSS
* @param string $defaultRssTitle Default title of the RSS feed, if not already provided in dotPath `feedTitle`
* @param array<string,string> $dotNotation dot notation to map JSON into RSS
* @param string $defaultRssTitle Default title of the RSS feed, if not already provided in dotNotation `feedTitle`
*/
public static function convertJsonToRss(array $jf, string $feedSourceUrl, array $dotPaths, string $defaultRssTitle = ''): ?string {
if (!isset($dotPaths['item']) || $dotPaths['item'] === '') {
public static function convertJsonToRss(array $jf, string $feedSourceUrl, array $dotNotation, string $defaultRssTitle = ''): ?string {
if (!isset($dotNotation['item']) || $dotNotation['item'] === '') {
return null; //no definition of item path, but we can't scrape anything without knowing this
}
@ -112,40 +112,40 @@ final class FreshRSS_dotNotation_Util
$view->html_url = $view->rss_url;
$view->entries = [];
$view->rss_title = isset($dotPaths['feedTitle'])
? (htmlspecialchars(FreshRSS_dotNotation_Util::getString($jf, $dotPaths['feedTitle']) ?? '', ENT_COMPAT, 'UTF-8') ?: $defaultRssTitle)
$view->rss_title = isset($dotNotation['feedTitle'])
? (htmlspecialchars(FreshRSS_dotNotation_Util::getString($jf, $dotNotation['feedTitle']) ?? '', ENT_COMPAT, 'UTF-8') ?: $defaultRssTitle)
: $defaultRssTitle;
$jsonItems = FreshRSS_dotNotation_Util::get($jf, $dotPaths['item']);
$jsonItems = FreshRSS_dotNotation_Util::get($jf, $dotNotation['item']);
if (!is_array($jsonItems) || count($jsonItems) === 0) {
return null;
}
foreach ($jsonItems as $jsonItem) {
$rssItem = [];
$rssItem['link'] = isset($dotPaths['itemUri']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemUri']) ?? '' : '';
$rssItem['link'] = isset($dotNotation['itemUri']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemUri']) ?? '' : '';
if (empty($rssItem['link'])) {
continue;
}
$rssItem['title'] = isset($dotPaths['itemTitle']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemTitle']) ?? '' : '';
$rssItem['author'] = isset($dotPaths['itemAuthor']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemAuthor']) ?? '' : '';
$rssItem['timestamp'] = isset($dotPaths['itemTimestamp']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemTimestamp']) ?? '' : '';
$rssItem['title'] = isset($dotNotation['itemTitle']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemTitle']) ?? '' : '';
$rssItem['author'] = isset($dotNotation['itemAuthor']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemAuthor']) ?? '' : '';
$rssItem['timestamp'] = isset($dotNotation['itemTimestamp']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemTimestamp']) ?? '' : '';
//get simple content, but if a path for HTML content has been provided, replace the simple content with HTML content
$rssItem['content'] = isset($dotPaths['itemContent']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemContent']) ?? '' : '';
$rssItem['content'] = isset($dotPaths['itemContentHTML'])
? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemContentHTML']) ?? ''
$rssItem['content'] = isset($dotNotation['itemContent']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemContent']) ?? '' : '';
$rssItem['content'] = isset($dotNotation['itemContentHTML'])
? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemContentHTML']) ?? ''
: $rssItem['content'];
if (isset($dotPaths['itemTimeFormat']) && is_string($dotPaths['itemTimeFormat'])) {
$dateTime = DateTime::createFromFormat($dotPaths['itemTimeFormat'], $rssItem['timestamp']);
if (isset($dotNotation['itemTimeFormat']) && is_string($dotNotation['itemTimeFormat'])) {
$dateTime = DateTime::createFromFormat($dotNotation['itemTimeFormat'], $rssItem['timestamp']);
if ($dateTime != false) {
$rssItem['timestamp'] = $dateTime->format(DateTime::ATOM);
}
}
if (isset($dotPaths['itemCategories'])) {
$jsonItemCategories = FreshRSS_dotNotation_Util::get($jsonItem, $dotPaths['itemCategories']);
if (isset($dotNotation['itemCategories'])) {
$jsonItemCategories = FreshRSS_dotNotation_Util::get($jsonItem, $dotNotation['itemCategories']);
if (is_string($jsonItemCategories) && $jsonItemCategories !== '') {
$rssItem['tags'] = [$jsonItemCategories];
} elseif (is_array($jsonItemCategories) && count($jsonItemCategories) > 0) {
@ -158,31 +158,31 @@ final class FreshRSS_dotNotation_Util
}
}
$rssItem['thumbnail'] = isset($dotPaths['itemThumbnail']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemThumbnail']) ?? '' : '';
$rssItem['thumbnail'] = isset($dotNotation['itemThumbnail']) ? FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemThumbnail']) ?? '' : '';
//Enclosures?
if (isset($dotPaths['itemAttachment'])) {
$jsonItemAttachments = FreshRSS_dotNotation_Util::get($jsonItem, $dotPaths['itemAttachment']);
if (isset($dotNotation['itemAttachment'])) {
$jsonItemAttachments = FreshRSS_dotNotation_Util::get($jsonItem, $dotNotation['itemAttachment']);
if (is_array($jsonItemAttachments) && count($jsonItemAttachments) > 0) {
$rssItem['attachments'] = [];
foreach ($jsonItemAttachments as $attachment) {
$rssAttachment = [];
$rssAttachment['url'] = isset($dotPaths['itemAttachmentUrl'])
? FreshRSS_dotNotation_Util::getString($attachment, $dotPaths['itemAttachmentUrl'])
$rssAttachment['url'] = isset($dotNotation['itemAttachmentUrl'])
? FreshRSS_dotNotation_Util::getString($attachment, $dotNotation['itemAttachmentUrl'])
: '';
$rssAttachment['type'] = isset($dotPaths['itemAttachmentType'])
? FreshRSS_dotNotation_Util::getString($attachment, $dotPaths['itemAttachmentType'])
$rssAttachment['type'] = isset($dotNotation['itemAttachmentType'])
? FreshRSS_dotNotation_Util::getString($attachment, $dotNotation['itemAttachmentType'])
: '';
$rssAttachment['length'] = isset($dotPaths['itemAttachmentLength'])
? FreshRSS_dotNotation_Util::get($attachment, $dotPaths['itemAttachmentLength'])
$rssAttachment['length'] = isset($dotNotation['itemAttachmentLength'])
? FreshRSS_dotNotation_Util::get($attachment, $dotNotation['itemAttachmentLength'])
: '';
$rssItem['attachments'][] = $rssAttachment;
}
}
}
if (isset($dotPaths['itemUid'])) {
$rssItem['guid'] = FreshRSS_dotNotation_Util::getString($jsonItem, $dotPaths['itemUid']);
if (isset($dotNotation['itemUid'])) {
$rssItem['guid'] = FreshRSS_dotNotation_Util::getString($jsonItem, $dotNotation['itemUid']);
}
if (empty($rssItem['guid'])) {

View File

@ -19,8 +19,8 @@ return array(
'http' => 'HTTP (haladó felhasználóknak HTTPS-el)',
'none' => 'nincs (veszélyes)',
'title' => 'Hitelesítés',
'token' => 'Master authentication token', // TODO
'token_help' => 'Allows access to all RSS outputs of the user as well as refreshing feeds without authentication:', // TODO
'token' => 'Fő hitelesítési token',
'token_help' => 'Lehetővé teszi a hozzáférést a felhasználó összes RSS-kimenetéhez, valamint a hírfolyamok frissítéséhez hitelesítés nélkül:',
'type' => 'Hitelesítési módszer',
'unsafe_autologin' => 'Engedélyezze a nem biztonságos automata bejelentkezést a következő formátummal: ',
),

View File

@ -120,20 +120,20 @@ return array(
'feeds' => 'Rendezés hírforrás szerint',
'order' => 'Rendezés dátum szerint',
'search' => 'Kifejezés',
'shareOpml' => 'Enable sharing by OPML of corresponding categories and feeds', // TODO
'shareRss' => 'Enable sharing by HTML &amp; RSS', // TODO
'shareOpml' => 'Engedélyezze a megfelelő kategóriák és hírcsatornák OPML-alapú megosztását',
'shareRss' => 'Engedélyezze a HTML &amp; RSS megosztást',
'state' => 'Státusz',
'tags' => 'Rendezés címke szerint',
'type' => 'Típus',
),
'get_all' => 'Minden cikk megjelenítése',
'get_all_labels' => 'Display articles with any label', // TODO
'get_all_labels' => 'Cikkek megjelenítése bármilyen címkével',
'get_category' => 'Listáz “%s” kategóriát',
'get_favorite' => 'Kedvenc cikkek megjelenítése',
'get_feed' => 'Listáz “%s” hírforrást',
'get_important' => 'Display articles from important feeds', // TODO
'get_label' => 'Display articles with “%s” label', // TODO
'help' => 'See the <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank">documentation for user queries and resharing by HTML / RSS / OPML</a>.', // TODO
'get_important' => 'Cikkek megjelenítése fontos hírforrásokból',
'get_label' => ' “%s” címkével rendelkező cikkek megjelenítése',
'help' => 'Lásd a <a href="https://freshrss.github.io/FreshRSS/en/users/user_queries.html" target="_blank"> dokumentációt a felhasználói lekérdezések és HTML/RSS/OPML megosztás témákban</a>.',
'name' => 'Név',
'no_filter' => 'Nincs szűrés',
'number' => 'Lekérdezés %d',
@ -141,11 +141,11 @@ return array(
'order_desc' => 'Újabb cikkek előre',
'search' => 'Keresse a “%s”',
'share' => array(
'_' => 'Share this query by link', // TODO
'help' => 'Give this link if you want to share this query with anyone', // TODO
'html' => 'Shareable link to the HTML page', // TODO
'opml' => 'Shareable link to the OPML list of feeds', // TODO
'rss' => 'Shareable link to the RSS feed', // TODO
'_' => 'Lekérdezés megosztása linkkel',
'help' => 'Ezt a linket küldd el hogy megoszd a lekérdezést',
'html' => 'Megosztható link a HTML oldalhoz',
'opml' => 'Megosztható link az OPML hírforrás listához',
'rss' => 'Megosztható link az RSS hírforráshoz',
),
'state_0' => 'Minden cikk',
'state_1' => 'Olvasott cikkek',

View File

@ -116,10 +116,10 @@ return array(
),
'tag' => array(
'created' => 'Címke “%s” létrehozva.',
'error' => 'Label could not be updated!', // TODO
'error' => 'Nem sikerült a címke frissítése!',
'name_exists' => 'Címke név már létezik.',
'renamed' => 'Címke “%s” átnevezve “%s”.',
'updated' => 'Label has been updated.', // TODO
'updated' => 'Címke frissítése megtörtént.',
),
'update' => array(
'can_apply' => 'Egy FreshRSS frissítés elérhető : <strong>Verzió %s</strong>.',

View File

@ -67,9 +67,9 @@ return array(
'empty' => 'Ez a hírforrás üres. Ellenőrizd hogy van e tartalom rajta.',
'error' => 'Ez a hírforrás nem működik. Ellenőrizd az elérhetőségét és frissítsd.',
'export-as-opml' => array(
'download' => 'Download', // TODO
'help' => 'XML file', // TODO
'label' => 'Export as OPML', // TODO
'download' => 'Letöltés',
'help' => 'XML fájl',
'label' => 'Exportálás OPML formátumban',
),
'filteractions' => array(
'_' => 'Szűrő műveletek',
@ -113,7 +113,7 @@ return array(
),
'item_title' => array(
'_' => 'elem cím',
'help' => 'Használja az <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath axis</a> <code>descendant::</code> mint <code>descendant::h2</code>',
'help' => 'Használja az <a href="https://developer.mozilla.org/docs/Web/XPath/Axes" target="_blank">XPath Axes</a> <code>descendant::</code> mint <code>descendant::h2</code>',
),
'item_uid' => array(
'_' => 'elem egyedi ID',
@ -127,44 +127,44 @@ return array(
'xpath' => 'XPath ehhez:',
),
'json_dotnotation' => array(
'_' => 'JSON (dot notation)', // TODO
'_' => 'JSON (pont jelölés)',
'feed_title' => array(
'_' => 'feed title', // TODO
'help' => 'Example: <code>meta.title</code> or a static string: <code>"My custom feed"</code>', // TODO
'_' => 'hírforrás címe',
'help' => 'Például: <code>meta.title</code> vagy egy statikus sztring: <code>"Az egyedi hírforrásom"</code>',
),
'help' => 'A JSON dot notated uses dots between objects and brackets for arrays (e.g. <code>data.items[0].title</code>)', // TODO
'help' => 'A JSON pontjelölés pontokat használ az objektumok között és zárójeleket a tömbökhöz (pl. <code>data.items[0].title</code>)',
'item' => array(
'_' => 'finding news <strong>items</strong><br /><small>(most important)</small>', // TODO
'help' => 'JSON path to the array containing the items, e.g. <code>newsItems</code>', // TODO
'_' => 'hírek keresése <strong>elemek</strong><br /><small>(legfontosabb)</small>',
'help' => 'JSON útvonal az elemeket tartalmazó tömbhöz, pl. <code>newsItems</code>',
),
'item_author' => 'item author', // TODO
'item_categories' => 'item tags', // TODO
'item_author' => 'elem szerző',
'item_categories' => 'elem címkék',
'item_content' => array(
'_' => 'item content', // TODO
'help' => 'Key under which the content is found, e.g. <code>content</code>', // TODO
'_' => 'elem tartalom',
'help' => 'Kulcs ami alatt a tartalom megtalálható, például <code>tartalom</code>',
),
'item_thumbnail' => array(
'_' => 'item thumbnail', // TODO
'help' => 'Example: <code>image</code>', // TODO
'_' => 'elem előnézeti kép',
'help' => 'Például: <code>image</code>',
),
'item_timeFormat' => array(
'_' => 'Custom date/time format', // TODO
'help' => 'Optional. A format supported by <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> such as <code>d-m-Y H:i:s</code>', // TODO
'_' => 'Egyedi dátum/idő formátum',
'help' => 'Opcionális. Egy a <a href="https://php.net/datetime.createfromformat" target="_blank"><code>DateTime::createFromFormat()</code></a> funkció által támogatott formátum, például: <code>d-m-Y H:i:s</code>',
),
'item_timestamp' => array(
'_' => 'item date', // TODO
'help' => 'The result will be parsed by <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a>', // TODO
'_' => 'elem dátum',
'help' => 'Az eredményt az <a href="https://php.net/strtotime" target="_blank"><code>strtotime()</code></a> php függvény fogja értelmezni',
),
'item_title' => 'item title', // TODO
'item_uid' => 'item unique ID', // TODO
'item_title' => 'elem címe',
'item_uid' => 'elem egyedi azonosító ID',
'item_uri' => array(
'_' => 'item link (URL)', // TODO
'help' => 'Example: <code>permalink</code>', // TODO
'_' => 'elem link (URL)',
'help' => 'Például: <code>permalink</code>',
),
'json' => 'dot notation for:', // TODO
'relative' => 'dot notated path (relative to item) for:', // TODO
'json' => 'pontjelölés ehhez:',
'relative' => 'pont jelölt útvonal (relatív az elemhez):',
),
'jsonfeed' => 'JSON Feed', // TODO
'jsonfeed' => 'JSON Hírforrás',
'rss' => 'RSS / Atom (alapértelmezett)',
'xml_xpath' => 'XML + XPath', // IGNORE
),
@ -178,10 +178,10 @@ return array(
'max_http_redir' => 'Max HTTP átirányítás',
'max_http_redir_help' => '0 vagy üresen hagyva kikapcsolt, -1 a végtelen átirányításhoz',
'method' => array(
'_' => 'HTTP Method', // TODO
'_' => 'HTTP Módszer',
),
'method_help' => 'The POST payload has automatic support for <code>application/x-www-form-urlencoded</code> and <code>application/json</code>', // TODO
'method_postparams' => 'Payload for POST', // TODO
'method_help' => 'A POST metódus hasznos adattartalma automatikusan támogatja az <code>application/x-www-form-urlencoded</code> és <code>application/json</code>',
'method_postparams' => 'POST metódus adattartalma',
'moved_category_deleted' => 'Ha kitörölsz egy kategóriát, az alá tartozó hírforrások automatikusan ide kerülnek <em>%s</em>.',
'mute' => 'némítás',
'no_selected' => 'Nincsen hírforrás kiválasztva.',
@ -245,7 +245,7 @@ return array(
'subscription_tools' => 'Hírforrás eszközök',
),
'tag' => array(
'auto_label' => 'Add this label to new articles', // TODO
'auto_label' => 'Adja hozzá automatikusan ezt a címkét az új cikkekhez',
'name' => 'Név',
'new_name' => 'Új név',
'old_name' => 'Régi név',

View File

@ -18,25 +18,25 @@
<div class="item search">
<?php if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::systemConf()->allow_anonymous) { ?>
<form action="<?= $this->html_url ?>" method="get">
<div class="stick">
<?php if (Minz_Request::controllerName() === 'index'): ?>
<?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
<?php } if (Minz_Request::paramString('get') !== '') { ?>
<input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
<?php } if (Minz_Request::paramInt('state') !== 0) { ?>
<input type="hidden" name="state" value="<?= Minz_Request::paramInt('state') ?>" />
<?php } ?>
<?php endif; ?>
<?php if (Minz_Request::paramString('user') !== '') { ?>
<input type="hidden" name="user" value="<?= Minz_User::name() ?>" />
<?php } if (ctype_alnum(Minz_Request::paramString('t'))) { ?>
<input type="hidden" name="t" value="<?= Minz_Request::paramString('t') ?>" />
<?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
<input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
<?php } if (ctype_lower(Minz_Request::paramString('f'))) { ?>
<input type="hidden" name="f" value="<?= Minz_Request::paramString('f') ?>" />
<?php if (Minz_Request::controllerName() === 'index'): ?>
<?php if (in_array(Minz_Request::actionName(), ['normal', 'global', 'reader'], true)) { ?>
<input type="hidden" name="a" value="<?= Minz_Request::actionName() ?>" />
<?php } if (Minz_Request::paramString('get') !== '') { ?>
<input type="hidden" name="get" value="<?= FreshRSS_Context::currentGet() ?>" />
<?php } if (Minz_Request::paramInt('state') !== 0) { ?>
<input type="hidden" name="state" value="<?= Minz_Request::paramInt('state') ?>" />
<?php } ?>
<?php endif; ?>
<?php if (Minz_Request::paramString('user') !== '') { ?>
<input type="hidden" name="user" value="<?= Minz_User::name() ?>" />
<?php } if (ctype_alnum(Minz_Request::paramString('t'))) { ?>
<input type="hidden" name="t" value="<?= Minz_Request::paramString('t') ?>" />
<?php } if (ctype_upper(Minz_Request::paramString('order'))) { ?>
<input type="hidden" name="order" value="<?= FreshRSS_Context::$order ?>" />
<?php } if (ctype_lower(Minz_Request::paramString('f'))) { ?>
<input type="hidden" name="f" value="<?= Minz_Request::paramString('f') ?>" />
<?php } ?>
<div class="stick">
<input type="search" name="search" id="search"
value="<?= htmlspecialchars(htmlspecialchars_decode(Minz_Request::paramString('search'), ENT_QUOTES), ENT_COMPAT, 'UTF-8') ?>"
placeholder="<?= _t('gen.menu.search') ?>" />

View File

@ -27,8 +27,8 @@ function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
case FreshRSS_Feed::KIND_XML_XPATH:
$outline['type'] = FreshRSS_Export_Service::TYPE_XML_XPATH;
break;
case FreshRSS_Feed::KIND_JSON_DOTPATH:
$outline['type'] = FreshRSS_Export_Service::TYPE_JSON_DOTPATH;
case FreshRSS_Feed::KIND_JSON_DOTNOTATION:
$outline['type'] = FreshRSS_Export_Service::TYPE_JSON_DOTNOTATION;
break;
case FreshRSS_Feed::KIND_JSONFEED:
$outline['type'] = FreshRSS_Export_Service::TYPE_JSONFEED;
@ -48,9 +48,9 @@ function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false): array {
$outline['frss:xPathItemThumbnail'] = $xPathSettings['itemThumbnail'] ?? null;
$outline['frss:xPathItemCategories'] = $xPathSettings['itemCategories'] ?? null;
$outline['frss:xPathItemUid'] = $xPathSettings['itemUid'] ?? null;
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH) {
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION) {
/** @var array<string,string> */
$jsonSettings = $feed->attributeArray('json_dotpath') ?? [];
$jsonSettings = $feed->attributeArray('json_dotnotation') ?? [];
$outline['frss:jsonItem'] = $jsonSettings['item'] ?? null;
$outline['frss:jsonItemTitle'] = $jsonSettings['itemTitle'] ?? null;
$outline['frss:jsonItemContent'] = $jsonSettings['itemContent'] ?? null;

View File

@ -413,7 +413,7 @@
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
<option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'selected="selected"' : '' ?> data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSONFEED ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_JSONFEED ? 'selected="selected"' : '' ?>><?= _t('sub.feed.kind.jsonfeed') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSON_DOTPATH ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH ? 'selected="selected"' : '' ?> data-show="json_dotpath"><?= _t('sub.feed.kind.json_dotnotation') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSON_DOTNOTATION ?>" <?= $this->feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION ? 'selected="selected"' : '' ?> data-show="json_dotnotation"><?= _t('sub.feed.kind.json_dotnotation') ?></option>
</select>
</div>
</div>
@ -514,9 +514,9 @@
</div>
</fieldset>
<fieldset id="json_dotpath">
<fieldset id="json_dotnotation">
<?php
$jsonSettings = Minz_Helper::htmlspecialchars_utf8($this->feed->attributeArray('json_dotpath') ?? []);
$jsonSettings = Minz_Helper::htmlspecialchars_utf8($this->feed->attributeArray('json_dotnotation') ?? []);
?>
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.json_dotnotation.help') ?></p>
<div class="form-group">

View File

@ -51,8 +51,8 @@
endif;
?></li><?php
endif; ?>
<li class="item title<?= (($topline_thumbnail !== 'none') || $topline_summary) ? ' multiline' : '' ?>" dir="auto"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>" class="item-element"><?= $this->entry->title() ?><?php
<li class="item titleAuthorSummaryDate">
<span class="item-element title<?= (($topline_thumbnail !== 'none') || $topline_summary) ? ' multiline' : '' ?>" dir="auto"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>"><?= $this->entry->title() ?><?php
if ($topline_display_authors):
?><span class="author"><?php
$authors = $this->entry->authors();
@ -65,11 +65,13 @@
}
?></span><?php
endif;
if ($topline_summary):
?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content(false)), 0, 500, 'UTF-8')) ?></div><?php
endif;
?></a></li>
<?php if ($topline_date) { ?><li class="item date"><time datetime="<?= $this->entry->machineReadableDate() ?>" class="item-element"><?= $this->entry->date() ?></time>&nbsp;</li><?php } ?>
?></a></span>
<?php
if ($topline_summary):
?><div class="summary"><?= trim(mb_substr(strip_tags($this->entry->content(false)), 0, 500, 'UTF-8')) ?></div><?php
endif; ?>
<?php if ($topline_date) { ?><span class="item-element date"><time datetime="<?= $this->entry->machineReadableDate() ?>"><?= $this->entry->date() ?></time>&nbsp;</span><?php } ?>
</li>
<?php if ($topline_link) { ?><li class="item link"><a target="_blank" rel="noreferrer" href="<?= $this->entry->link() ?>" class="item-element" title="<?=
_t('conf.shortcut.see_on_website') ?>"><?= _i('link') ?></a></li><?php } ?>
</ul>

View File

@ -72,7 +72,7 @@
<option value="<?= FreshRSS_Feed::KIND_HTML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.html_xpath') ?></option>
<option value="<?= FreshRSS_Feed::KIND_XML_XPATH ?>" data-show="html_xpath"><?= _t('sub.feed.kind.xml_xpath') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSONFEED ?>"><?= _t('sub.feed.kind.jsonfeed') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSON_DOTPATH ?>" data-show="json_dotpath"><?= _t('sub.feed.kind.json_dotnotation') ?></option>
<option value="<?= FreshRSS_Feed::KIND_JSON_DOTNOTATION ?>" data-show="json_dotnotation"><?= _t('sub.feed.kind.json_dotnotation') ?></option>
</select>
</div>
</div>
@ -166,7 +166,7 @@
</div>
</div>
</fieldset>
<fieldset id="json_dotpath">
<fieldset id="json_dotnotation">
<p class="help"><?= _i('help') ?> <?= _t('sub.feed.kind.json_dotnotation.help') ?></p>
<div class="form-group">
<label class="group-name" for="jsonFeedTitle"><small><?= _t('sub.feed.kind.json_dotnotation.json') ?></small><br />

View File

@ -121,6 +121,14 @@ cd /usr/share/FreshRSS
```sh
cd /usr/share/FreshRSS
./cli/db-backup.php
# Back-up all users respective database to `data/users/*/backup.sqlite`
./cli/db-restore.php --delete-backup --force-overwrite
# Restore all users respective database from `data/users/*/backup.sqlite`
# --delete-backup: delete `data/users/*/backup.sqlite` after successful import
# --force-overwrite: will clear the users respective database before import
./cli/db-optimize.php --user username
# Optimize database (reduces the size) for a given user (perform `OPTIMIZE TABLE` in MySQL, `VACUUM` in SQLite)
```

20
cli/db-backup.php Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$ok = true;
foreach (listUsers() as $username) {
$username = cliInitUser($username);
$filename = DATA_PATH . '/users/' . $username . '/backup.sqlite';
@unlink($filename);
echo 'FreshRSS backup database to SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$ok &= $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_EXPORT);
}
done((bool)$ok);

65
cli/db-restore.php Executable file
View File

@ -0,0 +1,65 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require(__DIR__ . '/_cli.php');
performRequirementCheck(FreshRSS_Context::systemConf()->db['type'] ?? '');
$cliOptions = new class extends CliOptionsParser {
public string $deleteBackup;
public string $forceOverwrite;
public function __construct() {
$this->addOption('deleteBackup', (new CliOption('delete-backup'))->withValueNone());
$this->addOption('forceOverwrite', (new CliOption('force-overwrite'))->withValueNone());
parent::__construct();
}
};
if (!empty($cliOptions->errors)) {
fail('FreshRSS error: ' . array_shift($cliOptions->errors) . "\n" . $cliOptions->usage);
}
FreshRSS_Context::initSystem(true);
Minz_User::change(Minz_User::INTERNAL_USER);
$ok = false;
try {
$error = initDb();
if ($error != '') {
$_SESSION['bd_error'] = $error;
} else {
$ok = true;
}
} catch (Exception $ex) {
$_SESSION['bd_error'] = $ex->getMessage();
}
if (!$ok) {
fail('FreshRSS database error: ' . (empty($_SESSION['bd_error']) ? 'Unknown error' : $_SESSION['bd_error']));
}
foreach (listUsers() as $username) {
$username = cliInitUser($username);
$filename = DATA_PATH . "/users/{$username}/backup.sqlite";
if (!file_exists($filename)) {
fwrite(STDERR, "FreshRSS SQLite backup not found for user “{$username}”!\n");
$ok = false;
continue;
}
echo 'FreshRSS restore database from SQLite for user “', $username, "”…\n";
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$clearFirst = isset($cliOptions->forceOverwrite);
$ok &= $databaseDAO->dbCopy($filename, FreshRSS_DatabaseDAO::SQLITE_IMPORT, $clearFirst);
if ($ok) {
if (isset($cliOptions->deleteBackup)) {
unlink($filename);
}
} else {
fwrite(STDERR, "FreshRSS database already exists for user “{$username}”!\n");
fwrite(STDERR, "If you would like to clear the user database first, use the option --force-overwrite\n");
}
invalidateHttpCache($username);
}
done((bool)$ok);

View File

@ -10,9 +10,19 @@ Do this before an upgrade.
This following tutorial demonstrates commands for backing up FreshRSS. It assumes that your main FreshRSS directory is `/usr/share/FreshRSS`. If youve installed it somewhere else, substitute your path as necessary.
### Creating a database backup
Back-up all users respective database to `data/users/*/backup.sqlite`
```sh
cd /usr/share/FreshRSS/
./cli/db-backup.php
```
### Creating a Backup of all Files
First, Enter the directory you wish to save your backup to. Here, for example, well save the backup to the user home directory
Enter the directory you wish to save your backup to.
Here, for example, well save the backup to the user home directory
```sh
cd ~
@ -52,7 +62,39 @@ And optionally, as cleanup, remove the copy of your backup from the FreshRSS dir
rm FreshRSS-backup.tgz
```
## Backing up Feeds
### Restore a database backup
> It is safer to stop your Web server and cron during maintenance operations.
Restore all users respective database from `data/users/*/backup.sqlite`
```sh
cd /usr/share/FreshRSS/
./cli/db-restore.php --delete-backup --force-overwrite
```
## Migrate database
Start by making an automatic backup of the all the user databases to SQLite files:
```sh
cd /usr/share/FreshRSS/
./cli/db-backup.php
```
Change your database setup:
- if you like to change database type (e.g. from MySQL to PostgreSQL), edit `data/config.php` accordingly.
- if you upgrade to a major PostgreSQL version, after a PostgreSQL backup, you may delete the old instance and start a new instance (remove the PostgreSQL volume if using Docker).
Restore all the user databases from the SQLite files:
```sh
./cli/db-restore.php --delete-backup --force-overwrite
```
See also our [Docker documentation to migrate database](https://github.com/FreshRSS/FreshRSS/blob/edge/Docker/README.md#migrate-database).
## Backing up selected content
### Feed list Export

View File

@ -49,7 +49,7 @@ To set up FreshRSS behind a reverse proxy with Caddy and using a subfolder, foll
Restart FreshRSS to ensure that it recognizes the new base URL:
```bash
docker-compose restart freshrss
docker compose restart freshrss
```
4. **Access FreshRSS:**

View File

@ -44,28 +44,28 @@ The following attributes are using similar naming conventions than [RSS-Bridge](
* `frss:xPathItemCategories`: XPath expression for extracting a list of categories (tags) from the item context.
* `frss:xPathItemUid`: XPath expression for extracting an items unique ID from the item context. If left empty, a hash is computed automatically.
### JSON+DotPath
### JSON+DotNotation
* `<outline type="JSON+DotPath" ...`: Similar to `HTML+XPath` but for JSON and using a dot/bracket syntax such as `object.object.array[2].property`.
* `<outline type="JSON+DotNotation" ...`: Similar to `HTML+XPath` but for JSON and using a dot/bracket syntax such as `object.object.array[2].property`.
* `frss:jsonItem`: JSON dot path for extracting the feed items from the source page.
* `frss:jsonItem`: JSON dot notation for extracting the feed items from the source page.
* Example: `data.items`
* `frss:jsonItemTitle`: JSON dot path for extracting the items title from the item context.
* `frss:jsonItemTitle`: JSON dot notation for extracting the items title from the item context.
* Example: `meta.title`
* `frss:jsonItemContent`: JSON dot path for extracting an items content from the item context.
* `frss:jsonItemContent`: JSON dot notation for extracting an items content from the item context.
* Example: `content`
* `frss:jsonItemUri`: JSON dot path for extracting an item link from the item context.
* `frss:jsonItemUri`: JSON dot notation for extracting an item link from the item context.
* Example: `meta.links[0]`
* `frss:jsonItemAuthor`: JSON dot path for extracting an item author from the item context.
* `frss:jsonItemTimestamp`: JSON dot path for extracting an item timestamp from the item context. The result will be parsed by [`strtotime()`](https://php.net/strtotime).
* `frss:jsonItemAuthor`: JSON dot notation for extracting an item author from the item context.
* `frss:jsonItemTimestamp`: JSON dot notation for extracting an item timestamp from the item context. The result will be parsed by [`strtotime()`](https://php.net/strtotime).
* `frss:jsonItemTimeFormat`: Date/Time format to parse the timestamp, according to [`DateTime::createFromFormat()`](https://php.net/datetime.createfromformat).
* `frss:jsonItemThumbnail`: JSON dot path for extracting an items thumbnail (image) URL from the item context.
* `frss:jsonItemCategories`: JSON dot path for extracting a list of categories (tags) from the item context.
* `frss:jsonItemUid`: JSON dot path for extracting an items unique ID from the item context. If left empty, a hash is computed automatically.
* `frss:jsonItemThumbnail`: JSON dot notation for extracting an items thumbnail (image) URL from the item context.
* `frss:jsonItemCategories`: JSON dot notation for extracting a list of categories (tags) from the item context.
* `frss:jsonItemUid`: JSON dot notation for extracting an items unique ID from the item context. If left empty, a hash is computed automatically.
### JSON Feed
* `<outline type="JSONFeed" ...`: Uses `JSON+DotPath` behind the scenes to parse a [JSON Feed](https://www.jsonfeed.org/).
* `<outline type="JSONFeed" ...`: Uses `JSON+DotNotation` behind the scenes to parse a [JSON Feed](https://www.jsonfeed.org/).
### cURL

View File

@ -1081,7 +1081,7 @@ function init_stream(stream) {
return true;
}
el = ev.target.closest('.item.title > a');
el = ev.target.closest('.item .title > a');
if (el) { // Allow default control/command-click behaviour such as open in background-tab
return ev.ctrlKey || ev.metaKey;
}
@ -1189,7 +1189,7 @@ function init_stream(stream) {
return;
}
let el = ev.target.closest('.item.title > a');
let el = ev.target.closest('.item .title > a');
if (el) {
if (ev.which == 1) {
if (ev.ctrlKey) { // Control+click

View File

@ -747,11 +747,11 @@ kbd {
background: var(--background-color-dark);
}
.flux.not_read:not(.current):hover .item.title {
background: inherit;
.flux.not_read:not(.current):hover .item .title {
background: var(--background-color-hover);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background: var(--background-color-hover);
}
@ -759,11 +759,11 @@ kbd {
background: var(--background-color-hover);
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--font-color-light);
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .title .author {
color: var(--font-color-dark);
}
@ -794,9 +794,9 @@ kbd {
filter: grayscale(0.8) brightness(1.7);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--font-color-light);
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -747,11 +747,11 @@ kbd {
background: var(--background-color-dark);
}
.flux.not_read:not(.current):hover .item.title {
background: inherit;
.flux.not_read:not(.current):hover .item .title {
background: var(--background-color-hover);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background: var(--background-color-hover);
}
@ -759,11 +759,11 @@ kbd {
background: var(--background-color-hover);
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--font-color-light);
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .title .author {
color: var(--font-color-dark);
}
@ -794,9 +794,9 @@ kbd {
filter: grayscale(0.8) brightness(1.7);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--font-color-light);
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -15,7 +15,7 @@
&:hover {
background: variables.$grey-lighter;
&:not(.current):hover .item.title {
&:not(.current):hover .item .title {
background: variables.$grey-lighter;
}
}
@ -33,13 +33,13 @@
&.not_read:not(.current) {
background: variables.$unread-bg;
&:hover .item.title {
&:hover .item .title {
background: variables.$unread-bg;
}
}
&.not_read {
.item.title {
.item .title {
a {
color: variables.$unread-font-color;
}
@ -51,7 +51,7 @@
}
}
.item.date {
.item .date {
color: color.scale(variables.$unread-font-color, $alpha: -50%);
}
}
@ -65,7 +65,7 @@
&.favorite:not(.current) {
background: variables.$fav-light;
&:hover .item.title {
&:hover .item .title {
background: variables.$fav-light;
}
}
@ -77,7 +77,8 @@
}
}
.item.date {
.flux_header .date,
.flux_content .bottom .date {
color: color.scale(variables.$main-font-color, $alpha: -50%);
font-size: 0.85rem;
}

View File

@ -989,7 +989,7 @@ main.prompt {
.flux .flux_header:hover {
background: #fcfaf8;
}
.flux .flux_header:hover:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title {
background: #fcfaf8;
}
.flux.current {
@ -1002,16 +1002,16 @@ main.prompt {
.flux.not_read:not(.current) {
background: #f2f6f8;
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background: #f2f6f8;
}
.flux.not_read .item.title a {
.flux.not_read .item .title a {
color: #161a38;
}
.flux.not_read .item.website a {
color: #161a38;
}
.flux.not_read .item.date {
.flux.not_read .item .date {
color: rgba(22, 26, 56, 0.5);
}
.flux.favorite {
@ -1021,14 +1021,15 @@ main.prompt {
.flux.favorite:not(.current) {
background: #fff6da;
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background: #fff6da;
}
.flux .website a {
color: #363330;
opacity: 0.75;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: rgba(54, 51, 48, 0.5);
font-size: 0.85rem;
}

View File

@ -989,7 +989,7 @@ main.prompt {
.flux .flux_header:hover {
background: #fcfaf8;
}
.flux .flux_header:hover:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title {
background: #fcfaf8;
}
.flux.current {
@ -1002,16 +1002,16 @@ main.prompt {
.flux.not_read:not(.current) {
background: #f2f6f8;
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background: #f2f6f8;
}
.flux.not_read .item.title a {
.flux.not_read .item .title a {
color: #161a38;
}
.flux.not_read .item.website a {
color: #161a38;
}
.flux.not_read .item.date {
.flux.not_read .item .date {
color: rgba(22, 26, 56, 0.5);
}
.flux.favorite {
@ -1021,14 +1021,15 @@ main.prompt {
.flux.favorite:not(.current) {
background: #fff6da;
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background: #fff6da;
}
.flux .website a {
color: #363330;
opacity: 0.75;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: rgba(54, 51, 48, 0.5);
font-size: 0.85rem;
}

View File

@ -452,15 +452,16 @@ button.as-link[disabled] {
background-color: var(--dark-background-color1);
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--dark-font-color8);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--dark-font-color6);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background-color: var(--dark-background-color1);
}

View File

@ -452,15 +452,16 @@ button.as-link[disabled] {
background-color: var(--dark-background-color1);
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--dark-font-color8);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--dark-font-color6);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background-color: var(--dark-background-color1);
}

View File

@ -693,6 +693,7 @@ th {
border-left: 2px solid #ecf0f1;
}
.flux:not(.current):hover .flux_header .title,
.flux .flux_header:hover {
background: #fff;
}
@ -728,9 +729,9 @@ th {
font-size: 0.9rem;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: #666;
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -693,6 +693,7 @@ th {
border-right: 2px solid #ecf0f1;
}
.flux:not(.current):hover .flux_header .title,
.flux .flux_header:hover {
background: #fff;
}
@ -728,9 +729,9 @@ th {
font-size: 0.9rem;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: #666;
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -14,7 +14,7 @@
&:hover {
background: variables.$grey-lighter;
&:not(.current):hover .item.title {
&:not(.current):hover .item .title {
background: variables.$grey-lighter;
}
}
@ -28,13 +28,13 @@
&.not_read:not(.current) {
background: variables.$unread-bg;
&:hover .item.title {
&:hover .item .title {
background: variables.$unread-bg;
}
}
&.not_read {
.item.title {
.item .title {
a {
color: variables.$unread-font-color;
}
@ -47,7 +47,7 @@
}
}
.item.date {
.item .date {
color: color.scale(variables.$unread-font-color, $alpha: -50%);
}
}
@ -61,7 +61,7 @@
&.favorite:not(.current) {
background: variables.$fav-light;
&:hover .item.title {
&:hover .item .title {
background: variables.$fav-light;
}
}
@ -73,7 +73,8 @@
}
}
.item.date {
.flux_header .date,
.flux_content .bottom .date {
color: color.scale(variables.$main-font-color, $alpha: -50%);
font-size: 0.85rem;
}

View File

@ -1007,7 +1007,7 @@ main.prompt {
.flux .flux_header:hover {
background: #f9fafb;
}
.flux .flux_header:hover:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title {
background: #f9fafb;
}
.flux.current {
@ -1017,16 +1017,16 @@ main.prompt {
.flux.not_read:not(.current) {
background: #f2f6f8;
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background: #f2f6f8;
}
.flux.not_read .item.title a {
.flux.not_read .item .title a {
color: #36c;
}
.flux.not_read .item.website a {
color: #36c;
}
.flux.not_read .item.date {
.flux.not_read .item .date {
color: rgba(51, 102, 204, 0.5);
}
.flux.favorite {
@ -1036,14 +1036,15 @@ main.prompt {
.flux.favorite:not(.current) {
background: #fff6da;
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background: #fff6da;
}
.flux .website a {
color: #303136;
opacity: 0.75;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: rgba(48, 49, 54, 0.5);
font-size: 0.85rem;
}

View File

@ -1007,7 +1007,7 @@ main.prompt {
.flux .flux_header:hover {
background: #f9fafb;
}
.flux .flux_header:hover:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title {
background: #f9fafb;
}
.flux.current {
@ -1017,16 +1017,16 @@ main.prompt {
.flux.not_read:not(.current) {
background: #f2f6f8;
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background: #f2f6f8;
}
.flux.not_read .item.title a {
.flux.not_read .item .title a {
color: #36c;
}
.flux.not_read .item.website a {
color: #36c;
}
.flux.not_read .item.date {
.flux.not_read .item .date {
color: rgba(51, 102, 204, 0.5);
}
.flux.favorite {
@ -1036,14 +1036,15 @@ main.prompt {
.flux.favorite:not(.current) {
background: #fff6da;
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background: #fff6da;
}
.flux .website a {
color: #303136;
opacity: 0.75;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: rgba(48, 49, 54, 0.5);
font-size: 0.85rem;
}

View File

@ -791,10 +791,6 @@ li.item.active {
padding: 0.25rem;
}
.flux .item.date {
font-size: 0.7rem;
}
.flux .bottom {
font-size: 0.8rem;
text-align: center;
@ -809,23 +805,23 @@ li.item.active {
background: var(--accent-bg);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background: var(--accent-bg);
transition: .3s;
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--text-default);
}
.flux .flux_header .item.title .summary {
.flux .flux_header .item .title .summary {
color: var(--text-accent);
font-size: 0.8rem;
font-style: italic;
opacity: 0.8;
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .title .author {
color: var(--text-accent);
opacity: 0.8;
}

View File

@ -791,10 +791,6 @@ li.item.active {
padding: 0.25rem;
}
.flux .item.date {
font-size: 0.7rem;
}
.flux .bottom {
font-size: 0.8rem;
text-align: center;
@ -809,23 +805,23 @@ li.item.active {
background: var(--accent-bg);
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
background: var(--accent-bg);
transition: .3s;
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--text-default);
}
.flux .flux_header .item.title .summary {
.flux .flux_header .item .title .summary {
color: var(--text-accent);
font-size: 0.8rem;
font-style: italic;
opacity: 0.8;
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .title .author {
color: var(--text-accent);
opacity: 0.8;
}

View File

@ -837,6 +837,7 @@ a:hover .icon {
}
.flux .flux_header:not(.current):hover .flux_header,
.flux:not(.current):hover .flux_header .title,
.flux.current .flux_header {
background-color: var(--background-color-hover);
}
@ -849,6 +850,7 @@ a:hover .icon {
background-color: var(--unread-article-background-color);
}
.flux.not_read:not(.current):hover .flux_header .title,
.flux.not_read:not(.current):hover .flux_header,
.flux.not_read.current .flux_header {
background-color: var(--unread-article-background-color-hover);
@ -862,6 +864,7 @@ a:hover .icon {
background-color: var(--favorite-article-background-color);
}
.flux.favorite:not(.current):hover .flux_header .title,
.flux.favorite:not(.current):hover .flux_header,
.flux.favorite.current .flux_header {
background-color: var(--favorite-article-background-color-hover);
@ -873,7 +876,7 @@ a:hover .icon {
cursor: pointer;
}
.flux .item.date {
.flux .item .date {
color: var(--font-color-grey);
font-size: 0.7rem;
}

View File

@ -837,6 +837,7 @@ a:hover .icon {
}
.flux .flux_header:not(.current):hover .flux_header,
.flux:not(.current):hover .flux_header .title,
.flux.current .flux_header {
background-color: var(--background-color-hover);
}
@ -849,6 +850,7 @@ a:hover .icon {
background-color: var(--unread-article-background-color);
}
.flux.not_read:not(.current):hover .flux_header .title,
.flux.not_read:not(.current):hover .flux_header,
.flux.not_read.current .flux_header {
background-color: var(--unread-article-background-color-hover);
@ -862,6 +864,7 @@ a:hover .icon {
background-color: var(--favorite-article-background-color);
}
.flux.favorite:not(.current):hover .flux_header .title,
.flux.favorite:not(.current):hover .flux_header,
.flux.favorite.current .flux_header {
background-color: var(--favorite-article-background-color-hover);
@ -873,7 +876,7 @@ a:hover .icon {
cursor: pointer;
}
.flux .item.date {
.flux .item .date {
color: var(--font-color-grey);
font-size: 0.7rem;
}

View File

@ -798,6 +798,7 @@ a.signin {
}
.flux .flux_header:hover,
.flux:not(.current):hover .flux_header .title,
.flux.current .flux_header {
background-color: var(--background-color-grey-hover);
}
@ -815,8 +816,8 @@ a.signin {
border-left: 2px solid var(--border-left-article-unread);
}
.flux .flux_header .item.title a,
.flux.not_read:not(.current):hover .flux_header .item.title {
.flux .flux_header .item .title a,
.flux.not_read:not(.current):hover .flux_header .item .title {
color: var(--font-color-link-title);
}
@ -828,6 +829,7 @@ a.signin {
background-color: var(--background-color-favorite);
}
.flux.favorite:not(.current):hover .flux_header .title,
.flux.favorite:not(.current) .flux_header:hover,
.flux.favorite.current .flux_header {
background-color: var(--background-color-favorite-hover);
@ -847,9 +849,9 @@ a.signin {
padding: 0.425rem;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--font-color-grey);
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -798,6 +798,7 @@ a.signin {
}
.flux .flux_header:hover,
.flux:not(.current):hover .flux_header .title,
.flux.current .flux_header {
background-color: var(--background-color-grey-hover);
}
@ -815,8 +816,8 @@ a.signin {
border-right: 2px solid var(--border-left-article-unread);
}
.flux .flux_header .item.title a,
.flux.not_read:not(.current):hover .flux_header .item.title {
.flux .flux_header .item .title a,
.flux.not_read:not(.current):hover .flux_header .item .title {
color: var(--font-color-link-title);
}
@ -828,6 +829,7 @@ a.signin {
background-color: var(--background-color-favorite);
}
.flux.favorite:not(.current):hover .flux_header .title,
.flux.favorite:not(.current) .flux_header:hover,
.flux.favorite.current .flux_header {
background-color: var(--background-color-favorite-hover);
@ -847,9 +849,9 @@ a.signin {
padding: 0.425rem;
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--font-color-grey);
font-size: 0.7rem;
}
.flux .bottom {

View File

@ -751,25 +751,25 @@ form th {
.flux .current {
background-color: var(--color-background-hover);
}
.flux .flux_header:hover:not(.current):hover .item.title,
.flux .current:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title,
.flux .current:not(.current):hover .item .title {
background-color: var(--color-background-hover);
}
.flux.favorite:not(.current) {
background-color: var(--color-background-stared);
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background-color: var(--color-background-stared);
}
.flux.not_read:not(.current) {
background-color: var(--color-background-unread);
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background-color: var(--color-background-unread);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--color-text-light-darker);
font-size: 0.7rem;
}
.flux .bottom {
font-size: 0.8rem;
@ -909,7 +909,7 @@ a.signin {
}
@media (max-width: 840px) {
body:not(.formLogin, .register) .header .item.title {
body:not(.formLogin, .register) .header .item .title {
display: none;
}
.form-group .group-name {
@ -948,7 +948,7 @@ a.signin {
width: 35px;
text-align: center;
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
top: auto !important;
}
.aside {

View File

@ -751,25 +751,25 @@ form th {
.flux .current {
background-color: var(--color-background-hover);
}
.flux .flux_header:hover:not(.current):hover .item.title,
.flux .current:not(.current):hover .item.title {
.flux .flux_header:hover:not(.current):hover .item .title,
.flux .current:not(.current):hover .item .title {
background-color: var(--color-background-hover);
}
.flux.favorite:not(.current) {
background-color: var(--color-background-stared);
}
.flux.favorite:not(.current):hover .item.title {
.flux.favorite:not(.current):hover .item .title {
background-color: var(--color-background-stared);
}
.flux.not_read:not(.current) {
background-color: var(--color-background-unread);
}
.flux.not_read:not(.current):hover .item.title {
.flux.not_read:not(.current):hover .item .title {
background-color: var(--color-background-unread);
}
.flux .item.date {
.flux .flux_header .date,
.flux .flux_content .bottom .date {
color: var(--color-text-light-darker);
font-size: 0.7rem;
}
.flux .bottom {
font-size: 0.8rem;
@ -909,7 +909,7 @@ a.signin {
}
@media (max-width: 840px) {
body:not(.formLogin, .register) .header .item.title {
body:not(.formLogin, .register) .header .item .title {
display: none;
}
.form-group .group-name {
@ -948,7 +948,7 @@ a.signin {
width: 35px;
text-align: center;
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
top: auto !important;
}
.aside {

View File

@ -969,7 +969,7 @@ form {
.current {
background-color: var(--color-background-hover);
&:not(.current):hover .item.title {
&:not(.current):hover .item .title {
background-color: var(--color-background-hover);
}
}
@ -977,7 +977,7 @@ form {
&.favorite:not(.current) {
background-color: var(--color-background-stared);
&:hover .item.title {
&:hover .item .title {
background-color: var(--color-background-stared);
}
}
@ -985,16 +985,14 @@ form {
&.not_read:not(.current) {
background-color: var(--color-background-unread);
&:hover .item.title {
&:hover .item .title {
background-color: var(--color-background-unread);
}
}
.item {
&.date {
color: var(--color-text-light-darker);
font-size: 0.7rem;
}
.flux_header .date,
.flux_content .bottom .date {
color: var(--color-text-light-darker);
}
.bottom {
@ -1166,7 +1164,7 @@ a.signin {
@media (max-width: 840px) {
body:not(.formLogin, .register) {
.header {
.item.title {
.item .title {
display: none;
}
}
@ -1218,7 +1216,7 @@ a.signin {
text-align: center;
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .item .title {
top: auto !important;
}

View File

@ -555,10 +555,6 @@ th {
padding: 5px;
}
.flux .item.date {
font-size: 0.7rem;
}
.flux:not(.current):hover .item.title {
}

View File

@ -555,10 +555,6 @@ th {
padding: 5px;
}
.flux .item.date {
font-size: 0.7rem;
}
.flux:not(.current):hover .item.title {
}

View File

@ -1263,47 +1263,38 @@ input[type="search"] {
width: 200px;
}
.flux:not(.current):hover .websitenone .item.title {
max-width: calc(100% - 3 * (2 * var(--frss-padding-flux-items) + 16px));
}
.flux .websiteicon .item.website {
width: calc(2 * var(--frss-padding-flux-items) + 16px);
}
.flux:not(.current):hover .websiteicon .item.title {
max-width: calc(100% - 4 * (2 * var(--frss-padding-flux-items) + 16px));
}
.flux .websitename .item.website {
width: 150px;
}
.flux:not(.current):hover .websitename .item.title {
max-width: calc(100% - 3 * (2 * var(--frss-padding-flux-items) + 16px) - 150px);
}
.website a:hover .favicon,
a.website:hover .favicon {
filter: grayscale(100%);
}
.flux.not_read .item.title,
.flux.current .item.title {
.flux.not_read .item .title,
.flux.current .item .title {
font-weight: bold;
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .flux_header .item .title:has(~ .date) {
padding-right: 0.5rem;
z-index: 2;
}
.flux:not(.current):hover .item .title {
background-color: inherit;
max-width: calc(100% - 320px);
position: absolute;
}
.flux:not(.current):hover .item.title.multiline {
position: initial;
.flux .item:has(.multiline) {
vertical-align: top;
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--frss-font-color-dark);
text-decoration: none;
}
@ -1355,7 +1346,27 @@ a.website:hover .favicon {
object-fit: cover;
}
.flux .flux_header .item.title .summary {
.flux .flux_header .item.titleAuthorSummaryDate {
position: relative;
overflow: hidden;
}
.flux .flux_header .item .title {
position: absolute;
top: 0;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
box-sizing: border-box;
max-width: 100%;
}
.flux .flux_header .item .title:has(~.date) {
padding-right: 155px;
}
.flux .flux_header .item .summary {
display: -webkit-box;
color: var(--frss-font-color-grey-dark);
font-size: 0.9rem;
@ -1365,18 +1376,28 @@ a.website:hover .favicon {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: break-spaces;
margin-top: 2.25rem;
margin-bottom: 0.5rem;
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .summary:has(~.date) {
max-width: 90%;
}
.flux .flux_header .item .title .author {
padding-left: 1rem;
color: var(--frss-font-color-grey-dark);
font-weight: normal;
}
.flux .flux_header .item.date {
.flux .flux_header .item .date {
position: absolute;
right: 0;
top: 0;
width: 155px;
text-align: right;
overflow: hidden;
z-index: 1;
}
.flux .flux_header .item > a {
@ -1386,6 +1407,11 @@ a.website:hover .favicon {
overflow: hidden;
}
.flux .flux_header .date,
.flux .flux_content .bottom .date {
font-size: 0.7rem;
}
.item.query > a {
display: list-item;
list-style-position: inside;
@ -2180,7 +2206,7 @@ html.slider-active {
}
.flux_header .item.website span,
.item.date, .day .date,
.item .date, .day .date,
.dropdown-menu > .no-mobile,
.no-mobile {
display: none;
@ -2223,6 +2249,10 @@ html.slider-active {
text-align: center;
}
.flux .flux_header .item .title:has(~.date) {
padding-right: 1rem;
}
#overlay .close {
position: relative;
}

View File

@ -1263,47 +1263,38 @@ input[type="search"] {
width: 200px;
}
.flux:not(.current):hover .websitenone .item.title {
max-width: calc(100% - 3 * (2 * var(--frss-padding-flux-items) + 16px));
}
.flux .websiteicon .item.website {
width: calc(2 * var(--frss-padding-flux-items) + 16px);
}
.flux:not(.current):hover .websiteicon .item.title {
max-width: calc(100% - 4 * (2 * var(--frss-padding-flux-items) + 16px));
}
.flux .websitename .item.website {
width: 150px;
}
.flux:not(.current):hover .websitename .item.title {
max-width: calc(100% - 3 * (2 * var(--frss-padding-flux-items) + 16px) - 150px);
}
.website a:hover .favicon,
a.website:hover .favicon {
filter: grayscale(100%);
}
.flux.not_read .item.title,
.flux.current .item.title {
.flux.not_read .item .title,
.flux.current .item .title {
font-weight: bold;
}
.flux:not(.current):hover .item.title {
.flux:not(.current):hover .flux_header .item .title:has(~ .date) {
padding-left: 0.5rem;
z-index: 2;
}
.flux:not(.current):hover .item .title {
background-color: inherit;
max-width: calc(100% - 320px);
position: absolute;
}
.flux:not(.current):hover .item.title.multiline {
position: initial;
.flux .item:has(.multiline) {
vertical-align: top;
}
.flux .flux_header .item.title a {
.flux .flux_header .item .title a {
color: var(--frss-font-color-dark);
text-decoration: none;
}
@ -1355,7 +1346,27 @@ a.website:hover .favicon {
object-fit: cover;
}
.flux .flux_header .item.title .summary {
.flux .flux_header .item.titleAuthorSummaryDate {
position: relative;
overflow: hidden;
}
.flux .flux_header .item .title {
position: absolute;
top: 0;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
box-sizing: border-box;
max-width: 100%;
}
.flux .flux_header .item .title:has(~.date) {
padding-left: 155px;
}
.flux .flux_header .item .summary {
display: -webkit-box;
color: var(--frss-font-color-grey-dark);
font-size: 0.9rem;
@ -1365,18 +1376,28 @@ a.website:hover .favicon {
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
white-space: break-spaces;
margin-top: 2.25rem;
margin-bottom: 0.5rem;
}
.flux .flux_header .item.title .author {
.flux .flux_header .item .summary:has(~.date) {
max-width: 90%;
}
.flux .flux_header .item .title .author {
padding-right: 1rem;
color: var(--frss-font-color-grey-dark);
font-weight: normal;
}
.flux .flux_header .item.date {
.flux .flux_header .item .date {
position: absolute;
left: 0;
top: 0;
width: 155px;
text-align: left;
overflow: hidden;
z-index: 1;
}
.flux .flux_header .item > a {
@ -1386,6 +1407,11 @@ a.website:hover .favicon {
overflow: hidden;
}
.flux .flux_header .date,
.flux .flux_content .bottom .date {
font-size: 0.7rem;
}
.item.query > a {
display: list-item;
list-style-position: inside;
@ -2180,7 +2206,7 @@ html.slider-active {
}
.flux_header .item.website span,
.item.date, .day .date,
.item .date, .day .date,
.dropdown-menu > .no-mobile,
.no-mobile {
display: none;
@ -2223,6 +2249,10 @@ html.slider-active {
text-align: center;
}
.flux .flux_header .item .title:has(~.date) {
padding-left: 1rem;
}
#overlay .close {
position: relative;
}