Просмотр исходного кода

Merge branch 'dev' into 320-template

Marien Fressinaud 12 лет назад
Родитель
Сommit
92993b26a8

+ 4 - 0
CHANGELOG

@@ -6,6 +6,10 @@
 	* FreshRSS can now be used from e.g.:
 		* (Android) News+ https://play.google.com/store/apps/details?id=com.noinnion.android.newsplus.extension.google_reader
 		* (Android) EasyRSS https://github.com/Alkarex/EasyRSS
+* Basic support for audio and video podcasts
+* Searching
+	* New search filters date: and pubdate: accepting ISO 8601 date intervals such as `date:2013-2014` or `pubdate:P1W`
+	* Possibility to combine search filters, e.g. `date:2014-05 intitle:FreshRSS intitle:Open great reader #Internet`
 
 
 ## 2014-02-19 FreshRSS 0.7.1

+ 37 - 35
app/Models/EntryDAO.php

@@ -478,48 +478,50 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
 		}
 		$search = '';
 		if ($filter !== '') {
+			require_once(LIB_PATH . '/lib_date.php');
 			$filter = trim($filter);
 			$filter = addcslashes($filter, '\\%_');
-			if (stripos($filter, 'intitle:') === 0) {
-				$filter = substr($filter, strlen('intitle:'));
-				$intitle = true;
-			} else {
-				$intitle = false;
-			}
-			if (stripos($filter, 'inurl:') === 0) {
-				$filter = substr($filter, strlen('inurl:'));
-				$inurl = true;
-			} else {
-				$inurl = false;
-			}
-			if (stripos($filter, 'author:') === 0) {
-				$filter = substr($filter, strlen('author:'));
-				$author = true;
-			} else {
-				$author = false;
-			}
 			$terms = array_unique(explode(' ', $filter));
-			sort($terms);	//Put #tags first
+			//sort($terms);	//Put #tags first	//TODO: Put the cheapest filters first
 			foreach ($terms as $word) {
 				$word = trim($word);
-				if (strlen($word) > 0) {
-					if ($intitle) {
-						$search .= 'AND e1.title LIKE ? ';
-						$values[] = '%' . $word .'%';
-					} elseif ($inurl) {
-						$search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
-						$values[] = '%' . $word .'%';
-					} elseif ($author) {
-						$search .= 'AND e1.author LIKE ? ';
+				if (stripos($word, 'intitle:') === 0) {
+					$word = substr($word, strlen('intitle:'));
+					$search .= 'AND e1.title LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'inurl:') === 0) {
+					$word = substr($word, strlen('inurl:'));
+					$search .= 'AND CONCAT(e1.link, e1.guid) LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'author:') === 0) {
+					$word = substr($word, strlen('author:'));
+					$search .= 'AND e1.author LIKE ? ';
+					$values[] = '%' . $word .'%';
+				} elseif (stripos($word, 'date:') === 0) {
+					$word = substr($word, strlen('date:'));
+					list($minDate, $maxDate) = parseDateInterval($word);
+					if ($minDate) {
+						$search .= 'AND e1.id >= ' . $minDate . '000000 ';
+					}
+					if ($maxDate) {
+						$search .= 'AND e1.id <= ' . $maxDate . '000000 ';
+					}
+				} elseif (stripos($word, 'pubdate:') === 0) {
+					$word = substr($word, strlen('pubdate:'));
+					list($minDate, $maxDate) = parseDateInterval($word);
+					if ($minDate) {
+						$search .= 'AND e1.date >= ' . $minDate . ' ';
+					}
+					if ($maxDate) {
+						$search .= 'AND e1.date <= ' . $maxDate . ' ';
+					}
+				} else {
+					if ($word[0] === '#' && isset($word[1])) {
+						$search .= 'AND e1.tags LIKE ? ';
 						$values[] = '%' . $word .'%';
 					} else {
-						if ($word[0] === '#' && isset($word[1])) {
-							$search .= 'AND e1.tags LIKE ? ';
-							$values[] = '%' . $word .'%';
-						} else {
-							$search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? ';
-							$values[] = '%' . $word .'%';
-						}
+						$search .= 'AND CONCAT(e1.title, UNCOMPRESS(e1.content_bin)) LIKE ? ';
+						$values[] = '%' . $word .'%';
 					}
 				}
 			}

+ 4 - 0
app/Models/Feed.php

@@ -259,6 +259,10 @@ class FreshRSS_Feed extends Minz_Model {
 					$mime = strtolower($enclosure->get_type());
 					if (strpos($mime, 'image/') === 0) {
 						$content .= '<br /><img src="' . $elink . '" alt="" />';
+					} elseif (strpos($mime, 'audio/') === 0) {
+						$content .= '<br /><audio src="' . $elink . '" controls="controls" />';
+					} elseif (strpos($mime, 'video/') === 0) {
+						$content .= '<br /><video src="' . $elink . '" controls="controls" />';
 					}
 				}
 			}

+ 3 - 0
app/Models/Themes.php

@@ -93,6 +93,9 @@ class FreshRSS_Themes extends Minz_Model {
 			'starred' => '★',
 			'tag' => '⚐',
 			'up' => '△',
+			'view-normal' => '☰',
+			'view-global' => '☷',
+			'view-reader' => '☕',
 		);
 		if (!isset($alts[$name])) {
 			return '';

+ 20 - 35
app/layout/nav_menu.phtml

@@ -169,37 +169,27 @@
 	</div>
 	<?php } ?>
 
-	<div class="dropdown" id="nav_menu_views">
-		<div id="dropdown-views" class="dropdown-target"></div>
-		<a class="dropdown-toggle btn" href="#dropdown-views"><?php echo Minz_Translate::t ('display'); ?> <?php echo FreshRSS_Themes::icon('down'); ?></a>
-		<ul class="dropdown-menu">
-			<li class="dropdown-close"><a href="#close">❌</a></li>
+	<?php $url_output = $this->url; ?>
+	<div class="stick" id="nav_menu_views">
+		<?php $url_output['params']['output'] = 'normal'; ?>
+		<a class="view_normal btn <?php echo $actual_view == 'normal'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('normal_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-normal"); ?>
+		</a>
 
-			<?php
-				$url_output = $this->url;
-				if ($actual_view !== 'normal') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'normal'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('normal_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view !== 'reader') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'reader'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('reader_view'); ?>
-				</a>
-			</li>
-			<?php } if($actual_view !== 'global') { ?>
-			<li class="item">
-				<?php $url_output['params']['output'] = 'global'; ?>
-				<a class="view_normal" href="<?php echo Minz_Url::display ($url_output); ?>">
-					<?php echo Minz_Translate::t ('global_view'); ?>
-				</a>
-			</li>
-			<?php } ?>
-		</ul>
+		<?php $url_output['params']['output'] = 'global'; ?>
+		<a class="view_global btn <?php echo $actual_view == 'global'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('global_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-global"); ?>
+		</a>
+
+		<?php $url_output['params']['output'] = 'reader'; ?>
+		<a class="view_reader btn <?php echo $actual_view == 'reader'? 'active' : ''; ?>" title="<?php echo Minz_Translate::t('reader_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon("view-reader"); ?>
+		</a>
+
+		<?php $url_output['params']['output'] = 'rss'; ?>
+		<a class="view_rss btn" target="_blank" title="<?php echo Minz_Translate::t ('rss_view'); ?>" href="<?php echo Minz_Url::display($url_output); ?>">
+			<?php echo FreshRSS_Themes::icon('rss'); ?>
+		</a>
 	</div>
 
 	<div class="item search">
@@ -240,11 +230,6 @@
 	<a class="btn" href="<?php echo Minz_Url::display ($url_order); ?>" title="<?php echo Minz_Translate::t ($title); ?>">
 		<?php echo FreshRSS_Themes::icon($icon); ?>
 	</a>
-
-	<?php $url_output['params']['output'] = 'rss'; ?>
-	<a class="btn view_rss" target="_blank" href="<?php echo Minz_Url::display ($url_output); ?>" title="<?php echo Minz_Translate::t ('rss_view'); ?>">
-		<?php echo FreshRSS_Themes::icon('rss'); ?>
-	</a>
 	
 	<?php if ($this->loginOk || Minz_Configuration::allowAnonymousRefresh()) { ?>
 	<a id="actualize" class="btn" href="<?php echo _url ('feed', 'actualize'); ?>"><?php echo FreshRSS_Themes::icon('refresh'); ?></a>

+ 6 - 4
app/views/configure/categorize.phtml

@@ -14,11 +14,13 @@
 				<?php echo Minz_Translate::t ('category_number', $i); ?>
 			</label>
 			<div class="group-controls">
-				<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
+				<div class="stick">
+					<input type="text" id="cat_<?php echo $cat->id (); ?>" name="categories[]" value="<?php echo $cat->name (); ?>" />
 
-				<?php if ($cat->nbFeed () > 0) { ?>
-				<button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button>
-				<?php } ?>
+					<?php if ($cat->nbFeed () > 0) { ?>
+					<button type="submit" class="btn btn-attention confirm" formaction="<?php echo _url ('feed', 'delete', 'id', $cat->id (), 'type', 'category'); ?>"><?php echo Minz_Translate::t ('ask_empty'); ?></button>
+					<?php } ?>
+				</div>
 				(<?php echo Minz_Translate::t ('number_feeds', $cat->nbFeed ()); ?>)
 
 				<?php if ($cat->id () === $this->defaultCategory->id ()) { ?>

+ 10 - 5
app/views/configure/feed.phtml

@@ -32,16 +32,21 @@
 		<div class="form-group">
 			<label class="group-name" for="website"><?php echo Minz_Translate::t ('website_url'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
-				<a target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				<div class="stick">
+					<input type="text" name="website" id="website" class="extend" value="<?php echo $this->flux->website (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->website (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
 			</div>
 		</div>
 		<div class="form-group">
 			<label class="group-name" for="url"><?php echo Minz_Translate::t ('feed_url'); ?></label>
 			<div class="group-controls">
-				<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
-				<a target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
-				  <a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
+				<div class="stick">
+					<input type="text" name="url" id="url" class="extend" value="<?php echo $this->flux->url (); ?>" />
+					<a class="btn" target="_blank" href="<?php echo $this->flux->url (); ?>"><?php echo FreshRSS_Themes::icon('link'); ?></a>
+				</div>
+
+				<a class="btn" target="_blank" href="http://validator.w3.org/feed/check.cgi?url=<?php echo $this->flux->url (); ?>"><?php echo Minz_Translate::t ('feed_validator'); ?></a>
 			</div>
 		</div>
 		<div class="form-group">

+ 19 - 11
app/views/configure/sharing.phtml

@@ -4,13 +4,16 @@
 	<a href="<?php echo _url ('index', 'index'); ?>"><?php echo Minz_Translate::t ('back_to_rss_feeds'); ?></a>
 
 	<form method="post" action="<?php echo _url ('configure', 'sharing'); ?>"
-		data-simple='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+		data-simple='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a>
 			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" /></div></div>'
-		data-advanced='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls"><a href="#" class="share remove"><?php echo FreshRSS_Themes::icon('close'); ?></a>
+		data-advanced='<div class="form-group"><label class="group-name">##label##</label><div class="group-controls">
 			<input type="hidden" id="share_##key##_type" name="share[##key##][type]" value="##type##" />
+			<div class="stick">
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
-			<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="##help##"><?php echo Minz_Translate::t ('more_information'); ?></a></div></div>'>
+			<a href="#" class="share remove btn btn-attention"><?php echo FreshRSS_Themes::icon('close'); ?></a></div>
+			<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="##help##"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+			</div></div>'>
 		<legend><?php echo Minz_Translate::t ('sharing'); ?></legend>
 		<?php foreach ($this->conf->sharing as $key => $sharing): ?>
 			<?php $share = $this->conf->shares[$sharing['type']]; ?>
@@ -19,25 +22,30 @@
 					<?php echo Minz_Translate::t ($sharing['type']); ?>
 				</label>
 				<div class="group-controls">
-					<a href='#' class='share remove'><?php echo FreshRSS_Themes::icon('close'); ?></a>
 					<input type='hidden' id='share_<?php echo $key;?>_type' name="share[<?php echo $key;?>][type]" value='<?php echo $sharing['type']?>' />
-					<?php if ($share['form'] === 'advanced'):?>
-						<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
-						<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
-						<?php echo FreshRSS_Themes::icon('help'); ?> <a target="_blank" href="<?php echo $share['help']?>"><?php echo Minz_Translate::t ('more_information'); ?></a>
-					<?php endif;?>
+					<?php if ($share['form'] === 'advanced'){ ?>
+						<div class="stick">
+							<input type="text" id="share_<?php echo $key;?>_name" name="share[<?php echo $key;?>][name]" class="extend" value="<?php echo $sharing['name']?>" placeholder="<?php echo Minz_Translate::t ('share_name'); ?>" size="64" />
+							<input type="url" id="share_<?php echo $key;?>_url" name="share[<?php echo $key;?>][url]" class="extend" value="<?php echo $sharing['url']?>" placeholder="<?php echo Minz_Translate::t ('share_url'); ?>" size="64" />
+							<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+						</div>
+
+						<a target="_blank" class="btn" title="<?php echo Minz_Translate::t('more_information'); ?>" href="<?php echo $share['help']?>"><?php echo FreshRSS_Themes::icon('help'); ?></a>
+					<?php } else { ?>
+					<a href='#' class='share remove btn btn-attention'><?php echo FreshRSS_Themes::icon('close'); ?></a>
+					<?php } ?>
 				</div>
 			</div>
 		<?php endforeach;?>
 
-		<div class="form-group form-actions">
+		<div class="form-group">
 			<div class="group-controls">
-				<a href='#' class='share add'><?php echo FreshRSS_Themes::icon('add'); ?></a>
 				<select>
 					<?php foreach($this->conf->shares as $key => $params):?>
 						<option value='<?php echo $key?>' data-form='<?php echo $params['form']?>' data-help='<?php if (!empty($params['help'])) {echo $params['help'];}?>'><?php echo Minz_Translate::t($key) ?></option>
 					<?php endforeach; ?>
 				</select>
+				<a href='#' class='share add btn'><?php echo FreshRSS_Themes::icon('add'); ?></a>
 			</div>
 		</div>
 

+ 9 - 5
app/views/configure/users.phtml

@@ -20,8 +20,10 @@
 		<div class="form-group">
 			<label class="group-name" for="passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
 			<div class="group-controls">
-				<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
-				<a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				<div class="stick">
+					<input type="password" id="passwordPlain" name="passwordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
+					<a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
 				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>
@@ -31,7 +33,7 @@
 			<label class="group-name" for="apiPasswordPlain"><?php echo Minz_Translate::t('password_api'); ?></label>
 			<div class="group-controls">
 				<input type="password" id="apiPasswordPlain" name="apiPasswordPlain" autocomplete="off" pattern=".{7,}" <?php echo cryptAvailable() ? '' : 'disabled="disabled" '; ?>/>
-				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
+				<a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a>
 			</div>
 		</div>
 		<?php } ?>
@@ -178,8 +180,10 @@
 		<div class="form-group">
 			<label class="group-name" for="new_user_passwordPlain"><?php echo Minz_Translate::t('password_form'); ?></label>
 			<div class="group-controls">
-				<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
-				<a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				<div class="stick">
+					<input type="password" id="new_user_passwordPlain" name="new_user_passwordPlain" autocomplete="off" pattern=".{7,}" />
+					<a class="btn toggle-password"/><?php echo FreshRSS_Themes::icon('key'); ?></a>
+				</div>
 				<noscript><b><?php echo Minz_Translate::t('javascript_should_be_activated'); ?></b></noscript>
 			</div>
 		</div>

+ 2 - 2
lib/SimplePie/SimplePie.php

@@ -1331,7 +1331,7 @@ class SimplePie
 		// First check to see if input has been overridden.
 		if ($this->input_encoding !== false)
 		{
-			$encodings[] = strtoupper($this->input_encoding);	//FreshRSS
+			$encodings[] = strtoupper($this->input_encoding);
 		}
 
 		$application_types = array('application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity');
@@ -1355,7 +1355,7 @@ class SimplePie
 			{
 				if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset))
 				{
-					$encodings[] = strtoupper($charset[1]);	//FreshRSS
+					$encodings[] = strtoupper($charset[1]);
 				}
 				else
 				{

+ 131 - 0
lib/lib_date.php

@@ -0,0 +1,131 @@
+<?php
+/**
+ * Author: Alexandre Alapetite http://alexandre.alapetite.fr
+ * 2014-06-01
+ * License: GNU AGPLv3 http://www.gnu.org/licenses/agpl-3.0.html
+ *
+ * Parser of ISO 8601 time intervals http://en.wikipedia.org/wiki/ISO_8601#Time_intervals
+ *	Examples: "2014-02/2014-04", "2014-02/04", "2014-06", "P1M"
+ */
+
+/*
+example('2014-03');
+example('201403');
+example('2014-03-30');
+example('2014-05-30T13');
+example('2014-05-30T13:30');
+example('2014-02/2014-04');
+example('2014-02--2014-04');
+example('2014-02/04');
+example('2014-02-03/05');
+example('2014-02-03T22:00/22:15');
+example('2014-02-03T22:00/15');
+example('2014-03/');
+example('/2014-03');
+example('2014-03/P1W');
+example('P1W/2014-05-25T23:59:59');
+example('P1Y/');
+example('P1Y');
+example('P2M/');
+example('P3W/');
+example('P4D/');
+example('PT5H/');
+example('PT6M/');
+example('PT7S/');
+example('P1DT1H/');
+
+function example($dateInterval) {
+	$dateIntervalArray = parseDateInterval($dateInterval);
+	echo $dateInterval, "\t=>\t",
+		$dateIntervalArray[0] == null ? 'null' : @date('c', $dateIntervalArray[0]), '/',
+		$dateIntervalArray[1] == null ? 'null' : @date('c', $dateIntervalArray[1]), "\n";
+}
+*/
+
+function _dateFloor($isoDate) {
+	$x = explode('T', $isoDate, 2);
+	$t = isset($x[1]) ? str_pad($x[1], 6, '0') : '000000';
+	return str_pad($x[0], 8, '01') . 'T' . $t;
+}
+
+function _dateCeiling($isoDate) {
+	$x = explode('T', $isoDate, 2);
+	$t = isset($x[1]) && strlen($x[1]) > 1 ? str_pad($x[1], 6, '59') : '235959';
+	switch (strlen($x[0])) {
+		case 4:
+			return $x[0] . '1231T' . $t;
+		case 6:
+			$d = @strtotime($x[0] . '01');
+			return $x[0] . date('t', $d) . 'T' . $t;
+		default:
+			return $x[0] . 'T' . $t;
+	}
+}
+
+function _noDelimit($isoDate) {
+	return $isoDate === null || $isoDate === '' ? null :
+		str_replace(array('-', ':'), '', $isoDate);	//FIXME: Bug with negative time zone
+}
+
+function _dateRelative($d1, $d2) {
+	if ($d2 === null) {
+		return $d1 !== null && $d1[0] !== 'P' ? $d1 : null;
+	} elseif ($d2 !== '' && $d2[0] != 'P' && $d1 !== null && $d1[0] !== 'P') {
+		$y2 = substr($d2, 0, 4);
+		if (strlen($y2) < 4 || !ctype_digit($y2)) {	//Does not start by a year
+			$d2 = _noDelimit($d2);
+			return substr($d1, 0, -strlen($d2)) . $d2;	//Add prefix from $d1
+		}
+	}
+	return _noDelimit($d2);
+}
+
+/**
+ * Parameter $dateInterval is a string containing an ISO 8601 time interval.
+ * Returns an array with the minimum and maximum Unix timestamp of this interval,
+ *  or null if open interval, or false if error.
+ */
+function parseDateInterval($dateInterval) {
+	$dateInterval = trim($dateInterval);
+	$dateInterval = str_replace('--', '/', $dateInterval);
+	$dateInterval = strtoupper($dateInterval);
+	$min = null;
+	$max = null;
+	$x = explode('/', $dateInterval, 2);
+	$d1 = _noDelimit($x[0]);
+	$d2 = _dateRelative($d1, count($x) > 1 ? $x[1] : null);
+	if ($d1 !== null && $d1[0] !== 'P') {
+		$min = @strtotime(_dateFloor($d1));
+	}
+	if ($d2 !== null) {
+		if ($d2[0] === 'P') {
+			try {
+				$di2 = new DateInterval($d2);
+				$dt1 = @date_create();	//new DateTime() would create an Exception if the default time zone is not defined
+				if ($min !== null && $min !== false) {
+					$dt1->setTimestamp($min);
+				}
+				$max = $dt1->add($di2)->getTimestamp() - 1;
+			} catch (Exception $e) {
+				$max = false;
+			}
+		} elseif ($d1 === null || $d1[0] !== 'P') {
+			$max = @strtotime(_dateCeiling($d2));
+		} else {
+			$max = @strtotime($d2);
+		}
+	}
+	if ($d1 !== null && $d1[0] === 'P') {
+		try {
+			$di1 = new DateInterval($d1);
+			$dt2 = @date_create();
+			if ($max !== null && $max !== false) {
+				$dt2->setTimestamp($max);
+			}
+			$min = $dt2->sub($di1)->getTimestamp() + 1;
+		} catch (Exception $e) {
+				$min = false;
+		}
+	}
+	return array($min, $max);
+}

+ 3 - 2
p/scripts/main.js

@@ -981,15 +981,16 @@ function init_share_observers() {
 	});
 
 	$('.share.add').on('click', function(e) {
-		e.preventDefault();
 		var opt = $(this).siblings('select').find(':selected');
 		var row = $(this).parents('form').data(opt.data('form'));
 		row = row.replace('##label##', opt.html(), 'g');
 		row = row.replace('##type##', opt.val(), 'g');
 		row = row.replace('##help##', opt.data('help'), 'g');
 		row = row.replace('##key##', shares, 'g');
-		$(this).parents('.form-actions').before(row);
+		$(this).parents('.form-group').before(row);
 		shares++;
+
+		return false;
 	});
 }
 

+ 1 - 0
p/themes/icons/view-global.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-61-867)" fill="#666" color="#000"><rect height="2" rx=".385" ry=".379" width="2" x="64" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="870"/><rect height="2" rx=".385" ry=".379" width="2" x="64" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="874.02"/><rect height="2" rx=".385" ry=".379" width="2" x="64" y="878"/><rect height="2" rx=".385" ry=".379" width="2" x="68" y="878"/><rect height="2" rx=".385" ry=".379" width="2" x="72" y="878"/></g></svg>

+ 1 - 0
p/themes/icons/view-normal.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-40-746)" fill="#666" color="#666"><path d="m43 749h10v2h-10z"/><path d="m43 753h10v2h-10z"/><path d="m43 757h10v2h-10z"/></g></svg>

+ 1 - 0
p/themes/icons/view-reader.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g transform="translate(-181-867)" fill="#666" color="#666"><path d="m 181,868 0,1 0,11 0,1 1,0 5,0 c 0.1754,0 0.52538,0.15166 0.8125,0.34375 0.28712,0.19209 0.46875,0.375 0.46875,0.375 L 189,882.4375 l 0.71875,-0.75 c 0,0 0.8963,-0.6875 1.28125,-0.6875 l 5,0 1,0 0,-1 0,-11 0,-1 -1,0 -5,0 c -0.87652,0 -1.56017,0.34756 -2.03125,0.6875 -0.0301,-0.0207 -0.031,-0.0105 -0.0625,-0.0312 C 188.44557,868.35254 187.82811,868 187,868 l -5,0 -1,0 z m 2,2 4,0 c 0.13821,0 0.51476,0.14746 0.8125,0.34375 0.29774,0.19629 0.5,0.375 0.5,0.375 l 0.71875,0.6875 0.6875,-0.71875 c 0,0 0.89975,-0.6875 1.28125,-0.6875 l 4,0 0,9 -4,0 c -0.87693,0 -1.56008,0.34735 -2.03125,0.6875 -0.0196,-0.0135 -0.011,-0.0177 -0.0312,-0.0312 C 188.47725,879.34834 187.83512,879 187,879 l -4,0 0,-9 z"/><g transform="scale(-1 1)"><rect height="2" rx=".375" width="3" x="-187" y="872"/><rect height="2" rx=".375" width="3" x="-187" y="875"/><rect height="2" rx=".375" width="3" x="-194" y="872"/><rect height="2" rx=".375" width="3" x="-194" y="875"/></g></g></svg>