From 8bc21b7876387a50a70a3658f95b3777a0f67cbb Mon Sep 17 00:00:00 2001 From: Ashutosh Dhundhara Date: Sat, 22 Mar 2014 13:09:50 +0530 Subject: [PATCH] Feature request: #1476 Favourite tables select box. Signed-off-by: Ashutosh Dhundhara Changes in "create_tables.sql". Signed-off-by: Ashutosh Dhundhara Minor fix in structure.lib.php. Signed-off-by: Ashutosh Dhundhara Ripped Trailing White spaces. Signed-off-by: Ashutosh Dhundhara Removed irrelevant comments from "FavouriteTable.class.php". Signed-off-by: Ashutosh Dhundhara Fixed Table Summary colspan. Signed-off-by: Ashutosh Dhundhara Changed Favourite to Favorite Signed-off-by: Ashutosh Dhundhara Fixed failing test. Signed-off-by: Ashutosh Dhundhara Feature request: #1476 Favourite tables select box. (Icon only) Signed-off-by: Ashutosh Dhundhara Gray and Golden Icons. Signed-off-by: Ashutosh Dhundhara Removed unwanted icon from "img". Signed-off-by: Ashutosh Dhundhara Fixed error in PMA_buildActionTitles_test.php. Signed-off-by: Ashutosh Dhundhara Re-positioned star icon and added tooltip to it. Signed-off-by: Ashutosh Dhundhara Ripped trailing spaces from "structure.lib.php" and "db_structure.js". Signed-off-by: Ashutosh Dhundhara --- config.sample.inc.php | 1 + db_structure.php | 23 +- examples/create_tables.sql | 14 + js/db_structure.js | 14 + js/navigation.js | 14 + libraries/FavoriteTable.class.php | 271 ++++++++++++++++++ libraries/Header.class.php | 1 + libraries/Util.class.php | 2 + libraries/config.default.php | 14 + libraries/config.values.php | 1 + libraries/config/messages.inc.php | 2 + libraries/config/setup.forms.php | 1 + libraries/config/user_preferences.forms.php | 1 + .../navigation/NavigationHeader.class.php | 18 +- libraries/structure.lib.php | 42 ++- .../common/PMA_buildActionTitles_test.php | 2 + themes/original/css/navigation.css.php | 13 +- themes/pmahomme/css/navigation.css.php | 8 +- themes/pmahomme/img/b_favorite.png | Bin 0 -> 563 bytes themes/pmahomme/img/b_no_favorite.png | Bin 0 -> 16426 bytes 20 files changed, 434 insertions(+), 8 deletions(-) create mode 100644 libraries/FavoriteTable.class.php create mode 100644 themes/pmahomme/img/b_favorite.png create mode 100644 themes/pmahomme/img/b_no_favorite.png diff --git a/config.sample.inc.php b/config.sample.inc.php index 2b3024d12d..0e57bc50d2 100644 --- a/config.sample.inc.php +++ b/config.sample.inc.php @@ -57,6 +57,7 @@ $cfg['Servers'][$i]['AllowNoPassword'] = false; // $cfg['Servers'][$i]['designer_coords'] = 'pma__designer_coords'; // $cfg['Servers'][$i]['userconfig'] = 'pma__userconfig'; // $cfg['Servers'][$i]['recent'] = 'pma__recent'; +// $cfg['Servers'][$i]['favorite'] = 'pma__favorite'; // $cfg['Servers'][$i]['users'] = 'pma__users'; // $cfg['Servers'][$i]['usergroups'] = 'pma__usergroups'; // $cfg['Servers'][$i]['navigationhiding'] = 'pma__navigationhiding'; diff --git a/db_structure.php b/db_structure.php index e848ad641e..63e36fc9c0 100644 --- a/db_structure.php +++ b/db_structure.php @@ -41,6 +41,7 @@ if ((!empty($_POST['submit_mult']) && isset($_POST['selected_tbl'])) $_POST['message'] = PMA_Message::success(); } } + require 'libraries/db_common.inc.php'; $url_query .= '&goto=db_structure.php'; @@ -132,12 +133,14 @@ $overhead_size = (double) 0; $hidden_fields = array(); $odd_row = true; $sum_row_count_pre = ''; - +// Instance of PMA_FavoriteTable class. +$fav_instance = PMA_FavoriteTable::getInstance(); foreach ($tables as $keyname => $current_table) { // Get valid statistics whatever is the table type $drop_query = ''; $drop_message = ''; + $already_favorite = false; $overhead = ''; $table_is_view = false; @@ -251,6 +254,24 @@ foreach ($tables as $keyname => $current_table) { list($do, $ignored) = PMA_getServerSlaveStatus( $server_slave_status, $truename ); + // Handle favorite table list. ----START---- + $already_favorite = PMA_checkFavoriteTable($db, $current_table['TABLE_NAME']); + + if (isset($_REQUEST['remove_favorite'])) { + if ($already_favorite) { + // If already in favorite list, remove it. + $favorite_table = $_REQUEST['favorite_table']; + $fav_instance->remove($db, $favorite_table); + } + } + + if (isset($_REQUEST['add_favorite'])) { + if (!$already_favorite) { + // Otherwise add to favorite list. + $favorite_table = $_REQUEST['favorite_table']; + $fav_instance->add($db, $favorite_table); + } + } // Handle favorite table list. ----ENDS---- list($html_output, $odd_row) = PMA_getHtmlForStructureTableRow( $i, $odd_row, $table_is_view, $current_table, diff --git a/examples/create_tables.sql b/examples/create_tables.sql index 8ffbf5ef84..953a4b6bbc 100644 --- a/examples/create_tables.sql +++ b/examples/create_tables.sql @@ -119,6 +119,20 @@ CREATE TABLE IF NOT EXISTS `pma__recent` ( -- -------------------------------------------------------- +-- +-- Table structure for table `pma__favorite` +-- + +CREATE TABLE IF NOT EXISTS `pma__favorite` ( + `username` varchar(64) NOT NULL, + `tables` text NOT NULL, + PRIMARY KEY (`username`) +) + COMMENT='Favorite tables' + DEFAULT CHARACTER SET utf8 COLLATE utf8_bin; + +-- -------------------------------------------------------- + -- -- Table structure for table `pma__table_uiprefs` -- diff --git a/js/db_structure.js b/js/db_structure.js index 63d6f43ee6..25161811fb 100644 --- a/js/db_structure.js +++ b/js/db_structure.js @@ -382,4 +382,18 @@ AJAX.registerOnload('db_structure.js', function () { return false; }); //end Calculate Real End for InnoDB + PMA_tooltip( + $("select[name*='funcs']"), + 'select', + PMA_messages.strFunctionHint + ); + // Add tooltip to favorite icons. + $(".favorite_table_anchor").each(function () { + PMA_tooltip( + $(this), + 'a', + $(this).attr("title") + ); + }); + }); // end $() diff --git a/js/navigation.js b/js/navigation.js index 92ec8db2e8..a41fbc8a4d 100644 --- a/js/navigation.js +++ b/js/navigation.js @@ -107,6 +107,20 @@ $(function () { } }); + /** + * Jump to favorite table + */ + $('#favoriteTable').live('change', function () { + if (this.value !== '') { + var arr = jQuery.parseJSON(this.value); + var $form = $(this).closest('form'); + $form.find('input[name=db]').val(arr.db); + $form.find('input[name=table]').val(arr.table); + $form.submit(); + $(this).prop('selectedIndex', 0); + } + }); + /** Create a Routine, Trigger or Event */ $('li.new_procedure a.ajax, li.new_function a.ajax').live('click', function (event) { event.preventDefault(); diff --git a/libraries/FavoriteTable.class.php b/libraries/FavoriteTable.class.php new file mode 100644 index 0000000000..4e445e8240 --- /dev/null +++ b/libraries/FavoriteTable.class.php @@ -0,0 +1,271 @@ +_pmaTable + = PMA_Util::backquote($GLOBALS['cfg']['Server']['pmadb']) . "." + . PMA_Util::backquote($GLOBALS['cfg']['Server']['favorite']); + } + $server_id = $GLOBALS['server']; + if (! isset($_SESSION['tmpval']['favorite_tables'][$server_id])) { + $_SESSION['tmpval']['favorite_tables'][$server_id] + = isset($this->_pmaTable) ? $this->getFromDb() : array(); + } + $this->tables =& $_SESSION['tmpval']['favorite_tables'][$server_id]; + } + + /** + * Returns class instance. + * + * @return PMA_FavoriteTable + */ + public static function getInstance() + { + if (is_null(self::$_instance)) { + self::$_instance = new PMA_FavoriteTable(); + } + return self::$_instance; + } + + /** + * Returns favorite tables from phpMyAdmin database. + * + * @return array + */ + public function getFromDb() + { + // Read from phpMyAdmin database, if favorite tables are not in session + $sql_query + = " SELECT `tables` FROM " . $this->_pmaTable . + " WHERE `username` = '" . $GLOBALS['cfg']['Server']['user'] . "'"; + + $return = array(); + $result = PMA_queryAsControlUser($sql_query, false); + if ($result) { + $row = $GLOBALS['dbi']->fetchArray($result); + if (isset($row[0])) { + $return = json_decode($row[0], true); + } + } + return $return; + } + + /** + * Save favorite tables into phpMyAdmin database. + * + * @return true|PMA_Message + */ + public function saveToDb() + { + $username = $GLOBALS['cfg']['Server']['user']; + $sql_query + = " REPLACE INTO " . $this->_pmaTable . " (`username`, `tables`)" . + " VALUES ('" . $username . "', '" + . PMA_Util::sqlAddSlashes( + json_encode($this->tables) + ) . "')"; + + $success = $GLOBALS['dbi']->tryQuery($sql_query, $GLOBALS['controllink']); + + if (! $success) { + $message = PMA_Message::error(__('Could not save favorite table!')); + $message->addMessage('

'); + $message->addMessage( + PMA_Message::rawError( + $GLOBALS['dbi']->getError($GLOBALS['controllink']) + ) + ); + return $message; + } + return true; + } + + /** + * Remove favorite tables from phpMyAdmin database. + * + * @return true|PMA_Message + */ + public function removeFromDb() + { + $username = $GLOBALS['cfg']['Server']['user']; + $sql_query + = " REPLACE INTO " . $this->_pmaTable . " (`username`, `tables`)" . + " VALUES ('" . $username . "', '" + . PMA_Util::sqlAddSlashes( + json_encode($this->tables) + ) . "')"; + + $success = $GLOBALS['dbi']->tryQuery($sql_query, $GLOBALS['controllink']); + + if (! $success) { + $message = PMA_Message::error(__('Could not remove favorite table!')); + $message->addMessage('

'); + $message->addMessage( + PMA_Message::rawError( + $GLOBALS['dbi']->getError($GLOBALS['controllink']) + ) + ); + return $message; + } + return true; + } + + /** + * Trim favorite table according to the NumFavoriteTables configuration. + * + * @return boolean True if trimming occurred + */ + public function trim() + { + $max = max($GLOBALS['cfg']['NumFavoriteTables'], 0); + $trimming_occurred = count($this->tables) > $max; + while (count($this->tables) > $max) { + array_pop($this->tables); + } + return $trimming_occurred; + } + + /** + * Return options for HTML select. + * + * @return string + */ + public function getHtmlSelectOption() + { + // trim and save, in case where the configuration is changed + if ($this->trim() && isset($this->_pmaTable)) { + $this->saveToDb(); + } + + $html = ''; + if (count($this->tables)) { + foreach ($this->tables as $table) { + $html .= ''; + } + } else { + $html .= ''; + } + return $html; + } + + /** + * Return HTML select. + * + * @return string + */ + public function getHtmlSelect() + { + $html = ''; + + return $html; + } + + /** + * Add favorite tables. + * + * @param string $db database name where the table is located + * @param string $table table name + * + * @return true|PMA_Message True if success, PMA_Message if not + */ + public function add($db, $table) + { + $table_arr = array(); + $table_arr['db'] = $db; + $table_arr['table'] = $table; + + // add only if this is new table + if (! isset($this->tables[0]) || $this->tables[0] != $table_arr) { + array_unshift($this->tables, $table_arr); + $this->tables = array_merge(array_unique($this->tables, SORT_REGULAR)); + $this->trim(); + if (isset($this->_pmaTable)) { + return $this->saveToDb(); + } + } + return true; + } + + /** + * Remove favorite tables. + * + * @param string $db database name where the table is located + * @param string $table table name + * + * @return true|PMA_Message True if success, PMA_Message if not + */ + public function remove($db, $table) + { + $table_arr = array(); + $table_arr['db'] = $db; + $table_arr['table'] = $table; + foreach ($this->tables as $key => $value) { + if ($value['db'] == $db && $value['table'] == $table) { + unset($this->tables[$key]); + } + } + if (isset($this->_pmaTable)) { + return $this->removeFromDb(); + } + return true; + } +} +?> diff --git a/libraries/Header.class.php b/libraries/Header.class.php index 454a3f8734..6219a5b862 100644 --- a/libraries/Header.class.php +++ b/libraries/Header.class.php @@ -11,6 +11,7 @@ if (! defined('PHPMYADMIN')) { require_once 'libraries/Scripts.class.php'; require_once 'libraries/RecentTable.class.php'; +require_once 'libraries/FavoriteTable.class.php'; require_once 'libraries/Menu.class.php'; require_once 'libraries/navigation/Navigation.class.php'; require_once 'libraries/url_generating.lib.php'; diff --git a/libraries/Util.class.php b/libraries/Util.class.php index d51eaab810..eef04c8fb1 100644 --- a/libraries/Util.class.php +++ b/libraries/Util.class.php @@ -3416,6 +3416,8 @@ class PMA_Util $titles['NoExport'] = self::getIcon('bd_export.png', __('Export')); $titles['Execute'] = self::getIcon('b_nextpage.png', __('Execute')); $titles['NoExecute'] = self::getIcon('bd_nextpage.png', __('Execute')); + $titles['Favorite'] = self::getIcon('b_favorite.png', __('')); + $titles['NoFavorite']= self::getIcon('b_no_favorite.png', __('')); return $titles; } diff --git a/libraries/config.default.php b/libraries/config.default.php index bfe685d5f6..9499ee3d6b 100644 --- a/libraries/config.default.php +++ b/libraries/config.default.php @@ -413,6 +413,13 @@ $cfg['Servers'][$i]['designer_coords'] = ''; */ $cfg['Servers'][$i]['recent'] = ''; +/** + * table to store favorite tables + * - leave blank for no favorite tables + * SUGGESTED: 'pma__favorite' + */ +$cfg['Servers'][$i]['favorite'] = ''; + /** * table to store UI preferences for tables * - leave blank for no "persistent" UI preferences @@ -906,6 +913,13 @@ $cfg['NavigationLogoLinkWindow'] = 'main'; */ $cfg['NumRecentTables'] = 10; +/** + * number of favorite tables displayed in the navigation panel + * + * @global integer $cfg['NumFavoriteTables'] + */ +$cfg['NumFavoriteTables'] = 10; + /** * display a JavaScript table filter in the navigation panel * when more then x tables are present diff --git a/libraries/config.values.php b/libraries/config.values.php index 33285614b9..d5b63ad45e 100644 --- a/libraries/config.values.php +++ b/libraries/config.values.php @@ -221,6 +221,7 @@ $cfg_db['_validators'] = array( 'Import/skip_queries' => 'validateNonNegativeNumber', 'InsertRows' => 'validatePositiveNumber', 'NumRecentTables' => 'validateNonNegativeNumber', + 'NumFavoriteTables' => 'validateNonNegativeNumber', 'LimitChars' => 'validatePositiveNumber', 'LoginCookieValidity' => 'validatePositiveNumber', 'LoginCookieStore' => 'validateNonNegativeNumber', diff --git a/libraries/config/messages.inc.php b/libraries/config/messages.inc.php index 9221dff703..ff850b5912 100644 --- a/libraries/config/messages.inc.php +++ b/libraries/config/messages.inc.php @@ -467,6 +467,8 @@ $strConfigNavigationTreePointerEnable_desc $strConfigNavigationTreePointerEnable_name = __('Enable highlighting'); $strConfigNumRecentTables_desc = __('Maximum number of recently used tables; set 0 to disable.'); +$strConfigNumFavoriteTables_desc + = __('Maximum number of favorite tables; set 0 to disable.'); $strConfigNumRecentTables_name = __('Recently used tables'); $strConfigRowActionLinks_desc = __('These are Edit, Copy and Delete links.'); $strConfigRowActionLinks_name = __('Where to show the table row links'); diff --git a/libraries/config/setup.forms.php b/libraries/config/setup.forms.php index e979966799..68c78c1459 100644 --- a/libraries/config/setup.forms.php +++ b/libraries/config/setup.forms.php @@ -131,6 +131,7 @@ $forms['Features']['Other_core_settings'] = array( 'MaxDbList', 'MaxTableList', 'NumRecentTables', + 'NumFavoriteTables', 'ShowHint', 'OBGzip', 'PersistentConnections', diff --git a/libraries/config/user_preferences.forms.php b/libraries/config/user_preferences.forms.php index 03467dc72d..ec194733d3 100644 --- a/libraries/config/user_preferences.forms.php +++ b/libraries/config/user_preferences.forms.php @@ -33,6 +33,7 @@ $forms['Features']['General'] = array( 'MaxDbList', 'MaxTableList', 'NumRecentTables', + 'NumFavoriteTables', 'ShowHint', 'SendErrorReports'); $forms['Features']['Text_fields'] = array( diff --git a/libraries/navigation/NavigationHeader.class.php b/libraries/navigation/NavigationHeader.class.php index 651de0af18..7577209b60 100644 --- a/libraries/navigation/NavigationHeader.class.php +++ b/libraries/navigation/NavigationHeader.class.php @@ -175,7 +175,7 @@ class PMA_NavigationHeader /** * Creates the code for displaying the links - * at the top of the navigation panel + * at the top of the navigation panel * * @return string HTML code for the links */ @@ -303,11 +303,27 @@ class PMA_NavigationHeader ) ); $retval .= PMA_RecentTable::getInstance()->getHtmlSelect(); + $retval .= $this->_favorite(); $retval .= ''; $retval .= ''; $retval .= ''; } return $retval; } + + /** + * Displays a drop-down choice of favorite tables + * + * @return string HTML code for the Favorite tables + */ + private function _favorite() + { + $retval = ''; + // display favorite tables + if ($GLOBALS['cfg']['NumFavoriteTables'] > 0) { + $retval .= PMA_FavoriteTable::getInstance()->getHtmlSelect(); + } + return $retval; + } } ?> diff --git a/libraries/structure.lib.php b/libraries/structure.lib.php index bcf4b7e4d1..0f25d9e6c3 100644 --- a/libraries/structure.lib.php +++ b/libraries/structure.lib.php @@ -181,7 +181,7 @@ function PMA_getHtmlBodyForTableSummary($num_tables, $server_slave_status, if ($server_slave_status) { $html_output .= '' . __('Replication') . '' . "\n"; } - $html_output .= '' + $html_output .= '' . __('Sum') . ''; $html_output .= '' @@ -451,6 +451,7 @@ function PMA_getHtmlForStructureTableRow( $collation, $formatted_size, $unit, $overhead, $create_time, $update_time, $check_time,$is_show_stats, $ignored, $do, $colspan_for_structure ) { + global $db; $html_output = ''; + $html_output .= '' + . (!$already_favorite ? $titles['NoFavorite'] : $titles['Favorite']) . ''; $html_output .= '' . $browse_table . ''; $html_output .= '' @@ -747,7 +765,7 @@ function PMA_tableHeader($db_is_system_schema = false, $replication = false) if ($db_is_system_schema) { $action_colspan = 3; } else { - $action_colspan = 6; + $action_colspan = 7; } $html_output = '' . "\n" @@ -2232,7 +2250,7 @@ function PMA_getHtmlForDisplayTableStats($showtable, $table_info_num_rows, (isset ($avg_size) ? $avg_size : ''), (isset ($avg_unit) ? $avg_unit : '') ); - + $html_output .= '' . ''; @@ -2675,4 +2693,22 @@ function PMA_displayTableBrowseForSelectedColumns($db, $table, $goto, null, $sql_query, null, null ); } + +/** + * Function to check if a table is already in favorite list. + * + * @param string $db current database + * @param string $table current table + * + * @return true|false + */ +function PMA_checkFavoriteTable($db, $current_table) +{ + foreach ($_SESSION['tmpval']['favorite_tables'][$GLOBALS['server']] as $key => $value) { + if ($value['db'] == $db && $value['table'] == $current_table) { + return true; + } + } + return false; +} ?> diff --git a/test/libraries/common/PMA_buildActionTitles_test.php b/test/libraries/common/PMA_buildActionTitles_test.php index b0d479c77e..34f599faf3 100644 --- a/test/libraries/common/PMA_buildActionTitles_test.php +++ b/test/libraries/common/PMA_buildActionTitles_test.php @@ -44,6 +44,8 @@ class PMA_BuildActionTitles_Test extends PHPUnit_Framework_TestCase $titles['NoExport'] = PMA_Util::getIcon('bd_export.png', __('Export')); $titles['Execute'] = PMA_Util::getIcon('b_nextpage.png', __('Execute')); $titles['NoExecute'] = PMA_Util::getIcon('bd_nextpage.png', __('Execute')); + $titles['Favorite'] = PMA_Util::getIcon('b_favorite.png', __('')); + $titles['NoFavorite'] = PMA_Util::getIcon('b_no_favorite.png', __('')); $this->assertEquals($titles, PMA_Util::buildActionTitles()); diff --git a/themes/original/css/navigation.css.php b/themes/original/css/navigation.css.php index f9fbc3e0f5..4a69562b15 100644 --- a/themes/original/css/navigation.css.php +++ b/themes/original/css/navigation.css.php @@ -70,15 +70,22 @@ if (! defined('PMA_MINIMUM_COMMON') && ! defined('TESTSUITE')) { padding: .3em; } -#pma_navigation div#recentTableList { +#pma_navigation div#recentTableList, +#pma_navigation div#FavoriteTableList { text-align: center; margin-bottom: 0.5em; } +#pma_navigation #recentTable, +#pma_navigation #FavoriteTable { + width: 200px; +} + #pma_navigation #pmalogo, #pma_navigation #serverChoice, #pma_navigation #navipanellinks, #pma_navigation #recentTableList, +#pma_navigation #FavoriteTableList, #pma_navigation #databaseList, #pma_navigation div.pageselector.dbselector { text-align: center; @@ -88,12 +95,14 @@ if (! defined('PMA_MINIMUM_COMMON') && ! defined('TESTSUITE')) { } #pma_navigation #recentTableList select, +#pma_navigation #FavoriteTableList select, #pma_navigation #serverChoice select { width: 80%; } -#pma_navigation #recentTableList { +#pma_navigation #recentTableList, +#pma_navigation #FavoriteTableList { margin-bottom: 0; padding-bottom: 0; } diff --git a/themes/pmahomme/css/navigation.css.php b/themes/pmahomme/css/navigation.css.php index 4326f81a7e..8895cdc162 100644 --- a/themes/pmahomme/css/navigation.css.php +++ b/themes/pmahomme/css/navigation.css.php @@ -70,6 +70,7 @@ if (! defined('PMA_MINIMUM_COMMON') && ! defined('TESTSUITE')) { #pma_navigation #serverChoice, #pma_navigation #navipanellinks, #pma_navigation #recentTableList, +#pma_navigation #favoriteTableList, #pma_navigation #databaseList, #pma_navigation div.pageselector.dbselector { text-align: center; @@ -77,7 +78,12 @@ if (! defined('PMA_MINIMUM_COMMON') && ! defined('TESTSUITE')) { border: 0; } -#pma_navigation #recentTableList select, +#pma_navigation #recentTable, +#pma_navigation #favoriteTable { + width: 200px; +} + +#pma_navigation #favoriteTableList select, #pma_navigation #serverChoice select { width: 80%; diff --git a/themes/pmahomme/img/b_favorite.png b/themes/pmahomme/img/b_favorite.png new file mode 100644 index 0000000000000000000000000000000000000000..90a4013c32041ed101281358b03534a303627ab3 GIT binary patch literal 563 zcmV-30?hr1P)5#WqL>^AH_$^bNz^)Qqpd4|COEnIG%{499cK>|^!5`q2@RjZbYKtoWP7IK$_OV0g%*T2?t z_K*lrg)}RTcfLPo_w${ZT|ewE4j%ZyZJ|9OLdb1%v$;cX-hu6wo8b4ZPg@3@w%2Bl zID|~xjx9i*e_=Nv4?b9TF9V}CvJzd1NA3EetftdxS&7N9m;@e@bG+&r z4XNtf-A>ZYlcSDVw`#6sSHqasDA>#HWF&$E^^VScRcgI~s!oR;P?Q@+O;)3dJS?ba z_F`(w^|E(qMN_6_TA@|ffjD(&Ty4pAZD+|IF=Wp?a$u-QKHnRA==D}ALx*_9FV{`F{}|lL-NWwQwMzZs3}iyf$ZMw!YeX1Y+F{;QXjdo$_o~m- zso~o7dD|{$+q3Lo7h!m2CVc;6R<&raIN#04tarcE{X{fpxHKD6l%%9+QYy~ zQ!m-|is5Fhim^!LT6Hla_mw`+-h|R;?6O^l%c9wMMjod2MR}7gB9&Iuv?)>5fF)kil2W>`HzlP~B^oQlQ^myIgaxFPnbRm}kh>yJ+q8H@FFQ_wKEmW(Hpx zbl7lV@f+Sor)K0Wy8d)&`!C?eSzh-1lkh}W6j{aJPN?6^h92RE`ps-;Kk`Ra%gx3* zqcLh<6u#NCpS_3SC7fpWCK7RYM6zl&;-J-_<%OID4@n2NesymrhBXWgJTfZKXXpgB zOXYiA_V&!S7GB2Te}LTkBd}#V?LBb5%ufG~zRavlE-?rLgf9wQd_Ig-;9`LAMS+XY zhp`G=3=qC3aPj#tR)LEF!WRWDJ|D&^a4|smqQJ%H!&n6_1_)mixcGb+tH8wo;fn$n zpATadxELUOQQ+e9VXOid1B5RMTzo!^Rp4TP@I`@(&xf%JTnrGtC~)!lFjj$!0m2sr zE)>Cc^gZxwu2Stsxz+gh#eAcJPm zKXJ#dI|(-I4_ez2#zF!@TAP~#?T>H0y1H}MT|FK4QOEjWY~b|i)8x|ne+CA4_YZHj z?w3-mj(~1}#rDt+5`61-zg`8;flIvoj&G99&5eNssKdkqN%rklw-zi*PU1CPuEKKk&zsqyhK65KWh_kHbi+r>}*P5$=oY0Q2y z2qagb)BNE5f1KLA=iB6#&;%Mr9+%hG$k`9ipnV!z)2l+_!AF`vboyT(oFVI<{+C?a z_-trH8~UJcCkD#6+2*q6JA0bgzk2N|v@0XpRk*ztL0`NC><3{6V4uAI&ofha4vqz2 z^uY?C9uKskKl*}C6Z_jR{_lTz1JaSHTW<}KYj6eKcKPxpf~_m#8GXhl3UYojbVf|ESVw_Bb;6r_gX+Z)!dsVpSfrpUn$#dt{$lAq?lh6-9Yod$| l`=~=(NA8{p%