1 /**************************************************************************************** 2 * Copyright (c) 2002 Mark Kretschmann <kretschmann@kde.org> * 3 * * 4 * This program is free software; you can redistribute it and/or modify it under * 5 * the terms of the GNU General Public License as published by the Free Software * 6 * Foundation; either version 2 of the License, or (at your option) any later * 7 * version. * 8 * * 9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 12 * * 13 * You should have received a copy of the GNU General Public License along with * 14 * this program. If not, see <http://www.gnu.org/licenses/>. * 15 ****************************************************************************************/ 16 17 #include "core/support/Amarok.h" 18 19 #include "core/meta/Meta.h" 20 #include "core/meta/support/MetaUtility.h" 21 #include "core/capabilities/SourceInfoCapability.h" 22 #include "core/playlists/PlaylistFormat.h" 23 24 #include <KConfigGroup> 25 #include <KLocalizedString> 26 #include <KSharedConfig> 27 28 #include <QApplication> 29 #include <QDateTime> 30 #include <QIcon> 31 #include <QLocale> 32 #include <QPixmapCache> 33 #include <QStandardPaths> 34 35 QPointer<KActionCollection> Amarok::actionCollectionObject; 36 QMutex Amarok::globalDirsMutex; 37 38 namespace Amarok 39 { 40 41 // TODO: sometimes we have a playcount but no valid datetime. 42 // in such a case we should maybe display "Unknown" and not "Never" verboseTimeSince(const QDateTime & datetime)43 QString verboseTimeSince( const QDateTime &datetime ) 44 { 45 if( datetime.isNull() || !datetime.toSecsSinceEpoch() ) 46 return i18nc( "The amount of time since last played", "Never" ); 47 48 const QDateTime now = QDateTime::currentDateTime(); 49 const int datediff = datetime.daysTo( now ); 50 51 // HACK: Fix 203522. Arithmetic overflow? 52 // Getting weird values from Plasma::DataEngine (LAST_PLAYED field). 53 if( datediff < 0 ) 54 return i18nc( "When this track was last played", "Unknown" ); 55 56 if( datediff >= 6*7 /*six weeks*/ ) { // return absolute month/year 57 QString month_year = datetime.date().toString(QStringLiteral("MM yyyy")); 58 return i18nc( "monthname year", "%1", month_year ); 59 } 60 61 //TODO "last week" = maybe within 7 days, but prolly before last Sunday 62 63 if( datediff >= 7 ) // return difference in weeks 64 return i18np( "One week ago", "%1 weeks ago", (datediff+3)/7 ); 65 66 const int timediff = datetime.secsTo( now ); 67 68 if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days 69 return datediff == 1 ? 70 i18n( "Yesterday" ) : 71 i18np( "One day ago", "%1 days ago", (timediff+12*60*60)/(24*60*60) ); 72 73 if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours 74 return i18np( "One hour ago", "%1 hours ago", (timediff+30*60)/(60*60) ); 75 76 //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" 77 78 if( timediff >= 0 ) // return difference in minutes 79 return timediff/60 ? 80 i18np( "One minute ago", "%1 minutes ago", (timediff+30)/60 ) : 81 i18n( "Within the last minute" ); 82 83 return i18n( "The future" ); 84 } 85 verboseTimeSince(uint time_t)86 QString verboseTimeSince( uint time_t ) 87 { 88 if( !time_t ) 89 return i18nc( "The amount of time since last played", "Never" ); 90 91 QDateTime dt; 92 dt.setSecsSinceEpoch( time_t ); 93 return verboseTimeSince( dt ); 94 } 95 conciseTimeSince(uint time_t)96 QString conciseTimeSince( uint time_t ) 97 { 98 if( !time_t ) 99 return i18nc( "The amount of time since last played", "0" ); 100 101 QDateTime datetime; 102 datetime.setSecsSinceEpoch( time_t ); 103 104 const QDateTime now = QDateTime::currentDateTime(); 105 const int datediff = datetime.daysTo( now ); 106 107 if( datediff >= 6*7 /*six weeks*/ ) { // return difference in months 108 return i18nc( "number of months ago", "%1M", datediff/7/4 ); 109 } 110 111 if( datediff >= 7 ) // return difference in weeks 112 return i18nc( "w for weeks", "%1w", (datediff+3)/7 ); 113 114 if( datediff == -1 ) 115 return i18nc( "When this track was last played", "Tomorrow" ); 116 117 const int timediff = datetime.secsTo( now ); 118 119 if( timediff >= 24*60*60 /*24 hours*/ ) // return difference in days 120 // xgettext: no-c-format 121 return i18nc( "d for days", "%1d", (timediff+12*60*60)/(24*60*60) ); 122 123 if( timediff >= 90*60 /*90 minutes*/ ) // return difference in hours 124 return i18nc( "h for hours", "%1h", (timediff+30*60)/(60*60) ); 125 126 //TODO are we too specific here? Be more fuzzy? ie, use units of 5 minutes, or "Recently" 127 128 if( timediff >= 60 ) // return difference in minutes 129 return QStringLiteral("%1'").arg( ( timediff + 30 )/60 ); 130 if( timediff >= 0 ) // return difference in seconds 131 return QStringLiteral("%1\"").arg( ( timediff + 1 )/60 ); 132 133 return i18n( "0" ); 134 } 135 manipulateThe(QString & str,bool reverse)136 void manipulateThe( QString &str, bool reverse ) 137 { 138 if( reverse ) 139 { 140 if( !str.startsWith( QLatin1String("the "), Qt::CaseInsensitive ) ) 141 return; 142 143 QString begin = str.left( 3 ); 144 str = str.append( ", %1" ).arg( begin ); 145 str = str.mid( 4 ); 146 return; 147 } 148 149 if( !str.endsWith( QLatin1String(", the"), Qt::CaseInsensitive ) ) 150 return; 151 152 QString end = str.right( 3 ); 153 str = str.prepend( "%1 " ).arg( end ); 154 155 uint newLen = str.length() - end.length() - 2; 156 157 str.truncate( newLen ); 158 } 159 generatePlaylistName(const Meta::TrackList & tracks)160 QString generatePlaylistName( const Meta::TrackList& tracks ) 161 { 162 QString datePart = QLocale::system().toString( QDateTime::currentDateTime(), 163 QLocale::ShortFormat ); 164 if( tracks.isEmpty() ) 165 { 166 return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ 167 the parentheses", 168 "Empty Playlist (%1)", datePart ); 169 } 170 171 bool singleArtist = true; 172 bool singleAlbum = true; 173 174 Meta::ArtistPtr artist = tracks.first()->artist(); 175 Meta::AlbumPtr album = tracks.first()->album(); 176 177 QString artistPart; 178 QString albumPart; 179 180 foreach( const Meta::TrackPtr track, tracks ) 181 { 182 if( artist != track->artist() ) 183 singleArtist = false; 184 185 if( album != track->album() ) 186 singleAlbum = false; 187 188 if ( !singleArtist && !singleAlbum ) 189 break; 190 } 191 192 if( ( !singleArtist && !singleAlbum ) || 193 ( !artist && !album ) ) 194 return i18nc( "A saved playlist with the current time (KLocalizedString::Shortdate) added between \ 195 the parentheses", 196 "Various Tracks (%1)", datePart ); 197 198 if( singleArtist ) 199 { 200 if( artist ) 201 artistPart = artist->prettyName(); 202 else 203 artistPart = i18n( "Unknown Artist(s)" ); 204 } 205 else if( album && album->hasAlbumArtist() && singleAlbum ) 206 { 207 artistPart = album->albumArtist()->prettyName(); 208 } 209 else 210 { 211 artistPart = i18n( "Various Artists" ); 212 } 213 214 if( singleAlbum ) 215 { 216 if( album ) 217 albumPart = album->prettyName(); 218 else 219 albumPart = i18n( "Unknown Album(s)" ); 220 } 221 else 222 { 223 albumPart = i18n( "Various Albums" ); 224 } 225 226 return i18nc( "A saved playlist titled <artist> - <album>", "%1 - %2", 227 artistPart, albumPart ); 228 } 229 actionCollection()230 KActionCollection* actionCollection() // TODO: constify? 231 { 232 if( !actionCollectionObject ) 233 { 234 actionCollectionObject = new KActionCollection( qApp ); 235 actionCollectionObject->setObjectName( QStringLiteral("Amarok-KActionCollection") ); 236 } 237 238 return actionCollectionObject.data(); 239 } 240 config(const QString & group)241 KConfigGroup config( const QString &group ) 242 { 243 //Slightly more useful config() that allows setting the group simultaneously 244 return KSharedConfig::openConfig()->group( group ); 245 } 246 247 namespace ColorScheme 248 { 249 QColor Base; 250 QColor Text; 251 QColor Background; 252 QColor Foreground; 253 QColor AltBase; 254 } 255 OverrideCursor(Qt::CursorShape cursor)256 OverrideCursor::OverrideCursor( Qt::CursorShape cursor ) 257 { 258 QApplication::setOverrideCursor( cursor == Qt::WaitCursor ? 259 Qt::WaitCursor : 260 Qt::BusyCursor ); 261 } 262 ~OverrideCursor()263 OverrideCursor::~OverrideCursor() 264 { 265 QApplication::restoreOverrideCursor(); 266 } 267 saveLocation(const QString & directory)268 QString saveLocation( const QString &directory ) 269 { 270 globalDirsMutex.lock(); 271 QString result = QStandardPaths::writableLocation( QStandardPaths::AppDataLocation ) + QDir::separator() + directory; 272 273 if( !result.endsWith( QDir::separator() ) ) 274 result.append( QDir::separator() ); 275 276 QDir dir( result ); 277 if( !dir.exists() ) 278 dir.mkpath( QStringLiteral( "." ) ); 279 280 globalDirsMutex.unlock(); 281 return result; 282 } 283 defaultPlaylistPath()284 QString defaultPlaylistPath() 285 { 286 return Amarok::saveLocation() + QLatin1String("current.xspf"); 287 } 288 cleanPath(const QString & path)289 QString cleanPath( const QString &path ) 290 { 291 /* Unicode uses combining characters to form accented versions of other characters. 292 * (Exception: Latin-1 table for compatibility with ASCII.) 293 * Those can be found in the Unicode tables listed at: 294 * http://en.wikipedia.org/w/index.php?title=Combining_character&oldid=255990982 295 * Removing those characters removes accents. :) */ 296 QString result = path; 297 298 // German umlauts 299 result.replace( QChar(0x00e4), QLatin1String("ae") ).replace( QChar(0x00c4), QLatin1String("Ae") ); 300 result.replace( QChar(0x00f6), QLatin1String("oe") ).replace( QChar(0x00d6), QLatin1String("Oe") ); 301 result.replace( QChar(0x00fc), QLatin1String("ue") ).replace( QChar(0x00dc), QLatin1String("Ue") ); 302 result.replace( QChar(0x00df), QLatin1String("ss") ); 303 304 // other special cases 305 result.replace( QChar(0x00C6), QLatin1String("AE") ); 306 result.replace( QChar(0x00E6), QLatin1String("ae") ); 307 308 result.replace( QChar(0x00D8), QLatin1String("OE") ); 309 result.replace( QChar(0x00F8), QLatin1String("oe") ); 310 311 // normalize in a form where accents are separate characters 312 result = result.normalized( QString::NormalizationForm_D ); 313 314 // remove accents from table "Combining Diacritical Marks" 315 for( int i = 0x0300; i <= 0x036F; i++ ) 316 { 317 result.remove( QChar( i ) ); 318 } 319 320 return result; 321 } 322 asciiPath(const QString & path)323 QString asciiPath( const QString &path ) 324 { 325 QString result = path; 326 for( int i = 0; i < result.length(); i++ ) 327 { 328 QChar c = result[ i ]; 329 if( c > QChar(0x7f) || c == QChar(0) ) 330 { 331 c = '_'; 332 } 333 result[ i ] = c; 334 } 335 return result; 336 } 337 vfatPath(const QString & path,PathSeparatorBehaviour behaviour)338 QString vfatPath( const QString &path, PathSeparatorBehaviour behaviour ) 339 { 340 if( path.isEmpty() ) 341 return QString(); 342 343 QString s = path; 344 345 QChar separator = ( behaviour == AutoBehaviour ) ? QDir::separator() : ( behaviour == UnixBehaviour ) ? '/' : '\\'; 346 347 if( behaviour == UnixBehaviour ) // we are on *nix, \ is a valid character in file or directory names, NOT the dir separator 348 s.replace( '\\', '_' ); 349 else 350 s.replace( QLatin1Char('/'), '_' ); // on windows we have to replace / instead 351 352 int start = 0; 353 #ifdef Q_OS_WIN 354 // exclude the leading "C:/" from special character replacement in the loop below 355 // bug 279560, bug 302251 356 if( QDir::isAbsolutePath( s ) ) 357 start = 3; 358 #endif 359 for( int i = start; i < s.length(); i++ ) 360 { 361 QChar c = s[ i ]; 362 if( c < QChar(0x20) || c == QChar(0x7F) // 0x7F = 127 = DEL control character 363 || c=='*' || c=='?' || c=='<' || c=='>' 364 || c=='|' || c=='"' || c==':' ) 365 c = '_'; 366 else if( c == '[' ) 367 c = '('; 368 else if ( c == ']' ) 369 c = ')'; 370 s[ i ] = c; 371 } 372 373 /* beware of reserved device names */ 374 uint len = s.length(); 375 if( len == 3 || (len > 3 && s[3] == '.') ) 376 { 377 QString l = s.left(3).toLower(); 378 if( l==QLatin1String("aux") || l==QLatin1String("con") || l==QLatin1String("nul") || l==QLatin1String("prn") ) 379 s = '_' + s; 380 } 381 else if( len == 4 || (len > 4 && s[4] == '.') ) 382 { 383 QString l = s.left(3).toLower(); 384 QString d = s.mid(3,1); 385 if( (l==QLatin1String("com") || l==QLatin1String("lpt")) && 386 (d==QLatin1String("0") || d==QLatin1String("1") || d==QLatin1String("2") || d==QLatin1String("3") || d==QLatin1String("4") || 387 d==QLatin1String("5") || d==QLatin1String("6") || d==QLatin1String("7") || d==QLatin1String("8") || d==QLatin1String("9")) ) 388 s = '_' + s; 389 } 390 391 // "clock$" is only allowed WITH extension, according to: 392 // http://en.wikipedia.org/w/index.php?title=Filename&oldid=303934888#Comparison_of_file_name_limitations 393 if( QString::compare( s, QStringLiteral("clock$"), Qt::CaseInsensitive ) == 0 ) 394 s = '_' + s; 395 396 /* max path length of Windows API */ 397 s = s.left(255); 398 399 /* whitespace or dot at the end of folder/file names or extensions are bad */ 400 len = s.length(); 401 if( s.at(len - 1) == ' ' || s.at(len - 1) == '.' ) 402 s[len - 1] = '_'; 403 404 for( int i = 1; i < s.length(); i++ ) // correct trailing whitespace in folder names 405 { 406 if( s.at(i) == separator && s.at(i - 1) == ' ' ) 407 s[i - 1] = '_'; 408 } 409 410 for( int i = 1; i < s.length(); i++ ) // correct trailing dot in folder names, excluding . and .. 411 { 412 if( s.at(i) == separator 413 && s.at(i - 1) == '.' 414 && !( i == 1 // ./any 415 || ( i == 2 && s.at(i - 2) == '.' ) // ../any 416 || ( i >= 2 && s.at(i - 2) == separator ) // any/./any 417 || ( i >= 3 && s.at(i - 3) == separator && s.at(i - 2) == '.' ) // any/../any 418 ) ) 419 s[i - 1] = '_'; 420 } 421 422 /* correct trailing spaces in file name itself, not needed for dots */ 423 int extensionIndex = s.lastIndexOf( QLatin1Char('.') ); 424 if( ( s.length() > 1 ) && ( extensionIndex > 0 ) ) 425 if( s.at(extensionIndex - 1) == ' ' ) 426 s[extensionIndex - 1] = '_'; 427 428 return s; 429 } 430 semiTransparentLogo(int dim)431 QPixmap semiTransparentLogo( int dim ) 432 { 433 QPixmap logo; 434 #define AMAROK_LOGO_CACHE_KEY QLatin1String("AmarokSemiTransparentLogo")+QString::number(dim) 435 if( !QPixmapCache::find( AMAROK_LOGO_CACHE_KEY, &logo ) ) 436 { 437 QImage amarokIcon = QIcon::fromTheme( QStringLiteral("amarok") ).pixmap( dim, dim ).toImage(); 438 amarokIcon = amarokIcon.convertToFormat( QImage::Format_ARGB32 ); 439 QRgb *data = reinterpret_cast<QRgb*>( amarokIcon.bits() ); 440 QRgb *end = data + amarokIcon.byteCount() / 4; 441 while(data != end) 442 { 443 unsigned char gray = qGray(*data); 444 *data = qRgba(gray, gray, gray, 127); 445 ++data; 446 } 447 logo = QPixmap::fromImage( amarokIcon ); 448 QPixmapCache::insert( AMAROK_LOGO_CACHE_KEY, logo ); 449 } 450 #undef AMAROK_LOGO_CACHE_KEY 451 return logo; 452 } 453 454 } // End namespace Amarok 455