diff options
author | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
---|---|---|
committer | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
commit | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/Plugins/Library/ml_wire | |
parent | 537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff) | |
download | winamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz |
Initial community commit
Diffstat (limited to 'Src/Plugins/Library/ml_wire')
104 files changed, 13705 insertions, 0 deletions
diff --git a/Src/Plugins/Library/ml_wire/AtomParse.h b/Src/Plugins/Library/ml_wire/AtomParse.h new file mode 100644 index 00000000..ab76a1f9 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/AtomParse.h @@ -0,0 +1,51 @@ +#ifndef NULLSOFT_ATOMPARSEH +#define NULLSOFT_ATOMPARSEH +#if 0 +#include "RFCDate.h" +#include "XMLNode.h" +#include "Feeds.h" +#include "../nu/AutoChar.h" +#include "ChannelSync.h" + +void ReadAtomItem(XMLNode *item, Channel &channel) +{ +} + +void ReadAtomChannel(XMLNode *node, Channel &newChannel) +{ + XMLNode *curNode = 0; + curNode = node->Get(L"title"); + if (curNode) + newChanneltitle = curNode->content; + + curNode = node->Get(L"subtitle"); + if (curNode) + newChannel.description = curNode->content; + + XMLNode::NodeList &links = node->GetList(L"link"); + XMLNode::NodeList::iterator linkItr; + for (linkItr=links.begin();linkItr!=links.end();linkItr++) + { + if ((*linkItr)->properties[L"rel"].empty() + || (*linkItr)->properties[L"rel"]== L"alternate") + newChannel.link = (*linkItr)->properties[L"href"]; + } + + XMLNode::NodeList &entries = node->GetList(L"entry"); + XMLNode::NodeList::iterator entryItr; + for (entryItr=entries.begin();entryItr!=entries.end();entryItr++) + { + } +} + +void ReadAtom(XMLNode *atom, ChannelSync *sync) +{ + sync->BeginChannelSync(); + Channel newChannel; + ReadAtomChannel(atom, newChannel); + sync->NewChannel(newChannel); + + sync->EndChannelSync(); +} +#endif +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/BackgroundDownloader.cpp b/Src/Plugins/Library/ml_wire/BackgroundDownloader.cpp new file mode 100644 index 00000000..9e8c0cac --- /dev/null +++ b/Src/Plugins/Library/ml_wire/BackgroundDownloader.cpp @@ -0,0 +1,338 @@ +#include <vector> +#include <atomic> + +#include "Main.h" +#include "Downloaded.h" +#include "BackgroundDownloader.h" + +#include "Feeds.h" +#include "DownloadStatus.h" +#include "DownloadsDialog.h" +#include "api__ml_wire.h" +#include "api/service/waServiceFactory.h" +#include "../../..\Components\wac_network\wac_network_http_receiver_api.h" + + +using namespace Nullsoft::Utility; + +#define SIMULTANEOUS_DOWNLOADS 2 +std::vector<DownloadToken> downloadsQueue; +LockGuard downloadsQueueLock; + +class DownloaderCallback : public ifc_downloadManagerCallback +{ +public: + DownloaderCallback( const wchar_t *url, const wchar_t *destination_filepath, const wchar_t *channel, const wchar_t *item, __time64_t publishDate ) + { + this->hFile = INVALID_HANDLE_VALUE; + this->url = _wcsdup( url ); + this->destination_filepath = _wcsdup( destination_filepath ); + this->channel = _wcsdup( channel ); + this->item = _wcsdup( item ); + this->publishDate = publishDate; + this->totalSize = 0; + this->downloaded = 0; + } + + void OnInit(DownloadToken token) + { + // ---- Inform the download status service of our presence---- + downloadStatus.AddDownloadThread(token, this->channel, this->item, this->destination_filepath); + } + + void OnConnect(DownloadToken token) + { + // ---- retrieve total size + api_httpreceiver *http = WAC_API_DOWNLOADMANAGER->GetReceiver(token); + if (http) + { + this->totalSize = http->content_length(); + } + + // ---- create file handle + hFile = CreateFile(destination_filepath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if ( hFile == INVALID_HANDLE_VALUE ) + { + WAC_API_DOWNLOADMANAGER->CancelDownload(token); + } + } + + void OnData(DownloadToken token, void *data, size_t datalen) + { + // ---- OnConnect copied here due to dlmgr OnData called first + // ---- retrieve total size + api_httpreceiver *http = WAC_API_DOWNLOADMANAGER->GetReceiver(token); + if ( !this->totalSize && http ) + { + this->totalSize = http->content_length(); + } + + if ( hFile == INVALID_HANDLE_VALUE ) + { + // ---- create file handle + hFile = CreateFile(destination_filepath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if ( hFile == INVALID_HANDLE_VALUE ) + { + WAC_API_DOWNLOADMANAGER->CancelDownload(token); + return; + } + } + // ---- OnConnect to be removed once dlmgr is fixed + + // ---- OnData + // ---- if file handle is invalid, then cancel download + if ( hFile == INVALID_HANDLE_VALUE ) + { + WAC_API_DOWNLOADMANAGER->CancelDownload(token); + return; + } + + this->downloaded = (size_t)WAC_API_DOWNLOADMANAGER->GetBytesDownloaded(token); + + if ( datalen > 0 ) + { + // ---- hFile is valid handle, and write to disk + DWORD numWritten = 0; + WriteFile(hFile, data,(DWORD)datalen, &numWritten, FALSE); + + // ---- failed writing the number of datalen characters, cancel download + if (numWritten != datalen) + { + WAC_API_DOWNLOADMANAGER->CancelDownload(token); + return; + } + } + + // if killswitch is turned on, then cancel download + if ( downloadStatus.UpdateStatus(token, this->downloaded, this->totalSize) ) + { + WAC_API_DOWNLOADMANAGER->CancelDownload(token); + } + } + + void OnCancel(DownloadToken token) + { + if ( hFile != INVALID_HANDLE_VALUE ) + { + CloseHandle(hFile); + DeleteFile(destination_filepath); + } + DownloadsUpdated(token,NULL); + downloadStatus.DownloadThreadDone(token); + + + { + AutoLock lock( downloadsQueueLock ); + + size_t l_index = 0; + for ( DownloadToken &l_download_token : downloadsQueue ) + { + if ( l_download_token == token ) + { + downloadsQueue.erase( downloadsQueue.begin() + l_index ); + break; + } + + ++l_index; + } + } + + for ( DownloadToken &l_download_token : downloadsQueue ) + { + if ( WAC_API_DOWNLOADMANAGER->IsPending( l_download_token ) ) + { + WAC_API_DOWNLOADMANAGER->ResumePendingDownload( l_download_token ); + break; + } + } + + this->Release(); + } + + void OnError(DownloadToken token, int error) + { + if ( hFile != INVALID_HANDLE_VALUE ) + { + CloseHandle(hFile); + DeleteFile(destination_filepath); + } + DownloadsUpdated(token,NULL); + downloadStatus.DownloadThreadDone(token); + + { + AutoLock lock(downloadsQueueLock); + for (size_t index = 0; index < downloadsQueue.size(); index++) + { + if (downloadsQueue.at(index) == token) + { + downloadsQueue.erase(downloadsQueue.begin() + index); + break; + } + } + for (size_t index = 0; index < downloadsQueue.size(); index++) + { + if(WAC_API_DOWNLOADMANAGER->IsPending(downloadsQueue.at(index))) + { + WAC_API_DOWNLOADMANAGER->ResumePendingDownload(downloadsQueue.at(index)); + break; + } + } + } + this->Release(); + } + + void OnFinish( DownloadToken token ) + { + if ( hFile != INVALID_HANDLE_VALUE ) + { + CloseHandle( hFile ); + + DownloadedFile *data = new DownloadedFile( this->url, this->destination_filepath, this->channel, this->item, this->publishDate ); + data->bytesDownloaded = this->downloaded; + data->totalSize = this->totalSize; + { + AutoLock lock( downloadedFiles ); + downloadedFiles.downloadList.push_back( *data ); + addToLibrary_thread( *data ); + + AddPodcastData( *data ); + + DownloadsUpdated( token, &downloadedFiles.downloadList[ downloadedFiles.downloadList.size() - 1 ] ); + } + downloadStatus.DownloadThreadDone( token ); + delete data; + } + else + { + DownloadsUpdated( token, NULL ); + downloadStatus.DownloadThreadDone( token ); + } + + { + AutoLock lock( downloadsQueueLock ); + size_t l_index = 0; + for ( DownloadToken &l_download_token : downloadsQueue ) + { + if ( l_download_token == token ) + { + downloadsQueue.erase( downloadsQueue.begin() + l_index ); + break; + } + + ++l_index; + } + + for ( DownloadToken &l_download_token : downloadsQueue ) + { + if ( WAC_API_DOWNLOADMANAGER->IsPending( l_download_token ) ) + { + WAC_API_DOWNLOADMANAGER->ResumePendingDownload( l_download_token ); + break; + } + } + } + + this->Release(); + } + + + int GetSource( wchar_t *source, size_t source_cch ) + { + return wcscpy_s( source, source_cch, this->channel ); + } + + int GetTitle( wchar_t *title, size_t title_cch ) + { + return wcscpy_s( title, title_cch, this->item ); + } + + int GetLocation( wchar_t *location, size_t location_cch ) + { + return wcscpy_s( location, location_cch, this->destination_filepath ); + } + + + size_t AddRef() + { + return _ref_count.fetch_add( 1 ); + } + + size_t Release() + { + if ( _ref_count.load() == 0 ) + return _ref_count.load(); + + LONG r = _ref_count.fetch_sub( 1 ); + if ( r == 0 ) + delete( this ); + + return r; + } + +private: // private destructor so no one accidentally calls delete directly on this reference counted object + ~DownloaderCallback() + { + if ( url ) + free( url ); + + if ( destination_filepath ) + free( destination_filepath ); + + if ( channel ) + free( channel ); + + if ( item ) + free( item ); + } + +protected: + RECVS_DISPATCH; + +private: + HANDLE hFile; + wchar_t *url; + wchar_t *destination_filepath; + wchar_t *channel; + wchar_t *item; + __time64_t publishDate; + size_t totalSize; + size_t downloaded; + + std::atomic<std::size_t> _ref_count = 1; +}; + +void BackgroundDownloader::Download( const wchar_t *url, const wchar_t *savePath, const wchar_t *channel, const wchar_t *item, __time64_t publishDate ) +{ + DownloaderCallback *callback = new DownloaderCallback( url, savePath, channel, item, publishDate ); + { + Nullsoft::Utility::AutoLock lock( downloadsQueueLock ); + if ( downloadsQueue.size() < SIMULTANEOUS_DOWNLOADS ) + { + DownloadToken dt = WAC_API_DOWNLOADMANAGER->DownloadEx( AutoChar( url ), callback, api_downloadManager::DOWNLOADEX_CALLBACK | api_downloadManager::DOWNLOADEX_UI ); + downloadsQueue.push_back( dt ); + } + else + { + DownloadToken dt = WAC_API_DOWNLOADMANAGER->DownloadEx( AutoChar( url ), callback, api_downloadManager::DOWNLOADEX_CALLBACK | api_downloadManager::DOWNLOADEX_PENDING | api_downloadManager::DOWNLOADEX_UI ); + downloadsQueue.push_back( dt ); + } + } +} + +BackgroundDownloader downloader; + +#define CBCLASS DownloaderCallback +START_DISPATCH; +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONFINISH, OnFinish ) +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONCANCEL, OnCancel ) +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONERROR, OnError ) +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONDATA, OnData ) +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONCONNECT, OnConnect ) +VCB( IFC_DOWNLOADMANAGERCALLBACK_ONINIT, OnInit ) +CB( IFC_DOWNLOADMANAGERCALLBACK_GETSOURCE, GetSource ) +CB( IFC_DOWNLOADMANAGERCALLBACK_GETTITLE, GetTitle ) +CB( IFC_DOWNLOADMANAGERCALLBACK_GETLOCATION, GetLocation ) +CB( ADDREF, AddRef ) +CB( RELEASE, Release ) +END_DISPATCH; +#undef CBCLASS
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/BackgroundDownloader.h b/Src/Plugins/Library/ml_wire/BackgroundDownloader.h new file mode 100644 index 00000000..6d2542a7 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/BackgroundDownloader.h @@ -0,0 +1,18 @@ +#ifndef NULLSOFT_BACKGROUNDDOWNLOADERH +#define NULLSOFT_BACKGROUNDDOWNLOADERH + +#include <windows.h> + +class BackgroundDownloader +{ +public: + //void SetSpeed(int kilobytesPerSecond); + + void Download(const wchar_t *url, const wchar_t *savePath, + const wchar_t *channel, const wchar_t *item, __time64_t publishDate); + + //void Shutdown(); +}; + +extern BackgroundDownloader downloader; +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ChannelCheck.h b/Src/Plugins/Library/ml_wire/ChannelCheck.h new file mode 100644 index 00000000..5a80e7ca --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ChannelCheck.h @@ -0,0 +1,14 @@ +#ifndef NULLSOFT_CHANNELCHECKH +#define NULLSOFT_CHANNELCHECKH +#include "ChannelSync.h" +class ChannelCheck : public ChannelSync +{ +public: + void NewChannel(const Channel &newChannel) + { + channel=newChannel; + } + Channel channel; +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ChannelRefresher.cpp b/Src/Plugins/Library/ml_wire/ChannelRefresher.cpp new file mode 100644 index 00000000..07539964 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ChannelRefresher.cpp @@ -0,0 +1,33 @@ +#include "main.h" +#include "ChannelRefresher.h" +#include <algorithm> + +#include "./subscriptionView.h" + +using namespace Nullsoft::Utility; +void ChannelRefresher::BeginChannelSync() +{} + +void ChannelRefresher::NewChannel(const Channel &newChannel) +{ + AutoLock lock (channels LOCKNAME("ChannelRefresher::NewChannel")); + ChannelList::iterator found; + for (found=channels.begin();found!=channels.end(); found++) + { + if (!wcscmp(found->url, newChannel.url)) + break; + } + if (found != channels.end()) + { + // todo, redo category indexing as necessary. + found->UpdateFrom(newChannel); + found->lastUpdate = _time64(0); + found->needsRefresh = false; + } +} + +void ChannelRefresher::EndChannelSync() +{ + HWND wnd = SubscriptionView_FindWindow(); + SubscriptionView_RefreshChannels(wnd, TRUE); +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ChannelRefresher.h b/Src/Plugins/Library/ml_wire/ChannelRefresher.h new file mode 100644 index 00000000..fef67cfe --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ChannelRefresher.h @@ -0,0 +1,14 @@ +#ifndef NULLSOFT_CHANNELREFRESHERH +#define NULLSOFT_CHANNELREFRESHERH + +#include "ChannelSync.h" +class ChannelRefresher: public ChannelSync +{ +public: + void BeginChannelSync(); + void NewChannel(const Channel &newChannel); + void EndChannelSync(); + +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ChannelSync.h b/Src/Plugins/Library/ml_wire/ChannelSync.h new file mode 100644 index 00000000..c7d5fbd9 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ChannelSync.h @@ -0,0 +1,31 @@ +#ifndef NULLSOFT_CHANNELSYNCH +#define NULLSOFT_CHANNELSYNCH + +#include "Feeds.h" + +/* +ChannelSync is a virtual base class (aka interface) used by the RSS downloader. +When you instantiate a downloader, you are required to give it a pointer to this interface. + +The downloader will call: + + BeginChannelSync(); + for (;;) // however many it encounters + NewChannel(newChannel); // called once for each channel it downloads + EndChannelSync(); + +If you have a class or data structure that wants updates, you'll have to mix-in +this interface or implement a data-structure-babysitter class (or however you want to deal with it) + +See the "WireManager" for an example of how to use it +*/ + +class ChannelSync +{ +public: + virtual void BeginChannelSync() {} + virtual void NewChannel(const Channel &newChannel) = 0; + virtual void EndChannelSync() {}} +; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Cloud.cpp b/Src/Plugins/Library/ml_wire/Cloud.cpp new file mode 100644 index 00000000..07930d47 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Cloud.cpp @@ -0,0 +1,235 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "Cloud.h" +#include "FeedParse.h" +#include "Defaults.h" +#include "./subscriptionView.h" +#include "ChannelRefresher.h" +#include "Util.h" +#include <algorithm> +#include <strsafe.h> + +/* benski> TODO rewrite Callback() so we don't have to reserve a thread */ +using namespace Nullsoft::Utility; + +#define CLOUD_TICK_MS 60000 +ChannelRefresher channelRefresher; + +static bool kill = false; + +static __time64_t RetrieveMinimalUpdateTime() +{ + __time64_t minUpdateTime = 0; + + AutoLock lock (channels LOCKNAME("RetrieveMinimalUpdateTime")); + ChannelList::iterator itr; + for (itr=channels.begin(); itr!=channels.end(); itr++) + { + if (itr->useDefaultUpdate) + { + if ( !updateTime ) autoUpdate = 0; + if ( autoUpdate && (!minUpdateTime || minUpdateTime && (updateTime < minUpdateTime)) ) + { + minUpdateTime = updateTime; + } + } + else // use the custom values + { + if ( !itr->updateTime ) itr->autoUpdate = 0; + if ( itr->autoUpdate && (!minUpdateTime || minUpdateTime && (itr->updateTime < minUpdateTime)) ) + { + minUpdateTime = itr->updateTime; + } + } + } + return minUpdateTime; +} + +int Cloud::CloudThreadPoolFunc(HANDLE handle, void *user_data, intptr_t id) +{ + Cloud *cloud = (Cloud *)user_data; + if (kill) + { + WASABI_API_THREADPOOL->RemoveHandle(0, cloud->cloudEvent); + CloseHandle(cloud->cloudEvent); + WASABI_API_THREADPOOL->RemoveHandle(0, cloud->cloudTimerEvent); + cloud->cloudTimerEvent.Close(); + SetEvent(cloud->cloudDone); + return 0; + } + + cloud->Callback(); + + // set waitable timer, overwrite previouse value if any + if (!kill) + { + __time64_t timeToWait = RetrieveMinimalUpdateTime(); + if ( timeToWait ) + cloud->cloudTimerEvent.Wait(timeToWait * 1000); + } + + return 0; +} + + +Cloud::Cloud() : cloudThread(0), cloudEvent(0), statusText(0) +{} + +Cloud::~Cloud() +{ + free(statusText); +} + +void Cloud::Quit() +{ + cloudDone= CreateEvent(NULL, FALSE, FALSE, NULL); + kill = true; + SetEvent(cloudEvent); + WaitForSingleObject(cloudDone, INFINITE); + CloseHandle(cloudDone); +} + +void Cloud::RefreshAll() +{ + AutoLock lock (channels LOCKNAME("RefreshAll")); + ChannelList::iterator itr; + for (itr = channels.begin();itr != channels.end();itr++) + { + itr->needsRefresh = true; + } + +} +void Cloud::Init() +{ + // setup a periodic callback so we can check on our times + cloudEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + + cloudThread = WASABI_API_THREADPOOL->ReserveThread(0); + + WASABI_API_THREADPOOL->AddHandle(cloudThread, cloudEvent, CloudThreadPoolFunc, this, 0, 0); + + WASABI_API_THREADPOOL->AddHandle(cloudThread, cloudTimerEvent, CloudThreadPoolFunc, this, 1, 0); + __time64_t timeToWait = RetrieveMinimalUpdateTime(); + if ( timeToWait ) + cloudTimerEvent.Wait(timeToWait * 1000); +} + +void Cloud::Refresh(Channel &channel) +{ + wchar_t lang_buf[1024] = {0}; + WASABI_API_LNGSTRINGW_BUF(IDS_RECEIVING_UPDATES_FOR, lang_buf, 1024); + if (channel.title) + StringCbCat(lang_buf, sizeof(lang_buf), channel.title); + else + lang_buf[0]=0; + SetStatus(lang_buf); + size_t oldSize = channel.items.size(); + + FeedParse downloader(&channelRefresher, false); + downloader.DownloadURL(channel.url); + + if (channel.items.size() > oldSize) + { + WASABI_API_LNGSTRINGW_BUF(IDS_GOT_NEW_ITEMS_FOR, lang_buf, 1024); + StringCbCat(lang_buf, sizeof(lang_buf), channel.title); + SetStatus(lang_buf); + } + else + SetStatus(L""); +} + +void Cloud::GetStatus(wchar_t *status, size_t len) +{ + AutoLock lock (statusGuard); + if (statusText) + StringCchCopy(status, len, statusText); + else + status[0]=0; +} + +void Cloud::SetStatus(const wchar_t *newStatus) +{ + AutoLock lock (statusGuard); + free(statusText); + statusText = _wcsdup(newStatus); + + HWND hView = SubscriptionView_FindWindow(); + if (NULL != hView) + SubscriptionView_SetStatus(hView, statusText); +} + +/* --- Private Methods of class Cloud --- */ + + +static void ForceLastUpdate(const Channel &channel) +{ + AutoLock lock (channels LOCKNAME("ChannelRefresher::NewChannel")); + ChannelList::iterator found; + for (found=channels.begin();found!=channels.end(); found++) + { + if (!wcscmp(found->url, channel.url)) + break; + } + if (found != channels.end()) + { + found->lastUpdate = _time64(0); + found->needsRefresh = false; + } +} +/* +@private +checks all channels and updates any that requiring refreshing. +*/ +void Cloud::Callback() +{ + __time64_t curTime = _time64(0); + + size_t i = 0; + Channel temp; + bool refreshed = false; + + while (true) // we need to lock the channels object before we check its size, etc, so we can't just use a "for" loop. + { + { // we want to minimize how long we have to lock, so we'll make a copy of the channel data + AutoLock lock (channels LOCKNAME("Callback")); + if (i >= channels.size()) + break; + temp = channels[i]; // make a copy the data so we can safely release the lock + channels[i].needsRefresh = false; // have to set this now. if the site is down or 404, then the refresh will never "complete". + } // end locking scope + + if (temp.needsRefresh) // need an immediate refresh? (usually set when the user clicks refresh or update-on-launch is on) + { + Refresh(temp); + refreshed = true; + } + else if (temp.useDefaultUpdate) // this flag is set unless the user chose custom update values + { + if (!updateTime) autoUpdate = 0; + if (autoUpdate && (temp.lastUpdate + updateTime) <= curTime) + { + Refresh(temp); + ForceLastUpdate(temp); + refreshed = true; + } + } + else // use the custom values + { + if (temp.updateTime == 0) temp.autoUpdate = 0; + if (temp.autoUpdate && (temp.lastUpdate + temp.updateTime) <= curTime) + { + Refresh(temp); + ForceLastUpdate(temp); + refreshed = true; + } + } + + i++; + } + + // if we're refreshing then save out + if (refreshed) + { + SaveAll(); + } +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Cloud.h b/Src/Plugins/Library/ml_wire/Cloud.h new file mode 100644 index 00000000..505d936e --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Cloud.h @@ -0,0 +1,35 @@ +#ifndef NULLSOFT_CLOUDH +#define NULLSOFT_CLOUDH + +#include <windows.h> + +#include "Feeds.h" +#include "../nu/threadpool/api_threadpool.h" +#include "../nu/threadpool/timerhandle.hpp" +#include "../nu/AutoLock.h" + +class Cloud +{ +public: + Cloud(); + ~Cloud(); + void Init(); + void Quit(); + void Refresh( Channel &channel ); + void GetStatus( wchar_t *status, size_t len ); + void RefreshAll(); + void Pulse() { SetEvent( cloudEvent ); } + +private: + static DWORD WINAPI CloudThread( void *param ); + void SetStatus( const wchar_t *newStatus ); + void Callback(); + ThreadID *cloudThread; + wchar_t *statusText; + Nullsoft::Utility::LockGuard statusGuard; + HANDLE cloudEvent, cloudDone; + TimerHandle cloudTimerEvent; + static int CloudThreadPoolFunc( HANDLE handle, void *user_data, intptr_t param ); +}; + +#endif diff --git a/Src/Plugins/Library/ml_wire/DESIGN.txt b/Src/Plugins/Library/ml_wire/DESIGN.txt new file mode 100644 index 00000000..47148235 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DESIGN.txt @@ -0,0 +1,12 @@ +
+
+
+
+major components:
+
+Cloud
+-----
+
+The cloud is the object which is responsible for doing period updates on RSS feeds.
+It automatically scans the feeds. You can manually refresh via the Refresh(string url) method
+
diff --git a/Src/Plugins/Library/ml_wire/Defaults.cpp b/Src/Plugins/Library/ml_wire/Defaults.cpp new file mode 100644 index 00000000..8c8da669 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Defaults.cpp @@ -0,0 +1,40 @@ +#include "main.h" +#include "Defaults.h" +#include <shlobj.h> +wchar_t defaultDownloadPath[MAX_PATH] = {0}, + serviceUrl[1024] = {0}; +__time64_t updateTime = 60 /* 1 minute */ * 60 /* 1 hour */ * 24 /* 1 day */; +int autoDownloadEpisodes = 1; +bool autoUpdate = true; +bool autoDownload = true; +bool updateOnLaunch = false; +bool needToMakePodcastsView=true; + +static BOOL UtilGetSpecialFolderPath( HWND hwnd, wchar_t *path, int folder ) +{ + ITEMIDLIST *pidl; // Shell Item ID List ptr + IMalloc *imalloc; // Shell IMalloc interface ptr + BOOL result; // Return value + + if ( SHGetSpecialFolderLocation( hwnd, folder, &pidl ) != NOERROR ) + return FALSE; + + result = SHGetPathFromIDList( pidl, path ); + + if ( SHGetMalloc( &imalloc ) == NOERROR ) + { + imalloc->Free( pidl ); + imalloc->Release(); + } + + return result; +} + +void BuildDefaultDownloadPath( HWND hwnd ) +{ + wchar_t defaultPath[ MAX_PATH ] = L""; + if ( !UtilGetSpecialFolderPath( hwnd, defaultPath, CSIDL_MYMUSIC ) ) + UtilGetSpecialFolderPath( hwnd, defaultPath, CSIDL_PERSONAL ); + + lstrcpyn( defaultDownloadPath, defaultPath, MAX_PATH ); +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Defaults.h b/Src/Plugins/Library/ml_wire/Defaults.h new file mode 100644 index 00000000..2ffb4757 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Defaults.h @@ -0,0 +1,34 @@ +#ifndef NULLSOFT_DEFAULTSH +#define NULLSOFT_DEFAULTSH +#include <windows.h> +extern wchar_t defaultDownloadPath[MAX_PATH], serviceUrl[1024]; +extern __time64_t updateTime; +extern int autoDownloadEpisodes; +extern bool autoUpdate; +extern bool autoDownload; +extern bool updateOnLaunch; +extern float htmlDividerPercent; +extern float channelDividerPercent; +extern int itemTitleWidth; +extern int itemDateWidth; +extern int itemMediaWidth; +extern int itemSizeWidth; + +#define DOWNLOADSCHANNELWIDTHDEFAULT 200 +#define DOWNLOADSITEMWIDTHDEFAULT 200 +#define DOWNLOADSPROGRESSWIDTHDEFAULT 100 +#define DOWNLOADSPATHWIDTHDEFAULTS 200 + +extern int downloadsChannelWidth; +extern int downloadsItemWidth; +extern int downloadsProgressWidth; +extern int downloadsPathWidth; + +extern bool needToMakePodcastsView; + +extern int currentItemSort; +extern bool itemSortAscending; +extern bool channelSortAscending; +extern int channelLastSelection; +void BuildDefaultDownloadPath(HWND); +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadManager.cpp b/Src/Plugins/Library/ml_wire/DownloadManager.cpp new file mode 100644 index 00000000..61f734b2 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadManager.cpp @@ -0,0 +1 @@ +#include "main.h"
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadStatus.cpp b/Src/Plugins/Library/ml_wire/DownloadStatus.cpp new file mode 100644 index 00000000..3ccaf419 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadStatus.cpp @@ -0,0 +1,153 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "DownloadStatus.h" +#include "DownloadsDialog.h" +#include "./navigation.h" + +#include <strsafe.h> + +DownloadStatus downloadStatus; + +using namespace Nullsoft::Utility; + +DownloadStatus::Status::Status() +{ + Init(); +} + +DownloadStatus::Status::Status( size_t _downloaded, size_t _maxSize, const wchar_t *_channel, const wchar_t *_item, const wchar_t *_path ) +{ + Init(); + + downloaded = _downloaded; + maxSize = _maxSize; + channel = _wcsdup( _channel ); + item = _wcsdup( _item ); + path = _wcsdup( _path ); +} + +const DownloadStatus::Status &DownloadStatus::Status::operator =( const DownloadStatus::Status © ) +{ + Reset(); + Init(); + + downloaded = copy.downloaded; + maxSize = copy.maxSize; + channel = _wcsdup( copy.channel ); + item = _wcsdup( copy.item ); + path = _wcsdup( copy.path ); + killswitch = copy.killswitch; + + return *this; +} + +DownloadStatus::Status::~Status() +{ + Reset(); +} + +void DownloadStatus::Status::Init() +{ + downloaded = 0; + maxSize = 0; + killswitch = 0; + channel = 0; + item = 0; + path = 0; +} + +void DownloadStatus::Status::Reset() +{ + if ( channel ) + { + free( channel ); + channel = 0; + } + + if ( item ) + { + free( item ); + item = 0; + } + + if ( path ) + { + free( path ); + path = 0; + } +} + +void DownloadStatus::AddDownloadThread(DownloadToken token, const wchar_t *channel, const wchar_t *item, const wchar_t *path) +{ + { + AutoLock lock(statusLock); + downloads[token] = Status(0,0,channel,item,path); + DownloadsUpdated(downloads[token],token); + } + + Navigation_ShowService(SERVICE_DOWNLOADS, SHOWMODE_AUTO); +} + +void DownloadStatus::DownloadThreadDone(DownloadToken token) +{ + { + AutoLock lock(statusLock); + downloads.erase(token); + } + + Navigation_ShowService(SERVICE_DOWNLOADS, SHOWMODE_AUTO); +} + +bool DownloadStatus::UpdateStatus(DownloadToken token, size_t downloaded, size_t maxSize) +{ + AutoLock lock(statusLock); + downloads[token].downloaded = downloaded; + downloads[token].maxSize = maxSize; + + return !!downloads[token].killswitch; +} + +bool DownloadStatus::CurrentlyDownloading() +{ + AutoLock lock(statusLock); + return !downloads.empty(); +} + +void DownloadStatus::GetStatusString( wchar_t *status, size_t len ) +{ + AutoLock lock( statusLock ); + Downloads::iterator itr; + size_t bytesDownloaded = 0, bytesTotal = 0, numDownloads = 0; + bool unknownTotal = false; + for ( itr = downloads.begin(); itr != downloads.end(); itr++ ) + { + Status &dlstatus = itr->second; + if ( dlstatus.maxSize ) + { + numDownloads++; + bytesDownloaded += dlstatus.downloaded; + bytesTotal += dlstatus.maxSize; + } + else // don't have a max size + { + if ( dlstatus.downloaded ) // if we've downloaded some then we just don't know the total + { + unknownTotal = true; + numDownloads++; + bytesDownloaded += dlstatus.downloaded; + } + } + } + + if ( 0 == numDownloads ) + { + status[ 0 ] = L'\0'; + } + else + { + if ( unknownTotal || bytesTotal == 0 ) + StringCchPrintf( status, len, WASABI_API_LNGSTRINGW( IDS_DOWNLOADING_KB_COMPLETE ), numDownloads, bytesDownloaded / 1024); + else + StringCchPrintf( status, len, WASABI_API_LNGSTRINGW( IDS_DOWNLOADING_KB_PROGRESS ), numDownloads, bytesDownloaded / 1024, bytesTotal / 1024, MulDiv( (int)bytesDownloaded, 100, (int)bytesTotal ) ); + } +} diff --git a/Src/Plugins/Library/ml_wire/DownloadStatus.h b/Src/Plugins/Library/ml_wire/DownloadStatus.h new file mode 100644 index 00000000..be3dd896 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadStatus.h @@ -0,0 +1,41 @@ +#ifndef NULLSOFT_DOWNLOADSTATUSH +#define NULLSOFT_DOWNLOADSTATUSH + +#include "../nu/AutoLock.h" +#include <map> + + +class DownloadStatus +{ +public: + class Status + { + public: + Status(); + ~Status(); + const Status &operator =(const Status ©); + Status(size_t _downloaded, size_t _maxSize, const wchar_t *channel, const wchar_t *item, const wchar_t *path); + size_t downloaded, maxSize; + + int killswitch; + wchar_t *channel; + wchar_t *item; + wchar_t *path; + private: + + void Init(); + void Reset(); + }; + + void AddDownloadThread(DownloadToken token, const wchar_t *channel, const wchar_t *item, const wchar_t *path); + void DownloadThreadDone(DownloadToken token); + bool UpdateStatus(DownloadToken token, size_t downloaded, size_t maxSize); + bool CurrentlyDownloading(); + void GetStatusString(wchar_t *status, size_t len); + typedef std::map<DownloadToken, Status> Downloads; + Downloads downloads; + Nullsoft::Utility::LockGuard statusLock; +}; + +extern DownloadStatus downloadStatus; +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadThread.cpp b/Src/Plugins/Library/ml_wire/DownloadThread.cpp new file mode 100644 index 00000000..67e1aed7 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadThread.cpp @@ -0,0 +1,222 @@ +#include "Main.h" + +#pragma warning(disable:4786) + +#include "DownloadThread.h" +#include "api__ml_wire.h" +#include "api/service/waServiceFactory.h" +#include "../../..\Components\wac_network\wac_network_http_receiver_api.h" +#include "errors.h" +#include <strsafe.h> + +extern int winampVersion; + +#define USER_AGENT_SIZE (10 /*User-Agent*/ + 2 /*: */ + 6 /*Winamp*/ + 1 /*/*/ + 1 /*5*/ + 3/*.21*/ + 1 /*Null*/) +void SetUserAgent( api_httpreceiver *http ) +{ + char user_agent[ USER_AGENT_SIZE ] = { 0 }; + int bigVer = ( ( winampVersion & 0x0000FF00 ) >> 12 ); + int smallVer = ( ( winampVersion & 0x000000FF ) ); + StringCchPrintfA( user_agent, USER_AGENT_SIZE, "User-Agent: Winamp/%01x.%02x", bigVer, smallVer ); + http->addheader( user_agent ); +} + +#define HTTP_BUFFER_SIZE 32768 + +static int FeedXMLHTTP( api_httpreceiver *http, obj_xml *parser, bool *noData ) +{ + char downloadedData[ HTTP_BUFFER_SIZE ] = { 0 }; + int xmlResult = API_XML_SUCCESS; + int downloadSize = http->get_bytes( downloadedData, HTTP_BUFFER_SIZE ); + if ( downloadSize ) + { + xmlResult = parser->xmlreader_feed( (void *)downloadedData, downloadSize ); + *noData = false; + } + else + *noData = true; + + return xmlResult; +} + + +DownloadThread::DownloadThread() : parser( 0 ), parserFactory( 0 ) +{ + parserFactory = plugin.service->service_getServiceByGuid( obj_xmlGUID ); + if ( parserFactory ) + parser = (obj_xml *)parserFactory->getInterface(); + + if ( parser ) + { + parser->xmlreader_setCaseSensitive(); + parser->xmlreader_registerCallback( L"*", &xmlDOM ); + parser->xmlreader_open(); + } +} + +DownloadThread::~DownloadThread() +{ + if ( parser ) + { + parser->xmlreader_unregisterCallback( &xmlDOM ); + parser->xmlreader_close(); + } + + if ( parserFactory && parser ) + parserFactory->releaseInterface( parser ); + + parserFactory = 0; + parser = 0; +} + +void URLToFileName( wchar_t *url ) +{ + while ( url && *url != 0 ) + { + switch ( *url ) + { + case ':': + case '/': + case '\\': + case '*': + case '?': + case '"': + case '<': + case '>': + case '|': + *url = '_'; + } + url++; + } +} + +#define FILE_BUFFER_SIZE 32768 +void DownloadThread::DownloadFile( const wchar_t *fileName ) +{ + if ( !parser ) + return; // no sense in continuing if there's no parser available + + HANDLE file = CreateFile( fileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL ); + if ( file == INVALID_HANDLE_VALUE ) + return; + + while ( true ) + { + char data[ FILE_BUFFER_SIZE ] = { 0 }; + DWORD bytesRead = 0; + if ( ReadFile( file, data, FILE_BUFFER_SIZE, &bytesRead, NULL ) && bytesRead ) + { + parser->xmlreader_feed( (void *)data, bytesRead ); + } + else + break; + } + + CloseHandle( file ); + parser->xmlreader_feed( 0, 0 ); + ReadNodes( fileName ); +} + +#ifdef _DEBUG +#include <iostream> +void ShowAllHeaders( api_httpreceiver *http ) +{ + std::cout << "--------------------" << std::endl; + const char *blah = http->getallheaders(); + while ( blah && *blah ) + { + std::cout << blah << std::endl; + blah += strlen( blah ) + 1; + } +} +#else +#define ShowAllHeaders(x) +#endif + +int RunXMLDownload( api_httpreceiver *http, obj_xml *parser ) +{ + int ret; + bool noData; + do + { + Sleep( 50 ); + ret = http->run(); + if ( FeedXMLHTTP( http, parser, &noData ) != API_XML_SUCCESS ) + return DOWNLOAD_ERROR_PARSING_XML; + } while ( ret == HTTPRECEIVER_RUN_OK ); + + // finish off the data + do + { + if ( FeedXMLHTTP( http, parser, &noData ) != API_XML_SUCCESS ) + return DOWNLOAD_ERROR_PARSING_XML; + } while ( !noData ); + + parser->xmlreader_feed( 0, 0 ); + if ( ret != HTTPRECEIVER_RUN_ERROR ) + return DOWNLOAD_SUCCESS; + else + return DOWNLOAD_CONNECTIONRESET; +} + + +int DownloadThread::DownloadURL( const wchar_t *url ) +{ + if ( !parser ) + return DOWNLOAD_NOPARSER; // no sense in continuing if there's no parser available + + api_httpreceiver *http = 0; + waServiceFactory *sf = plugin.service->service_getServiceByGuid( httpreceiverGUID ); + if ( sf ) + http = (api_httpreceiver *)sf->getInterface(); + + if ( !http ) + return DOWNLOAD_NOHTTP; + + http->AllowCompression(); + http->open( API_DNS_AUTODNS, HTTP_BUFFER_SIZE, mediaLibrary.GetProxy() ); + + SetUserAgent( http ); + + http->connect( AutoChar( url ) ); + int ret; + + do + { + Sleep( 50 ); + ret = http->run(); + if ( ret == -1 ) // connection failed + break; + + // ---- check our reply code ---- + int status = http->get_status(); + switch ( status ) + { + case HTTPRECEIVER_STATUS_CONNECTING: + case HTTPRECEIVER_STATUS_READING_HEADERS: + break; + + case HTTPRECEIVER_STATUS_READING_CONTENT: + { + ShowAllHeaders( http ); // benski> don't cut, only enabled in debug mode + int downloadError; + downloadError = RunXMLDownload( http, parser ); + if ( downloadError == DOWNLOAD_SUCCESS ) + ReadNodes( url ); + sf->releaseInterface( http ); + return downloadError; + } + break; + case HTTPRECEIVER_STATUS_ERROR: + default: + sf->releaseInterface( http ); + return DOWNLOAD_404; + } + } while ( ret == HTTPRECEIVER_RUN_OK ); + + const char *er = http->geterrorstr(); + + sf->releaseInterface( http ); + + return DOWNLOAD_404; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadThread.h b/Src/Plugins/Library/ml_wire/DownloadThread.h new file mode 100644 index 00000000..7e2061c9 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadThread.h @@ -0,0 +1,28 @@ +#ifndef NULLSOFT_DOWNLOADTHREADH +#define NULLSOFT_DOWNLOADTHREADH + +#include "../xml/obj_xml.h" +#include "../xml/XMLDOM.h" +#include "../nu/Alias.h" +#include "api__ml_wire.h" +#include <api/service/waServiceFactory.h> + + +class DownloadThread +{ +public: + DownloadThread(); + virtual ~DownloadThread(); + + virtual void ReadNodes(const wchar_t *url) = 0; + + int DownloadURL(const wchar_t *url); + void DownloadFile(const wchar_t *fileName); +protected: + XMLDOM xmlDOM; +private: + obj_xml *parser; + waServiceFactory *parserFactory; + +}; +#endif diff --git a/Src/Plugins/Library/ml_wire/Downloaded.cpp b/Src/Plugins/Library/ml_wire/Downloaded.cpp new file mode 100644 index 00000000..e088a376 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Downloaded.cpp @@ -0,0 +1,121 @@ +#include "main.h" +#include "Downloaded.h" + +DownloadList downloadedFiles; +using namespace Nullsoft::Utility; +Nullsoft::Utility::LockGuard downloadedLock; + +DownloadedFile::DownloadedFile() +{ + Init(); +} + +DownloadedFile::DownloadedFile(const wchar_t *_url, const wchar_t *_path, const wchar_t *_channel, const wchar_t *_item, __time64_t publishDate) +{ + Init(); + + this->publishDate = publishDate; + + SetChannel( _channel ); + SetItem( _item ); + SetPath( _path ); + SetURL( _url ); +} + +DownloadedFile::DownloadedFile( const DownloadedFile © ) +{ + Init(); + + operator =( copy ); +} + +DownloadedFile::~DownloadedFile() +{ + Reset(); +} + + +void DownloadedFile::Init() +{ + url = 0; + path = 0; + channel = 0; + item = 0; + bytesDownloaded = 0; + totalSize = 0; + publishDate = 0; +} + +void DownloadedFile::Reset() +{ + if ( url ) + { + free( url ); + url = 0; + } + + if ( path ) + { + free( path ); + path = 0; + } + + if ( channel ) + { + free( channel ); + channel = 0; + } + + if ( item ) + { + free( item ); + item = 0; + } +} + +void DownloadedFile::SetPath( const wchar_t *_path ) +{ + if ( path ) + free( path ); + + path = _wcsdup( _path ); +} + +void DownloadedFile::SetURL( const wchar_t *_url ) +{ + if ( url ) + free( url ); + + url = _wcsdup( _url ); +} + +void DownloadedFile::SetItem( const wchar_t *_item ) +{ + free( item ); + item = _wcsdup( _item ); +} + +void DownloadedFile::SetChannel( const wchar_t *_channel ) +{ + free( channel ); + channel = _wcsdup( _channel ); +} + +const DownloadedFile &DownloadedFile::operator =( const DownloadedFile © ) +{ + Reset(); + Init(); + + SetChannel( copy.channel ); + SetItem( copy.item ); + + bytesDownloaded = copy.bytesDownloaded; + totalSize = copy.totalSize; + publishDate = copy.publishDate; + downloadDate = copy.downloadDate; + + SetPath( copy.path ); + SetURL( copy.url ); + + return *this; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Downloaded.h b/Src/Plugins/Library/ml_wire/Downloaded.h new file mode 100644 index 00000000..2ba1ace8 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Downloaded.h @@ -0,0 +1,79 @@ +#ifndef NULLSOFT_DOWNLOADEDH +#define NULLSOFT_DOWNLOADEDH + +#include "../nu/AutoLock.h" +#include "../nu/AutoCharFn.h" +#include "..\..\General\gen_ml/ml.h" +#include <vector> +#include "../nu/MediaLibraryInterface.h" + +class DownloadedFile +{ +public: + DownloadedFile(); + DownloadedFile( const wchar_t *_url, const wchar_t *_path, const wchar_t *_channel, const wchar_t *_item, __time64_t publishDate ); + DownloadedFile( const DownloadedFile © ); + ~DownloadedFile(); + + const DownloadedFile &operator =( const DownloadedFile © ); + + void SetPath( const wchar_t *_path ); + void SetURL( const wchar_t *_url ); + void SetItem( const wchar_t *_item ); + void SetChannel( const wchar_t *_channel ); + + size_t bytesDownloaded = 0; + size_t totalSize = 0; + + __time64_t publishDate; + __time64_t downloadDate = 0; + + wchar_t *path = 0; + wchar_t *url = 0; + wchar_t *channel = 0; + wchar_t *item = 0; + + +private: + void Init(); + void Reset(); +}; + +class DownloadList +{ +public: + typedef std::vector<DownloadedFile> DownloadedFileList; + typedef DownloadedFileList::iterator iterator; + typedef DownloadedFileList::const_iterator const_iterator; + + operator Nullsoft::Utility::LockGuard &() { return downloadedLock; } + + void Remove( size_t index ) { downloadList.erase( downloadList.begin() + index ); } + + bool RemoveAndDelete( int index ) + { + SendMessage( mediaLibrary.library, WM_ML_IPC, (WPARAM)downloadList[ index ].path, ML_IPC_DB_REMOVEITEMW ); + + if ( !DeleteFile( downloadList[ index ].path ) && GetLastError() != ERROR_FILE_NOT_FOUND ) + return false; + + downloadList.erase( downloadList.begin() + index ); + + return true; + } + + DownloadedFileList downloadList; + Nullsoft::Utility::LockGuard downloadedLock; + + iterator begin() { return downloadList.begin(); } + iterator end() { return downloadList.end(); } +}; + +extern DownloadList downloadedFiles; +extern int downloadsItemSort; +extern bool downloadsSortAscending; + +void CleanupDownloads(); +__time64_t filetime(const wchar_t *file); + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadsDialog.cpp b/Src/Plugins/Library/ml_wire/DownloadsDialog.cpp new file mode 100644 index 00000000..83fe8ffb --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadsDialog.cpp @@ -0,0 +1,1453 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "Downloaded.h" +#include "./navigation.h" +#include "DownloadStatus.h" +#include "Defaults.h" +#include "../nu/listview.h" +#include "..\..\General\gen_ml/ml_ipc.h" +#include "..\..\General\gen_ml/ml_ipc_0313.h" +#include <vector> +#include "../nu/menushortcuts.h" +#include <commctrl.h> +#include <shlwapi.h> +#include <shellapi.h> +#include <strsafe.h> +#include <algorithm> + +HWND current_window = 0; +int groupBtn = 1, enqueuedef = 0, customAllowed = 0; +HMENU g_context_menus2 = NULL; +viewButtons view = {0}; + +#ifndef HDF_SORTUP +#define HDF_SORTUP 0x0400 +#define HDF_SORTDOWN 0x0200 +#endif // !HDF_SORTUP + +using namespace Nullsoft::Utility; + +enum +{ + COL_CHANNEL = 0, + COL_ITEM, + COL_PROGRESS, + COL_PATH, + NUM_COLUMNS, +}; + +int downloadsChannelWidth = DOWNLOADSCHANNELWIDTHDEFAULT; +int downloadsItemWidth = DOWNLOADSITEMWIDTHDEFAULT; +int downloadsProgressWidth = DOWNLOADSPROGRESSWIDTHDEFAULT; +int downloadsPathWidth = DOWNLOADSPATHWIDTHDEFAULTS; + + +W_ListView downloadList; +int downloadsItemSort = -1; // -1 means no sort active +bool downloadsSortAscending = true; + +enum +{ + DOWNLOADSDIALOG_TIMER_UPDATESTATUSBAR = 0, +}; + +class DownloadListItem +{ +public: + DownloadedFile *f; + DownloadToken token; + wchar_t *channel, *item, *path; + wchar_t status[ 20 ]; + + DownloadListItem( DownloadedFile *fi ) : token( 0 ), channel( 0 ), item( 0 ), path( 0 ) + { + f = new DownloadedFile( *fi ); + ZeroMemory( status, sizeof( status ) ); + } + + DownloadListItem( DownloadToken token, const wchar_t *channel0, const wchar_t *item0, const wchar_t *path0, size_t downloaded, size_t maxSize ) : token( token ), f( 0 ) + { + if ( maxSize ) + StringCchPrintf( status, 20, WASABI_API_LNGSTRINGW( IDS_DOWNLOADING_PERCENT ), (int)( downloaded / ( maxSize / 100 ) ) ); + else + { + WASABI_API_LNGSTRINGW_BUF( IDS_DOWNLOADING, status, 20 ); + } + + channel = channel0 ? _wcsdup( channel0 ) : NULL; + item = item0 ? _wcsdup( item0 ) : NULL; + path = path0 ? _wcsdup( path0 ) : NULL; + } + + ~DownloadListItem() + { + clean(); + + if ( f ) + delete f; + } + + void clean() + { + if ( channel ) + { + free( channel ); + channel = 0; + } + + if ( item ) + { + free( item ); + item = 0; + } + + if ( path ) + { + free( path ); + path = 0; + } + } +}; + +static std::vector<DownloadListItem*> listContents; + +bool GetDownload( int &download ) +{ + download = ListView_GetNextItem( downloadList.getwnd(), download, LVNI_ALL | LVNI_SELECTED ); + if ( download == -1 ) + return false; + else + return true; +} + +void Downloads_Play( bool enqueue = false ) +{ + int download = -1; + AutoLock lock( downloadedFiles ); + while ( GetDownload( download ) ) + { + if ( !enqueue ) + { + if ( listContents[ download ]->f ) + mediaLibrary.PlayFile( listContents[ download ]->f->path ); + else if ( listContents[ download ]->path ) + mediaLibrary.PlayFile( listContents[ download ]->path ); + + enqueue = true; + } + else + { + if ( listContents[ download ]->f ) + mediaLibrary.EnqueueFile( listContents[ download ]->f->path ); + else if ( listContents[ download ]->path ) + mediaLibrary.EnqueueFile( listContents[ download ]->path ); + } + } +} + +void DownloadsUpdated( const DownloadStatus::Status &s, DownloadToken token ) +{ + listContents.push_back( new DownloadListItem( token, s.channel, s.item, s.path, s.downloaded, s.maxSize ) ); + + downloadList.SetVirtualCountAsync( (int)listContents.size() ); +} + +void DownloadsUpdated( DownloadToken token, const DownloadedFile *f ) +{ + for ( DownloadListItem *l_content : listContents ) + { + if ( l_content->token == token ) + { + l_content->token = 0; + if ( f ) + { + l_content->f = new DownloadedFile( *f ); + + l_content->clean(); + } + else + lstrcpyn( l_content->status, L"Error", 20 ); + + break; + } + } + + PostMessage( downloadList.getwnd(), LVM_REDRAWITEMS, 0, listContents.size() ); +} + +void DownloadsUpdated() +{ + for ( DownloadListItem *l_content : listContents ) + delete l_content; + + listContents.clear(); + + for ( DownloadedFile &l_download : downloadedFiles.downloadList ) + listContents.push_back( new DownloadListItem( &l_download ) ); + + { + AutoLock lock( downloadStatus.statusLock ); + for ( DownloadStatus::Downloads::iterator itr = downloadStatus.downloads.begin(); itr != downloadStatus.downloads.end(); itr++ ) + { + listContents.push_back( new DownloadListItem( itr->first, itr->second.channel, itr->second.item, itr->second.path, itr->second.downloaded, itr->second.maxSize ) ); + } + } + + downloadList.SetVirtualCountAsync( (int)listContents.size() ); +// Navigation_ShowService( SERVICE_DOWNLOADS, SHOWMODE_AUTO ); +} + +static void CleanupDownloads() +{ + { + AutoLock lock( downloadedFiles ); + DownloadList::DownloadedFileList &downloads = downloadedFiles.downloadList; + DownloadList::iterator itr, next; + for ( itr = downloads.begin(); itr != downloads.end();) + { + next = itr; + ++next; + if ( !PathFileExists( itr->path ) ) + downloads.erase( itr ); + else + itr = next; + } + } + +// Navigation_ShowService(SERVICE_DOWNLOADS, SHOWMODE_AUTO); +} + +void Downloads_UpdateStatusBar(HWND hwndDlg) +{ + wchar_t status[256]=L""; + downloadStatus.GetStatusString(status, 256); + SetWindowText(GetDlgItem(hwndDlg, IDC_STATUS), status); +} + +void Downloads_Paint(HWND hwndDlg) +{ + int tab[] = { IDC_DOWNLOADLIST | DCW_SUNKENBORDER, }; + dialogSkinner.Draw(hwndDlg, tab, sizeof(tab) / sizeof(tab[0])); +} + + +static HRGN g_rgnUpdate = NULL; +static int offsetX = 0, offsetY = 0; + +typedef struct _LAYOUT +{ + INT id; + HWND hwnd; + INT x; + INT y; + INT cx; + INT cy; + DWORD flags; + HRGN rgn; +} +LAYOUT, PLAYOUT; + +#define SETLAYOUTPOS(_layout, _x, _y, _cx, _cy) { _layout->x=_x; _layout->y=_y;_layout->cx=_cx;_layout->cy=_cy;_layout->rgn=NULL; } +#define SETLAYOUTFLAGS(_layout, _r) \ + { \ + BOOL fVis; \ + fVis = (WS_VISIBLE & (LONG)GetWindowLongPtr(_layout->hwnd, GWL_STYLE)); \ + if (_layout->x == _r.left && _layout->y == _r.top) _layout->flags |= SWP_NOMOVE; \ + if (_layout->cx == (_r.right - _r.left) && _layout->cy == (_r.bottom - _r.top)) _layout->flags |= SWP_NOSIZE; \ + if ((SWP_HIDEWINDOW & _layout->flags) && !fVis) _layout->flags &= ~SWP_HIDEWINDOW; \ + if ((SWP_SHOWWINDOW & _layout->flags) && fVis) _layout->flags &= ~SWP_SHOWWINDOW; \ + } + +#define LAYOUTNEEEDUPDATE(_layout) ((SWP_NOMOVE | SWP_NOSIZE) != ((SWP_NOMOVE | SWP_NOSIZE | SWP_HIDEWINDOW | SWP_SHOWWINDOW) & _layout->flags)) + +#define GROUP_MIN 0x1 +#define GROUP_MAX 0x2 +#define GROUP_STATUSBAR 0x1 +#define GROUP_MAIN 0x2 + +static void LayoutWindows(HWND hwnd, BOOL fRedraw, BOOL fUpdateAll = FALSE) +{ + static INT controls[] = + { + GROUP_STATUSBAR, IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM, IDC_REMOVE, IDC_CLEANUP, IDC_STATUS, + GROUP_MAIN, IDC_DOWNLOADLIST + }; + + INT index; + RECT rc, rg, ri; + LAYOUT layout[sizeof(controls)/sizeof(controls[0])], *pl; + BOOL skipgroup; + HRGN rgn = NULL; + + GetClientRect(hwnd, &rc); + + if (rc.right == rc.left || rc.bottom == rc.top) + return; + + if ( rc.right > WASABI_API_APP->getScaleX( 4 ) ) + rc.right -= WASABI_API_APP->getScaleX( 4 ); + + SetRect( &rg, rc.left, rc.top, rc.right, rc.top ); + + pl = layout; + skipgroup = FALSE; + + InvalidateRect(hwnd, NULL, TRUE); + + for (index = 0; index < sizeof(controls) / sizeof(*controls); index++) + { + if (controls[index] >= GROUP_MIN && controls[index] <= GROUP_MAX) // group id + { + skipgroup = FALSE; + switch (controls[index]) + { + case GROUP_STATUSBAR: + { + wchar_t buffer[128] = {0}; + WASABI_API_LNGSTRINGW_BUF(IDC_PLAY, buffer, ARRAYSIZE(buffer)); + LRESULT idealSize = MLSkinnedButton_GetIdealSize(GetDlgItem(hwnd, IDC_PLAY), buffer); + + SetRect(&rg, rc.left + WASABI_API_APP->getScaleX(1), + rc.bottom - WASABI_API_APP->getScaleY(HIWORD(idealSize)), + rc.right, rc.bottom); + rc.bottom = rg.top - WASABI_API_APP->getScaleY(3); + break; + } + case GROUP_MAIN: + SetRect(&rg, rc.left + WASABI_API_APP->getScaleX(1), rc.top, rc.right, rc.bottom); + break; + } + continue; + } + if (skipgroup) continue; + + pl->id = controls[index]; + pl->hwnd = GetDlgItem(hwnd, pl->id); + + if ( !pl->hwnd ) + continue; + + GetWindowRect(pl->hwnd, &ri); + MapWindowPoints(HWND_DESKTOP, hwnd, (LPPOINT)&ri, 2); + pl->flags = SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW | SWP_NOCOPYBITS; + + switch (pl->id) + { + case IDC_PLAY: + case IDC_ENQUEUE: + case IDC_CUSTOM: + case IDC_REMOVE: + case IDC_CLEANUP: + if ( IDC_CUSTOM != pl->id || customAllowed ) + { + if ( groupBtn && pl->id == IDC_PLAY && enqueuedef == 1 ) + { + pl->flags |= SWP_HIDEWINDOW; + break; + } + + if ( groupBtn && pl->id == IDC_ENQUEUE && enqueuedef != 1 ) + { + pl->flags |= SWP_HIDEWINDOW; + break; + } + + if ( groupBtn && ( pl->id == IDC_PLAY || pl->id == IDC_ENQUEUE ) && customAllowed ) + { + pl->flags |= SWP_HIDEWINDOW; + break; + } + + wchar_t buffer[ 128 ] = { 0 }; + GetWindowTextW( pl->hwnd, buffer, ARRAYSIZE( buffer ) ); + + LRESULT idealSize = MLSkinnedButton_GetIdealSize( pl->hwnd, buffer ); + LONG width = LOWORD( idealSize ) + WASABI_API_APP->getScaleX( 6 ); + + SETLAYOUTPOS( pl, rg.left, rg.bottom - WASABI_API_APP->getScaleY( HIWORD( idealSize ) ), width, WASABI_API_APP->getScaleY( HIWORD( idealSize ) ) ); + + pl->flags |= ( ( rg.right - rg.left ) > width ) ? SWP_SHOWWINDOW : SWP_HIDEWINDOW; + + if ( SWP_SHOWWINDOW & pl->flags ) + rg.left += ( pl->cx + WASABI_API_APP->getScaleX( 4 ) ); + } + else + pl->flags |= SWP_HIDEWINDOW; + break; + case IDC_STATUS: + SETLAYOUTPOS(pl, rg.left, rg.top, rg.right - rg.left, (rg.bottom - rg.top)); + pl->flags |= (pl->cx > 16) ? SWP_SHOWWINDOW : SWP_HIDEWINDOW; + break; + case IDC_DOWNLOADLIST: + pl->flags |= (rg.top < rg.bottom) ? SWP_SHOWWINDOW : SWP_HIDEWINDOW; + SETLAYOUTPOS( pl, rg.left, rg.top + WASABI_API_APP->getScaleY( 1 ), rg.right - rg.left + WASABI_API_APP->getScaleY( 1 ), ( rg.bottom - rg.top ) - WASABI_API_APP->getScaleY( 2 ) ); + break; + } + + SETLAYOUTFLAGS(pl, ri); + if ( LAYOUTNEEEDUPDATE( pl ) ) + { + if ( SWP_NOSIZE == ( ( SWP_HIDEWINDOW | SWP_SHOWWINDOW | SWP_NOSIZE ) & pl->flags ) && ri.left == ( pl->x + offsetX ) && ri.top == ( pl->y + offsetY ) && IsWindowVisible( pl->hwnd ) ) + { + SetRect( &ri, pl->x, pl->y, pl->cx + pl->x, pl->y + pl->cy ); + ValidateRect( hwnd, &ri ); + } + + pl++; + } + else if ( ( fRedraw || ( !offsetX && !offsetY ) ) && IsWindowVisible( pl->hwnd ) ) + { + ValidateRect( hwnd, &ri ); + if ( GetUpdateRect( pl->hwnd, NULL, FALSE ) ) + { + if ( !rgn ) + rgn = CreateRectRgn( 0, 0, 0, 0 ); + + GetUpdateRgn( pl->hwnd, rgn, FALSE ); + OffsetRgn( rgn, pl->x, pl->y ); + InvalidateRgn( hwnd, rgn, FALSE ); + } + } + } + + if (pl != layout) + { + LAYOUT *pc; + HDWP hdwp = BeginDeferWindowPos((INT)(pl - layout)); + for(pc = layout; pc < pl && hdwp; pc++) + { + hdwp = DeferWindowPos(hdwp, pc->hwnd, NULL, pc->x, pc->y, pc->cx, pc->cy, pc->flags); + } + + if (hdwp) + EndDeferWindowPos(hdwp); + + if ( !rgn ) + rgn = CreateRectRgn( 0, 0, 0, 0 ); + + if (fRedraw) + { + GetUpdateRgn(hwnd, rgn, FALSE); + for ( pc = layout; pc < pl && hdwp; pc++ ) + { + if ( pc->rgn ) + { + OffsetRgn( pc->rgn, pc->x, pc->y ); + CombineRgn( rgn, rgn, pc->rgn, RGN_OR ); + } + } + + RedrawWindow(hwnd, NULL, rgn, RDW_INVALIDATE | RDW_UPDATENOW | RDW_ERASENOW | RDW_ALLCHILDREN); + } + + if (g_rgnUpdate) + { + GetUpdateRgn(hwnd, g_rgnUpdate, FALSE); + for(pc = layout; pc < pl && hdwp; pc++) + { + if (pc->rgn) + { + OffsetRgn(pc->rgn, pc->x, pc->y); + CombineRgn(g_rgnUpdate, g_rgnUpdate, pc->rgn, RGN_OR); + } + } + } + + for(pc = layout; pc < pl && hdwp; pc++) if (pc->rgn) DeleteObject(pc->rgn); + } + + if ( rgn ) + DeleteObject( rgn ); + + ValidateRgn(hwnd, NULL); +} + +void Downloads_DisplayChange(HWND hwndDlg) +{ + ListView_SetTextColor(downloadList.getwnd(), dialogSkinner.Color(WADLG_ITEMFG)); + ListView_SetBkColor(downloadList.getwnd(), dialogSkinner.Color(WADLG_ITEMBG)); + ListView_SetTextBkColor(downloadList.getwnd(), dialogSkinner.Color(WADLG_ITEMBG)); + downloadList.SetFont(dialogSkinner.GetFont()); + LayoutWindows(hwndDlg, TRUE); +} + +static void DownloadsDialog_SkinControls(HWND hwnd, const INT *itemList, INT itemCount, UINT skinType, UINT skinStyle) +{ + MLSKINWINDOW skinWindow = {0}; + skinWindow.style = skinStyle; + skinWindow.skinType = skinType; + + for(INT i = 0; i < itemCount; i++) + { + skinWindow.hwndToSkin = GetDlgItem(hwnd, itemList[i]); + if (NULL != skinWindow.hwndToSkin) + { + MLSkinWindow(plugin.hwndLibraryParent, &skinWindow); + } + } +} + +static void DownloadDialog_InitializeList(HWND hwnd) +{ + HWND hControl = GetDlgItem(hwnd, IDC_DOWNLOADLIST); + if (NULL == hControl) return; + + UINT styleEx = (UINT)GetWindowLongPtr(hControl, GWL_EXSTYLE); + SetWindowLongPtr(hControl, GWL_EXSTYLE, styleEx & ~WS_EX_NOPARENTNOTIFY); + + styleEx = LVS_EX_DOUBLEBUFFER | LVS_EX_FULLROWSELECT | LVS_EX_INFOTIP; + SendMessage(hControl, LVM_SETEXTENDEDLISTVIEWSTYLE, styleEx, styleEx); + SendMessage(hControl, LVM_SETUNICODEFORMAT, (WPARAM)TRUE, 0L); + + MLSKINWINDOW skinWindow; + skinWindow.style = SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWLVS_ALTERNATEITEMS; + skinWindow.skinType = SKINNEDWND_TYPE_LISTVIEW; + skinWindow.hwndToSkin = hControl; + MLSkinWindow(plugin.hwndLibraryParent, &skinWindow); +} + +bool COL_CHANNEL_Sort(const DownloadListItem* item1, const DownloadListItem* item2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + (item1->f?item1->f->channel:item1->channel), -1, + (item2->f?item2->f->channel:item2->channel), -1)); +} + +bool COL_ITEM_Sort(const DownloadListItem* item1, const DownloadListItem* item2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + (item1->f?item1->f->item:item1->item), -1, + (item2->f?item2->f->item:item2->item), -1)); +} + +bool COL_PROGRESS_Sort(const DownloadListItem* item1, const DownloadListItem* item2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + (item1->f?WASABI_API_LNGSTRINGW(IDS_DONE):item1->status), -1, + (item2->f?WASABI_API_LNGSTRINGW(IDS_DONE):item2->status), -1)); +} + +bool COL_PATH_Sort(const DownloadListItem* item1, const DownloadListItem* item2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, + (item1->f?item1->f->path:item1->path), -1, + (item2->f?item2->f->path:item2->path), -1)); +} + +static BOOL Downloads_SortItems(int sortColumn) +{ + AutoLock lock (downloadedFiles); + switch (sortColumn) + { + case COL_CHANNEL: + std::sort(listContents.begin(), listContents.end(), COL_CHANNEL_Sort); + return TRUE; + case COL_ITEM: + std::sort(listContents.begin(), listContents.end(), COL_ITEM_Sort); + return TRUE; + case COL_PROGRESS: + std::sort(listContents.begin(), listContents.end(), COL_PROGRESS_Sort); + return TRUE; + case COL_PATH: + std::sort(listContents.begin(), listContents.end(), COL_PATH_Sort); + return TRUE; + } + return FALSE; +} + +static void Downloads_SetListSortColumn(HWND hwnd, INT listId, INT index, BOOL fAscending) +{ + HWND hItems = GetDlgItem(hwnd, listId); + if (NULL == hItems) return; + + HWND hHeader = (HWND)SNDMSG(hItems, LVM_GETHEADER, 0, 0L); + if (NULL == hHeader) return; + + HDITEM item; + item.mask = HDI_FORMAT; + // reset first (ml req) + INT count = (INT)SNDMSG(hHeader, HDM_GETITEMCOUNT, 0, 0L); + for (INT i = 0; i < count; i++) + { + if (index != i && FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, i, (LPARAM)&item)) + { + if (0 != ((HDF_SORTUP | HDF_SORTDOWN) & item.fmt)) + { + item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN); + SNDMSG(hHeader, HDM_SETITEM, i, (LPARAM)&item); + } + } + } + + if (FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, index, (LPARAM)&item)) + { + INT fmt = item.fmt & ~(HDF_SORTUP | HDF_SORTDOWN); + fmt |= (FALSE == fAscending) ? HDF_SORTDOWN : HDF_SORTUP; + if (fmt != item.fmt) + { + item.fmt = fmt; + SNDMSG(hHeader, HDM_SETITEM, index, (LPARAM)&item); + } + } +} + +static BOOL Downloads_Sort(HWND hwnd, INT iColumn, bool fAscending) +{ + BOOL result = TRUE; + downloadsSortAscending = fAscending; + Downloads_SortItems(iColumn); + Downloads_SetListSortColumn(hwnd, IDC_DOWNLOADLIST, iColumn, fAscending); + + if (FALSE != result) + { + HWND hItems = GetDlgItem(hwnd, IDC_DOWNLOADLIST); + if (NULL != hItems) + InvalidateRect(hItems, NULL, TRUE); + } + + return TRUE; +} + +void Downloads_UpdateButtonText(HWND hwndDlg, int _enqueuedef) +{ + if (groupBtn) + { + switch(_enqueuedef) + { + case 1: + SetDlgItemTextW(hwndDlg, IDC_PLAY, view.enqueue); + customAllowed = FALSE; + break; + + default: + // v5.66+ - re-use the old predixis parts so the button can be used functionally via ml_enqplay + // pass the hwnd, button id and plug-in id so the ml plug-in can check things as needed + pluginMessage p = {ML_MSG_VIEW_BUTTON_HOOK_IN_USE, (INT_PTR)_enqueuedef, 0, 0}; + + wchar_t *pszTextW = (wchar_t *)SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_SEND_PLUGIN_MESSAGE, (WPARAM)&p); + if (pszTextW && pszTextW[0] != 0) + { + // set this to be a bit different so we can just use one button and not the + // mixable one as well (leaving that to prevent messing with the resources) + SetDlgItemTextW(hwndDlg, IDC_PLAY, pszTextW); + customAllowed = TRUE; + } + else + { + SetDlgItemTextW(hwndDlg, IDC_PLAY, view.play); + customAllowed = FALSE; + } + break; + } + } +} + +static void Downloads_ManageButtons( HWND hwndDlg ) +{ + int has_selection = downloadList.GetSelectedCount(); + + const int buttonids[] = { IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM, IDC_REMOVE }; + for ( size_t i = 0; i != sizeof( buttonids ) / sizeof( buttonids[ 0 ] ); i++ ) + { + HWND controlHWND = GetDlgItem( hwndDlg, buttonids[ i ] ); + EnableWindow( controlHWND, has_selection ); + } +} + +void Downloads_Init(HWND hwndDlg) +{ + HWND hLibrary = plugin.hwndLibraryParent; + current_window = hwndDlg; + + if (!view.play) + { + SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_GET_VIEW_BUTTON_TEXT, (WPARAM)&view); + } + + HACCEL accel = WASABI_API_LOADACCELERATORSW(IDR_VIEW_DOWNLOAD_ACCELERATORS); + if (accel) + WASABI_API_APP->app_addAccelerators(hwndDlg, &accel, 1, TRANSLATE_MODE_CHILD); + + g_context_menus2 = WASABI_API_LOADMENU(IDR_MENU1); + groupBtn = ML_GROUPBTN_VAL(); + enqueuedef = (ML_ENQDEF_VAL() == 1); + + // v5.66+ - re-use the old predixis parts so the button can be used functionally via ml_enqplay + // pass the hwnd, button id and plug-in id so the ml plug-in can check things as needed + pluginMessage p = {ML_MSG_VIEW_BUTTON_HOOK, (INT_PTR)hwndDlg, (INT_PTR)MAKELONG(IDC_CUSTOM, IDC_ENQUEUE), (INT_PTR)L"ml_downloads"}; + wchar_t *pszTextW = (wchar_t *)SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_SEND_PLUGIN_MESSAGE, (WPARAM)&p); + if (pszTextW && pszTextW[0] != 0) + { + // set this to be a bit different so we can just use one button and not the + // mixable one as well (leaving that to prevent messing with the resources) + customAllowed = TRUE; + SetDlgItemTextW(hwndDlg, IDC_CUSTOM, pszTextW); + } + else + customAllowed = FALSE; + + MLSkinWindow2(hLibrary, hwndDlg, SKINNEDWND_TYPE_AUTO, SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS); + + const INT szControls[] = {IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM}; + DownloadsDialog_SkinControls(hwndDlg, szControls, ARRAYSIZE(szControls), SKINNEDWND_TYPE_AUTO, + SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | (groupBtn ? SWBS_SPLITBUTTON : 0)); + + const INT szControlz[] = {IDC_REMOVE, IDC_CLEANUP, IDC_STATUS}; + DownloadsDialog_SkinControls(hwndDlg, szControlz, ARRAYSIZE(szControlz), SKINNEDWND_TYPE_AUTO, + SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS); + + DownloadDialog_InitializeList(hwndDlg); + Downloads_UpdateStatusBar(hwndDlg); + + downloadList.setwnd(GetDlgItem(hwndDlg, IDC_DOWNLOADLIST)); + downloadList.AddCol(WASABI_API_LNGSTRINGW(IDS_CHANNEL), downloadsChannelWidth); + downloadList.AddCol(WASABI_API_LNGSTRINGW(IDS_ITEM), downloadsItemWidth); + downloadList.AddCol(WASABI_API_LNGSTRINGW(IDS_PROGRESS), downloadsProgressWidth); + downloadList.AddCol(WASABI_API_LNGSTRINGW(IDS_PATH), downloadsPathWidth); + + DownloadsUpdated(); + + downloadList.SetVirtualCount((int)listContents.size()); + Downloads_UpdateButtonText(hwndDlg, enqueuedef == 1); + Downloads_ManageButtons(hwndDlg); + Downloads_DisplayChange(hwndDlg); + Downloads_Sort(hwndDlg, downloadsItemSort, downloadsSortAscending); + SetTimer(hwndDlg, DOWNLOADSDIALOG_TIMER_UPDATESTATUSBAR , 1000, 0); +} + +void Downloads_Timer( HWND hwndDlg, UINT timerId ) +{ + switch ( timerId ) + { + case DOWNLOADSDIALOG_TIMER_UPDATESTATUSBAR: + Downloads_UpdateStatusBar( hwndDlg ); + { + AutoLock lock( downloadStatus.statusLock ); + for ( DownloadListItem *l_content : listContents ) + { + if ( l_content->token ) + { + size_t d = downloadStatus.downloads[ l_content->token ].downloaded; + size_t s = downloadStatus.downloads[ l_content->token ].maxSize; + + if ( s ) + StringCchPrintf( l_content->status, 20, WASABI_API_LNGSTRINGW( IDS_DOWNLOADING_PERCENT ), (int)( d / ( s / 100 ) ) ); + else + { + WASABI_API_LNGSTRINGW_BUF( IDS_DOWNLOADING, l_content->status, 20 ); + } + + PostMessage( downloadList.getwnd(), LVM_REDRAWITEMS, 0, listContents.size() ); + } + } + } + break; + } +} + +static INT Downloads_GetListSortColumn(HWND hwnd, INT listId, bool *fAscending) +{ + HWND hItems = GetDlgItem(hwnd, listId); + if (NULL != hItems) + { + HWND hHeader = (HWND)SNDMSG(hItems, LVM_GETHEADER, 0, 0L); + if (NULL != hHeader) + { + HDITEM item; + item.mask = HDI_FORMAT; + + INT count = (INT)SNDMSG(hHeader, HDM_GETITEMCOUNT, 0, 0L); + for (INT i = 0; i < count; i++) + { + if (FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, i, (LPARAM)&item) && + 0 != ((HDF_SORTUP | HDF_SORTDOWN) & item.fmt)) + { + if (NULL != fAscending) + { + *fAscending = (0 != (HDF_SORTUP & item.fmt)); + } + return i; + } + } + } + } + return -1; +} + +void Downloads_Destroy( HWND hwndDlg ) +{ + downloadsChannelWidth = downloadList.GetColumnWidth( COL_CHANNEL ); + downloadsItemWidth = downloadList.GetColumnWidth( COL_ITEM ); + downloadsProgressWidth = downloadList.GetColumnWidth( COL_PROGRESS ); + downloadsPathWidth = downloadList.GetColumnWidth( COL_PATH ); + + for ( DownloadListItem *l_content : listContents ) + delete l_content; + + listContents.clear(); + + downloadList.setwnd( NULL ); + + bool fAscending; + downloadsItemSort = Downloads_GetListSortColumn( hwndDlg, IDC_DOWNLOADLIST, &fAscending ); + downloadsSortAscending = ( -1 != downloadsItemSort ) ? ( FALSE != fAscending ) : true; +} + +void Downloads_Remove( bool del = false, HWND parent = NULL ) +{ + int d = -1; + int r = 0; + while ( GetDownload( d ) ) + { + int download = d - r; + DownloadListItem *item = listContents[ download ]; + if ( item->f ) + { + AutoLock lock( downloadedFiles ); + int j = 0; + for ( DownloadList::iterator i = downloadedFiles.begin(); i != downloadedFiles.end(); i++ ) + { + if ( !_wcsicmp( i->path, item->f->path ) ) + { + if ( del ) + { + if ( !downloadedFiles.RemoveAndDelete( j ) ) + MessageBox( parent, WASABI_API_LNGSTRINGW( IDS_DELETEFAILED ), downloadedFiles.downloadList[ j ].path, 0 ); + } + else + downloadedFiles.Remove( j ); + + delete item; + listContents.erase(listContents.begin() + download); + r++; + break; + } + + ++j; + } + } + else if ( item->token ) + { + AutoLock lock( downloadStatus.statusLock ); + downloadStatus.downloads[ item->token ].killswitch = 1; + delete item; + listContents.erase( listContents.begin() + download ); + r++; + } + else + { + delete item; + listContents.erase( listContents.begin() + download ); + r++; + } + } + + downloadList.SetVirtualCountAsync( (int)listContents.size() ); + Navigation_ShowService(SERVICE_DOWNLOADS, SHOWMODE_AUTO); +} + +void Downloads_Delete( HWND parent ) +{ + wchar_t message[ 256 ] = { 0 }; + int c = downloadList.GetSelectedCount(); + + if ( !c ) + return; + else if ( c == 1 ) + WASABI_API_LNGSTRINGW_BUF( IDS_PERM_DELETE_ARE_YOU_SURE, message, 256 ); + else + StringCchPrintf( message, 256, WASABI_API_LNGSTRINGW( IDS_PERM_DELETE_THESE_ARE_YOU_SURE ), c ); + + if ( MessageBox( NULL, message, WASABI_API_LNGSTRINGW( IDS_DELETION ), MB_ICONWARNING | MB_YESNO ) == IDNO ) + return; + + Downloads_Remove( true, parent ); +} + +void Downloads_CleanUp(HWND hwndDlg) +{ + wchar_t titleStr[64] = {0}; + if ( MessageBox( hwndDlg, WASABI_API_LNGSTRINGW( IDS_CLEAR_ALL_FINISHED_DOWNLOADS ), WASABI_API_LNGSTRINGW_BUF( IDS_CLEAN_UP_LIST, titleStr, 64 ), MB_ICONWARNING | MB_YESNO ) == IDNO ) + return; + + { + AutoLock lock( downloadedFiles ); + downloadedFiles.downloadList.clear(); + } + DownloadsUpdated(); +} + +void Downloads_InfoBox( HWND parent ) +{ + int download = -1; + if ( GetDownload( download ) ) + { + const wchar_t *fn; + if ( listContents[ download ]->f ) + fn = listContents[ download ]->f->path; + else + fn = listContents[ download ]->path; + + if ( fn ) + { + infoBoxParamW p = { parent, fn }; + SendMessage( plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&p, IPC_INFOBOXW ); + } + } +} + +void Downloads_SelectAll() +{ + int l = downloadList.GetCount(); + for ( int i = 0; i < l; i++ ) + downloadList.SetSelected( i ); +} + +static void exploreItemFolder( HWND hwndDlg ) +{ + if ( downloadList.GetSelectionMark() >= 0 ) + { + int download = -1; + while ( GetDownload( download ) ) + { + wchar_t *file; + if ( listContents[ download ]->f ) + file = listContents[ download ]->f->path; + else + file = listContents[ download ]->path; + + WASABI_API_EXPLORERFINDFILE->AddFile( file ); + } + WASABI_API_EXPLORERFINDFILE->ShowFiles(); + } +} + +int we_are_drag_and_dropping = 0; + +static void Downloads_OnColumnClick(HWND hwnd, NMLISTVIEW *plv) +{ + bool fAscending; + INT iSort = Downloads_GetListSortColumn(hwnd, IDC_DOWNLOADLIST, &fAscending); + fAscending = (-1 != iSort && iSort == plv->iSubItem) ? (!fAscending) : true; + Downloads_Sort(hwnd, plv->iSubItem, fAscending); +} + +LRESULT DownloadList_Notify( LPNMHDR l, HWND hwndDlg ) +{ + switch ( l->code ) + { + case LVN_COLUMNCLICK: + Downloads_OnColumnClick( hwndDlg, (NMLISTVIEW *)l ); + break; + case NM_DBLCLK: + Downloads_Play( ( ( !!( GetAsyncKeyState( VK_SHIFT ) & 0x8000 ) ) ^ ML_ENQDEF_VAL() ) ); + break; + case LVN_BEGINDRAG: + we_are_drag_and_dropping = 1; + SetCapture( hwndDlg ); + break; + case LVN_ITEMCHANGED: + Downloads_ManageButtons( hwndDlg ); + break; + case LVN_GETDISPINFO: + NMLVDISPINFO *lpdi = (NMLVDISPINFO *)l; + size_t item = lpdi->item.iItem; + + if ( item < 0 || item >= listContents.size() ) + return 0; + + if ( FALSE == downloadsSortAscending ) + item = listContents.size() - item - 1; + + DownloadListItem *l = listContents[ item ]; + + if ( lpdi->item.mask & LVIF_TEXT ) + { + lpdi->item.pszText[ 0 ] = 0; + switch ( lpdi->item.iSubItem ) + { + case COL_CHANNEL: + if ( !l->token && l->f ) + lstrcpyn( lpdi->item.pszText, l->f->channel, lpdi->item.cchTextMax ); + else + lstrcpyn( lpdi->item.pszText, l->channel, lpdi->item.cchTextMax ); + break; + case COL_ITEM: + if ( !l->token && l->f ) + lstrcpyn( lpdi->item.pszText, l->f->item, lpdi->item.cchTextMax ); + else + lstrcpyn( lpdi->item.pszText, l->item, lpdi->item.cchTextMax ); + break; + case COL_PROGRESS: + if( !l->token && l->f ) + WASABI_API_LNGSTRINGW_BUF( IDS_DONE, lpdi->item.pszText, lpdi->item.cchTextMax ); + else + lstrcpyn( lpdi->item.pszText, l->status, lpdi->item.cchTextMax ); + break; + case COL_PATH: + if ( !l->token && l->f ) + lstrcpyn( lpdi->item.pszText, l->f->path, lpdi->item.cchTextMax ); + else + { + if ( l->path ) + lstrcpyn( lpdi->item.pszText, l->path, lpdi->item.cchTextMax ); + } + + break; + } + } + + break; + } + + return 0; +} + +void listbuild( wchar_t **buf, int &buf_size, int &buf_pos, const wchar_t *tbuf ) +{ + if ( !*buf ) + { + *buf = (wchar_t *)calloc( 4096, sizeof( wchar_t ) ); + if ( *buf ) + { + buf_size = 4096; + buf_pos = 0; + } + else + { + buf_size = buf_pos = 0; + } + } + int newsize = buf_pos + lstrlenW( tbuf ) + 1; + if ( newsize < buf_size ) + { + size_t old_buf_size = buf_size; + buf_size = newsize + 4096; + wchar_t *new_buf = (wchar_t *)realloc( *buf, ( buf_size + 1 ) * sizeof( wchar_t ) ); + if ( new_buf ) + { + *buf = new_buf; + } + else + { + new_buf = (wchar_t*)calloc( ( buf_size + 1 ), sizeof( wchar_t ) ); + if ( new_buf ) + { + memcpy( new_buf, *buf, ( old_buf_size * sizeof( wchar_t ) ) ); + free( *buf ); + *buf = new_buf; + } + else buf_size = (int)old_buf_size; + } + } + + StringCchCopyW( *buf + buf_pos, buf_size, tbuf ); + buf_pos = newsize; +} + +wchar_t *getSelectedList() +{ + wchar_t *path = NULL; + int buf_pos = 0; + int buf_size = 0; + int download = -1; + + while ( GetDownload( download ) ) + { + if ( listContents[ download ]->f ) + listbuild( &path, buf_size, buf_pos, listContents[ download ]->f->path ); + } + + if ( path ) + path[ buf_pos ] = 0; + + return path; +} + +void SwapPlayEnqueueInMenu( HMENU listMenu ) +{ + int playPos = -1, enqueuePos = -1; + MENUITEMINFOW playItem = { sizeof( MENUITEMINFOW ), 0, }, enqueueItem = { sizeof( MENUITEMINFOW ), 0, }; + + int numItems = GetMenuItemCount( listMenu ); + + for ( int i = 0; i < numItems; i++ ) + { + UINT id = GetMenuItemID( listMenu, i ); + if ( id == IDC_PLAY ) + { + playItem.fMask = MIIM_ID; + playPos = i; + GetMenuItemInfoW( listMenu, i, TRUE, &playItem ); + } + else if ( id == IDC_ENQUEUE ) + { + enqueueItem.fMask = MIIM_ID; + enqueuePos = i; + GetMenuItemInfoW( listMenu, i, TRUE, &enqueueItem ); + } + } + + playItem.wID = IDC_ENQUEUE; + enqueueItem.wID = IDC_PLAY; + SetMenuItemInfoW( listMenu, playPos, TRUE, &playItem ); + SetMenuItemInfoW( listMenu, enqueuePos, TRUE, &enqueueItem ); +} + +void SyncMenuWithAccelerators( HWND hwndDlg, HMENU menu ) +{ + HACCEL szAccel[ 24 ] = { 0 }; + INT c = WASABI_API_APP->app_getAccelerators( hwndDlg, szAccel, sizeof( szAccel ) / sizeof( szAccel[ 0 ] ), FALSE ); + AppendMenuShortcuts( menu, szAccel, c, MSF_REPLACE ); +} + +void UpdateMenuItems( HWND hwndDlg, HMENU menu ) +{ + bool swapPlayEnqueue = false; + if ( ML_ENQDEF_VAL() ) + { + SwapPlayEnqueueInMenu( menu ); + swapPlayEnqueue = true; + } + + SyncMenuWithAccelerators( hwndDlg, menu ); + if ( swapPlayEnqueue ) + SwapPlayEnqueueInMenu( menu ); +} + +int IPC_LIBRARY_SENDTOMENU = 0; +librarySendToMenuStruct s = { 0 }; + +static void DownloadList_RightClick(HWND hwndDlg, HWND listHwnd, POINTS pts) +{ + POINT pt; + POINTSTOPOINT(pt, pts); + + RECT controlRect, headerRect; + if (FALSE == GetClientRect(listHwnd, &controlRect)) + SetRectEmpty(&controlRect); + else + MapWindowPoints(listHwnd, HWND_DESKTOP, (POINT*)&controlRect, 2); + + if ( -1 == pt.x && -1 == pt.y ) + { + RECT itemRect; + int selected = downloadList.GetNextSelected(); + if ( selected != -1 ) // if something is selected we'll drop the menu from there + { + downloadList.GetItemRect( selected, &itemRect ); + ClientToScreen( listHwnd, (POINT *)&itemRect ); + } + else // otherwise we'll drop it from the top-left corner of the listview, adjusting for the header location + { + GetWindowRect( listHwnd, &itemRect ); + + HWND hHeader = (HWND)SNDMSG( listHwnd, LVM_GETHEADER, 0, 0L ); + RECT headerRect; + if ( ( WS_VISIBLE & GetWindowLongPtr( hHeader, GWL_STYLE ) ) && GetWindowRect( hHeader, &headerRect ) ) + { + itemRect.top += ( headerRect.bottom - headerRect.top ); + } + } + + pt.x = itemRect.left; + pt.y = itemRect.top; + } + + HWND hHeader = (HWND)SNDMSG(listHwnd, LVM_GETHEADER, 0, 0L); + if ( 0 == ( WS_VISIBLE & GetWindowLongPtr( hHeader, GWL_STYLE ) ) || FALSE == GetWindowRect( hHeader, &headerRect ) ) + { + SetRectEmpty( &headerRect ); + } + + if ( FALSE != PtInRect( &headerRect, pt ) ) + { + return; + } + + LVHITTESTINFO hitTest; + hitTest.pt = pt; + MapWindowPoints( HWND_DESKTOP, listHwnd, &hitTest.pt, 1 ); + + int index = ( downloadList.GetNextSelected() != -1 ? ListView_HitTest( listHwnd, &hitTest ) : -1 ); + + HMENU baseMenu = WASABI_API_LOADMENU( IDR_MENU1 ); + + if ( baseMenu == NULL ) + return; + + HMENU menu = GetSubMenu( baseMenu, 2 ); + if ( menu != NULL ) + { + UINT enableExtras = MF_BYCOMMAND | MF_ENABLED; + if ( index == -1 ) + enableExtras |= ( MF_GRAYED | MF_DISABLED ); + + EnableMenuItem( menu, IDC_PLAY, enableExtras ); + EnableMenuItem( menu, IDC_ENQUEUE, enableExtras ); + EnableMenuItem( menu, IDC_REMOVE, enableExtras ); + EnableMenuItem( menu, IDC_DELETE, enableExtras ); + EnableMenuItem( menu, IDC_INFOBOX, enableExtras ); + EnableMenuItem( menu, ID_DOWNLOADS_EXPLORERITEMFOLDER, enableExtras ); + EnableMenuItem( menu, 2, MF_BYPOSITION | ( index == -1 ? ( MF_GRAYED | MF_DISABLED ) : MF_ENABLED ) ); + + { // send-to menu shit... + ZeroMemory( &s, sizeof( s ) ); + IPC_LIBRARY_SENDTOMENU = SendMessage( plugin.hwndWinampParent, WM_WA_IPC, ( WPARAM ) & "LibrarySendToMenu", IPC_REGISTER_WINAMP_IPCMESSAGE ); + if ( IPC_LIBRARY_SENDTOMENU > 65536 && SendMessage( plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)0, IPC_LIBRARY_SENDTOMENU ) == 0xffffffff ) + { + s.mode = 1; + s.hwnd = hwndDlg; + s.data_type = ML_TYPE_FILENAMESW; + s.ctx[ 1 ] = 1; + s.build_hMenu = GetSubMenu( menu, 2 ); + } + } + + UpdateMenuItems( hwndDlg, menu ); + + int r = Menu_TrackPopup( plugin.hwndLibraryParent, menu, TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTBUTTON, pt.x, pt.y, hwndDlg, NULL ); + if ( !SendMessage( hwndDlg, WM_COMMAND, r, 0 ) ) + { + s.menu_id = r; // more send to menu shit... + if ( s.mode == 2 && SendMessage( plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&s, IPC_LIBRARY_SENDTOMENU ) == 0xffffffff ) + { + s.mode = 3; + s.data_type = ML_TYPE_FILENAMESW; + wchar_t *path = getSelectedList(); + if ( path ) + { + s.data = path; + SendMessage( plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&s, IPC_LIBRARY_SENDTOMENU ); + free( path ); + } + } + } + + if ( s.mode ) + { // yet more send to menu shit... + s.mode = 4; + SendMessage( plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&s, IPC_LIBRARY_SENDTOMENU ); // cleanup + } + + if ( NULL != s.build_hMenu ) + { + DestroyMenu( s.build_hMenu ); + s.build_hMenu = NULL; + } + } + + DestroyMenu( baseMenu ); +} + +static void Downloads_ContextMenu( HWND hwndDlg, WPARAM wParam, LPARAM lParam ) +{ + HWND sourceWindow = (HWND)wParam; + if ( sourceWindow == downloadList.getwnd() ) + DownloadList_RightClick( hwndDlg, sourceWindow, MAKEPOINTS( lParam ) ); +} + +BOOL Downloads_ButtonPopupMenu( HWND hwndDlg, int buttonId, HMENU menu, int flags ) +{ + RECT r; + HWND buttonHWND = GetDlgItem( hwndDlg, buttonId ); + GetWindowRect( buttonHWND, &r ); + UpdateMenuItems( hwndDlg, menu ); + MLSkinnedButton_SetDropDownState( buttonHWND, TRUE ); + UINT tpmFlags = TPM_RIGHTBUTTON | TPM_LEFTBUTTON | TPM_BOTTOMALIGN | TPM_LEFTALIGN; + if ( !( flags & BPM_WM_COMMAND ) ) + tpmFlags |= TPM_RETURNCMD; + int x = Menu_TrackPopup( plugin.hwndLibraryParent, menu, tpmFlags, r.left, r.top, hwndDlg, NULL ); + if ( ( flags & BPM_ECHO_WM_COMMAND ) && x ) + SendMessage( hwndDlg, WM_COMMAND, MAKEWPARAM( x, 0 ), 0 ); + MLSkinnedButton_SetDropDownState( buttonHWND, FALSE ); + return x; +} + +static void Downloads_Play( HWND hwndDlg, HWND from, UINT idFrom ) +{ + HMENU listMenu = GetSubMenu( g_context_menus2, 0 ); + int count = GetMenuItemCount( listMenu ); + if ( count > 2 ) + { + for ( int i = 2; i < count; i++ ) + { + DeleteMenu( listMenu, 2, MF_BYPOSITION ); + } + } + + Downloads_ButtonPopupMenu( hwndDlg, idFrom, listMenu, BPM_WM_COMMAND ); +} + +static BOOL WINAPI DownloadDialog_DlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) +{ + switch (msg) + { + case WM_INITMENUPOPUP: // yet yet more send to menu shit... + if (wParam && (HMENU)wParam == s.build_hMenu && s.mode==1) + { + if (SendMessage(plugin.hwndWinampParent,WM_WA_IPC,(WPARAM)&s,IPC_LIBRARY_SENDTOMENU)==0xffffffff) + s.mode=2; + } + break; + + case WM_CONTEXTMENU: + Downloads_ContextMenu(hwndDlg, wParam, lParam); + return TRUE; + + case WM_NOTIFYFORMAT: + return NFR_UNICODE; + + case WM_INITDIALOG: + Downloads_Init(hwndDlg); + break; + + case WM_NOTIFY: + { + LPNMHDR l = (LPNMHDR)lParam; + if (l->idFrom == IDC_DOWNLOADLIST) + return DownloadList_Notify(l,hwndDlg); + } + break; + + case WM_DESTROY: + Downloads_Destroy(hwndDlg); + return 0; + + case WM_DISPLAYCHANGE: + Downloads_DisplayChange(hwndDlg); + return 0; + + case WM_TIMER: + Downloads_Timer(hwndDlg, wParam); + break; + + case WM_MOUSEMOVE: + if (we_are_drag_and_dropping && GetCapture() == hwndDlg) + { + POINT p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + ClientToScreen(hwndDlg, &p); + mlDropItemStruct m; + ZeroMemory(&m, sizeof(mlDropItemStruct)); + m.type = ML_TYPE_FILENAMESW; + m.p = p; + SendMessage(plugin.hwndLibraryParent,WM_ML_IPC,(WPARAM)&m,ML_IPC_HANDLEDRAG); + } + break; + + case WM_LBUTTONUP: + if (we_are_drag_and_dropping && GetCapture() == hwndDlg) + { + we_are_drag_and_dropping = 0; + ReleaseCapture(); + POINT p = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + ClientToScreen(hwndDlg, &p); + mlDropItemStruct m = {0}; + m.type = ML_TYPE_FILENAMESW; + m.p = p; + m.flags = ML_HANDLEDRAG_FLAG_NOCURSOR; + SendMessage(plugin.hwndLibraryParent,WM_ML_IPC,(WPARAM)&m,ML_IPC_HANDLEDRAG); + if (m.result > 0) + { + m.flags = 0; + m.result = 0; + wchar_t* path = getSelectedList(); + if(path) + { + m.data = path; + SendMessage(plugin.hwndLibraryParent,WM_ML_IPC,(WPARAM)&m,ML_IPC_HANDLEDROP); + free(path); + } + } + } + break; + + case WM_PAINT: + { + int tab[] = { IDC_DOWNLOADLIST|DCW_SUNKENBORDER}; + dialogSkinner.Draw(hwndDlg, tab, sizeof(tab) / sizeof(tab[0])); + } + return 0; + + case WM_WINDOWPOSCHANGED: + if ((SWP_NOSIZE | SWP_NOMOVE) != ((SWP_NOSIZE | SWP_NOMOVE) & ((WINDOWPOS*)lParam)->flags) || + (SWP_FRAMECHANGED & ((WINDOWPOS*)lParam)->flags)) + { + LayoutWindows(hwndDlg, !(SWP_NOREDRAW & ((WINDOWPOS*)lParam)->flags)); + } + return 0; + + case WM_USER + 0x200: + SetWindowLongPtr(hwndDlg, DWLP_MSGRESULT, 1); // yes, we support no - redraw resize + return TRUE; + + case WM_USER + 0x201: + offsetX = (short)LOWORD(wParam); + offsetY = (short)HIWORD(wParam); + g_rgnUpdate = (HRGN)lParam; + return TRUE; + + case WM_APP + 104: + { + Downloads_UpdateButtonText(hwndDlg, wParam); + LayoutWindows(hwndDlg, TRUE); + return 0; + } + + case WM_COMMAND: + switch ( LOWORD( wParam ) ) + { + case IDC_PLAY: + case IDC_ENQUEUE: + case IDC_CUSTOM: + if ( HIWORD( wParam ) == MLBN_DROPDOWN ) + { + Downloads_Play( hwndDlg, (HWND)lParam, LOWORD( wParam ) ); + } + else + { + bool action; + if ( LOWORD( wParam ) == IDC_PLAY ) + { + action = ( HIWORD( wParam ) == 1 ) ? ML_ENQDEF_VAL() == 1 : 0; + } + else if ( LOWORD( wParam ) == IDC_ENQUEUE ) + { + action = ( HIWORD( wParam ) == 1 ) ? ML_ENQDEF_VAL() != 1 : 1; + } + else + break; + + Downloads_Play( action ); + } + break; + case IDC_REMOVE: + Downloads_Remove(); + break; + case IDC_DELETE: + Downloads_Delete( hwndDlg ); + break; + case IDC_CLEANUP: + Downloads_CleanUp( hwndDlg ); + break; + case IDC_INFOBOX: + Downloads_InfoBox( hwndDlg ); + break; + case IDC_SELECTALL: + Downloads_SelectAll(); + break; + case ID_DOWNLOADS_EXPLORERITEMFOLDER: + exploreItemFolder( hwndDlg ); + break; + default: + return 0; + } + return 1; + } + return 0; +} + +HWND CALLBACK DownloadDialog_Create( HWND hParent, OmService *service ) +{ + return WASABI_API_CREATEDIALOGPARAMW( IDD_DOWNLOADS, hParent, DownloadDialog_DlgProc, (LPARAM)service ); +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadsDialog.h b/Src/Plugins/Library/ml_wire/DownloadsDialog.h new file mode 100644 index 00000000..baef5cd8 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadsDialog.h @@ -0,0 +1,13 @@ +#ifndef NULLSOFT_DOWNLOADSDIALOGH +#define NULLSOFT_DOWNLOADSDIALOGH + +#include "DownloadStatus.h" + +class OmService; +HWND CALLBACK DownloadDialog_Create(HWND hParent, OmService *service); + +void DownloadsUpdated(); +void DownloadsUpdated( DownloadToken token, const DownloadedFile *f ); +void DownloadsUpdated( const DownloadStatus::Status &s, DownloadToken token ); + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/DownloadsParse.cpp b/Src/Plugins/Library/ml_wire/DownloadsParse.cpp new file mode 100644 index 00000000..9f552fab --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadsParse.cpp @@ -0,0 +1,204 @@ +#include "Main.h" +#include "DownloadsParse.h" +#include "Downloaded.h" +#include "Defaults.h" +#include "ParseUtil.h" +#include <wchar.h> +#include <locale.h> + +static __time64_t filetime( const wchar_t *file ) +{ + if ( !file || !*file ) + return 0; + + WIN32_FIND_DATA f = { 0 }; + + HANDLE h = FindFirstFile( file, &f ); + if ( h == INVALID_HANDLE_VALUE ) + return 0; + + FindClose( h ); + SYSTEMTIME s = { 0 }; + FileTimeToSystemTime( &f.ftCreationTime, &s ); + + tm t = { 0 }; + t.tm_year = s.wYear - 1900; + t.tm_mon = s.wMonth - 1; + t.tm_mday = s.wDay; + t.tm_hour = s.wHour; + t.tm_min = s.wMinute; + t.tm_sec = s.wMinute; + + return _mktime64( &t ); +} + +static void ReadDownload( const XMLNode *item, bool addToLib = false ) +{ + DownloadedFile newDownloaded; + + const wchar_t *channel = GetContent( item, L"channel" ); + newDownloaded.SetChannel( channel ); + + const wchar_t *item_str = GetContent( item, L"item" ); + newDownloaded.SetItem( item_str ); + + const wchar_t *url = GetContent( item, L"url" ); + newDownloaded.SetURL( url ); + + const wchar_t *path = GetContent( item, L"path" ); + newDownloaded.SetPath( path ); + + const wchar_t *publishDate = GetContent( item, L"publishDate" ); + + if ( publishDate && publishDate[ 0 ] ) + newDownloaded.publishDate = _wtoi( publishDate ); + else + newDownloaded.publishDate = filetime( newDownloaded.path ); + + if ( addToLib ) + addToLibrary( newDownloaded ); + + downloadedFiles.downloadList.push_back( newDownloaded ); +} + +static void ReadPreferences( const XMLNode *item ) +{ + const XMLNode *curNode; + + curNode = item->Get(L"download"); + if ( curNode ) + { + const wchar_t *prop = curNode->GetProperty( L"downloadpath" ); + if ( prop ) + lstrcpyn( defaultDownloadPath, prop, MAX_PATH ); + + autoDownload = PropertyIsTrue( curNode, L"autodownload" ); + + prop = curNode->GetProperty( L"autoDownloadEpisodes" ); + if ( prop ) + autoDownloadEpisodes = _wtoi( prop ); + needToMakePodcastsView = PropertyIsTrue( curNode, L"needToMakePodcastsView" ); + } + + curNode = item->Get(L"update"); + if ( curNode ) + { + const wchar_t *prop = curNode->GetProperty( L"updatetime" ); + if ( prop ) + updateTime = _wtoi64( prop ); + + autoUpdate = PropertyIsTrue( curNode, L"autoupdate" ); + updateOnLaunch = PropertyIsTrue( curNode, L"updateonlaunch" ); + } + + curNode = item->Get(L"subscriptions"); + if ( curNode ) + { + _locale_t C_locale = WASABI_API_LNG->Get_C_NumericLocale(); + const wchar_t *prop = curNode->GetProperty( L"htmldivider" ); + if ( prop ) + htmlDividerPercent = (float)_wtof_l( prop, C_locale ); + + prop = curNode->GetProperty( L"channeldivider" ); + if ( prop && prop[ 0 ] ) + channelDividerPercent = (float)_wtof_l( prop, C_locale ); + + prop = curNode->GetProperty( L"itemtitlewidth" ); + if ( prop && prop[ 0 ] ) + itemTitleWidth = _wtoi( prop ); + + prop = curNode->GetProperty( L"itemdatewidth" ); + if ( prop && prop[ 0 ] ) + itemDateWidth = _wtoi( prop ); + + prop = curNode->GetProperty( L"itemmediawidth" ); + if ( prop && prop[ 0 ] ) + itemMediaWidth = _wtoi( prop ); + + prop = curNode->GetProperty( L"itemsizewidth" ); + if ( prop && prop[ 0 ] ) + itemSizeWidth = _wtoi( prop ); + + prop = curNode->GetProperty( L"currentitemsort" ); + if ( prop && prop[ 0 ] ) + currentItemSort = _wtoi( prop ); + + itemSortAscending = !PropertyIsFalse( curNode, L"itemsortascending" ); + + channelSortAscending = !PropertyIsFalse( curNode, L"channelsortascending" ); + + prop = curNode->GetProperty( L"channelLastSelection" ); + if ( prop && prop[ 0 ] ) + channelLastSelection = _wtoi( prop ); + } + + curNode = item->Get(L"downloadsView"); + if ( curNode ) + { + const wchar_t *prop = curNode->GetProperty( L"downloadsChannelWidth" ); + if ( prop && prop[ 0 ] ) + downloadsChannelWidth = _wtoi( prop ); + if ( downloadsChannelWidth <= 0 ) + downloadsChannelWidth = DOWNLOADSCHANNELWIDTHDEFAULT; + + prop = curNode->GetProperty( L"downloadsItemWidth" ); + if ( prop && prop[ 0 ] ) + downloadsItemWidth = _wtoi( prop ); + if ( downloadsItemWidth <= 0 ) + downloadsItemWidth = DOWNLOADSITEMWIDTHDEFAULT; + + prop = curNode->GetProperty( L"downloadsProgressWidth" ); + if ( prop && prop[ 0 ] ) + downloadsProgressWidth = _wtoi( prop ); + if ( downloadsProgressWidth <= 0 ) + downloadsProgressWidth = DOWNLOADSPROGRESSWIDTHDEFAULT; + + prop = curNode->GetProperty( L"downloadsPathWidth" ); + if ( prop && prop[ 0 ] ) + downloadsPathWidth = _wtoi( prop ); + if ( downloadsPathWidth <= 0 ) + downloadsPathWidth = DOWNLOADSPATHWIDTHDEFAULTS; + + prop = curNode->GetProperty( L"downloadsItemSort" ); + if ( prop && prop[ 0 ] ) + downloadsItemSort = _wtoi( prop ); + + downloadsSortAscending = !PropertyIsFalse( curNode, L"downloadsSortAscending" ); + } + + curNode = item->Get(L"service"); + if ( curNode ) + { + const wchar_t *prop = curNode->GetProperty( L"url" ); + if ( prop ) + lstrcpyn( serviceUrl, prop, MAX_PATH ); + } +} + +void DownloadsParse::ReadNodes( const wchar_t *url ) +{ + XMLNode::NodeList::const_iterator itr; + const XMLNode *curNode = xmlDOM.GetRoot(); + + curNode = curNode->Get( L"winamp:preferences" ); + if ( curNode ) + { + int version = 1; + const wchar_t *prop = curNode->GetProperty( L"version" ); + if ( prop && prop[ 0 ] ) + version = _wtoi( prop ); + + ReadPreferences( curNode ); + + curNode = curNode->Get( L"downloads" ); + if ( curNode ) + { + const XMLNode::NodeList *downloadsList = curNode->GetList( L"download" ); + if ( downloadsList ) + { + for ( itr = downloadsList->begin(); itr != downloadsList->end(); itr++ ) + ReadDownload( *itr, version < 2 ); + } + } + } +} diff --git a/Src/Plugins/Library/ml_wire/DownloadsParse.h b/Src/Plugins/Library/ml_wire/DownloadsParse.h new file mode 100644 index 00000000..8f70ceb9 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/DownloadsParse.h @@ -0,0 +1,13 @@ +#ifndef NULLSOFT_DOWNLOADSPARSEH +#define NULLSOFT_DOWNLOADSPARSEH + +#include "DownloadThread.h" + +class DownloadsParse : public DownloadThread +{ +public: + virtual void ReadNodes(const wchar_t *url); + +}; + +#endif diff --git a/Src/Plugins/Library/ml_wire/ExternalCOM.cpp b/Src/Plugins/Library/ml_wire/ExternalCOM.cpp new file mode 100644 index 00000000..0586810d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ExternalCOM.cpp @@ -0,0 +1,85 @@ +#include "main.h" +#include "./externalCOM.h" +#include "./util.h" +#include "./rssCOM.h" + +#define DISPTABLE_CLASS ExternalCOM + +DISPTABLE_BEGIN() + DISPENTRY_ADD(DISPATCH_PODCAST, L"Podcast", OnPodcast) +DISPTABLE_END + +#undef DISPTABLE_CLASS + +ExternalCOM::ExternalCOM() +{} + +ExternalCOM::~ExternalCOM() +{} + +HRESULT ExternalCOM::CreateInstance(ExternalCOM **instance) +{ + if (NULL == instance) return E_POINTER; + + *instance = new ExternalCOM(); + if (NULL == *instance) return E_OUTOFMEMORY; + + return S_OK; +} + +STDMETHODIMP_( ULONG ) ExternalCOM::AddRef( void ) +{ + return _ref.fetch_add( 1 ); +} + +STDMETHODIMP_( ULONG ) ExternalCOM::Release( void ) +{ + if ( 0 == _ref.load() ) + return _ref.load(); + + LONG r = _ref.fetch_sub( 1 ); + if ( 0 == r ) + delete( this ); + + return r; +} + +STDMETHODIMP ExternalCOM::QueryInterface(REFIID riid, PVOID *ppvObject) +{ + if (NULL == ppvObject) return E_POINTER; + + if (IsEqualIID(riid, IID_IDispatch)) + *ppvObject = static_cast<IDispatch*>(this); + else if (IsEqualIID(riid, IID_IUnknown)) + *ppvObject = static_cast<IUnknown*>(this); + else + { + *ppvObject = NULL; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + + +HRESULT ExternalCOM::OnPodcast(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) +{ + if (NULL != pvarResult) + { + VariantInit(pvarResult); + + RssCOM *rss; + if (SUCCEEDED(RssCOM::CreateInstance(&rss))) + { + V_VT(pvarResult) = VT_DISPATCH; + V_DISPATCH(pvarResult) = rss; + } + else + { + V_VT(pvarResult) = VT_NULL; + } + + } + return S_OK; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ExternalCOM.h b/Src/Plugins/Library/ml_wire/ExternalCOM.h new file mode 100644 index 00000000..df792ad6 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ExternalCOM.h @@ -0,0 +1,40 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_EXTERNAL_HEADER +#define NULLSOFT_PODCAST_PLUGIN_EXTERNAL_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> +#include <atomic> + +#include "../nu/dispatchTable.h" + +class ExternalCOM : public IDispatch +{ +public: + typedef enum + { + DISPATCH_PODCAST = 777, + } DispatchCodes; + +protected: + ExternalCOM(); + ~ExternalCOM(); + +public: + static HRESULT CreateInstance(ExternalCOM **instance); + + /* IUnknown*/ + STDMETHOD(QueryInterface)(REFIID riid, PVOID *ppvObject); + STDMETHOD_(ULONG, AddRef)(void); + STDMETHOD_(ULONG, Release)(void); + +protected: + DISPTABLE_INCLUDE(); + DISPHANDLER_REGISTER(OnPodcast); + + std::atomic<std::size_t> _ref = 1; +}; + +#endif //NULLSOFT_PODCAST_PLUGIN_EXTERNAL_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Factory.cpp b/Src/Plugins/Library/ml_wire/Factory.cpp new file mode 100644 index 00000000..c1ba56b3 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Factory.cpp @@ -0,0 +1,64 @@ +#include "api__ml_wire.h" +#include "Factory.h" +#include "Wire.h" + +static const char serviceName[] = "Podcasts"; + +FOURCC PodcastsFactory::GetServiceType() +{ + return WaSvc::UNIQUE; +} + +const char *PodcastsFactory::GetServiceName() +{ + return serviceName; +} + +GUID PodcastsFactory::GetGUID() +{ + return api_podcastsGUID; +} + +void *PodcastsFactory::GetInterface( int global_lock ) +{ +// if (global_lock) +// plugin.service->service_lock(this, (void *)ifc); + return &channels; +} + +int PodcastsFactory::SupportNonLockingInterface() +{ + return 1; +} + +int PodcastsFactory::ReleaseInterface( void *ifc ) +{ + //plugin.service->service_unlock(ifc); + return 1; +} + +const char *PodcastsFactory::GetTestString() +{ + return 0; +} + +int PodcastsFactory::ServiceNotify( int msg, int param1, int param2 ) +{ + return 1; +} + +#ifdef CBCLASS +#undef CBCLASS +#endif + +#define CBCLASS PodcastsFactory +START_DISPATCH; +CB( WASERVICEFACTORY_GETSERVICETYPE, GetServiceType ) +CB( WASERVICEFACTORY_GETSERVICENAME, GetServiceName ) +CB( WASERVICEFACTORY_GETGUID, GetGUID ) +CB( WASERVICEFACTORY_GETINTERFACE, GetInterface ) +CB( WASERVICEFACTORY_SUPPORTNONLOCKINGGETINTERFACE, SupportNonLockingInterface ) +CB( WASERVICEFACTORY_RELEASEINTERFACE, ReleaseInterface ) +CB( WASERVICEFACTORY_GETTESTSTRING, GetTestString ) +CB( WASERVICEFACTORY_SERVICENOTIFY, ServiceNotify ) +END_DISPATCH; diff --git a/Src/Plugins/Library/ml_wire/Factory.h b/Src/Plugins/Library/ml_wire/Factory.h new file mode 100644 index 00000000..dae98439 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Factory.h @@ -0,0 +1,22 @@ +#ifndef NULLSOFT_ML_WIRE_FACTORY_H +#define NULLSOFT_ML_WIRE_FACTORY_H +#include "api__ml_wire.h" +#include <api/service/waservicefactory.h> +#include <api/service/services.h> + +class PodcastsFactory : public waServiceFactory +{ +public: + FOURCC GetServiceType(); + const char *GetServiceName(); + GUID GetGUID(); + void *GetInterface(int global_lock); + int SupportNonLockingInterface(); + int ReleaseInterface(void *ifc); + const char *GetTestString(); + int ServiceNotify(int msg, int param1, int param2); + +protected: + RECVS_DISPATCH; +}; +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/FeedParse.cpp b/Src/Plugins/Library/ml_wire/FeedParse.cpp new file mode 100644 index 00000000..f39c7a7f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/FeedParse.cpp @@ -0,0 +1,38 @@ +#include "Main.h" +#include "FeedParse.h" + + +#include "RFCDate.h" + +#include "RSSParse.h" +#include "AtomParse.h" +#ifdef DEBUG +#include <iostream> +static void DisplayNodes(XMLNode &node) +{ + XMLNode::NodeMap::iterator nodeItr; + for (nodeItr = node.nodes.begin();nodeItr != node.nodes.end(); nodeItr++) + { + + for (XMLNode::NodeList::iterator itr = nodeItr->second.begin(); itr != nodeItr->second.end(); itr++) + { + std::wcerr << L"<" << nodeItr->first << L">" << std::endl; + DisplayNodes(**itr); + std::wcerr << L"</" << nodeItr->first << L">" << std::endl; + } + + } +} +#endif + +void FeedParse::ReadNodes(const wchar_t *url) +{ + const XMLNode *curNode = xmlDOM.GetRoot(); + + curNode = curNode->Get(L"rss"); + if (curNode) + { + ReadRSS(curNode, sync, loadingOwnFeed, url); + return ; + } +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/FeedParse.h b/Src/Plugins/Library/ml_wire/FeedParse.h new file mode 100644 index 00000000..580c2dd6 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/FeedParse.h @@ -0,0 +1,24 @@ +#ifndef NULLSOFT_FEEDPARSEH +#define NULLSOFT_FEEDPARSEH + +#include "DownloadThread.h" +#include "ChannelSync.h" +class FeedParse : public DownloadThread +{ +public: + FeedParse(ChannelSync *_sync, bool doWinampSpecificTags = false) + : sync(_sync), loadingOwnFeed(doWinampSpecificTags) + {} + + ~FeedParse() + { + sync = 0; + } + + virtual void ReadNodes(const wchar_t *url); +private: + ChannelSync *sync; + bool loadingOwnFeed; +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/FeedUtil.cpp b/Src/Plugins/Library/ml_wire/FeedUtil.cpp new file mode 100644 index 00000000..bf5b5ae8 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/FeedUtil.cpp @@ -0,0 +1,37 @@ +#include "main.h" +#include "FeedUtil.h" +#include "ChannelCheck.h" +#include "FeedParse.h" +#include "errors.h" +#include "./defaults.h" + + +int DownloadFeedInformation(Channel &newFeed) +{ + ChannelCheck check; + FeedParse downloader(&check, false); + + int ret = downloader.DownloadURL(newFeed.url); + if (ret != DOWNLOAD_SUCCESS) + return ret; + + if (!check.channel.title || !check.channel.title[0]) + return DOWNLOAD_NOTRSS; + + newFeed.SetTitle(check.channel.title); + if (check.channel.ttl) + { + newFeed.updateTime = check.channel.ttl * 60; + newFeed.autoUpdate = true; + } + else + { + newFeed.updateTime = ::updateTime; + newFeed.autoUpdate = ::autoUpdate; + } + + if (check.channel.url && check.channel.url[0]) + newFeed.SetURL(check.channel.url); + + return DOWNLOAD_SUCCESS; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/FeedUtil.h b/Src/Plugins/Library/ml_wire/FeedUtil.h new file mode 100644 index 00000000..466ef98d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/FeedUtil.h @@ -0,0 +1,8 @@ +#ifndef NULLSOFT_FEEDUTILH +#define NULLSOFT_FEEDUTILH + +#include "Feeds.h" + + +int DownloadFeedInformation(Channel &channel); +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Feeds.cpp b/Src/Plugins/Library/ml_wire/Feeds.cpp new file mode 100644 index 00000000..9e81705d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Feeds.cpp @@ -0,0 +1,298 @@ +#include "main.h" +#include "Feeds.h" +#include "./util.h" +#include "./defaults.h" +#include <algorithm> +#include <shlwapi.h> +#include "BackgroundDownloader.h" +#include <strsafe.h> + +static bool operator == (const RSS::Item &a, const RSS::Item &b) +{ + if(a.guid && a.guid[0] && b.guid && b.guid[0]) + return !wcscmp(a.guid, b.guid); + + if(a.publishDate && b.publishDate) + return a.publishDate == b.publishDate; + + if (a.url && a.url[0] && b.url && b.url[0]) + return !wcscmp(a.url, b.url); + + return a.url == b.url; +} + +Channel::Channel() +{ + Init(); +} + +Channel::Channel(const Channel ©) +{ + Init(); + operator =(copy); +} + +const Channel &Channel::operator =(const Channel ©) +{ + Reset(); + Init(); + url = _wcsdup(copy.url); + title = _wcsdup(copy.title); + link = _wcsdup(copy.link); + description = _wcsdup(copy.description); + ttl=copy.ttl; + updateTime=copy.updateTime; + lastUpdate=copy.lastUpdate; + autoDownloadEpisodes=copy.autoDownloadEpisodes; + autoDownload=copy.autoDownload; + autoUpdate=copy.autoUpdate; + useDefaultUpdate=copy.useDefaultUpdate; + needsRefresh=copy.needsRefresh; + items=copy.items; + return *this; +} + +Channel::~Channel() +{ + Reset(); +} + +void Channel::Init() +{ + url =0; + title = 0; + link = 0; + description = 0; + ttl=0; + lastUpdate=0; + autoUpdate = ::autoUpdate; + updateTime = ::updateTime; + autoDownload = ::autoDownload; + autoDownloadEpisodes = ::autoDownloadEpisodes; + useDefaultUpdate=true; + needsRefresh=false; +} + +void Channel::Reset() +{ + free(url); + free(title); + free(link); + free(description); +} + +void Channel::UpdateFrom(const Channel ©) +{ + if (copy.url && copy.url[0]) + SetURL(copy.url); + + SetTitle(copy.title); + SetLink(copy.link); + SetDescription(copy.description); + if (copy.ttl) + ttl=copy.ttl; + + ItemList::const_iterator itr; + + for (itr=copy.items.begin();itr!=copy.items.end();itr++) + { + const RSS::Item &b = *itr; + if ( b.url && b.url[0] ) + { + ((RSS::Item*)&b)->downloaded = IsPodcastDownloaded(b.url); + } + } + + // update to the latest default setting + if (useDefaultUpdate) + { + autoUpdate = ::autoUpdate; + updateTime = ::updateTime; + autoDownload = ::autoDownload; + autoDownloadEpisodes = ::autoDownloadEpisodes; + } + + items.clear(); // benski> added for 5.23 + for (itr=copy.items.begin();itr!=copy.items.end();itr++) + { + items.insert(items.begin(), *itr); + } + + if(autoDownload) + { + SortByDate(); + size_t idx = items.size(); + if (idx) + { + int episodeCount = 0; + do + { + idx--; + const RSS::Item &b = items[idx]; + if(b.url && b.url[0]) + { + episodeCount++; + if (!b.downloaded) + { + WCHAR szPath[MAX_PATH *2] = {0}; + if (SUCCEEDED(((RSS::Item*)&b)->GetDownloadFileName(title, szPath, ARRAYSIZE(szPath), TRUE))) + { + wchar_t* url = urlencode(b.url); + downloader.Download(url, szPath, title, b.itemName, b.publishDate); + ((RSS::Item*)&b)->downloaded = true; + free(url); + } + } + + } + } while (episodeCount<autoDownloadEpisodes && idx); + } + } +} + +bool Channel::operator == (const Channel &compare) +{ + // changed from basing on the title as this allows for podcasts + // with the same name to still work instead of being mangled as + // was able to happen when this based things on the title value + if (!compare.url || !compare.url[0]) + return false; + return !wcscmp(url, compare.url); +} + +bool TitleMediaSort(const RSS::Item &item1, const RSS::Item &item2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, item1.itemName, -1, item2.itemName, -1)); +} + +void Channel::SortByTitle() +{ + std::sort(items.begin(), items.end(), TitleMediaSort); +} + +static bool ItemMediaSort(const RSS::Item &item1, const RSS::Item &item2) +{ + if (!item1.url || !item1.url[0]) + return false; + + if (!item2.url || !item2.url[0]) + return true; + + if (!item2.listened) + return false; + + if (item1.listened) + return false; + return true; +} + +bool ParseDuration(const wchar_t *duration, int *out_hours, int *out_minutes, int *out_seconds); +static bool ItemMediaTimeSort(const RSS::Item &item1, const RSS::Item &item2) +{ + if (!item1.duration || !item1.duration[0]) + return false; + + if (!item2.duration || !item2.duration[0]) + return true; + + int h1, h2, m1, m2, s1, s2; + if (!ParseDuration(item1.duration, &h1, &m1, &s1)) + return false; + + if (!ParseDuration(item2.duration, &h2, &m2, &s2)) + return true; + + if (h1 < h2) + return true; + else if (h1 > h2) + return false; + + if (m1 < m2) + return true; + else if (m1 > m2) + return false; + + if (s1 < s2) + return true; + else + return false; +} + +static bool ItemMediaSizeSort(const RSS::Item &item1, const RSS::Item &item2) +{ + if (!item1.size) + return false; + + if (!item2.size) + return true; + + return item1.size < item2.size; +} + +void Channel::SortByMedia() +{ + std::sort(items.begin(), items.end(), ItemMediaSort); +} + +void Channel::SortByMediaTime() +{ + std::sort(items.begin(), items.end(), ItemMediaTimeSort); +} + +void Channel::SortByMediaSize() +{ + std::sort(items.begin(), items.end(), ItemMediaSizeSort); +} + +bool ItemDateSort(const RSS::Item &item1, const RSS::Item &item2) +{ + return (item1.publishDate < item2.publishDate); +} + +void Channel::SortByDate() +{ + std::sort(items.begin(), items.end(), ItemDateSort); +} + +int Channel::GetTitle(wchar_t *str, size_t len) +{ + if (str && len) + { + str[0]=0; + if (title && title[0]) + StringCchCopyW(str, len, title); + return 0; + } + return 1; +} + +void Channel::SetURL(const wchar_t *val) +{ + free(url); + url = _wcsdup(val); +} + +void Channel::SetTitle(const wchar_t *val) +{ + free(title); + title = _wcsdup(val); +} + +void Channel::SetLink(const wchar_t *val) +{ + free(link); + link = _wcsdup(val); +} + +void Channel::SetDescription(const wchar_t *val) +{ + free(description); + description = _wcsdup(val); +} + +#undef CBCLASS +#define CBCLASS Channel +START_DISPATCH; +CB(IFC_PODCAST_GETTITLE, GetTitle) +END_DISPATCH; +#undef CBCLASS
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Feeds.h b/Src/Plugins/Library/ml_wire/Feeds.h new file mode 100644 index 00000000..a8075627 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Feeds.h @@ -0,0 +1,50 @@ +#ifndef NULLSOFT_FEEDSH +#define NULLSOFT_FEEDSH + +#include "ifc_podcast.h" +#include "Item.h" +#include <vector> + +class Channel : public ifc_podcast +{ +public: + typedef std::vector<RSS::Item> ItemList; + Channel(); + Channel(const Channel ©); + const Channel &operator =(const Channel ©); + ~Channel(); + void SortByTitle(), SortByMedia(), SortByMediaTime(), SortByDate(), SortByMediaSize(); + bool operator == (const Channel &compare); + //void operator = (const Channel ©); + void UpdateFrom(const Channel ©); + + unsigned int ttl; + __time64_t updateTime, lastUpdate; + int autoDownloadEpisodes; + bool autoUpdate; + bool useDefaultUpdate; + bool autoDownload; + bool needsRefresh; + // TODO: std::wstring downloadLocation; + ItemList items; + + void SetURL(const wchar_t *val); + void SetTitle(const wchar_t *val); + void SetLink(const wchar_t *val); + void SetDescription(const wchar_t *val); + + wchar_t *url, *title, *link, *description; + +public: // ifc_podcast interface + int GetTitle(wchar_t *str, size_t len); + +private: + void Init(); + void Reset(); + + +protected: + RECVS_DISPATCH; +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/FeedsDialog.h b/Src/Plugins/Library/ml_wire/FeedsDialog.h new file mode 100644 index 00000000..1409b1ac --- /dev/null +++ b/Src/Plugins/Library/ml_wire/FeedsDialog.h @@ -0,0 +1,5 @@ +#ifndef NULLSOFT_FEEDSDIALOGH +#define NULLSOFT_FEEDSDIALOGH + +BOOL CALLBACK FeedsProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Item.cpp b/Src/Plugins/Library/ml_wire/Item.cpp new file mode 100644 index 00000000..37126a9d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Item.cpp @@ -0,0 +1,174 @@ +#include "Item.h" +#include "util.h" +#include "defaults.h" +#include <shlwapi.h> +#include <strsafe.h> + +void RSS::Item::Reset() +{ + free(itemName); + free(url); + free(sourceUrl); + free(guid); + free(description); + free(link); + free(duration); +} + +void RSS::Item::Init() +{ + listened = false; + publishDate = 0; + generatedDate = true; + downloaded=false; + itemName=0; + url=0; + sourceUrl=0; + guid=0; + description=0; + link=0; + duration=0; + size=0; +} + +RSS::Item::Item() +{ + Init(); +} + +RSS::Item::~Item() +{ + Reset(); +} + +RSS::Item::Item(const RSS::Item ©) +{ + Init(); + operator =(copy); +} + +const RSS::Item &RSS::Item::operator =(const RSS::Item ©) +{ + Reset(); + Init(); + listened=copy.listened; + publishDate = copy.publishDate; + generatedDate = copy.generatedDate; + downloaded=copy.downloaded; + itemName=_wcsdup(copy.itemName); + url=_wcsdup(copy.url); + sourceUrl=_wcsdup(copy.sourceUrl); + guid=_wcsdup(copy.guid); + description=_wcsdup(copy.description); + link=_wcsdup(copy.link); + duration=wcsdup(copy.duration); + size = copy.size; + return *this; +} + +HRESULT RSS::Item::GetDownloadFileName(const wchar_t *channelName, wchar_t *buffer, int bufferMax, BOOL fValidatePath) const +{ + if (NULL == buffer || NULL == channelName) return E_INVALIDARG; + buffer[0] = L'\0'; + + WCHAR szBuffer[MAX_PATH] = {0}; + + if (FAILED(StringCchCopyN(szBuffer, ARRAYSIZE(szBuffer), channelName, 100))) + return E_UNEXPECTED; + + Plugin_CleanDirectory(szBuffer); + Plugin_ReplaceBadPathChars(szBuffer); + + if (L'\0' == *szBuffer) + StringCchCopy(szBuffer, ARRAYSIZE(szBuffer), L"UnknownChannel"); + + if (FALSE == PathCombine(buffer, defaultDownloadPath, szBuffer)) + return E_FAIL; + + if (FALSE != fValidatePath && FAILED(Plugin_EnsurePathExist(buffer))) + return E_FAIL; + + LPWSTR cursor = szBuffer; + size_t remaining = ARRAYSIZE(szBuffer); + + tm* time = _localtime64(&publishDate); + if(NULL != time && publishDate > 0) + { + StringCchPrintfEx(cursor, remaining, &cursor, &remaining, STRSAFE_NULL_ON_FAILURE, + L"%04d-%02d-%02d - ", time->tm_year+1900, time->tm_mon+1, time->tm_mday); + } + + LPWSTR t = cursor; + if (FAILED(StringCchCopyNEx(cursor, remaining, itemName, 100, &cursor, &remaining, 0))) + return E_UNEXPECTED; + + INT offset = Plugin_CleanDirectory(t); + if (0 != offset) + { + remaining += offset; + cursor -= offset; + } + + if (t == cursor) + StringCchCopyEx(cursor, remaining, L"UnknownItem", &cursor, &remaining, 0); + else + Plugin_ReplaceBadPathChars(t); + + if (FAILED(Plugin_FileExtensionFromUrl(cursor, (INT)remaining, url, L".mp3"))) + return E_UNEXPECTED; + + if (FALSE == PathAppend(buffer, szBuffer)) + return E_FAIL; + + return S_OK; +} + +void RSS::MutableItem::SetLink(const wchar_t *value) +{ + free(link); + link = _wcsdup(value); +} + +void RSS::MutableItem::SetItemName(const wchar_t *value) +{ + free(itemName); + itemName = _wcsdup(value); +} + +void RSS::MutableItem::SetURL(const wchar_t *value) +{ + free(url); + url = _wcsdup(value); +} + +void RSS::MutableItem::SetSourceURL(const wchar_t *value) +{ + free(sourceUrl); + sourceUrl = _wcsdup(value); +} + +void RSS::MutableItem::SetGUID(const wchar_t *value) +{ + free(guid); + guid = _wcsdup(value); +} + +void RSS::MutableItem::SetDescription(const wchar_t *value) +{ + free(description); + description = _wcsdup(value); +} + +void RSS::MutableItem::SetDuration(const wchar_t *value) +{ + free(duration); + duration = _wcsdup(value); +} + +void RSS::MutableItem::SetSize(const wchar_t * _size) +{ + if (_size) + size = _wtoi64(_size); + else + size=0; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Item.h b/Src/Plugins/Library/ml_wire/Item.h new file mode 100644 index 00000000..e7a5e82b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Item.h @@ -0,0 +1,50 @@ +#pragma once +#include <bfc/platform/types.h> +#include <time.h> +#include <windows.h> + +namespace RSS +{ + class Item + { + public: + Item(); + ~Item(); + Item(const Item ©); + const Item &operator =(const Item ©); + + HRESULT GetDownloadFileName(const wchar_t *channelName, wchar_t *buffer, int bufferMax, BOOL fValidatePath) const; + bool listened; + bool downloaded; + __time64_t publishDate; + bool generatedDate; + + //protected: + wchar_t *itemName; + wchar_t *url; + wchar_t *sourceUrl; + wchar_t *guid; + wchar_t *description; + wchar_t *link; + wchar_t *duration; + int64_t size; + + private: + void Init(); + void Reset(); + }; + + class MutableItem : public Item + { + public: + void SetItemName(const wchar_t *value); + void SetLink(const wchar_t *value); + void SetURL(const wchar_t *value); + void SetSourceURL(const wchar_t *value); + void SetGUID(const wchar_t *value); + void SetDescription(const wchar_t *value); + void SetDuration(const wchar_t *value); + void SetSize(const wchar_t * _size); + }; + +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/JSAPI2_Creator.cpp b/Src/Plugins/Library/ml_wire/JSAPI2_Creator.cpp new file mode 100644 index 00000000..9956fd3f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/JSAPI2_Creator.cpp @@ -0,0 +1,94 @@ +#include "main.h" +#include "JSAPI2_Creator.h" +#include "JSAPI2_PodcastsAPI.h" +#include "api__ml_wire.h" + +IDispatch *JSAPI2_Creator::CreateAPI(const wchar_t *name, const wchar_t *key, JSAPI::ifc_info *info) +{ + if (!_wcsicmp(name, L"Podcasts")) + return new JSAPI2::PodcastsAPI(key, info); + else + return 0; +} + +int JSAPI2_Creator::PromptForAuthorization(HWND parent, const wchar_t *group, const wchar_t *action, const wchar_t *authorization_key, JSAPI2::api_security::AuthorizationData *data) +{ + if (group && !_wcsicmp(group, L"podcasts")) + { + const wchar_t *title_str = AGAVE_API_JSAPI2_SECURITY->GetAssociatedName(authorization_key); + return AGAVE_API_JSAPI2_SECURITY->SecurityPrompt(parent, title_str, L"This service is trying to subscribe you to a podcast.", JSAPI2::svc_apicreator::AUTHORIZATION_FLAG_GROUP_ONLY); + } + else + return JSAPI2::svc_apicreator::AUTHORIZATION_UNDEFINED; +} + + +#define CBCLASS JSAPI2_Creator +START_DISPATCH; +CB(JSAPI2_SVC_APICREATOR_CREATEAPI, CreateAPI); +CB(JSAPI2_SVC_APICREATOR_PROMPTFORAUTHORIZATION, PromptForAuthorization); +END_DISPATCH; +#undef CBCLASS + +static JSAPI2_Creator jsapi2_svc; +static const char serviceName[] = "Podcast Javascript Objects"; + +// {EE2C54DB-E609-410a-A962-573BA2F9C3AC} +static const GUID jsapi2_factory_guid = +{ 0xee2c54db, 0xe609, 0x410a, { 0xa9, 0x62, 0x57, 0x3b, 0xa2, 0xf9, 0xc3, 0xac } }; + +FOURCC JSAPI2Factory::GetServiceType() +{ + return jsapi2_svc.getServiceType(); +} + +const char *JSAPI2Factory::GetServiceName() +{ + return serviceName; +} + +GUID JSAPI2Factory::GetGUID() +{ + return jsapi2_factory_guid; +} + +void *JSAPI2Factory::GetInterface(int global_lock) +{ +// if (global_lock) +// plugin.service->service_lock(this, (void *)ifc); + return &jsapi2_svc; +} + +int JSAPI2Factory::SupportNonLockingInterface() +{ + return 1; +} + +int JSAPI2Factory::ReleaseInterface(void *ifc) +{ + //plugin.service->service_unlock(ifc); + return 1; +} + +const char *JSAPI2Factory::GetTestString() +{ + return 0; +} + +int JSAPI2Factory::ServiceNotify(int msg, int param1, int param2) +{ + return 1; +} + +#define CBCLASS JSAPI2Factory +START_DISPATCH; +CB(WASERVICEFACTORY_GETSERVICETYPE, GetServiceType) +CB(WASERVICEFACTORY_GETSERVICENAME, GetServiceName) +CB(WASERVICEFACTORY_GETGUID, GetGUID) +CB(WASERVICEFACTORY_GETINTERFACE, GetInterface) +CB(WASERVICEFACTORY_SUPPORTNONLOCKINGGETINTERFACE, SupportNonLockingInterface) +CB(WASERVICEFACTORY_RELEASEINTERFACE, ReleaseInterface) +CB(WASERVICEFACTORY_GETTESTSTRING, GetTestString) +CB(WASERVICEFACTORY_SERVICENOTIFY, ServiceNotify) +END_DISPATCH; +#undef CBCLASS
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/JSAPI2_Creator.h b/Src/Plugins/Library/ml_wire/JSAPI2_Creator.h new file mode 100644 index 00000000..fd85a3f0 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/JSAPI2_Creator.h @@ -0,0 +1,31 @@ +#pragma once + +#include "../Winamp/JSAPI2_svc_apicreator.h" + +#include <api/service/waservicefactory.h> +#include <api/service/services.h> + +class JSAPI2Factory : public waServiceFactory +{ +public: + FOURCC GetServiceType(); + const char *GetServiceName(); + GUID GetGUID(); + void *GetInterface(int global_lock); + int SupportNonLockingInterface(); + int ReleaseInterface(void *ifc); + const char *GetTestString(); + int ServiceNotify(int msg, int param1, int param2); + +protected: + RECVS_DISPATCH; +}; + + +class JSAPI2_Creator : public JSAPI2::svc_apicreator +{ + IDispatch *CreateAPI(const wchar_t *name, const wchar_t *key, JSAPI::ifc_info *info); + int PromptForAuthorization(HWND parent, const wchar_t *group, const wchar_t *action, const wchar_t *authorization_key, JSAPI2::api_security::AuthorizationData *data); +protected: + RECVS_DISPATCH; +};
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.cpp b/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.cpp new file mode 100644 index 00000000..bab563d1 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.cpp @@ -0,0 +1,109 @@ +#include "JSAPI2_PodcastsAPI.h" +#include "../Winamp/JSAPI.h" +#include "api__ml_wire.h" +#include "./rssCOM.h" + +JSAPI2::PodcastsAPI::PodcastsAPI( const wchar_t *_key, JSAPI::ifc_info *_info ) +{ + info = _info; + key = _key; +} + +enum +{ + DISP_PODCASTS_SUBSCRIBE, +}; + +#define DISP_TABLE \ + CHECK_ID(Subscribe, DISP_PODCASTS_SUBSCRIBE)\ + +#define CHECK_ID(str, id) if (wcscmp(rgszNames[i], L## #str) == 0) { rgdispid[i] = id; continue; } +HRESULT JSAPI2::PodcastsAPI::GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgdispid) +{ + bool unknowns = false; + for (unsigned int i = 0;i != cNames;i++) + { + DISP_TABLE + + rgdispid[i] = DISPID_UNKNOWN; + unknowns = true; + + } + if (unknowns) + return DISP_E_UNKNOWNNAME; + else + return S_OK; +} + +HRESULT JSAPI2::PodcastsAPI::GetTypeInfo(unsigned int itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo) +{ + return E_NOTIMPL; +} + +HRESULT JSAPI2::PodcastsAPI::GetTypeInfoCount(unsigned int FAR * pctinfo) +{ + return E_NOTIMPL; +} + +HRESULT JSAPI2::PodcastsAPI::Subscribe(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) +{ + JSAPI_VERIFY_METHOD(wFlags); + JSAPI_VERIFY_PARAMCOUNT(pdispparams, 1); + + if (AGAVE_API_JSAPI2_SECURITY->GetActionAuthorization(L"podcasts", L"subscribe", key, info, JSAPI2::api_security::ACTION_PROMPT) == JSAPI2::api_security::ACTION_ALLOWED) + { + RssCOM::SubscribeUrl(JSAPI_PARAM(pdispparams, 1).bstrVal, pvarResult); + } + else + { + JSAPI_INIT_RESULT(pvarResult, VT_BOOL); + JSAPI_SET_RESULT(pvarResult, boolVal, VARIANT_FALSE); + } + + return S_OK; +} + +#undef CHECK_ID +#define CHECK_ID(str, id) case id: return str(wFlags, pdispparams, pvarResult, puArgErr); +HRESULT JSAPI2::PodcastsAPI::Invoke(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, EXCEPINFO FAR * pexecinfo, unsigned int FAR *puArgErr) +{ + switch (dispid) + { + DISP_TABLE + } + return DISP_E_MEMBERNOTFOUND; +} + +STDMETHODIMP JSAPI2::PodcastsAPI::QueryInterface(REFIID riid, PVOID *ppvObject) +{ + if (!ppvObject) + return E_POINTER; + + else if (IsEqualIID(riid, IID_IDispatch)) + *ppvObject = (IDispatch *)this; + else if (IsEqualIID(riid, IID_IUnknown)) + *ppvObject = this; + else + { + *ppvObject = NULL; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + +ULONG JSAPI2::PodcastsAPI::AddRef(void) +{ + return _refCount.fetch_add( 1 ); +} + + +ULONG JSAPI2::PodcastsAPI::Release( void ) +{ + LONG lRef = _refCount.fetch_sub( 1 ); + if ( lRef == 0 ) + delete this; + + return lRef; +} diff --git a/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.h b/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.h new file mode 100644 index 00000000..faee324a --- /dev/null +++ b/Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.h @@ -0,0 +1,30 @@ +#pragma once + +#include <ocidl.h> +#include <atomic> + +#include "../Winamp/JSAPI_Info.h" + +namespace JSAPI2 +{ + class PodcastsAPI : public IDispatch + { + public: + PodcastsAPI(const wchar_t *_key, JSAPI::ifc_info *info); + STDMETHOD(QueryInterface)(REFIID riid, PVOID *ppvObject); + STDMETHOD_(ULONG, AddRef)(void); + STDMETHOD_(ULONG, Release)(void); + // *** IDispatch Methods *** + STDMETHOD (GetIDsOfNames)(REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgdispid); + STDMETHOD (GetTypeInfo)(unsigned int itinfo, LCID lcid, ITypeInfo FAR* FAR* pptinfo); + STDMETHOD (GetTypeInfoCount)(unsigned int FAR * pctinfo); + STDMETHOD (Invoke)(DISPID dispid, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, EXCEPINFO FAR * pexecinfo, unsigned int FAR *puArgErr); + private: + const wchar_t *key; + volatile std::atomic<std::size_t> _refCount = 1; + JSAPI::ifc_info *info; + + STDMETHOD (Subscribe)(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr); + + }; +} diff --git a/Src/Plugins/Library/ml_wire/Loader.cpp b/Src/Plugins/Library/ml_wire/Loader.cpp new file mode 100644 index 00000000..74c944b5 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Loader.cpp @@ -0,0 +1,100 @@ +#include "main.h" +#include "api.h" +#include "../winamp/wa_ipc.h" +#include "DownloadStatus.h" +using namespace Nullsoft::Utility; +static WNDPROC wa_oldWndProc=0; + +/* protocol must be all lower case */ +bool ProtocolMatch(const char *file, const char *protocol) +{ + size_t protSize = strlen(protocol); + for (size_t i=0;i!=protSize;i++) + { + if (!file[i] + || tolower(file[i]) != protocol[i]) + return false; + } + return true; +} + +LRESULT CALLBACK LoaderProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ +#if 0 // not ready to take links yet... too buggy/weird at this point ... + if (uMsg == WM_COPYDATA) + { + COPYDATASTRUCT *copyData = (COPYDATASTRUCT *)lParam; + if (copyData->dwData == IPC_ENQUEUEFILE) + { + const char *file = (const char *)copyData->lpData; + if (ProtocolMatch(file, "feed://")) + { + Channel newFeed; + newFeed.url = AutoWide((const char *)copyData->lpData); + if (DownloadFeedInformation(newFeed)==DOWNLOADRSS_SUCCESS) + { + AutoLock lock(channels); + channels.push_back(newFeed); + } + return 0; + } + else + if (ProtocolMatch(file, "http://")) + { + // nothing for now, we want to do a head request tho + JNL_HTTPGet head; + head.connect(file, 0, "HEAD"); + int ret; + do + { + ret = head.run(); + Sleep(50); + } while (ret != -1 && ret != 1); + + if (ret!=-1) + { + char *contentType = head.getheader("Content-Type"); +// if (contentType) + //MessageBoxA(NULL, contentType, contentType, MB_OK); + if (strstr(contentType, "application/rss+xml") == contentType) + { + MessageBox(NULL, L"woo!", L"woo!", MB_OK); + return 0; + } + if (strstr(contentType, "application/xml") == contentType) + { + MessageBox(NULL, L"regular xml", L"application/xml", MB_OK); + return 0; + } + if (strstr(contentType, "text/xml") == contentType) + { + MessageBox(NULL, L"regular xml", L"text/xml", MB_OK); + return 0; + } + + } + + } + } + } +#endif + if (wa_oldWndProc) + return CallWindowProc(wa_oldWndProc, hwnd, uMsg, wParam, lParam); + else + return 0; +} + +void BuildLoader(HWND winampWindow) +{ + if (IsWindowUnicode(winampWindow)) + wa_oldWndProc=(WNDPROC) SetWindowLongPtrW(winampWindow,GWLP_WNDPROC,(LONG_PTR)LoaderProc); + else + wa_oldWndProc=(WNDPROC) SetWindowLongPtrA(winampWindow,GWLP_WNDPROC,(LONG_PTR)LoaderProc); +} + +void DestroyLoader(HWND winampWindow) +{ + //if (wa_oldWndProc) + // SetWindowLong(winampWindow,GWL_WNDPROC,(LONG)wa_oldWndProc); + //wa_oldWndProc=0; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Loader.h b/Src/Plugins/Library/ml_wire/Loader.h new file mode 100644 index 00000000..e11d792f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Loader.h @@ -0,0 +1,6 @@ +#ifndef NULLSOFT_LOADERH +#define NULLSOFT_LOADERH + +void BuildLoader(HWND winampWindow); + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Main.cpp b/Src/Plugins/Library/ml_wire/Main.cpp new file mode 100644 index 00000000..37e0ff6b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Main.cpp @@ -0,0 +1,469 @@ +#include "main.h" +#include "Cloud.h" +#include "DownloadThread.h" +#include "OPMLParse.h" +#include "FeedsDialog.h" +#include "DownloadsParse.h" +#include "XMLWriter.h" +#include "FeedParse.h" +#include "DownloadsDialog.h" +#include "Preferences.h" +#include "..\..\General\gen_ml/ml.h" +#include "Defaults.h" +#include "Wire.h" +#include "..\..\General\gen_ml/ml_ipc_0313.h" +#include "RSSCOM.h" + +#include "api__ml_wire.h" +#include "Downloaded.h" +#include "DownloadStatus.h" +#include "Factory.h" +#include "JSAPI2_Creator.h" +#include "./navigation.h" + +#include <strsafe.h> + +#include "PCastFactory.h" + +Cloud cloud; +WireManager channelMgr; + +int treeId = 0, allId = 0 +#if 0 + , discoverId = 0 +#endif + ; +MLTREEITEMW downloadsTree; +wchar_t downloadsStr[64] = {0}, *ml_cfg = 0, + feedsXmlFileName[1024] = {0}, + feedsXmlFileNameBackup[1024] = {0}, + rssXmlFileName[1024] = {0}, + rssXmlFileNameBackup[1024] = {0}; + +ATOM VIEWPROP = 0; + +api_downloadManager *WAC_API_DOWNLOADMANAGER = 0; + +static int Init(); +static void Quit(); +static INT_PTR MessageProc(int message_type, INT_PTR param1, INT_PTR param2, INT_PTR param3); + +DWORD threadStorage=TLS_OUT_OF_INDEXES; +extern "C" winampMediaLibraryPlugin plugin = + { + MLHDR_VER, + "nullsoft(ml_wire.dll)", + Init, + Quit, + MessageProc, + 0, + 0, + 0, + }; + +static prefsDlgRecW preferences; + +static wchar_t preferencesName[64] = {0}; +void SaveChannels(ChannelList &channels) +{ + // generate a backup of the feeds.xml to cope with it being wiped randomly for some + // people or it not being able to be saved in time e.g. during a forced OS shutdown + CopyFile(feedsXmlFileName, feedsXmlFileNameBackup, FALSE); + SaveChannels(feedsXmlFileName, channels); +} + +void SaveAll(bool rss_only) +{ + if (AGAVE_API_STATS) + AGAVE_API_STATS->SetStat(api_stats::PODCAST_COUNT, (int)channels.size()); + + if(!rss_only) + SaveChannels(channels); + + // generate a backup of the rss.xml to cope with it being wiped randomly for some + // people or it not being able to be saved in time e.g. during a forced OS shutdown + CopyFile(rssXmlFileName, rssXmlFileNameBackup, FALSE); + SaveSettings(rssXmlFileName, downloadedFiles); +} + +static PodcastsFactory podcastsFactory; + +HANDLE hMainThread = NULL; + +HCURSOR hDragNDropCursor = NULL; +int winampVersion = 0; + +JSAPI2::api_security *AGAVE_API_JSAPI2_SECURITY = 0; +JSAPI2Factory jsapi2Creator; + +obj_ombrowser *browserManager = NULL; +api_application *applicationApi = NULL; +api_stats *AGAVE_API_STATS = 0; +api_threadpool *WASABI_API_THREADPOOL = 0; +api_explorerfindfile *WASABI_API_EXPLORERFINDFILE = 0; + +// wasabi based services for localisation support +api_language *WASABI_API_LNG = 0; +HINSTANCE WASABI_API_LNG_HINST = 0; +HINSTANCE WASABI_API_ORIG_HINST = 0; + +static PCastFactory pcastFactory; + +static void CALLBACK InitTimer(HWND hwnd, UINT uMsg, UINT_PTR eventId, ULONG elapsed) +{ + KillTimer(hwnd, eventId); + + { + Nullsoft::Utility::AutoLock lock (channels LOCKNAME("feeds.xml load!")); + + FeedParse downloader(&channelMgr, true); + downloader.DownloadFile(feedsXmlFileName); + if (AGAVE_API_STATS) + AGAVE_API_STATS->SetStat(api_stats::PODCAST_COUNT, (int)channels.size()); + } + + if (updateOnLaunch) + { + cloud.RefreshAll(); + } + + cloud.Init(); + cloud.Pulse(); +} + +int Init() +{ + hMainThread = GetCurrentThread(); + hDragNDropCursor = LoadCursor( GetModuleHandle( L"gen_ml.dll" ), MAKEINTRESOURCE( ML_IDC_DRAGDROP ) ); + threadStorage = TlsAlloc(); + + if ( 0 == VIEWPROP ) + { + VIEWPROP = GlobalAddAtom( L"Nullsoft_PodcastView" ); + if ( VIEWPROP == 0 ) + return 1; + } + + winampVersion = SendMessage( plugin.hwndWinampParent, WM_WA_IPC, 0, IPC_GETVERSION ); + ml_cfg = (wchar_t*)SendMessage(plugin.hwndWinampParent, WM_WA_IPC, 0, IPC_GETMLINIFILEW); + + plugin.service->service_register( &podcastsFactory ); + plugin.service->service_register( &jsapi2Creator ); + plugin.service->service_register( &pcastFactory ); + + // loader so that we can get the localisation service api for use + waServiceFactory *sf = plugin.service->service_getServiceByGuid( languageApiGUID ); + if ( sf ) + WASABI_API_LNG = reinterpret_cast<api_language*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( JSAPI2::api_securityGUID ); + if ( sf ) + AGAVE_API_JSAPI2_SECURITY = reinterpret_cast<JSAPI2::api_security*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( applicationApiServiceGuid ); + if ( sf ) + WASABI_API_APP = reinterpret_cast<api_application*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( OBJ_OmBrowser ); + if ( sf ) + OMBROWSERMNGR = reinterpret_cast<obj_ombrowser*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( AnonymousStatsGUID ); + if ( sf ) + AGAVE_API_STATS = reinterpret_cast<api_stats*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( ThreadPoolGUID ); + if ( sf ) + WASABI_API_THREADPOOL = reinterpret_cast<api_threadpool*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( DownloadManagerGUID ); + if ( sf ) + WAC_API_DOWNLOADMANAGER = reinterpret_cast<api_downloadManager*>( sf->getInterface() ); + + sf = plugin.service->service_getServiceByGuid( ExplorerFindFileApiGUID ); + if ( sf ) + WASABI_API_EXPLORERFINDFILE = reinterpret_cast<api_explorerfindfile*>( sf->getInterface() ); + + // need to have this initialised before we try to do anything with localisation features + WASABI_API_START_LANG( plugin.hDllInstance, MlWireLangGUID ); + + static wchar_t szDescription[ 256 ]; + StringCchPrintfW( szDescription, ARRAYSIZE( szDescription ), WASABI_API_LNGSTRINGW( IDS_PLUGIN_NAME ), PLUGIN_VERSION_MAJOR, PLUGIN_VERSION_MINOR ); + plugin.description = (char*)szDescription; + + mediaLibrary.library = plugin.hwndLibraryParent; + mediaLibrary.winamp = plugin.hwndWinampParent; + mediaLibrary.instance = plugin.hDllInstance; + + RssCOM *rss; + if (SUCCEEDED(RssCOM::CreateInstance(&rss))) + { + DispatchInfo dispatchInfo; + dispatchInfo.name = (LPWSTR)rss->GetName(); + dispatchInfo.dispatch = rss; + + SENDWAIPC(plugin.hwndWinampParent, IPC_ADD_DISPATCH_OBJECT, (WPARAM)&dispatchInfo); + rss->Release(); + } + + BuildDefaultDownloadPath( plugin.hwndWinampParent ); + + preferences.hInst = WASABI_API_LNG_HINST; + preferences.dlgID = IDD_PREFERENCES; + preferences.proc = (void *)PreferencesDialogProc; + preferences.name = WASABI_API_LNGSTRINGW_BUF( IDS_PODCAST_DIRECTORY, preferencesName, 64 ); + preferences.where = 6; + + mediaLibrary.AddPreferences( preferences ); + + wchar_t g_path[MAX_PATH] = {0}; + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds", g_path, MAX_PATH ); + CreateDirectoryW( g_path, NULL ); + + wchar_t oldxmlFileName[1024] = {0}, oldxmlFileNameBackup[ 1024 ] = { 0 }; + mediaLibrary.BuildPath( L"Plugins\\ml\\rss.xml", oldxmlFileName, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds\\rss.xml", rssXmlFileName, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\rss.xml.backup", oldxmlFileNameBackup, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds\\rss.xml.backup", rssXmlFileNameBackup, 1024 ); + + if ( PathFileExists( oldxmlFileName ) && !PathFileExists(rssXmlFileName)) + { + MoveFile( oldxmlFileName, rssXmlFileName ); + MoveFile( oldxmlFileNameBackup, rssXmlFileNameBackup ); + } + + { + DownloadsParse downloader; + downloader.DownloadFile(rssXmlFileName); + } + + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds.xml", oldxmlFileName, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds\\feeds.xml", feedsXmlFileName, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds.xml.backup", oldxmlFileNameBackup, 1024 ); + mediaLibrary.BuildPath( L"Plugins\\ml\\feeds\\feeds.xml.backup", feedsXmlFileNameBackup, 1024 ); + + if ( PathFileExists( oldxmlFileName ) && !PathFileExists( feedsXmlFileName ) ) + { + MoveFile( oldxmlFileName, feedsXmlFileName ); + MoveFile( oldxmlFileNameBackup, feedsXmlFileNameBackup ); + } + + Navigation_Initialize(); + SetTimer( plugin.hwndLibraryParent, 0x498, 10, InitTimer ); + + return 0; +} + +void Quit() +{ + // If there are still files downloading, cancel download to remove incomplete downloaded files + while ( downloadStatus.CurrentlyDownloading() ) + { + Nullsoft::Utility::AutoLock lock( downloadStatus.statusLock ); + DownloadToken dltoken = downloadStatus.downloads.begin()->first; + WAC_API_DOWNLOADMANAGER->CancelDownload( dltoken ); + } + + cloud.Quit(); + CloseDatabase(); + + plugin.service->service_deregister( &podcastsFactory ); + plugin.service->service_deregister( &jsapi2Creator ); + + waServiceFactory *sf = plugin.service->service_getServiceByGuid( OBJ_OmBrowser ); + if ( sf != NULL ) + sf->releaseInterface( OMBROWSERMNGR ); + + sf = plugin.service->service_getServiceByGuid( applicationApiServiceGuid ); + if ( sf != NULL ) + sf->releaseInterface(WASABI_API_APP); + + sf = plugin.service->service_getServiceByGuid(AnonymousStatsGUID); + if ( sf != NULL ) + sf->releaseInterface(AGAVE_API_STATS); + + sf = plugin.service->service_getServiceByGuid(ThreadPoolGUID); + if ( sf != NULL ) + sf->releaseInterface(WASABI_API_THREADPOOL); + + sf = plugin.service->service_getServiceByGuid(ExplorerFindFileApiGUID); + if ( sf != NULL ) + sf->releaseInterface(WASABI_API_EXPLORERFINDFILE); + + sf = plugin.service->service_getServiceByGuid(DownloadManagerGUID); + if ( sf != NULL ) + sf->releaseInterface(WAC_API_DOWNLOADMANAGER); + + if ( VIEWPROP != 0 ) + { + GlobalDeleteAtom(VIEWPROP); + VIEWPROP = 0; + } +} + +static INT_PTR Podcast_OnContextMenu( INT_PTR param1, HWND hHost, POINTS pts) +{ + HNAVITEM hItem = (HNAVITEM)param1; + HNAVITEM myItem = Navigation_FindService( SERVICE_PODCAST, NULL, NULL); + + HNAVITEM podcastItem = MLNavItem_GetChild( plugin.hwndLibraryParent, myItem); + HNAVITEM subscriptionItem = Navigation_FindService( SERVICE_SUBSCRIPTION, podcastItem, NULL); + + if ( hItem != myItem && hItem != subscriptionItem ) + return FALSE; + + POINT pt; + POINTSTOPOINT( pt, pts ); + if ( pt.x == -1 || pt.y == -1 ) + { + NAVITEMGETRECT itemRect; + itemRect.fItem = FALSE; + itemRect.hItem = hItem; + if ( MLNavItem_GetRect( plugin.hwndLibraryParent, &itemRect ) ) + { + MapWindowPoints( hHost, HWND_DESKTOP, (POINT*)&itemRect.rc, 2 ); + pt.x = itemRect.rc.left + 2; + pt.y = itemRect.rc.top + 2; + } + } + + HMENU hMenu = WASABI_API_LOADMENU( IDR_MENU1 ); + int subMenuId = ( hItem == subscriptionItem )? 4 : 3; + + HMENU subMenu = ( NULL != hMenu ) ? GetSubMenu( hMenu, subMenuId ) : NULL; + if ( subMenu != NULL ) + { + INT r = Menu_TrackPopup( plugin.hwndLibraryParent, subMenu, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_NONOTIFY | TPM_RETURNCMD | TPM_RIGHTBUTTON | TPM_LEFTBUTTON, pt.x, pt.y, hHost, NULL ); + + switch(r) + { + case ID_NAVIGATION_DIRECTORY: + MLNavItem_Select( plugin.hwndLibraryParent, myItem ); + break; + case ID_NAVIGATION_PREFERENCES: + SENDWAIPC( plugin.hwndWinampParent, IPC_OPENPREFSTOPAGE, &preferences ); + break; + case ID_NAVIGATION_HELP: + SENDWAIPC( plugin.hwndWinampParent, IPC_OPEN_URL, L"https://help.winamp.com/hc/articles/8112346487060-Podcast-Directory" ); + break; + case ID_NAVIGATION_REFRESHALL: + cloud.RefreshAll(); + cloud.Pulse(); + break; + } + } + + if ( hMenu != NULL ) + DestroyMenu( hMenu ); + + return TRUE; +} + +INT_PTR MessageProc( int msg, INT_PTR param1, INT_PTR param2, INT_PTR param3 ) +{ + INT_PTR result = 0; + if ( Navigation_ProcessMessage( msg, param1, param2, param3, &result ) != FALSE ) + return result; + + switch ( msg ) + { + case ML_MSG_NOTOKTOQUIT: + { + if (downloadStatus.CurrentlyDownloading()) + { + wchar_t titleStr[32] = {0}; + if ( MessageBox( plugin.hwndLibraryParent, WASABI_API_LNGSTRINGW( IDS_CANCEL_DOWNLOADS_AND_QUIT ), WASABI_API_LNGSTRINGW_BUF( IDS_CONFIRM_QUIT, titleStr, 32 ), MB_YESNO | MB_ICONQUESTION ) == IDNO ) + return TRUE; + } + + return 0; + } + case ML_MSG_CONFIG: + mediaLibrary.GoToPreferences(preferences._id); + return TRUE; + case ML_MSG_NAVIGATION_CONTEXTMENU: + return Podcast_OnContextMenu( param1, (HWND)param2, MAKEPOINTS( param3 ) ); + case ML_MSG_VIEW_PLAY_ENQUEUE_CHANGE: + enqueuedef = param1; + groupBtn = param2; + PostMessage( current_window, WM_APP + 104, param1, param2 ); + return 0; + } + + return FALSE; +} + + +#define TREE_IMAGE_LOCAL_PODCASTS 108 + +void addToLibrary(const DownloadedFile& d) +{ + itemRecordW item = { 0 }; + + item.year = -1; + item.track = -1; + item.tracks = -1; + item.length = -1; + item.rating = -1; + item.lastplay = -1; + item.lastupd = -1; + item.filetime = -1; + item.filesize = -1; + item.bitrate = -1; + item.type = -1; + item.disc = -1; + item.discs = -1; + item.bpm = -1; + item.playcount = -1; + item.filename = _wcsdup( d.path ); + + setRecordExtendedItem(&item,L"ispodcast",L"1"); + setRecordExtendedItem(&item,L"podcastchannel",d.channel); + + wchar_t buf[40] = {0}; + _i64tow(d.publishDate,buf,10); + if(d.publishDate) + setRecordExtendedItem(&item,L"podcastpubdate",buf); + + LMDB_FILE_ADD_INFOW fai = {item.filename,-1,-1}; + SendMessage(plugin.hwndLibraryParent, WM_ML_IPC, (WPARAM)&fai, ML_IPC_DB_ADDORUPDATEFILEW); + SendMessage(plugin.hwndLibraryParent, WM_ML_IPC, (WPARAM)&item, ML_IPC_DB_UPDATEITEMW); + PostMessage(plugin.hwndLibraryParent, WM_ML_IPC, 0, ML_IPC_DB_SYNCDB); + freeRecord(&item); + + if(needToMakePodcastsView) { + mlSmartViewInfo m = { sizeof( mlSmartViewInfo ),2,L"Podcasts", L"ispodcast = 1",461315,TREE_IMAGE_LOCAL_PODCASTS,0 }; + WASABI_API_LNGSTRINGW_BUF( IDS_PODCASTS, m.smartViewName, 128 ); + SendMessage( plugin.hwndLibraryParent, WM_ML_IPC, (WPARAM)&m, ML_IPC_SMARTVIEW_ADD ); + needToMakePodcastsView = false; + } +} + +typedef struct +{ + const DownloadedFile *d; + volatile UINT done; +} apc_addtolib_waiter; + +static VOID CALLBACK apc_addtolib(ULONG_PTR dwParam) +{ + apc_addtolib_waiter *w = (apc_addtolib_waiter *)dwParam; + addToLibrary(*w->d); + w->done=1; +} + +void addToLibrary_thread(const DownloadedFile& d) +{ + apc_addtolib_waiter w = { &d, 0 }; + if ( !QueueUserAPC( apc_addtolib, hMainThread, (ULONG_PTR)&w ) ) + return; + + while ( !w.done ) + SleepEx( 5, true ); +} + + +extern "C" __declspec(dllexport) winampMediaLibraryPlugin *winampGetMediaLibraryPlugin() +{ + return &plugin; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Main.h b/Src/Plugins/Library/ml_wire/Main.h new file mode 100644 index 00000000..ea9d9a2d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Main.h @@ -0,0 +1,69 @@ +#ifndef NULLSOFT_MAINH +#define NULLSOFT_MAINH + +#include "Wire.h" +#include "Downloaded.h" + +#define PLUGIN_VERSION_MAJOR 1 +#define PLUGIN_VERSION_MINOR 80 + +#define SERVICE_PODCAST 720 +#define SERVICE_SUBSCRIPTION 721 +#define SERVICE_DOWNLOADS 722 + +#define BAD_CHANNEL ((size_t)-1) +#define BAD_ITEM ((size_t)-1) + +void SaveChannels(ChannelList &channels); +void SaveAll(bool rss_only=false); +void HookTerminate(); + +void DestroyLoader(HWND); +void BuildLoader(HWND); +extern int winampVersion; +void addToLibrary(const DownloadedFile& d); // call in winamp main thread only +void addToLibrary_thread(const DownloadedFile& d); // call from any thread + +bool AddPodcastData(const DownloadedFile &data); +bool IsPodcastDownloaded(const wchar_t *url); +void CloseDatabase(); + +#include "resource.h" +#include "../nu/DialogSkinner.h" +#include "../nu/MediaLibraryInterface.h" +#include "../nu/AutoChar.h" +#include "../nu/AutoWide.h" +#include "../nu/AutoLock.h" +#include "..\..\General\gen_ml/menu.h" +#include <windows.h> +#include <shlwapi.h> + +extern ATOM VIEWPROP; +extern winampMediaLibraryPlugin plugin; + +#include "../Components/wac_downloadManager/wac_downloadManager_api.h" + +#define ML_ENQDEF_VAL() (!!GetPrivateProfileInt(L"gen_ml_config", L"enqueuedef", 0, ml_cfg)) +#define ML_GROUPBTN_VAL() (!!GetPrivateProfileInt(L"gen_ml_config", L"groupbtn", 1, ml_cfg)) +extern wchar_t* ml_cfg; +wchar_t *urlencode(wchar_t *p); + +extern HWND current_window; +extern int groupBtn, enqueuedef, customAllowed; +extern viewButtons view; + +void SwapPlayEnqueueInMenu(HMENU listMenu); +void SyncMenuWithAccelerators(HWND hwndDlg, HMENU menu); +void Downloads_UpdateButtonText(HWND hwndDlg, int _enqueuedef); +void listbuild(wchar_t **buf, int &buf_size, int &buf_pos, const wchar_t *tbuf); + +enum +{ + BPM_ECHO_WM_COMMAND=0x1, // send WM_COMMAND and return value + BPM_WM_COMMAND = 0x2, // just send WM_COMMAND +}; + +BOOL Downloads_ButtonPopupMenu(HWND hwndDlg, int buttonId, HMENU menu, int flags=0); +void UpdateMenuItems(HWND hwndDlg, HMENU menu); + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/MessageProcessor.cpp b/Src/Plugins/Library/ml_wire/MessageProcessor.cpp new file mode 100644 index 00000000..6067965c --- /dev/null +++ b/Src/Plugins/Library/ml_wire/MessageProcessor.cpp @@ -0,0 +1,7 @@ +#include "main.h" +#include "MessageProcessor.h" + +#define CBCLASS MessageProcessor +START_DISPATCH; +CB(API_MESSAGEPROCESSOR_PROCESS_MESSAGE, ProcessMessage) +END_DISPATCH;
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/MessageProcessor.h b/Src/Plugins/Library/ml_wire/MessageProcessor.h new file mode 100644 index 00000000..c48e4b8a --- /dev/null +++ b/Src/Plugins/Library/ml_wire/MessageProcessor.h @@ -0,0 +1,35 @@ +#ifndef NULLSOFT_ML_WIRE_MESSAGEPROCESSOR_H +#define NULLSOFT_ML_WIRE_MESSAGEPROCESSOR_H + +#include <api/application/api_messageprocessor.h> +#include "main.h" +#ifndef WM_FORWARDMSG +#define WM_FORWARDMSG 0x037F +#endif + +class MessageProcessor : public api_messageprocessor +{ +public: + bool ProcessMessage(MSG *msg) + { + + if (msg->message < WM_KEYFIRST || msg->message > WM_KEYLAST) + return false; + + HWND hWndCtl = ::GetFocus(); + + if (IsChild(browserHWND, hWndCtl)) + { + // find a direct child of the dialog from the window that has focus + while(::GetParent(hWndCtl) != browserHWND) + hWndCtl = ::GetParent(hWndCtl); + + if (activeBrowser->translateKey(*msg)) + return true; + } + return false; + } +protected: + RECVS_DISPATCH; +}; +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/OPMLParse.cpp b/Src/Plugins/Library/ml_wire/OPMLParse.cpp new file mode 100644 index 00000000..862ec36b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/OPMLParse.cpp @@ -0,0 +1,33 @@ +#if 0 +#include "main.h" +#include "OPMLParse.h" + +#ifdef DEBUG +#include <iostream> +static void DisplayNodes(XMLNode &node) +{ + XMLNode::NodeMap::iterator nodeItr; + for (nodeItr = node.nodes.begin();nodeItr != node.nodes.end(); nodeItr++) + { + + for (XMLNode::NodeList::iterator itr = nodeItr->second.begin(); itr != nodeItr->second.end(); itr++) + { + std::wcerr << L"<" << nodeItr->first << L">" << std::endl; + DisplayNodes(**itr); + std::wcerr << L"</" << nodeItr->first << L">" << std::endl; + } + + } +} +#endif + +void OPMLParse::ReadNodes(const wchar_t *url) +{ +// DisplayNodes(xmlNode); + Alias<XMLNode> curNode; + curNode = xmlNode.Get(L"opml"); + if (!curNode) + return; + +} +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/OPMLParse.h b/Src/Plugins/Library/ml_wire/OPMLParse.h new file mode 100644 index 00000000..5921ceef --- /dev/null +++ b/Src/Plugins/Library/ml_wire/OPMLParse.h @@ -0,0 +1,28 @@ +#ifndef NULLSOFT_OPMLPARSEH +#define NULLSOFT_OPMLPARSEH + +#include "DownloadThread.h" +#include "ChannelSync.h" +class OPMLParse : public DownloadThread +{ +public: + OPMLParse(ChannelSync *_sync) + : sync(_sync) + { + + } + + + ~OPMLParse() + { + sync = 0; + } + + virtual void ReadNodes(const wchar_t *url); + +private: + + ChannelSync *sync; +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/PCastFactory.cpp b/Src/Plugins/Library/ml_wire/PCastFactory.cpp new file mode 100644 index 00000000..48b22f65 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/PCastFactory.cpp @@ -0,0 +1,60 @@ +#include "api__ml_wire.h" +#include ".\pcastfactory.h" +#include "Wire.h" + +static PCastURIHandler PCastUH; + +static const char serviceName[] = "PCast"; + +FOURCC PCastFactory::GetServiceType() +{ + return svc_urihandler::getServiceType(); +} + +const char *PCastFactory::GetServiceName() +{ + return serviceName; +} + +void *PCastFactory::GetInterface(int global_lock) +{ +// if (global_lock) +// plugin.service->service_lock(this, (void *)ifc); + return &PCastUH; +} + +int PCastFactory::SupportNonLockingInterface() +{ + return 1; +} + +int PCastFactory::ReleaseInterface(void *ifc) +{ + //plugin.service->service_unlock(ifc); + return 1; +} + +const char *PCastFactory::GetTestString() +{ + return NULL; +} + +int PCastFactory::ServiceNotify(int msg, int param1, int param2) +{ + return 0; +} + +#ifdef CBCLASS +#undef CBCLASS +#endif + +#define CBCLASS PCastFactory +START_DISPATCH; +CB( WASERVICEFACTORY_GETSERVICETYPE, GetServiceType ) +CB( WASERVICEFACTORY_GETSERVICENAME, GetServiceName ) +CB( WASERVICEFACTORY_GETINTERFACE, GetInterface ) +CB( WASERVICEFACTORY_SUPPORTNONLOCKINGGETINTERFACE, SupportNonLockingInterface ) +CB( WASERVICEFACTORY_RELEASEINTERFACE, ReleaseInterface ) +CB( WASERVICEFACTORY_GETTESTSTRING, GetTestString ) +CB( WASERVICEFACTORY_SERVICENOTIFY, ServiceNotify ) +END_DISPATCH; diff --git a/Src/Plugins/Library/ml_wire/PCastFactory.h b/Src/Plugins/Library/ml_wire/PCastFactory.h new file mode 100644 index 00000000..0b6d1fdc --- /dev/null +++ b/Src/Plugins/Library/ml_wire/PCastFactory.h @@ -0,0 +1,24 @@ +#pragma once + +#include "api__ml_wire.h" + +#include "api/service/waservicefactory.h" +#include "api/service/services.h" + +#include "PCastURIHandler.h" + +class PCastFactory : public waServiceFactory +{ +public: + FOURCC GetServiceType(); + const char *GetServiceName(); + GUID GetGUID(); + void *GetInterface(int global_lock); + int SupportNonLockingInterface(); + int ReleaseInterface(void *ifc); + const char *GetTestString(); + int ServiceNotify(int msg, int param1, int param2); + +protected: + RECVS_DISPATCH; +}; diff --git a/Src/Plugins/Library/ml_wire/PCastURIHandler.cpp b/Src/Plugins/Library/ml_wire/PCastURIHandler.cpp new file mode 100644 index 00000000..4b32c9c3 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/PCastURIHandler.cpp @@ -0,0 +1,258 @@ +#include "main.h" +#include "./pcasturihandler.h" +#include "./Feeds.h" +#include "./FeedUtil.h" +#include "../nu/AutoLock.h" +#include "./wire.h" +#include "./errors.h" +//#include "../Agave/URIHandler/svc_urihandler.h" +//#include <api/service/waservicefactory.h> +#include "api__ml_wire.h" +#include "./cloud.h" +#include "./SubscriptionView.h" +#include "./resource.h" +#include "navigation.h" +#include "..\..\General\gen_ml/ml_ipc_0313.h" +#include <strsafe.h> + +using namespace Nullsoft::Utility; + +extern ChannelList channels; +extern Cloud cloud; + + +static uint8_t quickhex(wchar_t c) +{ + int hexvalue = c; + if (hexvalue & 0x10) + hexvalue &= ~0x30; + else + { + hexvalue &= 0xF; + hexvalue += 9; + } + return hexvalue; +} + +static uint8_t DecodeEscape(const wchar_t *&str) +{ + uint8_t a = quickhex(*++str); + uint8_t b = quickhex(*++str); + str++; + return a * 16 + b; +} + +static void DecodeEscapedUTF8(wchar_t *&output, const wchar_t *&input) +{ + uint8_t utf8_data[1024] = {0}; // hopefully big enough!! + int num_utf8_words=0; + bool error=false; + + while (input && *input == '%' && num_utf8_words < sizeof(utf8_data)) + { + if (iswxdigit(input[1]) && iswxdigit(input[2])) + { + utf8_data[num_utf8_words++]=DecodeEscape(input); + } + else if (input[1] == '%') + { + input+=2; + utf8_data[num_utf8_words++]='%'; + } + else + { + error = true; + break; + } + } + + int len = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)utf8_data, num_utf8_words, 0, 0); + MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)utf8_data, num_utf8_words, output, len); + output += len; + + if (error) + { + *output++ = *input++; + } +} + +static void UrlDecode(const wchar_t *input, wchar_t *output, size_t len) +{ + const wchar_t *stop = output+len-4; // give ourself a cushion large enough to hold a full UTF-16 sequence + const wchar_t *itr = input; + while (itr && *itr) + { + if (output >= stop) + { + *output=0; + return; + } + + switch (*itr) + { + case '%': + DecodeEscapedUTF8(output, itr); + break; + case '&': + *output = 0; + return; + default: + *output++ = *itr++; + break; + } + } + *output = 0; +} +// first parameter has param name either null or = terminated, second is null terminated +static bool ParamCompare(const wchar_t *url_param, const wchar_t *param_name) +{ + while (url_param && *url_param && *param_name && *url_param!=L'=') + { + if (*url_param++ != *param_name++) + return false; + } + return true; +} + +static bool get_request_parm(const wchar_t *params, const wchar_t *param_name, wchar_t *value, size_t value_len) +{ + size_t param_name_len = wcslen(param_name); + const wchar_t *t=params; + while (t && *t && *t != L'?') // find start of parameters + t++; + + while (t && *t) + { + t++; // skip ? or & + if (ParamCompare(t, param_name)) + { + while (t && *t && *t != L'=' && *t != '&') // find start of value + t++; + switch(*t) + { + case L'=': + UrlDecode(++t, value, value_len); + return true; + case 0: + case L'&': // no value + *value=0; + return true; + default: // shouldn't get here + return false; + } + } + while (t && *t && *t != L'&') // find next parameter + t++; + } + + return false; +} + +int PCastURIHandler::ProcessFilename(const wchar_t *filename) +{ + if ( + (wcsnicmp(filename, L"pcast://", 8)) == 0 || + (wcsnicmp(filename, L"feed://", 7) == 0) || + (wcsnicmp(filename, L"winamp://Podcast/Subscribe", 26) == 0) || + (wcsnicmp(filename, L"winamp://Podcast/Search", 23) == 0) + ) + { + wchar_t *tempFilename = NULL; + wchar_t url[1024] = {0}; + if (wcsnicmp(filename, L"winamp://Podcast/Subscribe", 26) == 0) + { + // extract/decode and use the url= parameter + if (get_request_parm(filename, L"url", url, 1024) && url[0]) + { + tempFilename = wcsdup(url); + } + else + { + // did not find a url parameter + return NOT_HANDLED; + } + } + else if (wcsnicmp(filename, L"winamp://Podcast/Search", 23) == 0) + { +// TODO: maybe: if (get_request_parm(filename, L"url", url, 1024) && url[0]) + { + HNAVITEM hItem = Navigation_FindService(SERVICE_PODCAST, NULL, NULL); + MLNavItem_Select(plugin.hwndLibraryParent, hItem); + return HANDLED; + } + /* + else + { + // did not find a url parameter + return NOT_HANDLED; + } + */ + } + else + { + // Use the full filename + tempFilename = wcsdup(filename); + } + + // subscription confirmation + WCHAR szText[1024] = {0}, szBuffer[1024] = {0}; + WASABI_API_LNGSTRINGW_BUF(IDS_PODCAST_SUBSCRIPTION_PROMP, szBuffer, ARRAYSIZE(szBuffer)); + StringCchPrintf(szText, ARRAYSIZE(szText), szBuffer, tempFilename); + + WASABI_API_LNGSTRINGW_BUF(IDS_PODCAST_SUBSCRIPTION_HEADER, szBuffer, ARRAYSIZE(szBuffer)); + + if (IDYES == MessageBox(plugin.hwndLibraryParent, szText, szBuffer, MB_YESNO | MB_ICONQUESTION | MB_TOPMOST | MB_SETFOREGROUND) ) + { + // add feed to channels, pulse the cloud, refresh the UI pane. + Channel newFeed; + newFeed.SetURL(tempFilename); + if (DownloadFeedInformation(newFeed)==DOWNLOAD_SUCCESS) + { + channels.channelGuard.Lock(); + channels.AddChannel(newFeed); + channels.channelGuard.Unlock(); + cloud.Pulse(); + HWND hView = SubscriptionView_FindWindow(); + if (NULL != hView) + { + SubscriptionView_RefreshChannels(hView, TRUE); + } + else + { + HNAVITEM myItem = Navigation_FindService(SERVICE_PODCAST, NULL, NULL); + HNAVITEM podcastItem = MLNavItem_GetChild(plugin.hwndLibraryParent, myItem); + HNAVITEM subscriptionItem = Navigation_FindService(SERVICE_SUBSCRIPTION, podcastItem, NULL); + MLNavItem_Select(plugin.hwndLibraryParent, subscriptionItem); + } + } + free(tempFilename); + + return HANDLED; + + } + else + free(tempFilename); + } + return NOT_HANDLED; +} + +int PCastURIHandler::IsMine(const wchar_t *filename) +{ + int i = 0; + if ( + (wcsnicmp(filename, L"pcast://", 8)) == 0 || + (wcsnicmp(filename, L"feed://", 7) == 0) || + (wcsnicmp(filename, L"winamp://Podcast/Subscribe", 26) == 0) || + (wcsnicmp(filename, L"winamp://Podcast/Search", 23) == 0) + ) + return HANDLED; + else + return NOT_HANDLED; +} + +#define CBCLASS PCastURIHandler +START_DISPATCH; +CB(PROCESSFILENAME, ProcessFilename); +CB(ISMINE, IsMine); +END_DISPATCH; +#undef CBCLASS
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/PCastURIHandler.h b/Src/Plugins/Library/ml_wire/PCastURIHandler.h new file mode 100644 index 00000000..22bca933 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/PCastURIHandler.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../Agave/URIHandler/svc_urihandler.h" + +// {DA5F8547-5D53-49d9-B9EC-C59318D11798} +static const GUID pcast_uri_handler_guid = +{ 0xda5f8547, 0x5d53, 0x49d9, { 0xb9, 0xec, 0xc5, 0x93, 0x18, 0xd1, 0x17, 0x98 } }; + + +class PCastURIHandler : + public svc_urihandler +{ +public: + static const char *getServiceName() { return "PCast URI Handler"; } + static GUID getServiceGuid() { return pcast_uri_handler_guid; } + int ProcessFilename(const wchar_t *filename); + int IsMine(const wchar_t *filename); // just like ProcessFilename but don't actually process + +protected: + RECVS_DISPATCH; +}; diff --git a/Src/Plugins/Library/ml_wire/ParseUtil.cpp b/Src/Plugins/Library/ml_wire/ParseUtil.cpp new file mode 100644 index 00000000..0a5c8d57 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ParseUtil.cpp @@ -0,0 +1,43 @@ +#include "ParseUtil.h" + +bool PropertyIsTrue( const XMLNode *item, const wchar_t *property ) +{ + if ( !item ) + return false; + + const wchar_t *value = item->GetProperty( property ); + if ( !value ) + return false; + + return !_wcsicmp( value, L"true" ); +} + +bool PropertyIsFalse( const XMLNode *item, const wchar_t *property ) +{ + if ( !item ) + return false; + + const wchar_t *value = item->GetProperty( property ); + if ( !value ) + return false; + + return !_wcsicmp( value, L"false" ); +} + +const wchar_t *GetContent( const XMLNode *item, const wchar_t *tag ) +{ + const XMLNode *curNode = item->Get( tag ); + if ( curNode ) + return curNode->GetContent(); + else + return 0; +} + +const wchar_t *GetProperty( const XMLNode *item, const wchar_t *tag, const wchar_t *property ) +{ + const XMLNode *curNode = item->Get( tag ); + if ( curNode ) + return curNode->GetProperty( property ); + else + return 0; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ParseUtil.h b/Src/Plugins/Library/ml_wire/ParseUtil.h new file mode 100644 index 00000000..a277d729 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ParseUtil.h @@ -0,0 +1,8 @@ +#pragma once +#include "../xml/XMLNode.h" +#include <bfc/platform/types.h> +bool PropertyIsTrue(const XMLNode *item, const wchar_t *property); +// not necessarily the opposite of PropertyIsTrue (returns false when field is empty +bool PropertyIsFalse(const XMLNode *item, const wchar_t *property); +const wchar_t *GetContent(const XMLNode *item, const wchar_t *tag); +const wchar_t *GetProperty(const XMLNode *item, const wchar_t *tag, const wchar_t *property);
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Preferences.cpp b/Src/Plugins/Library/ml_wire/Preferences.cpp new file mode 100644 index 00000000..ec7a20eb --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Preferences.cpp @@ -0,0 +1,160 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "../winamp/wa_ipc.h" +#include "Defaults.h" +#include "UpdateTime.h" +#include "UpdateAutoDownload.h" +#include "./cloud.h" +#include <shlobj.h> + +extern Cloud cloud; + +void Preferences_Init(HWND hwndDlg) +{ + WCHAR szBuffer[256] = {0}; + for (int i = 0;i < Update::TIME_NUMENTRIES;i++) + { + const wchar_t *str = Update::GetTitle(i, szBuffer, ARRAYSIZE(szBuffer)); + SendMessage(GetDlgItem(hwndDlg, IDC_UPDATELIST), CB_ADDSTRING, 0, (LPARAM) str); + } + int selection = Update::GetSelection(updateTime, autoUpdate); + SendMessage(GetDlgItem(hwndDlg, IDC_UPDATELIST), CB_SETCURSEL, selection, 0); + + WCHAR adBuffer[256] = {0}; + for (int i = 0;i < UpdateAutoDownload::AUTODOWNLOAD_NUMENTRIES;i++) + { + const wchar_t *str = UpdateAutoDownload::GetTitle(i, adBuffer, ARRAYSIZE(adBuffer)); + SendMessage(GetDlgItem(hwndDlg, IDC_AUTODOWNLOADLIST), CB_ADDSTRING, 0, (LPARAM) str); + } + selection = UpdateAutoDownload::GetSelection(autoDownloadEpisodes, autoDownload); + SendMessage(GetDlgItem(hwndDlg, IDC_AUTODOWNLOADLIST), CB_SETCURSEL, selection, 0); + + SetDlgItemText(hwndDlg, IDC_DOWNLOADLOCATION, defaultDownloadPath); + SetDlgItemText(hwndDlg, IDC_DIRECTORYURL, serviceUrl); + CheckDlgButton(hwndDlg, IDC_UPDATEONLAUNCH, updateOnLaunch?BST_CHECKED:BST_UNCHECKED); +} + +BOOL CALLBACK browseEnumProc(HWND hwnd, LPARAM lParam) +{ + wchar_t cl[32] = {0}; + GetClassNameW(hwnd, cl, ARRAYSIZE(cl)); + if (!lstrcmpiW(cl, WC_TREEVIEW)) + { + PostMessage(hwnd, TVM_ENSUREVISIBLE, 0, (LPARAM)TreeView_GetSelection(hwnd)); + return FALSE; + } + + return TRUE; +} + +int CALLBACK WINAPI BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData) +{ + if(uMsg == BFFM_INITIALIZED) + { + SendMessageW(hwnd, BFFM_SETSELECTIONW, 1, (LPARAM)defaultDownloadPath); + + // this is not nice but it fixes the selection not working correctly on all OSes + EnumChildWindows(hwnd, browseEnumProc, 0); + } + return 0; +} + +void Preferences_Browse(HWND hwndDlg) +{ + wchar_t folder[MAX_PATH] = {0}; + BROWSEINFO browse = {0}; + lstrcpyn(folder, defaultDownloadPath, MAX_PATH); + browse.hwndOwner = hwndDlg; + browse.lpszTitle = WASABI_API_LNGSTRINGW(IDS_CHOOSE_FOLDER); + browse.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE; + browse.lpfn = BrowseCallbackProc; + LPITEMIDLIST itemList = SHBrowseForFolder(&browse); + if (itemList) + { + SHGetPathFromIDList(itemList, folder); + lstrcpyn(defaultDownloadPath, folder, MAX_PATH); + SetWindowText(GetDlgItem(hwndDlg, IDC_DOWNLOADLOCATION), folder); + LPMALLOC malloc; + SHGetMalloc(&malloc); + malloc->Free(itemList); + } +} + +void Preferences_UpdateOnLaunch( HWND hwndDlg ) +{ + updateOnLaunch = ( IsDlgButtonChecked( hwndDlg, IDC_UPDATEONLAUNCH ) == BST_CHECKED ); +} + +void Preferences_UpdateList(HWND hwndDlg) +{ + LRESULT timeSelection; + timeSelection = SendMessage(GetDlgItem(hwndDlg, IDC_UPDATELIST), CB_GETCURSEL, 0, 0); + if (timeSelection != CB_ERR) + { + autoUpdate = Update::GetAutoUpdate(timeSelection); + updateTime = Update::GetTime(timeSelection); + if ( autoUpdate ) + cloud.Pulse(); // update the waitable timer + } +} + +void Preferences_AutoDownloadList(HWND hwndDlg) +{ + LRESULT episodeSelection; + episodeSelection = SendMessage(GetDlgItem(hwndDlg, IDC_AUTODOWNLOADLIST), CB_GETCURSEL, 0, 0); + if (episodeSelection != CB_ERR) + { + autoDownload = UpdateAutoDownload::GetAutoDownload(episodeSelection); + autoDownloadEpisodes = UpdateAutoDownload::GetAutoDownloadEpisodes(episodeSelection); + } +} + +BOOL CALLBACK PreferencesDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch (uMsg) + { + case WM_INITDIALOG: + Preferences_Init(hwndDlg); + break; + case WM_COMMAND: + switch (LOWORD(wParam)) + { + case IDC_BROWSE: + Preferences_Browse(hwndDlg); + break; + case IDC_UPDATELIST: + Preferences_UpdateList(hwndDlg); + break; + case IDC_AUTODOWNLOADLIST: + Preferences_AutoDownloadList(hwndDlg); + break; + case IDC_UPDATEONLAUNCH: + Preferences_UpdateOnLaunch(hwndDlg); + break; + case IDC_DOWNLOADLOCATION: + if (HIWORD(wParam) == EN_CHANGE) + { + GetDlgItemText(hwndDlg, IDC_DOWNLOADLOCATION, defaultDownloadPath, MAX_PATH); + if (!PathFileExists(defaultDownloadPath)) + { + BuildDefaultDownloadPath(plugin.hwndWinampParent); + SetDlgItemText(hwndDlg, IDC_DOWNLOADLOCATION, defaultDownloadPath); + } + } + break; + case IDC_DIRECTORYURL: + if (HIWORD(wParam) == EN_CHANGE) + { + GetDlgItemText(hwndDlg, IDC_DIRECTORYURL, serviceUrl, ARRAYSIZE(serviceUrl)); + } + break; + } + break; + case WM_DESTROY: + // ensure we save the changed settings + SaveAll(true); + break; + } + + return 0; +} diff --git a/Src/Plugins/Library/ml_wire/Preferences.h b/Src/Plugins/Library/ml_wire/Preferences.h new file mode 100644 index 00000000..c0615aab --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Preferences.h @@ -0,0 +1,6 @@ +#ifndef NULLSOFT_PREFERENCESH +#define NULLSOFT_PREFERENCESH + +BOOL CALLBACK PreferencesDialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam); + +#endif diff --git a/Src/Plugins/Library/ml_wire/RFCDate.cpp b/Src/Plugins/Library/ml_wire/RFCDate.cpp new file mode 100644 index 00000000..ab57f9af --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RFCDate.cpp @@ -0,0 +1,216 @@ +#include "main.h" +#include "RFCDate.h" +#include <strsafe.h> + +void MakeDateString(__time64_t convertTime, wchar_t *date_str, size_t len) +{ + SYSTEMTIME sysTime = {0}; + tm *newtime = _localtime64(&convertTime); + + sysTime.wYear = newtime->tm_year + 1900; + sysTime.wMonth = newtime->tm_mon + 1; + sysTime.wDayOfWeek = newtime->tm_wday; + sysTime.wDay = newtime->tm_mday; + sysTime.wHour = newtime->tm_hour; + sysTime.wMinute = newtime->tm_min; + sysTime.wSecond = newtime->tm_sec; + + GetDateFormat(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &sysTime, NULL, date_str, (int)len); + +} + +void MakeRFCDateString(__time64_t convertTime, wchar_t *data_str, size_t len) +{ + SYSTEMTIME sysTime = {0}; + tm *newtime = _gmtime64(&convertTime); + + sysTime.wYear = newtime->tm_year + 1900; + sysTime.wMonth = newtime->tm_mon + 1; + sysTime.wDayOfWeek = newtime->tm_wday; + sysTime.wDay = newtime->tm_mday; + sysTime.wHour = newtime->tm_hour; + sysTime.wMinute = newtime->tm_min; + sysTime.wSecond = newtime->tm_sec; + + wchar_t rfcDate[64] = {0}, rfcTime[64] = {0}; + GetDateFormat(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT), 0, &sysTime, L"ddd',' d MMM yyyy ", rfcDate, 64); + GetTimeFormat(MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT), 0, &sysTime, L"HH':'mm':'ss 'GMT'", rfcTime, 64); + StringCchPrintf(data_str,len,L"%s%s",rfcDate,rfcTime); +} + +static int getMonthOfYear(const wchar_t *str); +static int validateTime(struct tm *tmloc, int gmoffset); +static const wchar_t *getNextField(const wchar_t *pos); +static int getGMOffset(const wchar_t *str); + +enum +{ + DAY_OF_MONTH = 0, + MONTH_OF_YEAR, + YEAR, + TIME, + TIMEZONE, + DATE_END +}; + +__time64_t MakeRFCDate(const wchar_t *date) +{ + __time64_t result = 0; + const wchar_t *pos = date; + tm tmloc = {0}; + const wchar_t *strmin; + const wchar_t *strsec; + int gmoffset = 1; + int state = DAY_OF_MONTH; + tzset(); + /* skip weekday if present */ + while (pos && *pos && !iswdigit(*pos)) pos++; + + do + { + switch (state) + { + case DAY_OF_MONTH: + tmloc.tm_mday = _wtoi(pos); + break; + case MONTH_OF_YEAR: + tmloc.tm_mon = getMonthOfYear(pos); + break; + case YEAR: + { + /* TODO: we're only accepting 4-digit dates...*/ + const wchar_t *test = pos; int numDigits = 0; + while (iswdigit(*test) && *test) { test++; numDigits++; } + if (numDigits == 2) // let's hope we never have 2 digit years! + tmloc.tm_year = _wtoi(pos) + 100; // assume 2 digit years are 20xx + else + tmloc.tm_year = _wtoi(pos) - 1900; + } + break; + case TIME: + strmin = wcschr(pos, L':'); + strsec = strmin ? wcschr(strmin + 1, L':') : 0; + + tmloc.tm_hour = _wtoi(pos); + if (!strmin) return _time64(0); + tmloc.tm_min = _wtoi(strmin + 1); + if (!strsec) return _time64(0); + tmloc.tm_sec = _wtoi(strsec + 1); + break; + case TIMEZONE: + gmoffset = getGMOffset(pos); + break; + case DATE_END: + pos = 0; + break; + } + + state++; + } + while ((pos = getNextField(pos))); + + tmloc.tm_isdst = 0; //_daylight; + + if (validateTime(&tmloc, gmoffset)) + { + result = _mktime64(&tmloc) - _timezone; + //if (_daylight) + } + + return result; +} + +const wchar_t *getNextField(const wchar_t *pos) +{ + if (!pos) + return 0; + while (pos && *pos && !iswspace(*pos)) pos++; + while (pos && *pos && iswspace(*pos)) pos++; + + return ((pos && *pos) ? pos : 0); +} + +int validateTime(struct tm *tmloc, int gmoffset) +{ + int result = 1; + + if (tmloc->tm_mday < 1 || tmloc->tm_mday > 31 || + tmloc->tm_mon < 0 || tmloc->tm_mon > 11 || + tmloc->tm_year < 0 || tmloc->tm_year > 2000 || + tmloc->tm_hour < 0 || tmloc->tm_hour > 23 || + tmloc->tm_min < 0 || tmloc->tm_min > 59 || + tmloc->tm_sec < 0 || tmloc->tm_sec > 59 || + gmoffset == 1) + result = 0; + + return result; +} + +int getMonthOfYear(const wchar_t *str) +{ + int mon = -1; + /* This is not the most efficient way to determine + the month (we could use integer comparisons, for instance) + but I don't think this will be a performance bottleneck :) + */ + + if (!wcsnicmp(str, L"Jan", 3)) + mon = 0; + else if (!wcsnicmp(str, L"Feb", 3)) + mon = 1; + else if (!wcsnicmp(str, L"Mar", 3)) + mon = 2; + else if (!wcsnicmp(str, L"Apr", 3)) + mon = 3; + else if (!wcsnicmp(str, L"May", 3)) + mon = 4; + else if (!wcsnicmp(str, L"Jun", 3)) + mon = 5; + else if (!wcsnicmp(str, L"Jul", 3)) + mon = 6; + else if (!wcsnicmp(str, L"Aug", 3)) + mon = 7; + else if (!wcsnicmp(str, L"Sep", 3)) + mon = 8; + else if (!wcsnicmp(str, L"Oct", 3)) + mon = 9; + else if (!wcsnicmp(str, L"Nov", 3)) + mon = 10; + else if (!wcsnicmp(str, L"Dec", 3)) + mon = 11; + + return mon; +} + +int getGMOffset(const wchar_t *str) +{ + int secs = 0; + /* See note in getMonthOfYear() */ + + if (!wcsnicmp(str, L"UT", 2) || !wcsnicmp(str, L"GMT", 3)) + secs = 0; + else if (!wcsnicmp(str, L"EDT", 3)) + secs = -4 * 3600; + else if (!wcsnicmp(str, L"PST", 3)) + secs = -8 * 3600; + else if (!wcsnicmp(str, L"EST", 3) || !wcsnicmp(str, L"CDT", 3)) + secs = -5 * 3600; + else if (!wcsnicmp(str, L"MST", 3) || !wcsnicmp(str, L"PDT", 3)) + secs = -7 * 3600; + else if (!wcsnicmp(str, L"CST", 3) || !wcsnicmp(str, L"MDT", 3)) + secs = -6 * 3600; + else if ( (str[0] == L'+' || str[0] == L'-') && + iswdigit(str[1]) && iswdigit(str[2]) && + iswdigit(str[3]) && iswdigit(str[4])) + { + secs = 3600 * (10 * (str[1] - 48) + str[2] - 48); + secs += 60 * (10 * (str[3] - 48) + str[4] - 48); + + if (str[0] == L'-') + secs = -secs; + } + else + secs = 1; + + return secs; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/RFCDate.h b/Src/Plugins/Library/ml_wire/RFCDate.h new file mode 100644 index 00000000..7606272f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RFCDate.h @@ -0,0 +1,7 @@ +#ifndef NULLSOFT_RFCDATEH +#define NULLSOFT_RFCDATEH + +void MakeRFCDateString(__time64_t convertTime, wchar_t *data_str, size_t len); +__time64_t MakeRFCDate(const wchar_t *date); +void MakeDateString(__time64_t convertTime, wchar_t *date_str, size_t len); +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/RSSCOM.cpp b/Src/Plugins/Library/ml_wire/RSSCOM.cpp new file mode 100644 index 00000000..2db9a2bf --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RSSCOM.cpp @@ -0,0 +1,232 @@ +#include "main.h" +#include "./rssCOM.h" +#include "./util.h" +#include "api__ml_wire.h" +#include "./cloud.h" +#include "./feedUtil.h" +#include "./defaults.h" +#include "./errors.h" + +#include "../winamp/jsapi.h" + +#include "./rssCOM.h" + +#include <strsafe.h> + +extern Cloud cloud; + + +#define DISPTABLE_CLASS RssCOM + +DISPTABLE_BEGIN() + DISPENTRY_ADD(DISPATCH_SUBSCRIBE, L"subscribe", OnSubscribe) +DISPTABLE_END + +#undef DISPTABLE_CLASS + + + +RssCOM::RssCOM() +{} + +RssCOM::~RssCOM() +{} + +HRESULT RssCOM::CreateInstance(RssCOM **instance) +{ + if (NULL == instance) return E_POINTER; + + *instance = new RssCOM(); + if (NULL == *instance) return E_OUTOFMEMORY; + + return S_OK; +} + +STDMETHODIMP_(ULONG) RssCOM::AddRef(void) +{ + return _ref.fetch_add( 1 ); +} + +STDMETHODIMP_(ULONG) RssCOM::Release(void) +{ + if (0 == _ref.load() ) + return _ref.load(); + + LONG r = _ref.fetch_sub( 1 ); + if (0 == r) + delete(this); + + return r; +} + +STDMETHODIMP RssCOM::QueryInterface(REFIID riid, PVOID *ppvObject) +{ + if (NULL == ppvObject) return E_POINTER; + + if (IsEqualIID(riid, IID_IDispatch)) + *ppvObject = static_cast<IDispatch*>(this); + else if (IsEqualIID(riid, IID_IUnknown)) + *ppvObject = static_cast<IUnknown*>(this); + else + { + *ppvObject = NULL; + return E_NOINTERFACE; + } + + AddRef(); + return S_OK; +} + + +HRESULT RssCOM::OnSubscribe(WORD wFlags, DISPPARAMS FAR *pdispparams, VARIANT FAR *pvarResult, unsigned int FAR *puArgErr) +{ + JSAPI_VERIFY_METHOD(wFlags); + JSAPI_VERIFY_PARAMCOUNT(pdispparams, 1); + + BSTR url; + + JSAPI_GETSTRING(url, pdispparams, 1, puArgErr); + + SubscribeUrl(url, pvarResult); + + return S_OK; +} + +struct SubscribeThreadData +{ + LPWSTR url; +}; +static LPCWSTR FormatLangString(LPWSTR pszBuffer, INT cchBufferMax, LPWSTR pszFormat, INT cchFormatMax, INT formatId, ...) +{ + HRESULT hr; + va_list argList; + va_start(argList, formatId); + + if(NULL != WASABI_API_LNGSTRINGW_BUF(formatId, pszFormat, cchFormatMax)) + { + hr = StringCchVPrintfExW(pszBuffer, cchBufferMax, NULL, NULL, STRSAFE_IGNORE_NULLS, pszFormat, argList); + } + else + { + hr = E_FAIL; + } + + va_end(argList); + + return (SUCCEEDED(hr)) ? pszBuffer : NULL; +} + +static DWORD WINAPI SubscribeThreadProc(void *param) +{ + SubscribeThreadData *data = (SubscribeThreadData *)param; + Channel newFeed; + newFeed.updateTime = updateTime; + newFeed.autoUpdate = autoUpdate; + newFeed.autoDownload = autoDownload; + newFeed.autoDownloadEpisodes = autoDownloadEpisodes; + newFeed.SetURL(data->url); + newFeed.needsRefresh = true; + + WCHAR szBuffer1[2048] = {0}, szBuffer2[2048] = {0}; + LPCWSTR pszMessage = NULL, pszTitle = NULL; + + switch (DownloadFeedInformation(newFeed)) + { + case DOWNLOAD_SUCCESS: + { + Nullsoft::Utility::AutoLock lock (channels LOCKNAME("AddURL")); + newFeed.autoDownload = ::autoDownload; + if (channels.AddChannel(newFeed)) + cloud.Pulse(); + } + break; + + case DOWNLOAD_DUPLICATE: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_ALREADY_SUBSCRIBED, newFeed.title, data->url); + break; + + case DOWNLOAD_404: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_FILE_NOT_FOUND, data->url); + break; + + case DOWNLOAD_TIMEOUT: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_CONNECTION_TIMED_OUT, data->url); + break; + + case DOWNLOAD_ERROR_PARSING_XML: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_ERROR_PARSING_XML, data->url); + break; + + case DOWNLOAD_NOTRSS: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_INVALID_RSS_FEED, data->url); + break; + + case DOWNLOAD_CONNECTIONRESET: + pszMessage = FormatLangString(szBuffer1, ARRAYSIZE(szBuffer1), szBuffer2, ARRAYSIZE(szBuffer2), IDS_CONNECTION_RESET, data->url); + break; + + case DOWNLOAD_NOHTTP: + pszMessage = WASABI_API_LNGSTRINGW_BUF(IDS_NO_JNETLIB, szBuffer1, ARRAYSIZE(szBuffer1)); + pszTitle = WASABI_API_LNGSTRINGW_BUF(IDS_JNETLIB_MISSING, szBuffer2, ARRAYSIZE(szBuffer2)); + break; + + case DOWNLOAD_NOPARSER: + pszMessage = WASABI_API_LNGSTRINGW_BUF(IDS_NO_EXPAT, szBuffer1, ARRAYSIZE(szBuffer1)); + pszTitle = WASABI_API_LNGSTRINGW_BUF(IDS_EXPAT_MISSING, szBuffer2, ARRAYSIZE(szBuffer2)); + break; + + + } + + if(NULL != pszMessage) + { + if (NULL == pszTitle) + pszTitle = WASABI_API_LNGSTRINGW_BUF(IDS_ERROR_SUBSCRIBING_TO_PODCAST, szBuffer2, ARRAYSIZE(szBuffer2)); + + MessageBox(plugin.hwndLibraryParent, pszMessage, pszTitle, MB_ICONERROR | MB_OK); + } + + Plugin_FreeString(data->url); + free(data); + return 0; +} + +LPCWSTR RssCOM::GetName() +{ + return L"Podcast"; +} + +HRESULT RssCOM::SubscribeUrl(BSTR url, VARIANT FAR *result) +{ + HRESULT hr; + SubscribeThreadData *data = (SubscribeThreadData*)malloc(sizeof(SubscribeThreadData)); + if (NULL != data) + { + data->url = Plugin_CopyString(url); + DWORD threadId; + HANDLE hThread = CreateThread(NULL, NULL, SubscribeThreadProc, (void*)data, NULL, &threadId); + if (NULL == hThread) + { + DWORD error = GetLastError(); + hr = HRESULT_FROM_WIN32(error); + Plugin_FreeString(data->url); + free(data); + } + else + { + CloseHandle(hThread); + hr = S_OK; + } + } + else + hr = E_OUTOFMEMORY; + + if (NULL != result) + { + VariantInit(result); + V_VT(result) = VT_BOOL; + V_BOOL(result) = (SUCCEEDED(hr) ? VARIANT_TRUE : VARIANT_FALSE); + } + + return hr; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/RSSCOM.h b/Src/Plugins/Library/ml_wire/RSSCOM.h new file mode 100644 index 00000000..39d0e58b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RSSCOM.h @@ -0,0 +1,42 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_RSS_COM_HEADER +#define NULLSOFT_PODCAST_PLUGIN_RSS_COM_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> +#include <atomic> + +#include "../nu/dispatchTable.h" + +class RssCOM : public IDispatch +{ +public: + typedef enum + { + DISPATCH_SUBSCRIBE = 0, + } DispatchCodes; + +protected: + RssCOM(); + ~RssCOM(); + +public: + static HRESULT CreateInstance(RssCOM **instance); + static HRESULT SubscribeUrl(BSTR url, VARIANT FAR *result); + static LPCWSTR GetName(); + + /* IUnknown*/ + STDMETHOD(QueryInterface)(REFIID riid, PVOID *ppvObject); + STDMETHOD_(ULONG, AddRef)(void); + STDMETHOD_(ULONG, Release)(void); + +protected: + DISPTABLE_INCLUDE(); + DISPHANDLER_REGISTER(OnSubscribe); + + std::atomic<std::size_t> _ref = 1; +}; + +#endif //NULLSOFT_PODCAST_PLUGIN_RSS_COM_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/RSSParse.cpp b/Src/Plugins/Library/ml_wire/RSSParse.cpp new file mode 100644 index 00000000..bf2d2d3e --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RSSParse.cpp @@ -0,0 +1,181 @@ +#include "Main.h" +#include "RSSParse.h" + +#include "RFCDate.h" +#include "../xml/XMLNode.h" +#include "Feeds.h" +#include "Defaults.h" +#include "ChannelSync.h" +#include "ParseUtil.h" +#include <strsafe.h> + +static void ReadWinampSpecificItem(const XMLNode *item, RSS::Item &newItem) +{ + newItem.listened = PropertyIsTrue(item, L"winamp:listened"); + newItem.downloaded = PropertyIsTrue(item, L"winamp:downloaded"); +} + +static void ReadRSSItem(XMLNode *item, Channel &channel, bool doWinampSpecificTags) +{ + RSS::MutableItem newItem; + if (doWinampSpecificTags) + ReadWinampSpecificItem(item, newItem); + + const wchar_t *pubdate = GetContent(item, L"pubDate"); + if (pubdate && pubdate[0]) + { + newItem.publishDate = MakeRFCDate(pubdate); + if (newItem.publishDate <= 0) + { + newItem.publishDate = _time64(0); + newItem.generatedDate = true; + } + else + newItem.generatedDate = false; + } + else + { + newItem.publishDate = _time64(0); + newItem.generatedDate = true; + } + + const wchar_t *itemName = GetContent(item, L"title"); + if (itemName && itemName[0]) + newItem.SetItemName(itemName); + else + { + wchar_t date_str[128] = {0}; + MakeRFCDateString(newItem.publishDate, date_str, 128); + newItem.SetItemName(date_str); + } + + newItem.SetLink(GetContent(item, L"link")); + newItem.SetURL(GetProperty(item, L"enclosure", L"url")); + const wchar_t* src = GetProperty(item, L"source", L"src"); + if (!src || !src[0]) + { + newItem.SetSourceURL(GetProperty(item, L"source", L"url")); + } + else + { + newItem.SetSourceURL(src/*GetProperty(item, L"source", L"src")*/); + } + newItem.SetSize(GetProperty(item, L"enclosure", L"length")); + + const wchar_t *guid = GetContent(item, L"guid"); + if (!guid || !guid[0]) + guid = GetProperty(item, L"enclosure", L"url"); + + if (guid && guid[0]) + { + newItem.SetGUID(guid); + } + else + { + wchar_t generated_guid[160] = {0}; + StringCbPrintf(generated_guid, sizeof(generated_guid), L"%s%s", channel.title, newItem.itemName); + newItem.SetGUID(generated_guid); + } + + const wchar_t *description = GetContent(item, L"description"); + if (!description || !description[0]) + description = GetContent(item, L"content:encoded"); + newItem.SetDescription(description); + + const wchar_t *duration = GetContent(item, L"itunes:duration"); + if (duration && duration[0]) + newItem.SetDuration(duration); + + if (newItem.itemName && newItem.itemName[0]) + { + channel.items.push_back(newItem); + } +} + +void ReadWinampSpecificChannel(const XMLNode *node, Channel &newChannel) +{ + const XMLNode *curNode = 0; + const wchar_t *lastupdate = node->GetProperty(L"winamp:lastupdate"); + if (lastupdate && lastupdate[0]) + newChannel.lastUpdate = MakeRFCDate(lastupdate); + + const wchar_t *winamp_url = GetContent(node, L"winamp:url"); + newChannel.SetURL(winamp_url); + + // set to preference value first + newChannel.updateTime = updateTime; + newChannel.autoUpdate = autoUpdate; + newChannel.autoDownload = autoDownload; + newChannel.autoDownloadEpisodes = autoDownloadEpisodes; + + curNode = node->Get(L"winamp:update"); + if (curNode) + { + newChannel.useDefaultUpdate = PropertyIsTrue(curNode, L"usedefaultupdate"); + newChannel.needsRefresh = PropertyIsTrue(curNode, L"needsrefresh"); + if (!newChannel.useDefaultUpdate) + { + newChannel.updateTime = _wtoi64(curNode->GetProperty(L"updatetime")); + newChannel.autoUpdate = PropertyIsTrue(curNode, L"autoupdate"); + } + } + + curNode = node->Get(L"winamp:download"); + if (curNode) + { + newChannel.autoDownload = PropertyIsTrue(curNode, L"autodownload"); + if (newChannel.autoDownload) + { + const wchar_t *prop = curNode->GetProperty(L"autoDownloadEpisodes"); + if (prop) + newChannel.autoDownloadEpisodes = _wtoi(prop); + } + } +} + +static void ReadRSSChannel(const XMLNode *node, Channel &newChannel, bool doWinampSpecificTags) +{ + XMLNode::NodeList::const_iterator itemItr; + if (doWinampSpecificTags) + ReadWinampSpecificChannel(node, newChannel); + + const wchar_t *title = GetContent(node, L"title"); + newChannel.SetTitle(title); + + const wchar_t *link = GetContent(node, L"link"); + newChannel.SetLink(link); + + const wchar_t *description = GetContent(node, L"description"); + newChannel.SetDescription(description); + + const wchar_t *ttl = GetContent(node, L"ttl"); + if (ttl) + newChannel.ttl = _wtoi(ttl); + + const XMLNode::NodeList *items = node->GetList(L"item"); + if (items) + { + for (itemItr = items->begin(); itemItr != items->end(); itemItr++) + ReadRSSItem(*itemItr, newChannel, doWinampSpecificTags); + } +} + +void ReadRSS(const XMLNode *rss, ChannelSync *sync, bool doWinampSpecificTags, const wchar_t *url) +{ + XMLNode::NodeList::const_iterator itr; + + sync->BeginChannelSync(); + const XMLNode::NodeList *channelList = rss->GetList(L"channel"); + if (channelList) + { + for (itr = channelList->begin(); itr != channelList->end(); itr ++) + { + Channel newChannel; + ReadRSSChannel(*itr, newChannel, doWinampSpecificTags); + if (!newChannel.url || !newChannel.url[0]) + newChannel.SetURL(url); + sync->NewChannel(newChannel); + } + } + sync->EndChannelSync(); +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/RSSParse.h b/Src/Plugins/Library/ml_wire/RSSParse.h new file mode 100644 index 00000000..9cdf6c79 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/RSSParse.h @@ -0,0 +1,9 @@ +#ifndef NULLSOFT_RSSPARSEH +#define NULLSOFT_RSSPARSEH + +#include "../xml/XMLNode.h" +#include "ChannelSync.h" + +void ReadRSS(const XMLNode *rss, ChannelSync *sync, bool doWinampSpecificTags, const wchar_t *url); + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/TODO.txt b/Src/Plugins/Library/ml_wire/TODO.txt new file mode 100644 index 00000000..db9d5965 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/TODO.txt @@ -0,0 +1,51 @@ +to fix
+----
+crash on shutdown
+
+
+/////------------ 1.1 below --------------
+automatically switch to 'custom' if you click on dropdown in "add" or "edit" url dialog
+
+need icon for listened media
+maybe one for read text?
+
+strip whitespace from beginning of titles
+
+multiple-select
+
+Allow for customizing the download location in add/edit url
+
+deletable items (needs to move to a separate 'deleted items' list so we don't re-add them next rss refresh)
+
+drag-n-drop from webpages
+
+once we get an HTTP 200, we should put the downloaded on the 'downloads' list, and be able to update the download percentage status as necessary
+
+BACKGROUND DOWNLOADER
+<<<
+avoid multiple downloads of the same thing
+avoid downlaoding things that have already been downloaded.
+range / if-range to handle download resuming
+save the last modified dates from "Last-Modified" header
+save unfinished downloads to an XML file and read on load
+>>>
+
+UNIFIED DOWNLOAD MANAGER CONCEPT !!!!!
+
+who needs updates
+downloaded file list
+downloads page (to refresh view)
+item object
+podcast page (to refresh view)
+
+
+new way of listing items
+---
+create a common "items" data structure that select channels add their items to.
+When a channel is select, it adds its items.
+When a channel is deselected, it removes its items.
+When a channel is refreshed, it re-adds its items (assuming the item-adder function protects against dupes)
+only potential issue is if a channel somehow "loses" items (or an item's GUID is changes)
+could be fixed by either 1) keeps track of "parent channel" in the items list
+2) rebuilding the entire items list on every channel refresh
+or 3) preventing GUID changes and item deletions (or forcing an item list rebuild if it does occur)
diff --git a/Src/Plugins/Library/ml_wire/UpdateAutoDownload.cpp b/Src/Plugins/Library/ml_wire/UpdateAutoDownload.cpp new file mode 100644 index 00000000..521a0b83 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/UpdateAutoDownload.cpp @@ -0,0 +1,57 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "UpdateAutoDownload.h" + +int UpdateAutoDownload::episodes[] = {0, // AUTODOWNLOAD_NEVER + 1, // AUTODOWNLOAD_LAST_ONE + 2, // AUTODOWNLOAD_LAST_TWO + 3, // AUTODOWNLOAD_LAST_THREE + 5, // AUTODOWNLOAD_LAST_FIVE +}; + +const wchar_t *UpdateAutoDownload::GetTitle(int position, wchar_t *buffer, int bufferMax) +{ + if (NULL == buffer) + return NULL; + + INT stringId = IDS_ERROR_FYEO; + switch (position) + { + case AUTODOWNLOAD_NEVER: stringId = IDS_ATD_NEVER; break; + case AUTODOWNLOAD_LAST_ONE: stringId = IDS_ATD_LASTONE; break; + case AUTODOWNLOAD_LAST_TWO: stringId = IDS_ATD_LASTTWO; break; + case AUTODOWNLOAD_LAST_THREE: stringId = IDS_ATD_LASTTHREE; break; + case AUTODOWNLOAD_LAST_FIVE: stringId = IDS_ATD_LASTFIVE; break; + } + return WASABI_API_LNGSTRINGW_BUF(stringId, buffer, bufferMax); +} + + +bool UpdateAutoDownload::GetAutoDownload(int selection) +{ + if (selection == AUTODOWNLOAD_NEVER) + return false; + else + return true; +} + +int UpdateAutoDownload::GetAutoDownloadEpisodes(int selection) +{ + if (selection >= 0 && selection < AUTODOWNLOAD_NUMENTRIES) + return episodes[selection]; + else + return 0; +} + +int UpdateAutoDownload::GetSelection(int selEpisodes, bool autoDownload) +{ + if (!autoDownload) + return AUTODOWNLOAD_NEVER; + + for (int i = AUTODOWNLOAD_LAST_ONE;i < AUTODOWNLOAD_NUMENTRIES;i++) + if (selEpisodes == episodes[i]) + return i; + + return AUTODOWNLOAD_LAST_ONE; + +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/UpdateAutoDownload.h b/Src/Plugins/Library/ml_wire/UpdateAutoDownload.h new file mode 100644 index 00000000..61ea9efc --- /dev/null +++ b/Src/Plugins/Library/ml_wire/UpdateAutoDownload.h @@ -0,0 +1,28 @@ +#ifndef NULLSOFT_UPDATEAUTODOWNLOADH +#define NULLSOFT_UPDATEAUTODOWNLOADH + +/* This header file is used by FeedsDialog.h +It provides a set of helper functions to deal with the combo box for auto download + +basically - converts between combo box choice and int +*/ +namespace UpdateAutoDownload +{ + enum + { + AUTODOWNLOAD_NEVER = 0, + AUTODOWNLOAD_LAST_ONE, + AUTODOWNLOAD_LAST_TWO, + AUTODOWNLOAD_LAST_THREE, + AUTODOWNLOAD_LAST_FIVE, + AUTODOWNLOAD_NUMENTRIES + }; + + extern int episodes[]; + + const wchar_t *GetTitle(int position, wchar_t *buffer, int bufferMax); + bool GetAutoDownload(int selection); + int GetAutoDownloadEpisodes(int selection); + int GetSelection(int selEpisodes, bool autoDownload); +} +#endif diff --git a/Src/Plugins/Library/ml_wire/UpdateTime.cpp b/Src/Plugins/Library/ml_wire/UpdateTime.cpp new file mode 100644 index 00000000..008a6118 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/UpdateTime.cpp @@ -0,0 +1,62 @@ +#include "main.h" +#include "api__ml_wire.h" +#include "UpdateTime.h" + +__time64_t Update::times[] = {0, // TIME_MANUALLY +60 /* 1 minute */ * 60 /* 1 hour */ * 24 /* 1 day */ * 7 /* 1 week */, // TIME_WEEKLY +60 /* 1 minute */ * 60 /* 1 hour */ * 24 /* 1 day */, // TIME_DAILY +60 /* 1 minute */ * 60 /* 1 hour */, // TIME_HOURLY +}; + +const wchar_t *Update::GetTitle( int position, wchar_t *buffer, int bufferMax ) +{ + if ( NULL == buffer ) + return NULL; + + INT stringId = IDS_ERROR_FYEO; + switch ( position ) + { + case TIME_MANUALLY: + stringId = IDS_UPD_MANUALLY; + break; + case TIME_WEEKLY: + stringId = IDS_UPD_WEEK; + break; + case TIME_DAILY: + stringId = IDS_UPD_DAY; + break; + case TIME_HOURLY: + stringId = IDS_UPD_HOUR; + break; + } + return WASABI_API_LNGSTRINGW_BUF( stringId, buffer, bufferMax ); +} + +bool Update::GetAutoUpdate(int selection) +{ + if (selection == TIME_MANUALLY) + return false; + else + return true; +} + +__time64_t Update::GetTime(int selection) +{ + if (selection >= 0 && selection < TIME_NUMENTRIES) + return times[selection]; + else + return 0; +} + +int Update::GetSelection(__time64_t selTime, bool autoUpdate) +{ + if (!autoUpdate) + return TIME_MANUALLY; + + for (int i = TIME_WEEKLY;i < TIME_NUMENTRIES;i++) + if (selTime >= times[i]) + return i; + + return TIME_DAILY; + +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/UpdateTime.h b/Src/Plugins/Library/ml_wire/UpdateTime.h new file mode 100644 index 00000000..3edc670d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/UpdateTime.h @@ -0,0 +1,29 @@ +#ifndef NULLSOFT_UPDATETIMEH +#define NULLSOFT_UPDATETIMEH + +#include <time.h> + +/* This header file is used by FeedsDialog.h +It provides a set of helper functions to deal with the combo box for update time + +basically - converts between combo box choice and __time64_t +*/ +namespace Update +{ + enum + { + TIME_MANUALLY = 0, + TIME_WEEKLY, + TIME_DAILY, + TIME_HOURLY, + TIME_NUMENTRIES + }; + + extern __time64_t times[]; + + const wchar_t *GetTitle(int position, wchar_t *buffer, int bufferMax); + bool GetAutoUpdate(int selection); + __time64_t GetTime(int selection); + int GetSelection(__time64_t selTime, bool autoUpdate); +} +#endif diff --git a/Src/Plugins/Library/ml_wire/Util.h b/Src/Plugins/Library/ml_wire/Util.h new file mode 100644 index 00000000..2966fa1b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Util.h @@ -0,0 +1,69 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_UTIL_HEADER +#define NULLSOFT_PODCAST_PLUGIN_UTIL_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> +#include "../nu/trace.h" + +#define CSTR_INVARIANT MAKELCID(MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), SORT_DEFAULT) + +#ifndef ARRAYSIZE +#define ARRAYSIZE(blah) (sizeof(blah)/sizeof(*blah)) +#endif + +#ifndef LONGX86 +#ifdef _WIN64 + #define LONGX86 LONG_PTR +#else /*_WIN64*/ + #define LONGX86 LONG +#endif /*_WIN64*/ +#endif // LONGX86 + +#ifdef __cplusplus + #define SENDMSG(__hwnd, __msgId, __wParam, __lParam) ::SendMessageW((__hwnd), (__msgId), (__wParam), (__lParam)) +#else + #define SENDMSG(__hwnd, __msgId, __wParam, __lParam) SendMessageW((__hwnd), (__msgId), (__wParam), (__lParam)) +#endif // __cplusplus + +#define SENDMLIPC(__hwndML, __ipcMsgId, __param) SENDMSG((__hwndML), WM_ML_IPC, (WPARAM)(__param), (LPARAM)(__ipcMsgId)) +#define SENDWAIPC(__hwndWA, __ipcMsgId, __param) SENDMSG((__hwndWA), WM_WA_IPC, (WPARAM)(__param), (LPARAM)(__ipcMsgId)) + + +#if defined (_WIN64) + #define MSGRESULT(__hwnd, __result) { SetWindowLongPtrW((__hwnd), DWLP_MSGRESULT, ((LONGX86)(LONG_PTR)(__result))); return TRUE; } +#else + #define MSGRESULT(__hwnd, __result) { SetWindowLongPtrW((__hwnd), DWL_MSGRESULT, ((LONGX86)(LONG_PTR)(__result))); return TRUE; } +#endif + +#define SENDCMD(__hwnd, __ctrlId, __eventId, __hctrl) (SENDMSG((__hwnd), WM_COMMAND, MAKEWPARAM(__ctrlId, __eventId), (LPARAM)(__hctrl))) + + +LPWSTR Plugin_MallocString(size_t cchLen); +LPWSTR Plugin_ReAllocString(LPWSTR pszString, size_t cchLen); +void Plugin_FreeString(LPWSTR pszString); +LPWSTR Plugin_CopyString(LPCWSTR pszSource); + +LPSTR Plugin_MallocAnsiString(size_t cchLen); +LPSTR Plugin_CopyAnsiString(LPCSTR pszSource); +void Plugin_FreeAnsiString(LPSTR pszString); + +LPWSTR Plugin_DuplicateResString(LPCWSTR pszResource); +void Plugin_FreeResString(LPWSTR pszResource); +HRESULT Plugin_CopyResString(LPWSTR pszBuffer, INT cchBufferMax, LPCWSTR pszString); + +LPSTR Plugin_WideCharToMultiByte(UINT codePage, DWORD dwFlags, LPCWSTR lpWideCharStr, INT cchWideChar, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar); +LPWSTR Plugin_MultiByteToWideChar(UINT codePage, DWORD dwFlags, LPCSTR lpMultiByteStr, INT cbMultiByte); + +void Plugin_SafeRelease(IUnknown *pUnk); + + +HRESULT Plugin_FileExtensionFromUrl(LPWSTR pszBuffer, INT cchBufferMax, LPCWSTR pszUrl, LPCWSTR defaultExtension); +void Plugin_ReplaceBadPathChars(LPWSTR pszPath); +INT Plugin_CleanDirectory(LPWSTR pszPath); + +HRESULT Plugin_EnsurePathExist(LPCWSTR pszDirectory); + +#endif // NULLSOFT_PODCAST_PLUGIN_UTIL_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/WantsDownloadStatus.h b/Src/Plugins/Library/ml_wire/WantsDownloadStatus.h new file mode 100644 index 00000000..39bf1e5d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/WantsDownloadStatus.h @@ -0,0 +1,14 @@ +#ifndef NULLSOFT_WANTSDOWNLOADSTATUSH +#define NULLSOFT_WANTSDOWNLOADSTATUSH + +class WantsDownloadStatus +{ +public: + virtual void DownloadStarted() {} + virtual void UpdateDownloadStatus(size_t downloaded, size_t totalBytes) {} + virtual void DownloadDone() {} + virtual void DownloadFailed() {} + +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/Wire.cpp b/Src/Plugins/Library/ml_wire/Wire.cpp new file mode 100644 index 00000000..e8069f19 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Wire.cpp @@ -0,0 +1,99 @@ +#include "Main.h" + +#include "./subscriptionView.h" +#include "FeedsDialog.h" +#include "Feeds.h" + +#include <algorithm> + + +ChannelList channels; + +//CategoryIndex sourceByCategory; +using namespace Nullsoft::Utility; +//LockGuard /*feedGuard, */channelGuard, categoryGuard; + +void WireManager::BeginChannelSync() +{ + // TODO something better than this =) + // like making 'touched' flags and delete untouched ones + //sources.clear(); + //sourceByCategory.clear(); +} + +void WireManager::NewChannel(const Channel &newChannel) +{ + AutoLock lock (channels LOCKNAME("NewChannel")); + for (ChannelList::iterator itr=channels.begin();itr!=channels.end(); itr++) + { + if (*itr == newChannel) + { + itr->UpdateFrom(newChannel); + return; + } + } + channels.push_back(newChannel); + +} + +void WireManager::EndChannelSync() +{ + //SubscriptionView_RefreshChannels(NULL, TRUE); +} + +bool ChannelTitleSort(Channel &channel1, Channel &channel2) +{ + return (CSTR_LESS_THAN == CompareStringW(LOCALE_USER_DEFAULT, NORM_IGNORECASE, channel1.title, -1, channel2.title, -1)); +} + +void ChannelList::SortByTitle() +{ + AutoLock lock (channelGuard LOCKNAME("SortByTitle")); + std::sort(channelList.begin(), channelList.end(), ChannelTitleSort); +} + +void ChannelList::push_back(const Channel &channel) +{ + channelList.push_back(channel); +} + +bool ChannelList::AddChannel(Channel &channel) +{ + ChannelList::iterator found; + for (found=channels.begin(); found!=channels.end(); found++) + { + if (!wcscmp(found->url, channel.url)) + break; + } + + if (found == channels.end()){ + channel.needsRefresh=true; + push_back(channel); + SaveChannels(*this); + return true; + } + + return false; +} + +size_t ChannelList::GetNumPodcasts() +{ + return channels.size(); +} + +ifc_podcast *ChannelList::EnumPodcast(size_t i) +{ + if (i < channels.size()) + return &channels[i]; + else + return 0; +} + +#undef CBCLASS +#define CBCLASS ChannelList +START_DISPATCH; +CB(API_PODCASTS_GETNUMPODCASTS, GetNumPodcasts) +CB(API_PODCASTS_ENUMPODCAST, EnumPodcast) +END_DISPATCH; +#undef CBCLASS + diff --git a/Src/Plugins/Library/ml_wire/Wire.h b/Src/Plugins/Library/ml_wire/Wire.h new file mode 100644 index 00000000..8049be62 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/Wire.h @@ -0,0 +1,62 @@ +#ifndef NULLSOFT_WIREH +#define NULLSOFT_WIREH + +#include "Feeds.h" +#include "../nu/AutoLock.h" +#include "api_podcasts.h" +#include <vector> + +class ChannelList : public api_podcasts +{ +public: + + ChannelList() : channelGuard( "Feed Guard" ) {} + + typedef std::vector<Channel> ChannelContainer; + typedef ChannelContainer::iterator iterator; + + size_t size() { return channelList.size(); } + bool empty() { return channelList.empty(); } + + void push_back( const Channel &channel ); + bool AddChannel( Channel &channel ); + + iterator begin() { return channelList.begin(); } + + iterator end() { return channelList.end(); } + + void RemoveChannel( int index ) { channelList.erase( channelList.begin() + index ); } + + Channel &operator[]( size_t index ) { return channelList[ index ]; } + + ChannelContainer channelList; + + operator Nullsoft::Utility::LockGuard &( ) { return channelGuard; } + + void SortByTitle(); + + Nullsoft::Utility::LockGuard channelGuard; + +public: // api_podcasts interface + size_t GetNumPodcasts(); + ifc_podcast *EnumPodcast( size_t i ); + +protected: + RECVS_DISPATCH; +}; + +extern ChannelList channels; +//extern CategoryIndex sourceByCategory; +#include "ChannelSync.h" + +class WireManager : public ChannelSync +{ +public: + void BeginChannelSync(); + void NewChannel( const Channel &newChannel ); + void EndChannelSync(); +}; + +extern WireManager channelMgr; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/XMLWriter.cpp b/Src/Plugins/Library/ml_wire/XMLWriter.cpp new file mode 100644 index 00000000..831db94e --- /dev/null +++ b/Src/Plugins/Library/ml_wire/XMLWriter.cpp @@ -0,0 +1,272 @@ +#include "Main.h" +#include "Feeds.h" +#include "RFCDate.h" +#include "Downloaded.h" +#include "../nu/AutoLock.h" +#include "Defaults.h" +#include "../Agave/Language/api_language.h" + +using namespace Nullsoft::Utility; + +static void XMLWriteString(FILE *fp, const wchar_t *str) +{ + for (const wchar_t *itr = str; *itr; itr=CharNextW(itr)) + { + switch (*itr) + { + case '<': fputws(L"<", fp); break; + case '>': fputws(L">", fp); break; + case '&': fputws(L"&", fp); break; + case '\"': fputws(L""", fp); break; + case '\'': fputws(L"'", fp); break; + default: fputwc(*itr, fp); break; + } + } +} + +static void InstaProp(FILE *fp, const wchar_t *property, const wchar_t *value) +{ + fwprintf(fp, L" %s=\"", property); + XMLWriteString(fp, value); + fputws(L"\"", fp); +} + +static void InstaProp(FILE *fp, const wchar_t *property, int value) +{ + fwprintf(fp, L" %s=\"%i\"", property, value); +} + +static void InstaPropI64(FILE *fp, const wchar_t *property, int64_t value) +{ + fwprintf(fp, L" %s=\"%I64d\"", property, value); +} + +static void InstaPropTime(FILE *fp, const wchar_t *property, __time64_t value) +{ + fwprintf(fp, L" %s=\"%I64u\"", property, value); +} + +static void InstaProp(FILE *fp, const wchar_t *property, float value) +{ + _fwprintf_l(fp, L" %s=\"%3.3f\"", WASABI_API_LNG->Get_C_NumericLocale(), property, value); +} + +static void InstaProp(FILE *fp, const wchar_t *property, bool value) +{ + fwprintf(fp, L" %s=\"", property); + if (value) + fputws(L"true\"", fp); + else + fputws(L"false\"", fp); +} + +static void InstaTag(FILE *fp, const wchar_t *tag, const wchar_t *content) +{ + if (content && !((INT_PTR)content < 65536) && *content) + { + fwprintf(fp, L"<%s>", tag); + XMLWriteString(fp, content); + fwprintf(fp, L"</%s>\r\n", tag); + } +} + +static void InstaTag(FILE *fp, const wchar_t *tag, unsigned int uint) +{ + fwprintf(fp, L"<%s>%u</%s>", tag, uint, tag); +} + +static void InstaTag(FILE *fp, const wchar_t *tag, __time64_t uint) +{ + fwprintf(fp, L"<%s>%I64u</%s>", tag, uint, tag); +} + +static void BuildXMLString(FILE *fp, const RSS::Item &item) +{ + fputws(L"<item", fp); + InstaProp(fp, L"winamp:listened", item.listened); + InstaProp(fp, L"winamp:downloaded", item.downloaded); + fputws(L">\r\n", fp); + + InstaTag(fp, L"title", item.itemName); + + if (item.url && item.url[0]) + { + fputws(L"<enclosure", fp); + InstaProp(fp, L"url", item.url); + InstaPropI64(fp, L"length", item.size); + fputws(L"/>\r\n", fp); + } + + InstaTag(fp, L"guid", item.guid); + InstaTag(fp, L"description", item.description); + InstaTag(fp, L"link", item.link); + + if (item.duration && item.duration[0]) + { + InstaTag(fp, L"itunes:duration", item.duration); + } + + if (item.publishDate) + { + wchar_t date_str[128] = {0}; + MakeRFCDateString(item.publishDate, date_str, 128); + InstaTag(fp, L"pubDate", date_str); + } + + if (item.sourceUrl && item.sourceUrl[0]) + { + fputws(L"<source",fp); + InstaProp(fp, L"src", item.sourceUrl); + fputws(L"/>\r\n", fp); + } + + fputws(L"</item>\r\n", fp); +} + +static void BuildXMLString(FILE *fp, Channel &channel) +{ + fputws(L"<channel", fp); + + wchar_t date_str[128] = {0}; + MakeRFCDateString(channel.lastUpdate, date_str, 128); + + InstaProp(fp, L"winamp:lastupdate", date_str); + fputws(L">\r\n", fp); + + // write update settings for this channel + fputws(L"<winamp:update", fp); + InstaProp(fp, L"usedefaultupdate", channel.useDefaultUpdate); + InstaProp(fp, L"needsrefresh", channel.needsRefresh); + + + if (!channel.useDefaultUpdate) + { + InstaProp(fp, L"autoupdate", channel.autoUpdate); + InstaPropTime(fp, L"updatetime", channel.updateTime); + } + fputws(L"/>\r\n", fp); + + if (!channel.useDefaultUpdate) + { + fputws(L"<winamp:download", fp); + InstaProp(fp, L"autodownload", channel.autoDownload); + + if (channel.autoDownload) + { + InstaProp(fp, L"autoDownloadEpisodes", channel.autoDownloadEpisodes); + } + fputws(L"/>\r\n", fp); + } + + InstaTag(fp, L"winamp:url", channel.url); + InstaTag(fp, L"title", channel.title); + InstaTag(fp, L"link", channel.link); + InstaTag(fp, L"description", channel.description); + + if (channel.ttl) + InstaTag(fp, L"ttl", channel.ttl); + fputws(L"\r\n", fp); + + Channel::ItemList::iterator itemItr; + for (itemItr = channel.items.begin();itemItr != channel.items.end(); itemItr++) + BuildXMLString(fp, *itemItr); + + fputws(L"</channel>\r\n", fp); +} + +void SaveChannels(const wchar_t *fileName, ChannelList &channels) +{ + FILE *fp = _wfopen(fileName, L"wb"); + if (fp) + { + ChannelList::iterator itr; + wchar_t BOM = 0xFEFF; + fwrite(&BOM, sizeof(BOM), 1, fp); + fputws(L"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\r\n", fp); + fputws(L"<rss version=\"2.0\" xmlns:winamp=\"http://www.winamp.com\">\r\n", fp); + + for (itr = channels.begin();itr != channels.end();itr++) + BuildXMLString(fp, *itr); + + fputws(L"</rss>", fp); + fclose(fp); + } +} + +static void BuildXMLPreferences(FILE *fp) +{ + fputws(L"<download", fp); + InstaProp(fp, L"downloadpath", defaultDownloadPath); + InstaProp(fp, L"autodownload", autoDownload); + InstaProp(fp, L"autoDownloadEpisodes", autoDownloadEpisodes); + InstaProp(fp, L"needToMakePodcastsView", needToMakePodcastsView); + fputws(L"/>\r\n", fp); + + fputws(L"<update", fp); + InstaPropTime(fp, L"updatetime", updateTime); + InstaProp(fp, L"autoupdate", autoUpdate); + InstaProp(fp, L"updateonlaunch", updateOnLaunch); + fputws(L"/>\r\n", fp); + + fputws(L"<subscriptions", fp); + InstaProp(fp, L"htmldivider", htmlDividerPercent); + InstaProp(fp, L"channeldivider", channelDividerPercent); + InstaProp(fp, L"itemtitlewidth", itemTitleWidth); + InstaProp(fp, L"itemdatewidth", itemDateWidth); + InstaProp(fp, L"itemmediawidth", itemMediaWidth); + InstaProp(fp, L"itemsizewidth", itemSizeWidth); + InstaProp(fp, L"currentitemsort", currentItemSort); + InstaProp(fp, L"itemsortascending", itemSortAscending); + InstaProp(fp, L"channelsortascending", channelSortAscending); + InstaProp(fp, L"channelLastSelection", channelLastSelection); + fputws(L"/>\r\n", fp); + + fputws(L"<downloadsView", fp); + InstaProp(fp, L"downloadsChannelWidth", downloadsChannelWidth); + InstaProp(fp, L"downloadsItemWidth", downloadsItemWidth); + InstaProp(fp, L"downloadsProgressWidth", downloadsProgressWidth); + InstaProp(fp, L"downloadsPathWidth", downloadsPathWidth); + + InstaProp(fp, L"downloadsItemSort", downloadsItemSort); + InstaProp(fp, L"downloadsSortAscending", downloadsSortAscending); + fputws(L"/>\r\n", fp); + + fputws(L"<service", fp); + InstaProp(fp, L"url", serviceUrl); + fputws(L"/>\r\n", fp); +} + +static void BuildXMLDownloads(FILE *fp, DownloadList &downloads) +{ + fputws(L"<downloads>", fp); + AutoLock lock (downloads); + DownloadList::const_iterator itr; + for ( itr = downloads.begin(); itr != downloads.end(); itr++ ) + { + fputws(L"<download>", fp); + InstaTag(fp, L"channel", itr->channel); + InstaTag(fp, L"item", itr->item); + InstaTag(fp, L"url", itr->url); + InstaTag(fp, L"path", itr->path); + InstaTag(fp, L"publishDate", itr->publishDate); + fputws(L"</download>\r\n", fp); + } + + fputws(L"</downloads>", fp); +} + +void SaveSettings(const wchar_t *fileName, DownloadList &downloads) +{ + FILE *fp = _wfopen(fileName, L"wb"); + if (fp) + { + wchar_t BOM = 0xFEFF; + fwrite(&BOM, sizeof(BOM), 1, fp); + fputws(L"<?xml version=\"1.0\" encoding=\"UTF-16\"?>\r\n", fp); + fputws(L"<winamp:preferences xmlns:winamp=\"http://www.winamp.com\" version=\"2\">\r\n", fp); + BuildXMLPreferences(fp); + BuildXMLDownloads(fp, downloads); + fputws(L"</winamp:preferences>", fp); + fclose(fp); + } +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/XMLWriter.h b/Src/Plugins/Library/ml_wire/XMLWriter.h new file mode 100644 index 00000000..b2ed586a --- /dev/null +++ b/Src/Plugins/Library/ml_wire/XMLWriter.h @@ -0,0 +1,8 @@ +#ifndef NULLSOFT_XMLWRITERH +#define NULLSOFT_XMLWRITERH +#include "Wire.h" +#include "Downloaded.h" +void SaveChannels(const wchar_t *fileName, ChannelList &channels); + +void SaveSettings(const wchar_t *fileName, DownloadList &downloads); +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/api__ml_wire.h b/Src/Plugins/Library/ml_wire/api__ml_wire.h new file mode 100644 index 00000000..9f261664 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/api__ml_wire.h @@ -0,0 +1,27 @@ +#ifndef NULLSOFT_ML_WIRE_API_H +#define NULLSOFT_ML_WIRE_API_H + +#include "../Winamp/JSAPI2_api_security.h" +extern JSAPI2::api_security *jsapi2_security; +#define AGAVE_API_JSAPI2_SECURITY jsapi2_security + +#include "../Agave/Language/api_language.h" + +#include <api/application/api_application.h> +#define WASABI_API_APP applicationApi + +#include "../omBrowser/obj_ombrowser.h" +extern obj_ombrowser *browserManager; +#define OMBROWSERMNGR browserManager + +#include "../Winamp/api_stats.h" +extern api_stats *statsApi; +#define AGAVE_API_STATS statsApi + +#include "../nu/threadpool/api_threadpool.h" +extern api_threadpool *threadPoolApi; +#define WASABI_API_THREADPOOL threadPoolApi + +#include "../Agave/ExplorerFindFile/api_explorerfindfile.h" + +#endif // !NULLSOFT_ML_WIRE_API_H
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/api_podcasts.h b/Src/Plugins/Library/ml_wire/api_podcasts.h new file mode 100644 index 00000000..5085a0b2 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/api_podcasts.h @@ -0,0 +1,46 @@ +#ifndef NULLSOFT_API_ML_WIRE_PODCASTS_H +#define NULLSOFT_API_ML_WIRE_PODCASTS_H + +#include <bfc/dispatch.h> + +class ifc_podcastsubscribecallback; +class ifc_podcast; + +class api_podcasts : public Dispatchable +{ +protected: + api_podcasts() {} + ~api_podcasts() {} + +public: + size_t GetNumPodcasts(); + ifc_podcast *EnumPodcast( size_t i ); + + // TODO: locking mechanism like api_playlists + // TODO: Remove playlists + // TODO: int Subscribe(const wchar_t *url, ifc_podcastsubscribecallback *callback); + /* TODO: method to download/parse a podcast channel w/o adding it to the list + maybe as part of a separate class? */ + + enum + { + API_PODCASTS_GETNUMPODCASTS = 0, + API_PODCASTS_ENUMPODCAST = 1, + }; +}; + +inline size_t api_podcasts::GetNumPodcasts() +{ + return _call( API_PODCASTS_GETNUMPODCASTS, (size_t)0 ); +} + +inline ifc_podcast *api_podcasts::EnumPodcast( size_t i ) +{ + return _call( API_PODCASTS_ENUMPODCAST, (ifc_podcast *)0, i ); +} + +// {4D2E9987-D955-45f0-A06F-371405E8B961} +static const GUID api_podcastsGUID = +{ 0x4d2e9987, 0xd955, 0x45f0, { 0xa0, 0x6f, 0x37, 0x14, 0x5, 0xe8, 0xb9, 0x61 } }; + +#endif // !NULLSOFT_API_ML_WIRE_PODCASTS_H
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/channelEditor.cpp b/Src/Plugins/Library/ml_wire/channelEditor.cpp new file mode 100644 index 00000000..d00bfa8b --- /dev/null +++ b/Src/Plugins/Library/ml_wire/channelEditor.cpp @@ -0,0 +1,687 @@ +#include "main.h" +#include "./util.h" +#include "./channelEditor.h" +#include "api__ml_wire.h" +#include "./defaults.h" +#include "./updateTime.h" +#include "./UpdateAutoDownload.h" +#include "./errors.h" +#include "./feeds.h" +#include "./feedUtil.h" +#include "./cloud.h" +#include "./subscriptionView.h" + + +#include <strsafe.h> + +using namespace Nullsoft::Utility; +extern Cloud cloud; + +typedef struct __CHANNELEDITOR +{ + HWND hOwner; + size_t iChannel; + UINT flags; + BOOL enableAutoDownload; + BOOL enableAutoUpdate; + __time64_t updateTime; + INT numOfAutoDownloadEpisodes; +} CHANNELEDITOR; + + +#define GetEditor(__hwnd) ((CHANNELEDITOR*)GetPropW((__hwnd), MAKEINTATOM(VIEWPROP))) + +static INT_PTR CALLBACK ChannelEditor_DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); + +INT_PTR ChannelEditor_Show(HWND hOwner, size_t channelIndex, UINT flags) +{ + if (0 == VIEWPROP) + return 0; + + if (NULL == hOwner) + hOwner = plugin.hwndLibraryParent; + + if (0 == (CEF_CREATENEW & flags) && BAD_CHANNEL == channelIndex) + return 0; + + CHANNELEDITOR editor; + ZeroMemory(&editor, sizeof(editor)); + + editor.hOwner = hOwner; + editor.iChannel = channelIndex; + editor.flags = flags; + + INT_PTR result = WASABI_API_DIALOGBOXPARAMW(IDD_ADDURL, hOwner, ChannelEditor_DialogProc, (LPARAM)&editor); + return result; +} + + +static BOOL ChannelEditor_SetUpdateTime(HWND hwnd, __time64_t updateTime, BOOL enableAutoUpdate) +{ + HWND hCombo = GetDlgItem(hwnd, IDC_UPDATELIST); + if (NULL == hCombo) return FALSE; + + INT selectedVal = Update::GetSelection(updateTime, (FALSE != enableAutoUpdate)); + INT iCount = (INT)SNDMSG(hCombo, CB_GETCOUNT, 0, 0L); + INT iSelect = CB_ERR; + for (INT i = 0; i < iCount && CB_ERR == iSelect; i++) + { + if (selectedVal == (INT)SNDMSG(hCombo, CB_GETITEMDATA, i, 0L)) + iSelect = i; + } + + if (CB_ERR == iSelect) + return FALSE; + + if (iSelect != (INT)SNDMSG(hCombo, CB_GETCURSEL, 0, 0L) && + CB_ERR == SNDMSG(hCombo, CB_SETCURSEL, iSelect, 0)) + { + return FALSE; + } + + return TRUE; +} + +static BOOL ChannelEditor_SetAutoDownload(HWND hwnd, INT numOfEpisodes, BOOL enableAutoDownload) +{ + HWND hCombo = GetDlgItem(hwnd, IDC_AUTODOWNLOADLIST); + if (NULL == hCombo) return FALSE; + + INT selectedVal = UpdateAutoDownload::GetSelection(numOfEpisodes, (FALSE != enableAutoDownload)); + INT iCount = (INT)SNDMSG(hCombo, CB_GETCOUNT, 0, 0L); + INT iSelect = CB_ERR; + for (INT i = 0; i < iCount && CB_ERR == iSelect; i++) + { + if (selectedVal == (INT)SNDMSG(hCombo, CB_GETITEMDATA, i, 0L)) + iSelect = i; + } + + if (CB_ERR == iSelect) + return FALSE; + + if (iSelect != (INT)SNDMSG(hCombo, CB_GETCURSEL, 0, 0L) && + CB_ERR == SNDMSG(hCombo, CB_SETCURSEL, iSelect, 0)) + { + return FALSE; + } + + return TRUE; +} + +static BOOL ChannelEditor_InitCombobox(HWND hwnd) +{ + HWND hCombo = GetDlgItem(hwnd, IDC_UPDATELIST); + HWND hAutoDownloadCombo = GetDlgItem(hwnd, IDC_AUTODOWNLOADLIST); + if ( NULL == hCombo || NULL == hAutoDownloadCombo ) return FALSE; + + WCHAR szBuffer[256] = {0}; + INT iPos = 0; + + SNDMSG(hCombo, WM_SETREDRAW, FALSE, 0L); + + for (INT i = 0; i < Update::TIME_NUMENTRIES; i++) + { + if (NULL != Update::GetTitle(i, szBuffer, ARRAYSIZE(szBuffer))) + { + iPos = (INT)SNDMSG(hCombo, CB_ADDSTRING, 0, (LPARAM)szBuffer); + if (CB_ERR != iPos) + { + SNDMSG(hCombo, CB_SETITEMDATA, (WPARAM)iPos, (LPARAM)i); + } + } + } + + SNDMSG(hCombo, WM_SETREDRAW, TRUE, 0L); + + SNDMSG(hAutoDownloadCombo, WM_SETREDRAW, FALSE, 0L); + + for (INT i = 0; i < UpdateAutoDownload::AUTODOWNLOAD_NUMENTRIES; i++) + { + if (NULL != UpdateAutoDownload::GetTitle(i, szBuffer, ARRAYSIZE(szBuffer))) + { + iPos = (INT)SNDMSG(hAutoDownloadCombo, CB_ADDSTRING, 0, (LPARAM)szBuffer); + if (CB_ERR != iPos) + { + SNDMSG(hAutoDownloadCombo, CB_SETITEMDATA, (WPARAM)iPos, (LPARAM)i); + } + } + } + + SNDMSG(hAutoDownloadCombo, WM_SETREDRAW, TRUE, 0L); + + return TRUE; +} + +static BOOL ChannelEditor_EnableUserSettings(HWND hwnd, BOOL fEnable) +{ + BOOL result; + UINT activeButton = (FALSE == fEnable) ? IDC_USEDEFAULTS : IDC_USECUSTOM; + + result = CheckRadioButton(hwnd, IDC_USEDEFAULTS, IDC_USECUSTOM, activeButton); + + if (FALSE != result) + { + SendDlgItemMessage(hwnd, activeButton, BM_CLICK, 0, 0L); + } + return result; +} + +#if 0 +static BOOL ChannelEditor_EnableAutoDownload(HWND hwnd, BOOL fEnable) +{ + BOOL result; + result = CheckDlgButton(hwnd, IDC_AUTODOWNLOAD, (FALSE != fEnable) ? BST_CHECKED : BST_UNCHECKED); + return result; +} +#endif + +static BOOL ChannelEditor_SetUrl(HWND hwnd, LPCWSTR pszUrl) +{ + HWND hEdit = GetDlgItem(hwnd, IDC_EDITURL); + if (NULL == hEdit) return FALSE; + + return SNDMSG(hEdit, WM_SETTEXT, 0, (LPARAM)pszUrl); +} +static void ChannelEditor_UpdateTitle(HWND hwnd, BOOL fModified) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + if (0 != (CEF_CREATENEW & editor->flags)) + return; + + + + WCHAR szBuffer[256] = {0}; + size_t remaining = ARRAYSIZE(szBuffer); + LPWSTR cursor = szBuffer; + + if (FALSE != fModified) + { + StringCchCopyEx(cursor, remaining, L"*", &cursor, &remaining, 0); + } + + WASABI_API_LNGSTRINGW_BUF(IDS_EDIT_CHANNEL, cursor, remaining); + INT cchLen = lstrlen(cursor); + cursor += cchLen; + remaining -= cchLen; + + AutoLock lock (channels); + LPCWSTR title = NULL; + if (editor->iChannel < channels.size()) + { + Channel *channel = &channels[editor->iChannel]; + title = channel->title; + if (NULL != title && L'\0' != title) + { + StringCchPrintf(cursor, remaining, L": %s", title); + } + } + + SendMessage(hwnd, WM_SETTEXT, 0, (LPARAM)szBuffer); +} + +static void ChannelEditor_UpdateModifiedState(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + if (0 != (CEF_CREATENEW & editor->flags)) + return; + + AutoLock lock (channels); + + if (editor->iChannel >= channels.size()) + return; + + BOOL fModified = FALSE; + + Channel *channel = &channels[editor->iChannel]; + + HWND hControl; + + if (FALSE == fModified && (NULL != (hControl = GetDlgItem(hwnd, IDC_USECUSTOM))) && + (BST_CHECKED == SNDMSG(hControl, BM_GETCHECK, 0, 0L)) != (false == channel->useDefaultUpdate)) + { + fModified = TRUE; + } + + if (false == channel->useDefaultUpdate) + { + if (editor->enableAutoDownload != (BOOL)channel->autoDownload || + editor->enableAutoUpdate != (BOOL)channel->autoUpdate || + editor->updateTime != channel->updateTime || + editor->numOfAutoDownloadEpisodes != channel->autoDownloadEpisodes) + { + fModified = TRUE; + } + } + + hControl = GetDlgItem(hwnd, IDOK); + if (NULL != hControl) EnableWindow(hControl, fModified); + + ChannelEditor_UpdateTitle(hwnd, fModified); +} + +static BOOL ChannelEditor_Reposition(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return FALSE; + + RECT rectEditor, rectOwner; + + if (0 != (CEF_CENTEROWNER & editor->flags)) + { + if (FALSE == GetWindowRect(editor->hOwner, &rectOwner) || + FALSE == GetWindowRect(hwnd, &rectEditor)) + { + return FALSE; + } + + LONG x = rectOwner.left + ((rectOwner.right - rectOwner.left) - (rectEditor.right - rectEditor.left))/2; + LONG y = rectOwner.top + ((rectOwner.bottom - rectOwner.top) - (rectEditor.bottom - rectEditor.top))/2; + SetWindowPos(hwnd, NULL, x, y, 0, 0, SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOSIZE); + SendMessage(hwnd, DM_REPOSITION, 0, 0L); + } + + + + return TRUE; +} + +static INT_PTR ChannelEditor_OnInitDialog(HWND hwnd, HWND hFocus, LPARAM lParam) +{ + + CHANNELEDITOR *editor = (CHANNELEDITOR*)lParam; + if (NULL == editor || FALSE == SetProp(hwnd, MAKEINTATOM(VIEWPROP), editor)) + { + EndDialog(hwnd, 0); + return 0; + } + + ChannelEditor_InitCombobox(hwnd); + + editor->enableAutoDownload = autoDownload; + editor->enableAutoUpdate = autoUpdate; + editor->updateTime = updateTime; + editor->numOfAutoDownloadEpisodes = autoDownloadEpisodes; + + if (0 != (CEF_CREATENEW & editor->flags)) + { + ChannelEditor_SetUrl(hwnd, L""); + ChannelEditor_EnableUserSettings(hwnd, FALSE); + } + else + { + AutoLock lock (channels); + if (editor->iChannel >= channels.size()) + { + EndDialog(hwnd, 0); + return 0; + } + Channel *channel = &channels[editor->iChannel]; + + HWND hControl; + + hControl = GetDlgItem(hwnd, IDC_EDITURL); + if (NULL != hControl) SNDMSG(hControl, EM_SETREADONLY, TRUE, 0L); + + hControl = GetDlgItem(hwnd, IDOK); + if (NULL != hControl) + { + WCHAR szBuffer[128] = {0}; + WASABI_API_LNGSTRINGW_BUF(IDS_SAVE, szBuffer, ARRAYSIZE(szBuffer)); + SNDMSG(hControl, WM_SETTEXT, 0, (LPARAM)szBuffer); + } + + if (false == channel->useDefaultUpdate) + { + editor->enableAutoDownload = channel->autoDownload; + editor->enableAutoUpdate = channel->autoUpdate; + editor->updateTime = channel->updateTime; + editor->numOfAutoDownloadEpisodes = channel->autoDownloadEpisodes; + } + + ChannelEditor_SetUrl(hwnd, channel->url); + ChannelEditor_EnableUserSettings(hwnd, !channel->useDefaultUpdate); + } + + ChannelEditor_Reposition(hwnd); + + return TRUE; +} + +static void ChannelEditor_OnDestroy(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + RemoveProp(hwnd, MAKEINTATOM(VIEWPROP)); +} + +static void ChannelEditor_UpdateUserSettings(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + HWND hControl; + FALSE; + + hControl = GetDlgItem(hwnd, IDC_USECUSTOM); + BOOL enableUser = (NULL != hControl && BST_CHECKED == (INT)SNDMSG(hControl, BM_GETCHECK, 0, 0L)); + + if (FALSE == enableUser) + { + ChannelEditor_SetUpdateTime(hwnd, updateTime, autoUpdate); + ChannelEditor_SetAutoDownload(hwnd, autoDownloadEpisodes, autoDownload); + } + else + { + ChannelEditor_SetUpdateTime(hwnd, editor->updateTime, editor->enableAutoUpdate); + ChannelEditor_SetAutoDownload(hwnd, editor->numOfAutoDownloadEpisodes, editor->enableAutoDownload); + } + + const INT szControl[] = { IDC_UPDATELIST, IDC_AUTODOWNLOADLIST, IDC_STATIC_UPDATEEVERY, IDC_STATIC_AUTODOWNLOAD, }; + for (INT i = 0; i < ARRAYSIZE(szControl); i++) + { + hControl = GetDlgItem(hwnd, szControl[i]); + if (NULL != hControl) EnableWindow(hControl, enableUser); + } + + ChannelEditor_UpdateModifiedState(hwnd); + +} + +static INT_PTR ChannelEditor_SaveChannel(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return 0; + + AutoLock lock (channels); + if (editor->iChannel >= channels.size()) + return 0; + + HWND hControl = GetDlgItem(hwnd, IDC_USECUSTOM); + if (NULL == hControl) return 0; + + Channel *channel = &channels[editor->iChannel]; + + if (BST_CHECKED == SNDMSG(hControl, BM_GETCHECK, 0, 0L)) + { + channel->useDefaultUpdate = false; + channel->autoDownload = (FALSE != editor->enableAutoDownload); + channel->updateTime = editor->updateTime; + channel->autoUpdate = (FALSE != editor->enableAutoUpdate); + channel->autoDownloadEpisodes = editor->numOfAutoDownloadEpisodes; + } + else + { + channel->useDefaultUpdate = true; + channel->autoDownload = autoDownload; + channel->updateTime = updateTime; + channel->autoUpdate = autoUpdate; + channel->autoDownloadEpisodes = autoDownloadEpisodes; + } + + if (channel->autoUpdate) + cloud.Pulse(); + return + IDOK; +} + +static DWORD AddUrlThread(void *vBuffer, HWND hwndDlg) +{ + Channel newFeed; + newFeed.autoUpdate = false; // TODO check defaults + newFeed.autoDownload = false; // leave this as false + newFeed.SetURL((const wchar_t *)vBuffer); + delete vBuffer; + + int downloadError = DownloadFeedInformation(newFeed); + if (downloadError != DOWNLOAD_SUCCESS) + return downloadError; + + // TODO check defaults; + if (IsDlgButtonChecked(hwndDlg, IDC_USECUSTOM) == BST_CHECKED) + { + LRESULT timeSelection; + timeSelection = SendMessage(GetDlgItem(hwndDlg, IDC_UPDATELIST), CB_GETCURSEL, 0, 0); + if (timeSelection != CB_ERR) + { + newFeed.autoUpdate = Update::GetAutoUpdate(timeSelection); + newFeed.updateTime = Update::GetTime(timeSelection); + } + LRESULT episodesSelection; + episodesSelection = SendMessage(GetDlgItem(hwndDlg, IDC_AUTODOWNLOADLIST), CB_GETCURSEL, 0, 0); + if (episodesSelection != CB_ERR) + { + newFeed.autoDownload = UpdateAutoDownload::GetAutoDownload(episodesSelection); + newFeed.autoDownloadEpisodes = UpdateAutoDownload::GetAutoDownloadEpisodes(episodesSelection); + } + newFeed.useDefaultUpdate = false; + } + else + { + newFeed.useDefaultUpdate = true; + newFeed.autoUpdate = autoUpdate; + newFeed.updateTime = updateTime; + newFeed.autoDownloadEpisodes = autoDownloadEpisodes; + newFeed.autoDownload = autoDownload; + } + + //newFeed.title += L" "; + AutoLock lock (channels LOCKNAME("AddURL")); + if (!channels.AddChannel(newFeed)) + { + wchar_t error_msg[1024] = {0}, titleStr[64] = {0}; + StringCchPrintf( error_msg, 1024, WASABI_API_LNGSTRINGW( IDS_CHANNEL_ALREADY_PRESENT ), newFeed.title, newFeed.url ); + MessageBox( hwndDlg, error_msg, WASABI_API_LNGSTRINGW_BUF( IDS_DUPLICATE_CHANNEL, titleStr, 64 ), MB_OK ); + + return DOWNLOAD_DUPLICATE; + } + else + { + cloud.Pulse(); // TODO why? + HWND hView = SubscriptionView_FindWindow(); + if (NULL != hView) + { + SubscriptionView_RefreshChannels(hView, TRUE); + } + + } + return DOWNLOAD_SUCCESS; // success +} + +static INT ChannelEditor_AddUrl(HWND hwnd) +{ + size_t length = GetWindowTextLength(GetDlgItem(hwnd, IDC_EDITURL)) + 1; + wchar_t *buffer = new wchar_t[length]; + ZeroMemory(buffer,sizeof(wchar_t)*length); + GetDlgItemText(hwnd, IDC_EDITURL, buffer, (int)length); + return AddUrlThread((void *)buffer, hwnd); +} + +static INT_PTR ChannelEditor_CreateChannel(HWND hwnd) +{ + INT result = ChannelEditor_AddUrl(hwnd); + if (DOWNLOAD_SUCCESS == result) + return IDOK; + + INT messageId = IDS_ERROR_ADDING_URL; + INT titleId = IDS_ERROR_ADDING_URL; + + switch (result) + { + case DOWNLOAD_ERROR_PARSING_XML: messageId = IDS_ERROR_PARSING_XML_FROM_SERVER; break; + case DOWNLOAD_NOTRSS: messageId = IDS_LINK_HAS_NO_RSS_INFO; break; + case DOWNLOAD_404: messageId = IDS_INVALID_LINK; break; + case DOWNLOAD_NOHTTP: messageId = IDS_NO_JNETLIB; titleId = IDS_JNETLIB_MISSING; break; + case DOWNLOAD_NOPARSER: messageId = IDS_NO_EXPAT; titleId = IDS_EXPAT_MISSING; break; + case DOWNLOAD_CONNECTIONRESET: messageId = IDS_CONNECTION_RESET_BY_PEER; break; + } + + if(result != DOWNLOAD_DUPLICATE) + { + WCHAR szMessage[512] = {0}, szTitle[128] = {0}; + + WASABI_API_LNGSTRINGW_BUF(messageId, szMessage, ARRAYSIZE(szMessage)); + WASABI_API_LNGSTRINGW_BUF(titleId, szTitle, ARRAYSIZE(szTitle)); + MessageBox(hwnd, szMessage, szTitle, MB_OK | MB_ICONERROR); + } + + return IDIGNORE; +} + +static void ChannelEditor_OnCancel(HWND hwnd) +{ + EndDialog(hwnd, IDCANCEL); +} + +static void ChannelEditor_OnOk(HWND hwnd) +{ + INT_PTR result = 0; + + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL != editor) + { + if (0 == (CEF_CREATENEW & editor->flags)) + { + result = ChannelEditor_SaveChannel(hwnd); + } + else + { + result = ChannelEditor_CreateChannel(hwnd); + } + } + + if (IDIGNORE != result) + { + EndDialog(hwnd, result); + } +} + +#if 0 +static void ChannelEditor_OnAutoDownloadChanged(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + HWND hControl = GetDlgItem(hwnd, IDC_USECUSTOM); + if (NULL == hControl || BST_CHECKED != SNDMSG(hControl, BM_GETCHECK, 0, 0L)) return; + + hControl = GetDlgItem(hwnd, IDC_AUTODOWNLOAD); + if (NULL == hControl) return; + + editor->enableAutoDownload = (BST_CHECKED == SNDMSG(hControl, BM_GETCHECK, 0, 0L)); + + ChannelEditor_UpdateModifiedState(hwnd); +} +#endif + +static void ChannelEditor_OnUrlChange(HWND hwnd) +{ + WCHAR szBuffer[4096] = {0}; + + HWND hControl = GetDlgItem(hwnd, IDC_EDITURL); + if (NULL == hControl || 0 == (INT)SNDMSG(hControl, WM_GETTEXT, (WPARAM)ARRAYSIZE(szBuffer), (LPARAM)szBuffer)) + szBuffer[0] = L'\0'; + + LPCWSTR p = szBuffer; + while (L' ' == *p && L'\0' != *p) p++; + BOOL enableButton = (L'\0' != *p); + + hControl = GetDlgItem(hwnd, IDOK); + if (NULL != hControl) + { + EnableWindow(hControl, enableButton); + } +} + +static void ChannelEditor_OnUpdateTimeChange(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + HWND hControl = GetDlgItem(hwnd, IDC_USECUSTOM); + if (NULL == hControl || BST_CHECKED != SNDMSG(hControl, BM_GETCHECK, 0, 0L)) return; + + hControl = GetDlgItem(hwnd, IDC_UPDATELIST); + if (NULL == hControl) return; + + INT iSelected = (INT)SNDMSG(hControl, CB_GETCURSEL, 0, 0L); + if (CB_ERR == iSelected) return; + + INT selectedVal = (INT)SNDMSG(hControl, CB_GETITEMDATA, iSelected, 0L); + if (selectedVal < 0 || selectedVal >= Update::TIME_NUMENTRIES) return; + + editor->updateTime = Update::GetTime(selectedVal); + editor->enableAutoUpdate = Update::GetAutoUpdate(selectedVal); + + ChannelEditor_UpdateModifiedState(hwnd); +} + +static void ChannelEditor_OnUpdateAutoDownloadChange(HWND hwnd) +{ + CHANNELEDITOR *editor = GetEditor(hwnd); + if (NULL == editor) return; + + HWND hControl = GetDlgItem(hwnd, IDC_USECUSTOM); + if (NULL == hControl || BST_CHECKED != SNDMSG(hControl, BM_GETCHECK, 0, 0L)) return; + + hControl = GetDlgItem(hwnd, IDC_AUTODOWNLOADLIST); + if (NULL == hControl) return; + + INT iSelected = (INT)SNDMSG(hControl, CB_GETCURSEL, 0, 0L); + if (CB_ERR == iSelected) return; + + INT selectedVal = (INT)SNDMSG(hControl, CB_GETITEMDATA, iSelected, 0L); + if (selectedVal < 0 || selectedVal >= UpdateAutoDownload::AUTODOWNLOAD_NUMENTRIES) return; + + editor->numOfAutoDownloadEpisodes = UpdateAutoDownload::GetAutoDownloadEpisodes(selectedVal); + editor->enableAutoDownload = UpdateAutoDownload::GetAutoDownload(selectedVal); + + ChannelEditor_UpdateModifiedState(hwnd); +} + +static void ChannelEditor_OnCommand(HWND hwnd, UINT commandId, UINT eventId, HWND hControl) +{ + switch(commandId) + { + case IDCANCEL: ChannelEditor_OnCancel(hwnd); break; + case IDOK: ChannelEditor_OnOk(hwnd); break; + case IDC_USEDEFAULTS: + case IDC_USECUSTOM: + switch(eventId) + { + case BN_CLICKED: ChannelEditor_UpdateUserSettings(hwnd); break; + } + break; + case IDC_EDITURL: + switch(eventId) + { + case EN_CHANGE: ChannelEditor_OnUrlChange(hwnd); break; + } + break; + case IDC_UPDATELIST: + switch(eventId) + { + case CBN_SELCHANGE: ChannelEditor_OnUpdateTimeChange(hwnd); break; + } + break; + case IDC_AUTODOWNLOADLIST: + switch(eventId) + { + case CBN_SELCHANGE: ChannelEditor_OnUpdateAutoDownloadChange(hwnd); break; + } + break; + } +} + +static INT_PTR CALLBACK ChannelEditor_DialogProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch(uMsg) + { + case WM_INITDIALOG: return ChannelEditor_OnInitDialog(hwnd, (HWND)wParam, lParam); + case WM_DESTROY: ChannelEditor_OnDestroy(hwnd); return TRUE; + case WM_COMMAND: ChannelEditor_OnCommand(hwnd, LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return TRUE; + } + return 0; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/channelEditor.h b/Src/Plugins/Library/ml_wire/channelEditor.h new file mode 100644 index 00000000..25a0c32a --- /dev/null +++ b/Src/Plugins/Library/ml_wire/channelEditor.h @@ -0,0 +1,15 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_CHANNEL_EDITOR_HEADER +#define NULLSOFT_PODCAST_PLUGIN_CHANNEL_EDITOR_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> + +#define CEF_CREATENEW 0x00000001 +#define CEF_CENTEROWNER 0x00000002 + +INT_PTR ChannelEditor_Show(HWND hOwner, size_t channelIndex, UINT flags); + +#endif //NULLSOFT_PODCAST_PLUGIN_CHANNEL_EDITOR_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/date.c b/Src/Plugins/Library/ml_wire/date.c new file mode 100644 index 00000000..bbe876c6 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/date.c @@ -0,0 +1 @@ +#include "time.h" diff --git a/Src/Plugins/Library/ml_wire/date.h b/Src/Plugins/Library/ml_wire/date.h new file mode 100644 index 00000000..58204571 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/date.h @@ -0,0 +1,20 @@ +#ifndef _OD_DATE_ +#define _OD_DATE_ + +#include <time.h> + + + +/* These functions all work with "Internet"(RFC 822) format Date/Time strings only */ +/* An example of an RFC 822 format Date/Time string is "Thu, 28 Aug 2003 21:30:47 EDT" */ + +/* converts the RFC 822 format date string into UTC Calendar time */ +time_t getDateSecs(char *date); + +/* returns a string that represents the given UTC Calendar time value as an + RFC 822 format string. The buffer is user-supplied and must be at least + 30 bytes in size. */ +char *getDateStr(time_t tmval, char *buffer, int gmt); + +#endif /*_OD_DATE_*/ + diff --git a/Src/Plugins/Library/ml_wire/db.cpp b/Src/Plugins/Library/ml_wire/db.cpp new file mode 100644 index 00000000..d46b2a7d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/db.cpp @@ -0,0 +1,171 @@ +#include <shlwapi.h> + +#include "api__ml_wire.h" +#include "Downloaded.h" +#include "../nde/nde_c.h" +#include "../nu/AutoLock.h" + +/* DB Schema +ChannelTitle +ItemUrl +ItemTitle +PublishDate +Length +Filename +*/ + +static Nullsoft::Utility::LockGuard dbcs; +static nde_table_t table = 0; +static nde_database_t db = 0; +using namespace Nullsoft::Utility; + +enum +{ + DB_ID_CHANNELTITLE = 0, + DB_ID_ITEMURL = 1, + DB_ID_ITEMTITLE = 2, + DB_ID_PUBLISHDATE = 3, + DB_ID_LENGTH = 4, + DB_ID_FILENAME = 5 +}; + +static bool OpenDatabase() +{ + AutoLock lock(dbcs); + if (!db) + db = NDE_CreateDatabase(); + + return true; +} + +void CloseDatabase() +{ + AutoLock lock( dbcs ); + if ( db ) + { + if ( table ) + NDE_Database_CloseTable( db, table ); + + NDE_DestroyDatabase( db ); + } + + db = 0; +} + +static void CreateFields(nde_table_t table) +{ + // create defaults + NDE_Table_NewColumnW(table, DB_ID_CHANNELTITLE, L"channeltitle", FIELD_STRING); + NDE_Table_NewColumnW(table, DB_ID_ITEMURL, L"itemurl", FIELD_STRING); + NDE_Table_NewColumnW(table, DB_ID_ITEMTITLE, L"itemtitle", FIELD_STRING); + NDE_Table_NewColumnW(table, DB_ID_PUBLISHDATE, L"publishdate", FIELD_DATETIME); + NDE_Table_NewColumnW(table, DB_ID_LENGTH, L"length", FIELD_INTEGER); + NDE_Table_NewColumnW(table, DB_ID_FILENAME, L"filename", FIELD_FILENAME); + NDE_Table_PostColumns(table); + NDE_Table_AddIndexByIDW(table, DB_ID_ITEMURL, L"itemurl"); +} + +static bool OpenTable() +{ + AutoLock lock( dbcs ); + if ( !OpenDatabase() ) + return false; + + if ( !table ) + { + const wchar_t *inidir = WASABI_API_APP->path_getUserSettingsPath(); + wchar_t tablePath[ MAX_PATH ] = { 0 }, indexPath[ MAX_PATH ] = { 0 }; + + PathCombineW( tablePath, inidir, L"plugins" ); + PathAppendW( tablePath, L"podcasts.dat" ); + PathCombineW( indexPath, inidir, L"plugins" ); + PathAppendW( indexPath, L"podcasts.idx" ); + + table = NDE_Database_OpenTable( db, tablePath, indexPath, NDE_OPEN_ALWAYS, NDE_CACHE ); + if ( table ) + CreateFields( table ); + } + + return !!table; +} + +static void db_add( nde_scanner_t s, unsigned char id, wchar_t *data ) +{ + if ( data ) + { + nde_field_t f = NDE_Scanner_NewFieldByID( s, id ); + NDE_StringField_SetString( f, data ); + } +} + +static void db_add_int( nde_scanner_t s, unsigned char id, int data ) +{ + if ( data ) + { + nde_field_t f = NDE_Scanner_NewFieldByID( s, id ); + NDE_IntegerField_SetValue( f, data ); + } +} + +static void db_add_time( nde_scanner_t s, unsigned char id, time_t data ) +{ + if ( data ) + { + nde_field_t f = NDE_Scanner_NewFieldByID( s, id ); + NDE_IntegerField_SetValue( f, static_cast<int>( data ) ); + } +} + +bool AddPodcastData( const DownloadedFile &data ) +{ + AutoLock lock( dbcs ); + if ( !OpenTable() ) + return false; + + nde_scanner_t s = NDE_Table_CreateScanner( table ); + if ( s ) + { + NDE_Scanner_New( s ); + db_add( s, DB_ID_CHANNELTITLE, data.channel ); + db_add( s, DB_ID_ITEMURL, data.url ); + db_add( s, DB_ID_ITEMTITLE, data.item ); + db_add_time( s, DB_ID_PUBLISHDATE, data.publishDate ); + db_add_int( s, DB_ID_LENGTH, (int)data.totalSize ); + db_add( s, DB_ID_FILENAME, data.path ); + NDE_Scanner_Post( s ); + NDE_Table_DestroyScanner( table, s ); + NDE_Table_Sync( table ); + + return true; + } + + return false; +} + +bool IsPodcastDownloaded( const wchar_t *url ) +{ + AutoLock lock( dbcs ); + if ( !OpenTable() ) + return false; + + nde_scanner_t s = NDE_Table_CreateScanner( table ); + if ( s ) + { + if ( NDE_Scanner_LocateString( s, DB_ID_ITEMURL, FIRST_RECORD, url ) ) + { + NDE_Table_DestroyScanner( table, s ); + return true; + } + + NDE_Table_DestroyScanner( table, s ); + } + + return false; +} + +void CompactDatabase() +{ + AutoLock lock( dbcs ); + if ( OpenTable() ) + NDE_Table_Compact( table ); +} diff --git a/Src/Plugins/Library/ml_wire/errors.h b/Src/Plugins/Library/ml_wire/errors.h new file mode 100644 index 00000000..0e83b151 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/errors.h @@ -0,0 +1,17 @@ +#ifndef NULLSOFT_ML_WIRE_ERRORS_H +#define NULLSOFT_ML_WIRE_ERRORS_H + +enum +{ + DOWNLOAD_SUCCESS = 0, + DOWNLOAD_404, + DOWNLOAD_TIMEOUT, + DOWNLOAD_NOTRSS, + DOWNLOAD_DUPLICATE, + DOWNLOAD_NOHTTP, + DOWNLOAD_NOPARSER, + DOWNLOAD_CONNECTIONRESET, + DOWNLOAD_ERROR_PARSING_XML, +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ifc_article.h b/Src/Plugins/Library/ml_wire/ifc_article.h new file mode 100644 index 00000000..d8887552 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ifc_article.h @@ -0,0 +1,15 @@ +#ifndef NULLSOFT_ML_WIRE_IFC_ARTICLE_H +#define NULLSOFT_ML_WIRE_IFC_ARTICLE_H + +#include <bfc/dispatch.h> + +class ifc_article +{ +protected: + ifc_article() {} + ~ifc_article() {} +public: + +}; + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ifc_podcast.h b/Src/Plugins/Library/ml_wire/ifc_podcast.h new file mode 100644 index 00000000..bc8c9b62 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ifc_podcast.h @@ -0,0 +1,39 @@ +#ifndef NULLSOFT_ML_WIRE_IFC_PODCAST_H +#define NULLSOFT_ML_WIRE_IFC_PODCAST_H + +#include <bfc/dispatch.h> +class ifc_podcast : public Dispatchable +{ +protected: + ifc_podcast() {} + ~ifc_podcast() {} +public: + //int GetUrl(wchar_t *str, size_t len); + int GetTitle(wchar_t *str, size_t len); + //int GetLink(wchar_t *str, size_t len); + //int GetDescription(wchar_t *str, size_t len); + size_t GetNumArticles(); + // TODO: ifc_article *EnumArticle(size_t i); + + enum + { + IFC_PODCAST_GETURL = 0, + IFC_PODCAST_GETTITLE = 1, + IFC_PODCAST_GETLINK = 2, + IFC_PODCAST_GETDESCRIPTION = 3, + IFC_PODCAST_GETNUMARTICLES = 4, + }; +}; + +inline int ifc_podcast::GetTitle(wchar_t *str, size_t len) +{ + return _call(IFC_PODCAST_GETTITLE, (int)1, str, len); +} + + +inline size_t ifc_podcast::GetNumArticles() +{ + return _call(IFC_PODCAST_GETNUMARTICLES, (size_t)0); +} + +#endif
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/layout.cpp b/Src/Plugins/Library/ml_wire/layout.cpp new file mode 100644 index 00000000..722dc71f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/layout.cpp @@ -0,0 +1,215 @@ +#include "./layout.h" + +#include <windows.h> + +BOOL Layout_Initialize( HWND hwnd, const INT *itemList, INT itemCount, LAYOUTITEM *layout ) +{ + if ( NULL == itemList || NULL == layout ) + return FALSE; + + LAYOUTITEM *item; + + for ( INT i = 0; i < itemCount; i++ ) + { + item = &layout[ i ]; + item->hwnd = GetDlgItem( hwnd, itemList[ i ] ); + if ( item->hwnd == NULL ) + continue; + + if ( FALSE == GetWindowRect( item->hwnd, &item->rect ) ) + SetRectEmpty( &item->rect ); + else + MapWindowPoints( HWND_DESKTOP, hwnd, (POINT *)&item->rect, 2 ); + + item->cx = item->rect.right - item->rect.left; + item->cy = item->rect.bottom - item->rect.top; + item->x = item->rect.left; + item->y = item->rect.top; + item->flags = 0; + } + + return TRUE; +} + +BOOL Layout_Perform( HWND hwnd, LAYOUTITEM *layout, INT layoutCount, BOOL fRedraw ) +{ + HDWP hdwp, hdwpTemp; + hdwp = BeginDeferWindowPos( layoutCount ); + if ( hdwp == NULL ) + return FALSE; + + UINT baseFlags = SWP_NOACTIVATE | SWP_NOZORDER; + if ( fRedraw == FALSE ) + baseFlags |= ( SWP_NOREDRAW | SWP_NOCOPYBITS ); + + LAYOUTITEM *item; + for ( INT i = 0; i < layoutCount; i++ ) + { + item = &layout[ i ]; + if ( item->hwnd == NULL ) + continue; + + UINT flags = baseFlags | ( item->flags & ~( SWP_HIDEWINDOW | SWP_SHOWWINDOW ) ); + if ( item->x == item->rect.left && item->y == item->rect.top ) + flags |= SWP_NOMOVE; + + if ( item->cx == ( item->rect.right - item->rect.left ) && item->cy == ( item->rect.bottom - item->rect.top ) ) + flags |= SWP_NOSIZE; + + if ( ( SWP_HIDEWINDOW & item->flags ) != 0 ) + { + UINT windowStyle = GetWindowLongPtr( item->hwnd, GWL_STYLE ); + if ( ( WS_VISIBLE & windowStyle ) != 0 ) + { + SetWindowLongPtr( item->hwnd, GWL_STYLE, windowStyle & ~WS_VISIBLE ); + if ( FALSE != fRedraw ) + { + RedrawWindow( hwnd, &item->rect, NULL, RDW_INVALIDATE | RDW_ERASE ); + } + } + } + + if ( ( SWP_NOSIZE | SWP_NOMOVE ) != ( ( SWP_NOSIZE | SWP_NOMOVE | SWP_FRAMECHANGED ) & flags ) ) + { + hdwpTemp = DeferWindowPos( hdwp, item->hwnd, NULL, item->x, item->y, item->cx, item->cy, flags ); + if ( hdwpTemp == NULL ) + break; + + hdwp = hdwpTemp; + } + } + + BOOL result = ( hdwp != NULL ) ? EndDeferWindowPos( hdwp ) : FALSE; + + for ( INT i = 0; i < layoutCount; i++ ) + { + item = &layout[ i ]; + if ( NULL != item->hwnd && 0 != ( SWP_SHOWWINDOW & item->flags ) ) + { + UINT windowStyle = GetWindowLongPtr( item->hwnd, GWL_STYLE ); + if ( 0 == ( WS_VISIBLE & windowStyle ) ) + { + SetWindowLongPtr( item->hwnd, GWL_STYLE, windowStyle | WS_VISIBLE ); + if ( FALSE != fRedraw ) + RedrawWindow( item->hwnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN ); + } + } + } + + return result; +} + +static void Layout_SetItemVisibility( const RECT *rect, LAYOUTITEM *item ) +{ + if ( NULL == item || NULL == item->hwnd ) + return; + + BOOL outsider = ( item->cx <= 0 || item->cy <= 0 || + item->x >= rect->right || item->y >= rect->bottom || + ( item->x + item->cx ) < rect->left || ( item->y + item->cy ) < rect->top ); + + UINT windowStyle = GetWindowLongPtr( item->hwnd, GWL_STYLE ); + if ( 0 == ( WS_VISIBLE & windowStyle ) ) + { + if ( !outsider ) + { + item->flags |= SWP_SHOWWINDOW; + } + } + else + { + if ( outsider ) + { + item->flags |= SWP_HIDEWINDOW; + } + } +} + +BOOL Layout_SetVisibility( const RECT *rect, LAYOUTITEM *layout, INT layoutCount ) +{ + if ( NULL == rect || NULL == layout ) + return FALSE; + + for ( INT i = 0; i < layoutCount; i++ ) + { + Layout_SetItemVisibility( rect, &layout[ i ] ); + } + + return TRUE; +} + +BOOL Layout_SetVisibilityEx( const RECT *rect, const INT *indexList, INT indexCount, LAYOUTITEM *layout ) +{ + if ( NULL == rect || NULL == indexList || NULL == layout ) + return FALSE; + + for ( INT i = 0; i < indexCount; i++ ) + { + Layout_SetItemVisibility( rect, &layout[ indexList[ i ] ] ); + } + + return TRUE; +} + +BOOL Layout_GetValidRgn( HRGN validRgn, POINTS parrentOffset, const RECT *validRect, LAYOUTITEM *layout, INT layoutCount ) +{ + if ( NULL == validRgn ) + return FALSE; + + SetRectRgn( validRgn, 0, 0, 0, 0 ); + + if ( NULL == layout ) + return FALSE; + + HRGN rgn = CreateRectRgn( 0, 0, 0, 0 ); + if ( NULL == rgn ) + return FALSE; + + LAYOUTITEM *item; + LONG l, t, r, b; + + for ( INT i = 0; i < layoutCount; i++ ) + { + item = &layout[ i ]; + if ( NULL != item->hwnd && 0 == ( ( SWP_HIDEWINDOW | SWP_SHOWWINDOW ) & item->flags ) ) + { + l = item->x + parrentOffset.x; + t = item->y + parrentOffset.y; + r = l + item->cx; + b = t + item->cy; + if ( 0 != ( SWP_NOREDRAW & item->flags ) || ( l == item->rect.left && t == item->rect.top && r == item->rect.right && b == item->rect.bottom ) ) + { + if ( NULL != validRect ) + { + if ( l < validRect->left ) + l = validRect->left; + + if ( t < validRect->top ) + t = validRect->top; + + if ( r > validRect->right ) + r = validRect->right; + + if ( b > validRect->bottom ) + b = validRect->bottom; + } + + if ( l < r && t < b ) + { + SetRectRgn( rgn, l, t, r, b ); + CombineRgn( validRgn, validRgn, rgn, RGN_OR ); + + if ( NULLREGION != GetUpdateRgn( item->hwnd, rgn, FALSE ) ) + { + OffsetRgn( rgn, parrentOffset.x, parrentOffset.y ); + CombineRgn( validRgn, validRgn, rgn, RGN_DIFF ); + } + } + } + } + } + + DeleteObject( rgn ); + + return TRUE; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/layout.h b/Src/Plugins/Library/ml_wire/layout.h new file mode 100644 index 00000000..9af73396 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/layout.h @@ -0,0 +1,43 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_LAYOUT_HEADER +#define NULLSOFT_PODCAST_PLUGIN_LAYOUT_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> + +typedef struct __LAYOUTITEM +{ + HWND hwnd; + LONG x; + LONG y; + LONG cx; + LONG cy; + UINT flags; + RECT rect; +} LAYOUTITEM; + + +#define LI_GET_R(__li) ((__li).x + (__li).cx) +#define LI_GET_B(__li) ((__li).y + (__li).cy) +#define LI_EXPAND_W(__li, __delta) { (__li).cx += (__delta); } +#define LI_EXPAND_H(__li, __delta) { (__li).cy += (__delta); } +#define LI_SHIFT_L(__li, __delta) { (__li).x += (__delta); } +#define LI_SHIFT_T(__li, __delta) { { (__li).y += (__delta); } +#define LI_SET_L(__li, __val) { (__li).x = (__val); } +#define LI_SET_T(__li, __val) { (__li).y = (__val); } +#define LI_SET_W(__li, __val) { (__li).cx = (__val); } +#define LI_SET_H(__li, __val) { (__li).cy = (__val); } +#define LI_SET_R(__li, __val) { (__li).cx = ((__val) - (__li).x); } +#define LI_SET_B(__li, __val) { (__li).cy = ((__val) - (__li).y); } + +BOOL Layout_Initialize(HWND hwnd, const INT *itemList, INT itemCount, LAYOUTITEM *layout); +BOOL Layout_SetVisibilityEx(const RECT *rect, const INT *indexList, INT indexCount, LAYOUTITEM *layout); +BOOL Layout_SetVisibility(const RECT *rect, LAYOUTITEM *layout, INT layoutCount); +BOOL Layout_Perform(HWND hwnd, LAYOUTITEM *layout, INT layoutCount, BOOL fRedraw); + +BOOL Layout_GetValidRgn(HRGN validRgn, POINTS parrentOffset, const RECT *validRect, LAYOUTITEM *layout, INT layoutCount); + + +#endif //NULLSOFT_PODCAST_PLUGIN_LAYOUT_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ml_podcast.rc b/Src/Plugins/Library/ml_wire/ml_podcast.rc new file mode 100644 index 00000000..d75df578 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ml_podcast.rc @@ -0,0 +1,396 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""afxres.h""\r\0" +END + +3 TEXTINCLUDE +BEGIN + "#include ""version.rc2""\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_PODCAST DIALOGEX 0, 0, 285, 277 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_CHANNELLIST,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_TABSTOP,0,0,122,181,WS_EX_CLIENTEDGE + CONTROL "",IDC_VDIV,"Static",SS_BLACKFRAME,122,0,5,182 + CONTROL "",IDC_ITEMLIST,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_TABSTOP,127,0,156,181,WS_EX_CLIENTEDGE + PUSHBUTTON "Directory",IDC_FINDNEW,2,183,41,11 + PUSHBUTTON "Add",IDC_ADD,45,183,35,11 + PUSHBUTTON "Edit",IDC_EDIT,83,183,35,11 + PUSHBUTTON "Delete",IDC_DELETE,121,183,35,11 + PUSHBUTTON "Update",IDC_REFRESH,159,183,35,11 + CONTROL "",IDC_HDIV,"Static",SS_BLACKFRAME,0,194,285,5 + CONTROL "",IDC_EPISODE_INFO,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_TABSTOP,0,198,285,10,WS_EX_CLIENTEDGE + CONTROL "",IDC_DESCRIPTION,"Static",SS_BLACKRECT,0,208,283,55 + PUSHBUTTON "Play",IDC_PLAY,0,266,37,11 + PUSHBUTTON "Enqueue",IDC_ENQUEUE,39,266,37,11 + PUSHBUTTON "Download",IDC_DOWNLOAD,78,266,40,11 + PUSHBUTTON "Visit site",IDC_VISIT,120,266,40,11 + CONTROL "",IDC_STATUS,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_WORDELLIPSIS | WS_GROUP,164,266,120,11 +END + +IDD_ADDURL DIALOGEX 0, 0, 270, 90 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +CAPTION "Add RSS Subscription" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_EDITURL,4,7,262,14,ES_AUTOHSCROLL + CONTROL "Use default settings",IDC_USEDEFAULTS,"Button",BS_AUTORADIOBUTTON,8,25,88,10 + CONTROL "Use custom settings",IDC_USECUSTOM,"Button",BS_AUTORADIOBUTTON,114,25,93,10 + LTEXT "Update Every:",IDC_STATIC_UPDATEEVERY,8,42,48,8,WS_DISABLED + COMBOBOX IDC_UPDATELIST,59,40,70,95,CBS_DROPDOWNLIST | WS_DISABLED | WS_VSCROLL | WS_TABSTOP + LTEXT "Download New Episodes:",IDC_STATIC_AUTODOWNLOAD,8,58,81,8,WS_DISABLED + COMBOBOX IDC_AUTODOWNLOADLIST,93,56,122,30,CBS_DROPDOWNLIST | WS_DISABLED | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Add",IDOK,162,73,50,13 + PUSHBUTTON "Cancel",IDCANCEL,216,73,50,13 +END + +IDD_DOWNLOADS DIALOGEX 0, 0, 266, 92 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_DOWNLOADLIST,"SysListView32",LVS_REPORT | LVS_ALIGNLEFT | LVS_OWNERDATA | WS_TABSTOP,0,0,264,79 + CONTROL "Play",IDC_PLAY,"Button",BS_OWNERDRAW | WS_TABSTOP,0,81,37,11 + CONTROL "Enqueue",IDC_ENQUEUE,"Button",BS_OWNERDRAW | WS_TABSTOP,40,81,37,11 + CONTROL "Remove",IDC_REMOVE,"Button",BS_OWNERDRAW | WS_TABSTOP,80,81,37,11 + CONTROL "Clean up",IDC_CLEANUP,"Button",BS_OWNERDRAW | WS_TABSTOP,120,81,37,11 + CONTROL "",IDC_STATUS,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | SS_ENDELLIPSIS | WS_GROUP,163,81,102,11 +END + +IDD_PREFERENCES DIALOGEX 0, 0, 272, 247 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Podcasts",IDC_STATIC,0,0,272,246 + GROUPBOX "Subscription Updates",IDC_STATIC,5,11,260,53 + LTEXT "Update every:",IDC_STATICUPDATEEVERY,11,28,50,9,SS_CENTERIMAGE + COMBOBOX IDC_UPDATELIST,65,27,94,131,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Update on launch",IDC_UPDATEONLAUNCH,"Button",BS_AUTOCHECKBOX | BS_LEFT | WS_TABSTOP,181,28,72,10 + LTEXT "Download New Episodes:",IDC_STATICAUTODOWNLOAD,11,47,84,9 + COMBOBOX IDC_AUTODOWNLOADLIST,99,45,112,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Podcast Download Location",IDC_STATIC,5,67,260,34 + EDITTEXT IDC_DOWNLOADLOCATION,11,81,198,13,ES_AUTOHSCROLL + PUSHBUTTON "Browse...",IDC_BROWSE,209,81,50,13 + GROUPBOX "'Podcast Directory' Service",IDC_STATIC,5,105,260,72 + LTEXT "This allows you to specify an alternative Podcast Directory service to display in the 'Podcast Directory' node. Leave blank to reset to the default service.",IDC_STATIC,11,116,248,18 + EDITTEXT IDC_DIRECTORYURL,11,138,248,13,ES_AUTOHSCROLL + LTEXT "Note: To subscribe to feeds when using an alternative Podcast Directory service, the service will need to use the available Winamp Javascript API.",IDC_STATIC,11,155,248,18,WS_DISABLED +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ADDURL, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 266 + TOPMARGIN, 7 + BOTTOMMARGIN, 86 + END + + IDD_PREFERENCES, DIALOG + BEGIN + BOTTOMMARGIN, 246 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_MENU1 MENU +BEGIN + POPUP "Podcast" + BEGIN + MENUITEM "Update podcast\tF5", IDC_REFRESH + MENUITEM "Edit podcast...\tF2", IDC_EDIT + MENUITEM "Delete podcast\tDel", IDC_DELETE + MENUITEM SEPARATOR + MENUITEM "Visit site\tF7", IDC_VISIT + MENUITEM SEPARATOR + MENUITEM "Add podcast...\tInsert", IDC_ADD + MENUITEM "Update all\tShift+F5", IDC_REFRESHALL + END + POPUP "Episode" + BEGIN + MENUITEM "Play media", IDC_PLAY + MENUITEM "Enqueue media", IDC_ENQUEUE + POPUP "Send to:" + BEGIN + MENUITEM "", ID_Menu + END + MENUITEM SEPARATOR + MENUITEM "Download media", IDC_DOWNLOAD + MENUITEM SEPARATOR + MENUITEM "Select all\tCtrl+A", IDC_SELECTALL + MENUITEM "Explore media folder\tCtrl+F", ID_DOWNLOADS_EXPLORERITEMFOLDER + MENUITEM "Visit site\tF7", IDC_VISIT + MENUITEM SEPARATOR + MENUITEM "Update podcast\tF5", IDC_REFRESH + END + POPUP "Downloads" + BEGIN + MENUITEM "Play file\tEnter", IDC_PLAY + MENUITEM "Enqueue file\tShift+Enter", IDC_ENQUEUE + POPUP "Send to:" + BEGIN + MENUITEM "", ID_Menu + END + MENUITEM SEPARATOR + MENUITEM "Select all\tCtrl+A", IDC_SELECTALL + MENUITEM "View file info...\tAlt+3", IDC_INFOBOX + MENUITEM "Explore item folder\tCtrl+F", ID_DOWNLOADS_EXPLORERITEMFOLDER + MENUITEM SEPARATOR + MENUITEM "Remove from list\tDel", IDC_REMOVE + MENUITEM "Delete file\tShift+Del", IDC_DELETE + END + POPUP "Navigation" + BEGIN + MENUITEM "&Preferences", ID_NAVIGATION_PREFERENCES + MENUITEM SEPARATOR + MENUITEM "Help", ID_NAVIGATION_HELP + END + POPUP "SubscriptionNavigation" + BEGIN + MENUITEM "Directory", ID_NAVIGATION_DIRECTORY + MENUITEM "&Preferences", ID_NAVIGATION_PREFERENCES + MENUITEM SEPARATOR + MENUITEM "Update all", ID_NAVIGATION_REFRESHALL + MENUITEM SEPARATOR + MENUITEM "Help", ID_NAVIGATION_HELP + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Accelerator +// + +IDR_VIEW_DOWNLOAD_ACCELERATORS ACCELERATORS +BEGIN + "F", ID_DOWNLOADS_EXPLORERITEMFOLDER, VIRTKEY, CONTROL, NOINVERT + VK_RETURN, IDC_CUSTOM, VIRTKEY, SHIFT, CONTROL, NOINVERT + VK_DELETE, IDC_DELETE, VIRTKEY, SHIFT, NOINVERT + VK_RETURN, IDC_ENQUEUE, VIRTKEY, SHIFT, NOINVERT + "3", IDC_INFOBOX, VIRTKEY, ALT, NOINVERT + VK_RETURN, IDC_PLAY, VIRTKEY, NOINVERT + VK_DELETE, IDC_REMOVE, VIRTKEY, NOINVERT + "A", IDC_SELECTALL, VIRTKEY, CONTROL, NOINVERT + VK_F7, IDC_VISIT, VIRTKEY, NOINVERT + VK_F5, IDC_REFRESH, VIRTKEY, NOINVERT + "D", IDC_DOWNLOAD, VIRTKEY, CONTROL, NOINVERT +END + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PLUGIN_NAME "Nullsoft Podcasts v%d.%02d" + 65535 "{1FF327B2-A41D-4c67-A58A-EB09BA1470D3}" +END + +STRINGTABLE +BEGIN + IDS_PODCASTS "Podcasts" + IDS_DOWNLOADING_KB_COMPLETE "Downloading %u files, %ukb complete." + IDS_DOWNLOADING_KB_PROGRESS + "Downloading %u files, %ukb of %ukb complete (%d%%)." + IDS_SUBSCRIPTIONS "Subscriptions" + IDS_DOWNLOADS "Downloads" + IDS_DOWNLOADING "Downloading..." + IDS_CHOOSE_FOLDER "Choose folder to store downloaded media." + IDS_ALREADY_SUBSCRIBED "You are already subscribed to %s.\n%s" + IDS_FILE_NOT_FOUND "Cannot connect to %s\nFile not Found." + IDS_CONNECTION_TIMED_OUT "Cannot connect to %s\nConnection timed out." + IDS_ERROR_PARSING_XML "Subscription to %s failed.\nError parsing XML data from server." + IDS_INVALID_RSS_FEED "Subscription to %s failed.\nDoes not appear to be a valid RSS feed." + IDS_NO_JNETLIB "HTTP Downloader library is missing.\nPlease reinstall Winamp." + IDS_JNETLIB_MISSING "JNetLib missing" + IDS_NO_EXPAT "XML Parsing library is missing.\nPlease reinstall Winamp." +END + +STRINGTABLE +BEGIN + IDS_EXPAT_MISSING "Expat missing" + IDS_CONNECTION_RESET "Subscription to %s failed.\nConnection reset by peer." + IDS_ERROR_SUBSCRIBING_TO_PODCAST "Error Subscribing to Podcast" + IDS_UPD_MANUALLY "Manually" + IDS_UPD_WEEK "Week" + IDS_UPD_DAY "Day" + IDS_UPD_12HRS "12 hours" + IDS_UPD_6HRS "6 hours" + IDS_UPD_3HRS "3 hours" + IDS_UPD_HOUR "Hour" + IDS_UPD_30MINS "30 minutes" + IDS_ERROR_FYEO "ERROR ! ! YOU SHOULDN'T SEE THIS" + IDS_WDAY_SUN "Sun" + IDS_WDAY_MON "Mon" + IDS_WDAY_TUE "Tue" + IDS_WDAY_WED "Wed" +END + +STRINGTABLE +BEGIN + IDS_WDAY_THU "Thu" + IDS_WDAY_FRI "Fri" + IDS_WDAY_SAT "Sat" + IDS_MONTH_JAN "Jan" + IDS_MONTH_FEB "Feb" + IDS_MONTH_MAR "Mar" + IDS_MONTH_APR "Apr" + IDS_MONTH_MAY "May" + IDS_MONTH_JUN "Jun" + IDS_MONTH_JUL "Jul" + IDS_MONTH_AUG "Aug" + IDS_MONTH_SEP "Sep" + IDS_MONTH_OCT "Oct" + IDS_MONTH_NOV "Nov" + IDS_MONTH_DEC "Dec" + IDS_RECEIVING_UPDATES_FOR "Retrieving updates for " +END + +STRINGTABLE +BEGIN + IDS_GOT_NEW_ITEMS_FOR "Got new items for " + IDS_CHANNEL_ALREADY_PRESENT "Podcast Already Present:\n%s\n%s" + IDS_DUPLICATE_CHANNEL "Duplicate Podcast" + IDS_ERROR_ADDING_URL "Error adding URL" + IDS_ERROR_PARSING_XML_FROM_SERVER "Error parsing XML data from server" + IDS_LINK_HAS_NO_RSS_INFO "Link does not contain valid RSS information" + IDS_INVALID_LINK "Invalid link (404 or timeout)" + IDS_CONNECTION_RESET_BY_PEER "Connection reset by peer." + IDS_DOWNLOADING_PERCENT "Downloading %d%%" + IDS_CHANNEL "Podcast" + IDS_ITEM "Episode" + IDS_PROGRESS "Progress" + IDS_PATH "Path" + IDS_PERM_DELETE_ARE_YOU_SURE + "This will permanently delete this file, are you sure?" + IDS_PERM_DELETE_THESE_ARE_YOU_SURE + "This will permanently delete these %d files, are you sure?" + IDS_DELETION "Deletion" +END + +STRINGTABLE +BEGIN + IDS_CLEAR_ALL_FINISHED_DOWNLOADS + "This will clear all finished downloads, are you sure?\n\nTip: You can view all your downloaded podcasts in Local Media/Podcasts" + IDS_CLEAN_UP_LIST "Clean Up List" + IDS_DONE "Done" + IDS_SAVE "Save" + IDS_REQUIRES_INTERNET_CONNECTION_ENSURE_CONNECTION + "The media library feature you are attempting to use requires an internet connection. Please make sure you are connected to the internet and try again." + IDS_ADD_TO_DOWNLOADS "Added to downloads." + IDS_NO_MEDIA_TO_DOWNLOAD "No media to download." + IDS_TEXT_ARTICLE "Text article" + IDS_DATE_ADDED "Date Added" + IDS_MEDIA_PRESENT "Media Present" + IDS_SURE_YOU_WANT_TO_REMOVE_THIS + "Are you sure you want to remove %s\n(%s)" + IDS_CONFIRM "Confirm" + IDS_CANCEL_DOWNLOADS_AND_QUIT + "You are currently downloading podcasts\nAre you sure you want to cancel these downloads and quit?" + IDS_CONFIRM_QUIT "Confirm Quit" + IDS_ARTICLE_WITH_MEDIA "Article with media" + IDS_PODCAST_DIRECTORY "Podcast Directory" +END + +STRINGTABLE +BEGIN + IDS_DELETEFAILED "Delete Failed" + IDS_SORRY "Sorry" + IDS_EDIT_CHANNEL "Edit Podcast" + IDS_ATD_NEVER "Never" + IDS_ATD_LASTONE "Most recent episode" + IDS_ATD_LASTTWO "Last 2 episodes" + IDS_ATD_LASTTHREE "Last 3 episodes" + IDS_ATD_LASTFIVE "Last 5 episodes" +END + +STRINGTABLE +BEGIN + IDS_PODCAST_SUBSCRIPTION_HEADER "Winamp Podcast Subscription" + IDS_PODCAST_SUBSCRIPTION_PROMP + "Are you sure that you want to subscribe to this podcast?\n\n%s" + IDS_MEDIA_TIME "Time" + IDS_EPISODE_INFO "Episode Info" + IDS_MEDIA_SIZE "Size" +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +#include "version.rc2" + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/Src/Plugins/Library/ml_wire/ml_wire.sln b/Src/Plugins/Library/ml_wire/ml_wire.sln new file mode 100644 index 00000000..2c3763b8 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ml_wire.sln @@ -0,0 +1,87 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29509.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ml_wire", "ml_wire.vcxproj", "{2BF2E8A5-18E0-47B4-822C-FF17077926FD}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "nde", "..\nde\nde.vcxproj", "{4D25C321-7F8B-424E-9899-D80A364BAF1A}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "nx", "..\replicant\nx\nx.vcxproj", "{57C90706-B25D-4ACA-9B33-95CDB2427C27}" + ProjectSection(ProjectDependencies) = postProject + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11} = {44AEBB50-1331-4F2E-8AEC-56C82DE16C11} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "nu", "..\replicant\nu\nu.vcxproj", "{F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "jnetlib", "..\replicant\jnetlib\jnetlib.vcxproj", "{E105A0A2-7391-47C5-86AC-718003524C3D}" + ProjectSection(ProjectDependencies) = postProject + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11} = {44AEBB50-1331-4F2E-8AEC-56C82DE16C11} + EndProjectSection +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "zlib", "..\replicant\zlib\zlib.vcxproj", "{44AEBB50-1331-4F2E-8AEC-56C82DE16C11}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Win32 = Debug|Win32 + Debug|x64 = Debug|x64 + Release|Win32 = Release|Win32 + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Debug|Win32.ActiveCfg = Debug|Win32 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Debug|Win32.Build.0 = Debug|Win32 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Debug|x64.ActiveCfg = Debug|x64 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Debug|x64.Build.0 = Debug|x64 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Release|Win32.ActiveCfg = Release|Win32 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Release|Win32.Build.0 = Release|Win32 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Release|x64.ActiveCfg = Release|x64 + {2BF2E8A5-18E0-47B4-822C-FF17077926FD}.Release|x64.Build.0 = Release|x64 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Debug|Win32.ActiveCfg = Debug|Win32 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Debug|Win32.Build.0 = Debug|Win32 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Debug|x64.ActiveCfg = Debug|x64 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Debug|x64.Build.0 = Debug|x64 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Release|Win32.ActiveCfg = Release|Win32 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Release|Win32.Build.0 = Release|Win32 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Release|x64.ActiveCfg = Release|x64 + {4D25C321-7F8B-424E-9899-D80A364BAF1A}.Release|x64.Build.0 = Release|x64 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Debug|Win32.ActiveCfg = Debug|Win32 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Debug|Win32.Build.0 = Debug|Win32 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Debug|x64.ActiveCfg = Debug|x64 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Debug|x64.Build.0 = Debug|x64 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Release|Win32.ActiveCfg = Release|Win32 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Release|Win32.Build.0 = Release|Win32 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Release|x64.ActiveCfg = Release|x64 + {57C90706-B25D-4ACA-9B33-95CDB2427C27}.Release|x64.Build.0 = Release|x64 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Debug|Win32.ActiveCfg = Debug|Win32 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Debug|Win32.Build.0 = Debug|Win32 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Debug|x64.ActiveCfg = Debug|x64 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Debug|x64.Build.0 = Debug|x64 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Release|Win32.ActiveCfg = Release|Win32 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Release|Win32.Build.0 = Release|Win32 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Release|x64.ActiveCfg = Release|x64 + {F1F5CD60-0D5B-4CEA-9EEB-2F87FF9AA915}.Release|x64.Build.0 = Release|x64 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Debug|Win32.ActiveCfg = Debug|Win32 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Debug|Win32.Build.0 = Debug|Win32 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Debug|x64.ActiveCfg = Debug|x64 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Debug|x64.Build.0 = Debug|x64 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Release|Win32.ActiveCfg = Release|Win32 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Release|Win32.Build.0 = Release|Win32 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Release|x64.ActiveCfg = Release|x64 + {E105A0A2-7391-47C5-86AC-718003524C3D}.Release|x64.Build.0 = Release|x64 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Debug|Win32.ActiveCfg = Debug|Win32 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Debug|Win32.Build.0 = Debug|Win32 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Debug|x64.ActiveCfg = Debug|x64 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Debug|x64.Build.0 = Debug|x64 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Release|Win32.ActiveCfg = Release|Win32 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Release|Win32.Build.0 = Release|Win32 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Release|x64.ActiveCfg = Release|x64 + {44AEBB50-1331-4F2E-8AEC-56C82DE16C11}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C4E799C1-027B-487B-8E1A-31F2D26A1AFE} + EndGlobalSection +EndGlobal diff --git a/Src/Plugins/Library/ml_wire/ml_wire.vcxproj b/Src/Plugins/Library/ml_wire/ml_wire.vcxproj new file mode 100644 index 00000000..8562012d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ml_wire.vcxproj @@ -0,0 +1,423 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="Debug|Win32"> + <Configuration>Debug</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Debug|x64"> + <Configuration>Debug</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|Win32"> + <Configuration>Release</Configuration> + <Platform>Win32</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|x64"> + <Configuration>Release</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + </ItemGroup> + <PropertyGroup Label="Globals"> + <ProjectGuid>{2BF2E8A5-18E0-47B4-822C-FF17077926FD}</ProjectGuid> + <RootNamespace>ml_wire</RootNamespace> + <WindowsTargetPlatformVersion>10.0.19041.0</WindowsTargetPlatformVersion> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <PlatformToolset>v142</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <PlatformToolset>v142</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <PlatformToolset>v142</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> + <ConfigurationType>DynamicLibrary</ConfigurationType> + <PlatformToolset>v142</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> + <ImportGroup Label="ExtensionSettings"> + </ImportGroup> + <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + </ImportGroup> + <PropertyGroup Label="UserMacros" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <LinkIncremental>false</LinkIncremental> + <OutDir>$(PlatformShortName)_$(Configuration)\</OutDir> + <IntDir>$(PlatformShortName)_$(Configuration)\</IntDir> + <IncludePath>$(IncludePath)</IncludePath> + <LibraryPath>$(LibraryPath)</LibraryPath> + <EmbedManifest>true</EmbedManifest> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <LinkIncremental>false</LinkIncremental> + <OutDir>$(PlatformShortName)_$(Configuration)\</OutDir> + <IntDir>$(PlatformShortName)_$(Configuration)\</IntDir> + <EmbedManifest>true</EmbedManifest> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <LinkIncremental>false</LinkIncremental> + <OutDir>$(PlatformShortName)_$(Configuration)\</OutDir> + <IntDir>$(PlatformShortName)_$(Configuration)\</IntDir> + <IncludePath>$(IncludePath)</IncludePath> + <LibraryPath>$(LibraryPath)</LibraryPath> + <EmbedManifest>true</EmbedManifest> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <LinkIncremental>false</LinkIncremental> + <OutDir>$(PlatformShortName)_$(Configuration)\</OutDir> + <IntDir>$(PlatformShortName)_$(Configuration)\</IntDir> + <EmbedManifest>true</EmbedManifest> + </PropertyGroup> + <PropertyGroup Label="Vcpkg"> + <VcpkgEnableManifest>false</VcpkgEnableManifest> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <VcpkgInstalledDir> + </VcpkgInstalledDir> + <VcpkgUseStatic>false</VcpkgUseStatic> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + <VcpkgTriplet>x86-windows-static-md</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <VcpkgInstalledDir> + </VcpkgInstalledDir> + <VcpkgUseStatic>false</VcpkgUseStatic> + <VcpkgTriplet>x86-windows-static-md</VcpkgTriplet> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <VcpkgInstalledDir> + </VcpkgInstalledDir> + <VcpkgUseStatic>false</VcpkgUseStatic> + <VcpkgTriplet>x86-windows-static-md</VcpkgTriplet> + <VcpkgConfiguration>Debug</VcpkgConfiguration> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <VcpkgInstalledDir> + </VcpkgInstalledDir> + <VcpkgUseStatic>false</VcpkgUseStatic> + <VcpkgTriplet>x86-windows-static-md</VcpkgTriplet> + </PropertyGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> + <ClCompile> + <Optimization>Disabled</Optimization> + <AdditionalIncludeDirectories>.;..\..\..\Wasabi;..\..\..\replicant;..;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_WIN32_IE=0x0A00;_DEBUG;WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <MinimalRebuild>false</MinimalRebuild> + <MultiProcessorCompilation>true</MultiProcessorCompilation> + <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> + <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary> + <BufferSecurityCheck>true</BufferSecurityCheck> + <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType> + <ForceConformanceInForLoopScope>true</ForceConformanceInForLoopScope> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <DisableSpecificWarnings>4996;%(DisableSpecificWarnings)</DisableSpecificWarnings> + <ProgramDataBaseFileName>$(IntDir)$(TargetName).pdb</ProgramDataBaseFileName> + </ClCompile> + <Link> + <AdditionalDependencies>comctl32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> + <GenerateDebugInformation>true</GenerateDebugInformation> + <ProgramDatabaseFile>$(IntDir)$(TargetName).pdb</ProgramDatabaseFile> + <SubSystem>Windows</SubSystem> + <RandomizedBaseAddress>false</RandomizedBaseAddress> + <ImportLibrary>$(IntDir)$(TargetName).lib</ImportLibrary> + <TargetMachine>MachineX86</TargetMachine> + <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers> + <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <MapFileName>$(IntDir)$(TargetName).map</MapFileName> + </Link> + <PostBuildEvent> + <Command>xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ + +xcopy /Y /D $(IntDir)$(TargetName).pdb ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ </Command> + <Message>Post build event: 'xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\'</Message> + </PostBuildEvent> + <ResourceCompile> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ResourceCompile> + <Manifest> + <OutputManifestFile>$(IntDir)$(TargetName)$(TargetExt).intermediate.manifest</OutputManifestFile> + </Manifest> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <ClCompile> + <Optimization>Disabled</Optimization> + <AdditionalIncludeDirectories>.;..\..\..\Wasabi;..\..\..\replicant;..;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_WIN32_IE=0x0A00;_DEBUG;WIN64;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <MinimalRebuild>false</MinimalRebuild> + <MultiProcessorCompilation>true</MultiProcessorCompilation> + <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks> + <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary> + <BufferSecurityCheck>true</BufferSecurityCheck> + <TreatWChar_tAsBuiltInType>true</TreatWChar_tAsBuiltInType> + <ForceConformanceInForLoopScope>true</ForceConformanceInForLoopScope> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>ProgramDatabase</DebugInformationFormat> + <DisableSpecificWarnings>4244;4996;%(DisableSpecificWarnings)</DisableSpecificWarnings> + <ProgramDataBaseFileName>$(IntDir)$(TargetName).pdb</ProgramDataBaseFileName> + </ClCompile> + <Link> + <AdditionalDependencies>comctl32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> + <GenerateDebugInformation>true</GenerateDebugInformation> + <ProgramDatabaseFile>$(IntDir)$(TargetName).pdb</ProgramDatabaseFile> + <SubSystem>Windows</SubSystem> + <RandomizedBaseAddress>false</RandomizedBaseAddress> + <ImportLibrary>$(IntDir)$(TargetName).lib</ImportLibrary> + <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers> + <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <MapFileName>$(IntDir)$(TargetName).map</MapFileName> + </Link> + <PostBuildEvent> + <Command>xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ + +xcopy /Y /D $(IntDir)$(TargetName).pdb ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ </Command> + <Message>Post build event: 'xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\'</Message> + </PostBuildEvent> + <ResourceCompile> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ResourceCompile> + <Manifest> + <OutputManifestFile>$(IntDir)$(TargetName)$(TargetExt).intermediate.manifest</OutputManifestFile> + </Manifest> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> + <ClCompile> + <Optimization>MinSpace</Optimization> + <FavorSizeOrSpeed>Size</FavorSizeOrSpeed> + <AdditionalIncludeDirectories>.;..\..\..\Wasabi;..\..\..\replicant;..;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_WIN32_IE=0x0A00;IGNORE_API_GRACENOTE;NDEBUG;WIN32;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <StringPooling>true</StringPooling> + <MultiProcessorCompilation>true</MultiProcessorCompilation> + <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + <BufferSecurityCheck>true</BufferSecurityCheck> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>None</DebugInformationFormat> + <DisableSpecificWarnings>4996;%(DisableSpecificWarnings)</DisableSpecificWarnings> + <ProgramDataBaseFileName>$(IntDir)$(TargetName).pdb</ProgramDataBaseFileName> + </ClCompile> + <Link> + <AdditionalDependencies>comctl32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> + <GenerateDebugInformation>false</GenerateDebugInformation> + <ProgramDatabaseFile>$(IntDir)$(TargetName).pdb</ProgramDatabaseFile> + <MapFileName>$(IntDir)$(TargetName).map</MapFileName> + <SubSystem>Windows</SubSystem> + <OptimizeReferences>true</OptimizeReferences> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <RandomizedBaseAddress>false</RandomizedBaseAddress> + <ImportLibrary>$(IntDir)$(TargetName).lib</ImportLibrary> + <TargetMachine>MachineX86</TargetMachine> + <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers> + </Link> + <PostBuildEvent> + <Command>xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ </Command> + <Message>Post build event: 'xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\'</Message> + </PostBuildEvent> + <ResourceCompile> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ResourceCompile> + <Manifest> + <OutputManifestFile>$(IntDir)$(TargetName)$(TargetExt).intermediate.manifest</OutputManifestFile> + </Manifest> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <ClCompile> + <Optimization>MinSpace</Optimization> + <FavorSizeOrSpeed>Size</FavorSizeOrSpeed> + <AdditionalIncludeDirectories>.;..\..\..\Wasabi;..\..\..\replicant;..;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_WIN32_IE=0x0A00;IGNORE_API_GRACENOTE;NDEBUG;WIN64;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <StringPooling>true</StringPooling> + <MultiProcessorCompilation>true</MultiProcessorCompilation> + <RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary> + <BufferSecurityCheck>true</BufferSecurityCheck> + <WarningLevel>Level3</WarningLevel> + <DebugInformationFormat>None</DebugInformationFormat> + <DisableSpecificWarnings>4244;4996;%(DisableSpecificWarnings)</DisableSpecificWarnings> + <ProgramDataBaseFileName>$(IntDir)$(TargetName).pdb</ProgramDataBaseFileName> + </ClCompile> + <Link> + <AdditionalDependencies>comctl32.lib;shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies> + <OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile> + <GenerateDebugInformation>false</GenerateDebugInformation> + <ProgramDatabaseFile>$(IntDir)$(TargetName).pdb</ProgramDatabaseFile> + <MapFileName>$(IntDir)$(TargetName).map</MapFileName> + <SubSystem>Windows</SubSystem> + <OptimizeReferences>true</OptimizeReferences> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <RandomizedBaseAddress>false</RandomizedBaseAddress> + <ImportLibrary>$(IntDir)$(TargetName).lib</ImportLibrary> + <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> + <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers> + </Link> + <PostBuildEvent> + <Command>xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\ </Command> + <Message>Post build event: 'xcopy /Y /D $(OutDir)$(TargetName)$(TargetExt) ..\..\..\..\Build\Winamp_$(PlatformShortName)_$(Configuration)\Plugins\'</Message> + </PostBuildEvent> + <ResourceCompile> + <PreprocessorDefinitions>_WIN32_WINNT=0x0601;WINVER=0x0601;_UNICODE;UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + </ResourceCompile> + <Manifest> + <OutputManifestFile>$(IntDir)$(TargetName)$(TargetExt).intermediate.manifest</OutputManifestFile> + </Manifest> + </ItemDefinitionGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\nde\nde.vcxproj"> + <Project>{4d25c321-7f8b-424e-9899-d80a364baf1a}</Project> + <CopyLocalSatelliteAssemblies>true</CopyLocalSatelliteAssemblies> + <ReferenceOutputAssembly>true</ReferenceOutputAssembly> + </ProjectReference> + <ProjectReference Include="..\..\..\Wasabi\Wasabi.vcxproj"> + <Project>{3e0bfa8a-b86a-42e9-a33f-ec294f823f7f}</Project> + </ProjectReference> + <ProjectReference Include="..\..\..\WAT\WAT.vcxproj"> + <Project>{c5714908-a71f-4644-bd95-aad8ee7914da}</Project> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <ClInclude Include="..\..\General\gen_ml\menu.h" /> + <ClInclude Include="..\..\..\nu\Alias.h" /> + <ClInclude Include="..\..\..\nu\ChildSizer.h" /> + <ClInclude Include="..\..\..\nu\DialogSkinner.h" /> + <ClInclude Include="..\..\..\nu\MediaLibraryInterface.h" /> + <ClInclude Include="..\..\..\nu\menushortcuts.h" /> + <ClInclude Include="..\..\..\xml\XMLDOM.h" /> + <ClInclude Include="..\..\..\xml\XMLNode.h" /> + <ClInclude Include="api__ml_wire.h" /> + <ClInclude Include="api_podcasts.h" /> + <ClInclude Include="BackgroundDownloader.h" /> + <ClInclude Include="ChannelCheck.h" /> + <ClInclude Include="channelEditor.h" /> + <ClInclude Include="ChannelRefresher.h" /> + <ClInclude Include="ChannelSync.h" /> + <ClInclude Include="Cloud.h" /> + <ClInclude Include="Defaults.h" /> + <ClInclude Include="Downloaded.h" /> + <ClInclude Include="DownloadsDialog.h" /> + <ClInclude Include="DownloadsParse.h" /> + <ClInclude Include="DownloadStatus.h" /> + <ClInclude Include="DownloadThread.h" /> + <ClInclude Include="errors.h" /> + <ClInclude Include="ExternalCOM.h" /> + <ClInclude Include="Factory.h" /> + <ClInclude Include="FeedParse.h" /> + <ClInclude Include="Feeds.h" /> + <ClInclude Include="FeedUtil.h" /> + <ClInclude Include="ifc_article.h" /> + <ClInclude Include="ifc_podcast.h" /> + <ClInclude Include="Item.h" /> + <ClInclude Include="JSAPI2_Creator.h" /> + <ClInclude Include="JSAPI2_PodcastsAPI.h" /> + <ClInclude Include="layout.h" /> + <ClInclude Include="Main.h" /> + <ClInclude Include="navigation.h" /> + <ClInclude Include="OPMLParse.h" /> + <ClInclude Include="ParseUtil.h" /> + <ClInclude Include="PCastFactory.h" /> + <ClInclude Include="PCastURIHandler.h" /> + <ClInclude Include="Preferences.h" /> + <ClInclude Include="resource.h" /> + <ClInclude Include="RFCDate.h" /> + <ClInclude Include="RSSCOM.h" /> + <ClInclude Include="RSSParse.h" /> + <ClInclude Include="service.h" /> + <ClInclude Include="subscriptionView.h" /> + <ClInclude Include="UpdateAutoDownload.h" /> + <ClInclude Include="UpdateTime.h" /> + <ClInclude Include="Util.h" /> + <ClInclude Include="WantsDownloadStatus.h" /> + <ClInclude Include="Wire.h" /> + <ClInclude Include="XMLWriter.h" /> + </ItemGroup> + <ItemGroup> + <ClCompile Include="..\..\General\gen_ml\menu.cpp" /> + <ClCompile Include="..\..\General\gen_ml\ml_lib.cpp" /> + <ClCompile Include="..\..\..\nu\ChildSizer.cpp" /> + <ClCompile Include="..\..\..\nu\DialogSkinner.cpp" /> + <ClCompile Include="..\..\..\nu\listview.cpp"> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)%(Filename)1.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)%(Filename)1.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)%(Filename)1.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)%(Filename)1.obj</ObjectFileName> + </ClCompile> + <ClCompile Include="..\..\..\nu\MediaLibraryInterface.cpp" /> + <ClCompile Include="..\..\..\nu\menushortcuts.cpp" /> + <ClCompile Include="..\..\..\xml\XMLDOM.cpp" /> + <ClCompile Include="..\..\..\xml\XMLNode.cpp" /> + <ClCompile Include="BackgroundDownloader.cpp" /> + <ClCompile Include="channelEditor.cpp" /> + <ClCompile Include="ChannelRefresher.cpp" /> + <ClCompile Include="Cloud.cpp" /> + <ClCompile Include="db.cpp" /> + <ClCompile Include="Defaults.cpp" /> + <ClCompile Include="Downloaded.cpp" /> + <ClCompile Include="DownloadsDialog.cpp" /> + <ClCompile Include="DownloadsParse.cpp" /> + <ClCompile Include="DownloadStatus.cpp" /> + <ClCompile Include="DownloadThread.cpp" /> + <ClCompile Include="ExternalCOM.cpp" /> + <ClCompile Include="Factory.cpp" /> + <ClCompile Include="FeedParse.cpp" /> + <ClCompile Include="Feeds.cpp" /> + <ClCompile Include="FeedUtil.cpp" /> + <ClCompile Include="item.cpp" /> + <ClCompile Include="JSAPI2_Creator.cpp" /> + <ClCompile Include="JSAPI2_PodcastsAPI.cpp" /> + <ClCompile Include="layout.cpp" /> + <ClCompile Include="Main.cpp" /> + <ClCompile Include="navigation.cpp" /> + <ClCompile Include="OPMLParse.cpp" /> + <ClCompile Include="ParseUtil.cpp" /> + <ClCompile Include="PCastFactory.cpp" /> + <ClCompile Include="PCastURIHandler.cpp" /> + <ClCompile Include="Preferences.cpp" /> + <ClCompile Include="RFCDate.cpp" /> + <ClCompile Include="RSSCOM.cpp" /> + <ClCompile Include="RSSParse.cpp" /> + <ClCompile Include="service.cpp" /> + <ClCompile Include="subscriptionView.cpp" /> + <ClCompile Include="UpdateAutoDownload.cpp" /> + <ClCompile Include="UpdateTime.cpp" /> + <ClCompile Include="util.cpp"> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)%(Filename)2.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">$(IntDir)%(Filename)2.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)%(Filename)2.obj</ObjectFileName> + <ObjectFileName Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">$(IntDir)%(Filename)2.obj</ObjectFileName> + </ClCompile> + <ClCompile Include="Wire.cpp" /> + <ClCompile Include="XMLWriter.cpp" /> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="ml_podcast.rc" /> + <ResourceCompile Include="png.rc" /> + </ItemGroup> + <ItemGroup> + <Text Include="DESIGN.txt" /> + <Text Include="TODO.txt" /> + </ItemGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> + <ImportGroup Label="ExtensionTargets"> + </ImportGroup> +</Project>
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/ml_wire.vcxproj.filters b/Src/Plugins/Library/ml_wire/ml_wire.vcxproj.filters new file mode 100644 index 00000000..afe62d41 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/ml_wire.vcxproj.filters @@ -0,0 +1,345 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <ClCompile Include="BackgroundDownloader.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="channelEditor.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="ChannelRefresher.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Cloud.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="db.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Defaults.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Downloaded.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="DownloadsDialog.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="DownloadsParse.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="DownloadStatus.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="DownloadThread.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="ExternalCOM.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Factory.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="FeedParse.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Feeds.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="FeedUtil.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="item.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="JSAPI2_Creator.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="JSAPI2_PodcastsAPI.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="layout.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Main.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="navigation.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="OPMLParse.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="ParseUtil.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="PCastFactory.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="PCastURIHandler.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="XMLWriter.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Preferences.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="RFCDate.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="RSSCOM.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="RSSParse.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="service.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="subscriptionView.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="UpdateAutoDownload.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="UpdateTime.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="util.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="Wire.cpp"> + <Filter>Source Files</Filter> + </ClCompile> + <ClCompile Include="..\..\..\nu\ChildSizer.cpp"> + <Filter>Source Files\nu</Filter> + </ClCompile> + <ClCompile Include="..\..\..\nu\DialogSkinner.cpp"> + <Filter>Source Files\nu</Filter> + </ClCompile> + <ClCompile Include="..\..\..\nu\listview.cpp"> + <Filter>Source Files\nu</Filter> + </ClCompile> + <ClCompile Include="..\..\..\nu\MediaLibraryInterface.cpp"> + <Filter>Source Files\nu</Filter> + </ClCompile> + <ClCompile Include="..\..\General\gen_ml\menu.cpp"> + <Filter>Source Files\gen_ml</Filter> + </ClCompile> + <ClCompile Include="..\..\..\nu\menushortcuts.cpp"> + <Filter>Source Files\nu</Filter> + </ClCompile> + <ClCompile Include="..\..\General\gen_ml\ml_lib.cpp"> + <Filter>Source Files\gen_ml</Filter> + </ClCompile> + <ClCompile Include="..\..\..\xml\XMLNode.cpp"> + <Filter>Source Files\XML</Filter> + </ClCompile> + <ClCompile Include="..\..\..\xml\XMLDOM.cpp"> + <Filter>Source Files\XML</Filter> + </ClCompile> + </ItemGroup> + <ItemGroup> + <ClInclude Include="api__ml_wire.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="api_podcasts.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="BackgroundDownloader.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ChannelCheck.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="channelEditor.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ChannelRefresher.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ChannelSync.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Cloud.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Defaults.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Downloaded.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="DownloadsDialog.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="DownloadsParse.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="DownloadStatus.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="DownloadThread.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="errors.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ExternalCOM.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Factory.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="FeedParse.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Feeds.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="FeedUtil.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ifc_article.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ifc_podcast.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Item.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="JSAPI2_Creator.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="JSAPI2_PodcastsAPI.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="layout.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Main.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="navigation.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="OPMLParse.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="ParseUtil.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="PCastFactory.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="PCastURIHandler.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Preferences.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="resource.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="RFCDate.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="RSSCOM.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="RSSParse.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="service.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="subscriptionView.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="UpdateAutoDownload.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="UpdateTime.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Util.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="WantsDownloadStatus.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="Wire.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="XMLWriter.h"> + <Filter>Header Files</Filter> + </ClInclude> + <ClInclude Include="..\..\..\nu\Alias.h"> + <Filter>Header Files\nu</Filter> + </ClInclude> + <ClInclude Include="..\..\..\nu\ChildSizer.h"> + <Filter>Header Files\nu</Filter> + </ClInclude> + <ClInclude Include="..\..\..\nu\DialogSkinner.h"> + <Filter>Header Files\nu</Filter> + </ClInclude> + <ClInclude Include="..\..\..\nu\MediaLibraryInterface.h"> + <Filter>Header Files\nu</Filter> + </ClInclude> + <ClInclude Include="..\..\General\gen_ml\menu.h"> + <Filter>Header Files\gen_ml</Filter> + </ClInclude> + <ClInclude Include="..\..\..\nu\menushortcuts.h"> + <Filter>Header Files\nu</Filter> + </ClInclude> + <ClInclude Include="..\..\..\xml\XMLNode.h"> + <Filter>Header Files\xml</Filter> + </ClInclude> + <ClInclude Include="..\..\..\xml\XMLDOM.h"> + <Filter>Header Files\xml</Filter> + </ClInclude> + </ItemGroup> + <ItemGroup> + <ResourceCompile Include="ml_podcast.rc"> + <Filter>Ressource Files</Filter> + </ResourceCompile> + <ResourceCompile Include="png.rc"> + <Filter>Ressource Files</Filter> + </ResourceCompile> + </ItemGroup> + <ItemGroup> + <Text Include="DESIGN.txt" /> + <Text Include="TODO.txt" /> + </ItemGroup> + <ItemGroup> + <Filter Include="Header Files"> + <UniqueIdentifier>{04424438-0f00-45d3-957e-f60ad4e3addc}</UniqueIdentifier> + </Filter> + <Filter Include="Ressource Files"> + <UniqueIdentifier>{ab23d144-c22c-4693-98c5-bc8255a28f57}</UniqueIdentifier> + </Filter> + <Filter Include="Source Files"> + <UniqueIdentifier>{8ca72875-5c2c-4336-984e-9aa6e1b15b2f}</UniqueIdentifier> + </Filter> + <Filter Include="Source Files\nu"> + <UniqueIdentifier>{d8fe42ef-d543-46b2-9045-646ded85494d}</UniqueIdentifier> + </Filter> + <Filter Include="Source Files\gen_ml"> + <UniqueIdentifier>{e6bd8d09-5297-423e-943c-22b3f0982d40}</UniqueIdentifier> + </Filter> + <Filter Include="Source Files\XML"> + <UniqueIdentifier>{59f272b6-8c65-4db5-95b5-bca0712a07a8}</UniqueIdentifier> + </Filter> + <Filter Include="Header Files\nu"> + <UniqueIdentifier>{d47a4f2b-363d-4281-aa4f-d76fdb3a22f6}</UniqueIdentifier> + </Filter> + <Filter Include="Header Files\gen_ml"> + <UniqueIdentifier>{df263acf-3409-47f8-9bee-6090095c081c}</UniqueIdentifier> + </Filter> + <Filter Include="Header Files\xml"> + <UniqueIdentifier>{b38287f4-c45a-41f8-9787-fe36ed08cb08}</UniqueIdentifier> + </Filter> + </ItemGroup> +</Project>
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/navigation.cpp b/Src/Plugins/Library/ml_wire/navigation.cpp new file mode 100644 index 00000000..b0cc9621 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/navigation.cpp @@ -0,0 +1,420 @@ +#include <strsafe.h> + +#include "main.h" +#include "./navigation.h" +#include "./util.h" +#include "./resource.h" +#include "api__ml_wire.h" +#include "./service.h" +#include "./subscriptionView.h" +#include "./downloadsDialog.h" +#include "../omBrowser/browserView.h" +#include "../winamp/wa_ipc.h" +#include "..\..\General\gen_ml/ml_ipc_0313.h" +#include "./Defaults.h" + + +#define NAVITEM_PREFIX L"podcast_svc_" + +#define E_NAVITEM_UNKNOWN E_NOINTERFACE + +static Nullsoft::Utility::LockGuard navigationLock; + +static INT Navigation_RegisterIcon( HWND hLibrary, INT iconIndex, LPCWSTR pszImage ) +{ + HMLIMGLST hmlilNavigation = MLNavCtrl_GetImageList( hLibrary ); + if ( hmlilNavigation == NULL ) + return -1; + + MLIMAGESOURCE mlis; + ZeroMemory( &mlis, sizeof( MLIMAGESOURCE ) ); + + mlis.cbSize = sizeof( MLIMAGESOURCE ); + mlis.hInst = NULL; + mlis.bpp = 24; + mlis.lpszName = pszImage; + mlis.type = SRC_TYPE_PNG; + mlis.flags = ISF_FORCE_BPP | ISF_PREMULTIPLY | ISF_LOADFROMFILE; + + MLIMAGELISTITEM item; + ZeroMemory( &item, sizeof( MLIMAGELISTITEM ) ); + item.cbSize = sizeof( MLIMAGELISTITEM ); + item.hmlil = hmlilNavigation; + item.filterUID = MLIF_FILTER3_UID; + item.pmlImgSource = &mlis; + + if (iconIndex >= 0) + { + INT count = MLImageList_GetImageCount( hLibrary, item.hmlil ); + if (iconIndex < count) + { + item.mlilIndex = iconIndex; + + return (FALSE != MLImageList_Replace(hLibrary, &item)) ? iconIndex : -1; + } + } + + + return MLImageList_Add( hLibrary, &item ); +} + +static HNAVITEM Navigation_CreateItem( HWND hLibrary, HNAVITEM hParent, OmService *service ) +{ + if ( hLibrary == NULL || service == NULL ) + return NULL; + + WCHAR szName[ 256 ] = { 0 }, szInvariant[ 64 ] = { 0 }; + if ( FAILED( service->GetName( szName, ARRAYSIZE( szName ) ) ) ) + return NULL; + + if ( FAILED( StringCchPrintf( szInvariant, ARRAYSIZE( szInvariant ), NAVITEM_PREFIX L"%u", service->GetId() ) ) ) + return NULL; + + NAVINSERTSTRUCT nis = { 0 }; + nis.hInsertAfter = NULL; + nis.hParent = hParent; + + WCHAR szIcon[ 512 ] = { 0 }; + INT iIcon = ( SUCCEEDED( service->GetIcon( szIcon, ARRAYSIZE( szIcon ) ) ) ) ? Navigation_RegisterIcon( hLibrary, -1, szIcon ) : -1; + + nis.item.cbSize = sizeof( NAVITEM ); + nis.item.mask = NIMF_TEXT | NIMF_STYLE | NIMF_TEXTINVARIANT | NIMF_PARAM | NIMF_IMAGE | NIMF_IMAGESEL; + + nis.item.id = 0; + nis.item.pszText = szName; + nis.item.pszInvariant = szInvariant; + nis.item.lParam = (LPARAM)service; + + nis.item.style = 0; + + UINT serviceFlags = service->GetFlags(); + + if ( ( OmService::flagRoot & serviceFlags ) != 0 ) + nis.item.style |= ( NIS_HASCHILDREN | NIS_DEFAULTIMAGE ); + + nis.item.styleMask = nis.item.style; + + nis.item.iImage = iIcon; + nis.item.iSelectedImage = iIcon; + + HNAVITEM hItem = MLNavCtrl_InsertItem( hLibrary, &nis ); + if ( hItem != NULL ) + service->AddRef(); + + return hItem; +} + +static HNAVITEM Navigation_GetMessageItem( INT msg, INT_PTR param1 ) +{ + HWND hLibrary = plugin.hwndLibraryParent; + HNAVITEM hItem = ( msg < ML_MSG_NAVIGATION_FIRST ) ? MLNavCtrl_FindItemById( hLibrary, param1 ) : (HNAVITEM)param1; + + return hItem; +} + +static BOOL Navigation_CheckInvariantName( LPCWSTR pszInvarian ) +{ + INT cchInvariant = ( NULL != pszInvarian ) ? lstrlen( pszInvarian ) : 0; + INT cchPrefix = ARRAYSIZE( NAVITEM_PREFIX ) - 1; + + return ( cchInvariant > cchPrefix && + CompareString( CSTR_INVARIANT, 0, NAVITEM_PREFIX, cchPrefix, pszInvarian, cchPrefix ) == CSTR_EQUAL ); +} + +static HRESULT Navigation_GetServiceInt( HWND hLibrary, HNAVITEM hItem, OmService **service ) +{ + WCHAR szBuffer[ 64 ] = { 0 }; + + if ( service == NULL ) + return E_POINTER; + + *service = NULL; + + if ( NULL == hLibrary || NULL == hItem ) + return E_INVALIDARG; + + NAVITEM itemInfo = {0}; + itemInfo.cbSize = sizeof( NAVITEM ); + itemInfo.hItem = hItem; + itemInfo.pszInvariant = szBuffer; + itemInfo.cchInvariantMax = ARRAYSIZE( szBuffer ); + itemInfo.mask = NIMF_PARAM | NIMF_TEXTINVARIANT; + + if ( FALSE == MLNavItem_GetInfo( hLibrary, &itemInfo ) ) + return E_FAIL; + + if ( FALSE == Navigation_CheckInvariantName( szBuffer ) ) + return E_NAVITEM_UNKNOWN; + + *service = (OmService *)itemInfo.lParam; + (*service)->AddRef(); + + return S_OK; +} + +HRESULT Navigation_GetService( HNAVITEM hItem, OmService **service ) +{ + return Navigation_GetServiceInt( plugin.hwndLibraryParent, hItem, service ); +} + +static HRESULT Navigation_CreateView( HNAVITEM hItem, HWND hParent, HWND *hView ) +{ + if ( NULL == hView ) + return E_POINTER; + + *hView = NULL; + + if ( hItem == NULL || hParent == NULL ) + return E_INVALIDARG; + + HRESULT hr; + + OmService *service; + hr = Navigation_GetServiceInt( plugin.hwndLibraryParent, hItem, &service ); + if ( SUCCEEDED( hr ) ) + { + if ( service->GetId() == SERVICE_PODCAST ) + service->SetUrl( serviceUrl[ 0 ] ? serviceUrl : L"https://client.winamp.com/podcasts" ); + + hr = service->CreateView( hParent, hView ); + service->Release(); + } + + return hr; +} + +static void Navigation_OnDestroy() +{ + if ( OMBROWSERMNGR != NULL ) + OMBROWSERMNGR->Finish(); +} + +HNAVITEM Navigation_FindService( UINT serviceId, HNAVITEM hStart, OmService **serviceOut ) +{ + HWND hLibrary = plugin.hwndLibraryParent; + + INT cchPrefix = ARRAYSIZE( NAVITEM_PREFIX ) - 1; + + WCHAR szBuffer[256] = {0}; + NAVITEM itemInfo = {0}; + + itemInfo.cbSize = sizeof( itemInfo ); + itemInfo.mask = NIMF_TEXTINVARIANT | NIMF_PARAM; + itemInfo.cchInvariantMax = ARRAYSIZE( szBuffer ); + itemInfo.pszInvariant = szBuffer; + + if ( hStart == NULL ) + hStart = MLNavCtrl_GetFirst( hLibrary ); + + itemInfo.hItem = hStart; + while ( itemInfo.hItem != NULL ) + { + if ( FALSE != MLNavItem_GetInfo( hLibrary, &itemInfo ) && + CSTR_EQUAL == CompareString( CSTR_INVARIANT, NORM_IGNORECASE, itemInfo.pszInvariant, cchPrefix, + NAVITEM_PREFIX, cchPrefix ) ) + { + OmService *service = (OmService *)itemInfo.lParam; + if ( service != NULL && service->GetId() == serviceId ) + { + if ( serviceOut != NULL ) + { + *serviceOut = service; + service->AddRef(); + } + + return itemInfo.hItem; + } + } + + itemInfo.hItem = MLNavItem_GetNext( hLibrary, itemInfo.hItem ); + } + + if ( serviceOut != NULL ) + *serviceOut = NULL; + + return NULL; +} + +int downloadsViewLoaded = -1; + +static int Navigation_CheckDownloadsView() +{ + if ( downloadsViewLoaded == -1 ) + { + pluginMessage p = { ML_MSG_DOWNLOADS_VIEW_LOADED, 0, 0, 0 }; + downloadsViewLoaded = SENDMLIPC( plugin.hwndLibraryParent, ML_IPC_SEND_PLUGIN_MESSAGE, (WPARAM)&p ); + } + + return downloadsViewLoaded; +} + +HRESULT Navigation_ShowService( UINT serviceId, INT showMode ) +{ + if ( serviceId == SERVICE_DOWNLOADS && Navigation_CheckDownloadsView() ) + return S_OK; + + Nullsoft::Utility::AutoLock lock3( navigationLock ); + + HNAVITEM hRoot = Navigation_FindService( SERVICE_PODCAST, NULL, NULL ); + if( hRoot == NULL ) + return E_UNEXPECTED; + + switch ( serviceId ) + { + case SERVICE_SUBSCRIPTION: + if ( SHOWMODE_AUTO == showMode ) + return E_INVALIDARG; + break; + + case SERVICE_DOWNLOADS: + if ( SHOWMODE_AUTO == showMode ) + { + Nullsoft::Utility::AutoLock lock1( downloadedFiles.downloadedLock ); + Nullsoft::Utility::AutoLock lock2( downloadStatus.statusLock ); + showMode = ( 0 != downloadedFiles.downloadList.size() || 0 != downloadStatus.downloads.size() ) ? SHOWMODE_SHOW : SHOWMODE_HIDE; + } + break; + default: + return E_INVALIDARG; + } + + if ( showMode != SHOWMODE_HIDE && showMode != SHOWMODE_SHOW ) + return E_INVALIDARG; + + HWND hLibrary = plugin.hwndLibraryParent; + + MLNavCtrl_BeginUpdate( hLibrary, NUF_LOCK_TOP ); + + OmService *service; + HNAVITEM hItem = MLNavItem_GetChild( hLibrary, hRoot ); + hItem = Navigation_FindService( serviceId, hItem, &service ); + + HRESULT hr = S_OK; + + if ( showMode == SHOWMODE_HIDE ) + { + if ( hItem == NULL ) + hr = S_FALSE; + else if ( MLNavCtrl_DeleteItem( hLibrary, hItem ) == FALSE ) + hr = E_FAIL; + } + else + { + if ( hItem != NULL ) + hr = S_FALSE; + else + { + switch ( serviceId ) + { + case SERVICE_SUBSCRIPTION: + hr = OmService::CreateLocal( SERVICE_SUBSCRIPTION, MAKEINTRESOURCE( IDS_SUBSCRIPTIONS ), MAKEINTRESOURCE( IDR_SUBSCRIPTION_ICON ), SubscriptionView_Create, &service ); + break; + case SERVICE_DOWNLOADS: + hr = OmService::CreateLocal( SERVICE_DOWNLOADS, MAKEINTRESOURCE( IDS_DOWNLOADS ), MAKEINTRESOURCE( IDR_DOWNLOAD_ICON ), DownloadDialog_Create, &service ); + break; + default: + hr = E_UNEXPECTED; + break; + } + + if ( SUCCEEDED( hr ) ) + { + if ( Navigation_CreateItem( hLibrary, hRoot, service ) == NULL ) + hr = E_FAIL; + } + } + } + + if ( service != NULL ) + service->Release(); + + MLNavCtrl_EndUpdate( hLibrary ); + + return hr; +} + +BOOL Navigation_Initialize( void ) +{ + HNAVITEM hParent = NULL; + OmService *service = NULL; + HWND hLibrary = plugin.hwndLibraryParent; + + MLNavCtrl_BeginUpdate( hLibrary, NUF_LOCK_TOP ); + + if ( SUCCEEDED( OmService::CreateRemote( SERVICE_PODCAST, MAKEINTRESOURCE( IDS_PODCAST_DIRECTORY ), + MAKEINTRESOURCE( IDR_DISCOVER_ICON ), + ( serviceUrl[ 0 ] ? serviceUrl : L"https://client.winamp.com/podcasts" ), &service ) ) ) + { + service->SetFlags( OmService::flagRoot, OmService::flagRoot ); + hParent = Navigation_CreateItem( hLibrary, hParent, service ); + service->Release(); + } + + if ( hParent != NULL ) + { + Navigation_ShowService( SERVICE_SUBSCRIPTION, SHOWMODE_SHOW ); + //Navigation_ShowService(SERVICE_DOWNLOADS, SHOWMODE_AUTO); + } + + MLNavCtrl_EndUpdate( hLibrary ); + + return TRUE; +} + +static void Navigation_OnDeleteItem( HNAVITEM hItem ) +{ + if ( hItem == NULL ) + return; + + HWND hLibrary = plugin.hwndLibraryParent; + if ( hLibrary == NULL ) + return; + + WCHAR szBuffer[64] = {0}; + NAVITEM itemInfo = {0}; + + itemInfo.cbSize = sizeof( itemInfo ); + itemInfo.hItem = hItem; + itemInfo.pszInvariant = szBuffer; + itemInfo.cchInvariantMax = ARRAYSIZE( szBuffer ); + itemInfo.mask = NIMF_PARAM | NIMF_TEXTINVARIANT | NIMF_IMAGE; + + if ( MLNavItem_GetInfo( hLibrary, &itemInfo ) != FALSE && Navigation_CheckInvariantName( szBuffer ) != FALSE ) + { + OmService *service = (OmService *)itemInfo.lParam; + + itemInfo.mask = NIMF_PARAM; + itemInfo.lParam = 0L; + + MLNavItem_SetInfo( hLibrary, &itemInfo ); + + service->Release(); + } +} + + +BOOL Navigation_ProcessMessage( INT msg, INT_PTR param1, INT_PTR param2, INT_PTR param3, INT_PTR *result ) +{ + if (msg < ML_MSG_TREE_BEGIN || msg > ML_MSG_TREE_END) + return FALSE; + + switch ( msg ) + { + case ML_MSG_TREE_ONCREATEVIEW: + { + HWND hView = NULL; + HRESULT hr = Navigation_CreateView( Navigation_GetMessageItem( msg, param1 ), (HWND)param2, &hView ); + *result = ( SUCCEEDED( hr ) ) ? (INT_PTR)hView : NULL; + + return TRUE; + } + case ML_MSG_NAVIGATION_ONDESTROY: + Navigation_OnDestroy(); + return TRUE; + case ML_MSG_NAVIGATION_ONDELETE: + Navigation_OnDeleteItem( Navigation_GetMessageItem( msg, param1 ) ); + return TRUE; + } + + return FALSE; +} diff --git a/Src/Plugins/Library/ml_wire/navigation.h b/Src/Plugins/Library/ml_wire/navigation.h new file mode 100644 index 00000000..1258739d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/navigation.h @@ -0,0 +1,24 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_NAVIGATION_HEADER +#define NULLSOFT_PODCAST_PLUGIN_NAVIGATION_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> + +typedef LPVOID HNAVITEM; +class OmService; + + +BOOL Navigation_Initialize(void); +BOOL Navigation_ProcessMessage(INT msg, INT_PTR param1, INT_PTR param2, INT_PTR param3, INT_PTR *result); + +#define SHOWMODE_HIDE ((INT)0) +#define SHOWMODE_SHOW ((INT)1) +#define SHOWMODE_AUTO ((INT)-1) + +HRESULT Navigation_ShowService(UINT serviceId, INT showMode); +HNAVITEM Navigation_FindService(UINT serviceId, HNAVITEM hStart, OmService **serviceOut); + +#endif //NULLSOFT_PODCAST_PLUGIN_NAVIGATION_HEADER diff --git a/Src/Plugins/Library/ml_wire/png.rc b/Src/Plugins/Library/ml_wire/png.rc new file mode 100644 index 00000000..ebe3b645 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/png.rc @@ -0,0 +1,15 @@ +#include "resource.h" +///////////////////////////////////////////////////////////////////////////// +// +// Data +// +IDR_DISCOVER_ICON RCDATA +".\\resources\\discoverIcon.png" +IDR_DOWNLOAD_ICON RCDATA +".\\resources\\downloadIcon.png" +IDR_SUBSCRIPTION_ICON RCDATA +".\\resources\\subscriptionIcon.png" +IDR_MEDIA_ICON RCDATA +".\\resources\\mediaIcon.png" +IDR_TEXT_ICON RCDATA +".\\resources\\textIcon.png"
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/resource.h b/Src/Plugins/Library/ml_wire/resource.h new file mode 100644 index 00000000..7c41d736 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resource.h @@ -0,0 +1,220 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by ml_podcast.rc +// +#define IDS_NULLSOFT_PODCAST 0 +#define IDS_PODCASTS 1 +#define IDS_DOWNLOADING_KB_COMPLETE 2 +#define IDS_DOWNLOADING_KB_PROGRESS 3 +#define IDS_SUBSCRIPTIONS 4 +#define IDS_DOWNLOADS 5 +#define IDS_DOWNLOADING 6 +#define IDS_CHOOSE_FOLDER 7 +#define IDS_ALREADY_SUBSCRIBED 8 +#define IDD_PODCAST 9 +#define IDS_FILE_NOT_FOUND 9 +#define IDS_CONNECTION_TIMED_OUT 10 +#define IDS_ERROR_PARSING_XML 11 +#define IDS_INVALID_RSS_FEED 12 +#define IDS_NO_JNETLIB 13 +#define IDS_JNETLIB_MISSING 14 +#define IDS_NO_EXPAT 15 +#define IDS_EXPAT_MISSING 16 +#define IDS_CONNECTION_RESET 17 +#define IDS_ERROR_SUBSCRIBING_TO_PODCAST 18 +#define IDS_UPD_MANUALLY 19 +#define IDS_UPD_WEEK 20 +#define IDS_UPD_DAY 21 +#define IDS_UPD_12HRS 22 +#define IDS_UPD_6HRS 23 +#define IDS_UPD_3HRS 24 +#define IDS_UPD_HOUR 25 +#define IDS_UPD_30MINS 26 +#define IDS_ERROR_FYEO 27 +#define IDS_WDAY_SUN 28 +#define IDS_WDAY_MON 29 +#define IDS_WDAY_TUE 30 +#define IDS_WDAY_WED 31 +#define IDS_WDAY_THU 32 +#define IDS_WDAY_FRI 33 +#define IDS_WDAY_SAT 34 +#define IDS_MONTH_JAN 35 +#define IDS_MONTH_FEB 36 +#define IDS_MONTH_MAR 37 +#define IDS_MONTH_APR 38 +#define IDS_MONTH_MAY 39 +#define IDS_MONTH_JUN 40 +#define IDS_MONTH_JUL 41 +#define IDS_MONTH_AUG 42 +#define IDS_MONTH_SEP 43 +#define IDS_MONTH_OCT 44 +#define IDS_MONTH_NOV 45 +#define IDS_MONTH_DEC 46 +#define IDS_RECEIVING_UPDATES_FOR 47 +#define IDS_GOT_NEW_ITEMS_FOR 48 +#define IDS_CHANNEL_ALREADY_PRESENT 49 +#define IDS_DUPLICATE_CHANNEL 50 +#define IDS_ERROR_ADDING_URL 51 +#define IDS_ERROR_PARSING_XML_FROM_SERVER 52 +#define IDS_LINK_HAS_NO_RSS_INFO 53 +#define IDS_INVALID_LINK 54 +#define IDS_CONNECTION_RESET_BY_PEER 55 +#define IDS_DOWNLOADING_PERCENT 56 +#define IDS_CHANNEL 57 +#define IDS_ITEM 58 +#define IDS_PROGRESS 59 +#define IDS_PATH 60 +#define IDS_PERM_DELETE_ARE_YOU_SURE 61 +#define IDS_PERM_DELETE_THESE_ARE_YOU_SURE 62 +#define IDS_DELETION 63 +#define IDS_CLEAR_ALL_FINISHED_DOWNLOADS 64 +#define IDS_CLEAN_UP_LIST 65 +#define IDS_DONE 66 +#define IDS_SAVE 67 +#define IDS_REQUIRES_INTERNET_CONNECTION_ENSURE_CONNECTION 68 +#define IDS_ADD_TO_DOWNLOADS 69 +#define IDS_NO_MEDIA_TO_DOWNLOAD 70 +#define IDS_TEXT_ARTICLE 71 +#define IDS_DATE_ADDED 72 +#define IDS_MEDIA_PRESENT 73 +#define IDS_SURE_YOU_WANT_TO_REMOVE_THIS 74 +#define IDS_CONFIRM 75 +#define IDS_CANCEL_DOWNLOADS_AND_QUIT 76 +#define IDS_CONFIRM_QUIT 77 +#define IDS_ARTICLE_WITH_MEDIA 78 +#define IDS_PODCASTS_DIRECTORY 79 +#define IDS_PODCAST_DIRECTORY 79 +#define IDS_DELETEFAILED 80 +#define IDS_STRING81 81 +#define IDS_SORRY 81 +#define IDS_EDIT_CHANNEL 82 +#define IDS_ATD_NEVER 83 +#define IDS_ATD_LASTONE 84 +#define IDS_ATD_LASTTWO 85 +#define IDS_ATD_LASTTHREE 86 +#define IDS_ATD_LASTFIVE 87 +#define IDD_PREFERENCES 104 +#define IDD_ADDURL 105 +#define IDD_DOWNLOADS 107 +#define IDB_ICON_TEXT 110 +#define IDB_BITMAP1 111 +#define IDB_ICON_MEDIA 111 +#define IDR_MENU1 112 +#define IDB_BITMAP2 114 +#define IDB_TREEIMAGE_DOWNLOAD 114 +#define IDB_BITMAP3 115 +#define IDB_TREEIMAGE_SUBSCRIPTION 115 +#define IDB_BITMAP4 116 +#define IDB_TREEIMAGE_DISCOVER 116 +#define IDS_PODCAST_SUBSCRIPTION_HEADER 120 +#define IDS_PODCAST_SUBSCRIPTION_PROMP 121 +#define IDR_VIEW_DOWNLOAD_ACCELERATORS 122 +#define IDS_MEDIA_TIME 125 +#define IDS_EPISODE_INFO 126 +#define IDS_MEDIA_SIZE 127 +#define IDS_BYTES 128 +#define IDS_KIB 129 +#define IDS_MIB 130 +#define IDS_GIB 131 +#define IDC_CUSTOM 1000 +#define IDC_CHANNELLIST 1002 +#define IDC_CHANNELLIST2 1003 +#define IDC_EPISODE_INFO 1003 +#define IDC_ITEMLIST 1006 +#define IDC_DESCRIPTION 1008 +#define IDC_FEEDLIST 1009 +#define IDC_NEW 1010 +#define IDC_DELETE 1011 +#define IDC_REFRESH 1012 +#define IDC_EDITDESCRIPTION 1013 +#define IDC_EDITURL 1014 +#define IDC_UPDATETIME 1017 +#define IDC_AUTODOWNLOAD 1018 +#define IDC_DEFAULTUPDATETIME 1019 +#define IDC_DEFAULTAUTODOWNLOAD 1020 +#define IDC_APPLYDEFAULTS 1021 +#define IDC_LOCATION 1022 +#define IDC_LOCATIONBROWSE 1024 +#define IDC_ADDURLEDIT 1027 +#define IDC_ADDURLBUTTON 1028 +#define IDC_LIST1 1029 +#define IDC_DOWNLOADLIST 1029 +#define IDC_STATICDESCRIPTION 1030 +#define IDC_STATICURL 1031 +#define IDC_STATICUPDATE 1032 +#define IDC_DOWNLOADGROUP 1033 +#define IDC_STATICAUTODOWNLOAD 1035 +#define IDC_FEEDSGROUP 1036 +#define IDC_DEFAULTSGROUP 1037 +#define IDC_STATICUPDATEEVERY 1038 +#define IDC_STATICDEFAULTAUTODOWNLOAD 1039 +#define IDC_ADD 1044 +#define IDC_EDIT 1045 +#define IDC_PLAY 1047 +#define IDC_ADDURL 1047 +#define IDC_ENQUEUE 1048 +#define IDC_USEDEFAULTS 1048 +#define IDC_VISIT 1049 +#define IDC_USECUSTOM 1049 +#define IDC_DOWNLOAD 1050 +#define IDC_CUSTOMGROUP 1050 +#define IDC_BUTTON1 1051 +#define IDC_CANCEL 1051 +#define IDC_BROWSE 1051 +#define IDC_ENQUEUE2 1051 +#define IDC_FINDNEW 1051 +#define IDC_UPDATELIST 1052 +#define IDC_VDIV 1054 +#define IDC_HDIV1 1055 +#define IDC_HDIV 1055 +#define IDC_BROWSER 1057 +#define IDC_STATUS 1058 +#define IDC_EDIT1 1059 +#define IDC_DOWNLOADLOCATION 1059 +#define IDC_SETTINGSBOX 1060 +#define IDC_CLEANUP 1060 +#define IDC_CHECK1 1062 +#define IDC_UPDATEONLAUNCH 1062 +#define IDC_STATIC_UPDATEEVERY 1064 +#define IDC_PLAYACTION 1065 +#define IDC_ENQUEUEACTION 1066 +#define IDC_AUTODOWNLOADLIST 1067 +#define IDC_STATIC_AUTODOWNLOAD 1068 +#define IDC_DIRECTORYURL 1069 +#define IDR_DISCOVER_ICON 20000 +#define IDR_DOWNLOAD_ICON 20001 +#define IDR_SUBSCRIPTION_ICON 20002 +#define IDR_MEDIA_ICON 20003 +#define IDR_TEXT_ICON 20004 +#define IDM_CHANNELS 40006 +#define IDC_REFRESHALL 40008 +#define ID_PLAYMEDIA_IDC 40018 +#define IDC_REMOVE 40024 +#define ID_DOWNLOADS_SENDTO 40025 +#define ID_Menu 40026 +#define IDC_INFOBOX 40028 +#define IDC_SELECTALL 40030 +#define ID_DOWNLOADS_SENDTO40031 40031 +#define ID_NAVIGATION_PREFERENCES 40032 +#define ID_NAVIGATION_HELP 40033 +#define ID_DOWNLOADS_EXPLORERITEMFOLDER 40034 +#define IDC_EXPLORERITEMFOLDER 40034 +#define ID_ENQUEUE 40036 +#define ID_SUBSCRIPTIONNAVIGATION_PREFERENCE 40043 +#define ID_SUBSCRIPTIONNAVIGATION_DIRECTORY 40044 +#define ID_NAVIGATION_DIRECTORY 40045 +#define ID_SUBSCRIPTIONNAVIGATION_HELP 40046 +#define ID_NAVIGATION_REFRESHALL 40047 +#define ID_EPISODE_SELECTALL 40048 +#define IDS_PLUGIN_NAME 65534 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 133 +#define _APS_NEXT_COMMAND_VALUE 40052 +#define _APS_NEXT_CONTROL_VALUE 1068 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Src/Plugins/Library/ml_wire/resources/discoverIcon.png b/Src/Plugins/Library/ml_wire/resources/discoverIcon.png Binary files differnew file mode 100644 index 00000000..6028b8bd --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resources/discoverIcon.png diff --git a/Src/Plugins/Library/ml_wire/resources/downloadIcon.png b/Src/Plugins/Library/ml_wire/resources/downloadIcon.png Binary files differnew file mode 100644 index 00000000..fadb028f --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resources/downloadIcon.png diff --git a/Src/Plugins/Library/ml_wire/resources/mediaIcon.png b/Src/Plugins/Library/ml_wire/resources/mediaIcon.png Binary files differnew file mode 100644 index 00000000..1f6313e2 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resources/mediaIcon.png diff --git a/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png b/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png Binary files differnew file mode 100644 index 00000000..f37b47fa --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png diff --git a/Src/Plugins/Library/ml_wire/resources/textIcon.png b/Src/Plugins/Library/ml_wire/resources/textIcon.png Binary files differnew file mode 100644 index 00000000..f6e15aef --- /dev/null +++ b/Src/Plugins/Library/ml_wire/resources/textIcon.png diff --git a/Src/Plugins/Library/ml_wire/service.cpp b/Src/Plugins/Library/ml_wire/service.cpp new file mode 100644 index 00000000..80d7b71c --- /dev/null +++ b/Src/Plugins/Library/ml_wire/service.cpp @@ -0,0 +1,273 @@ +#include "main.h" +#include "./service.h" +#include "api__ml_wire.h" +#include "./util.h" +#include "./resource.h" +#include "./externalCOM.h" + +#include "../winamp/wa_ipc.h" +#include <strsafe.h> + +#define IS_INVALIDISPATCH(__disp) (((IDispatch *)1) == (__disp) || NULL == (__disp)) + +OmService::OmService( UINT nId ) : id( nId ) +{} + +OmService::~OmService() +{ + Plugin_FreeResString( name ); + Plugin_FreeResString( url ); + Plugin_FreeResString( icon ); +} + + +HRESULT OmService::CreateRemote( UINT nId, LPCWSTR pszName, LPCWSTR pszIcon, LPCWSTR pszUrl, OmService **instance ) +{ + if ( instance == NULL ) + return E_POINTER; + + *instance = NULL; + + if ( nId == 0 || pszName == NULL ) + return E_INVALIDARG; + + OmService *service = new OmService( nId ); + if ( service == NULL ) + return E_OUTOFMEMORY; + + service->SetName( pszName ); + service->SetIcon( pszIcon ); + service->SetUrl( pszUrl ); + + *instance = service; + + return S_OK; +} + +HRESULT OmService::CreateLocal( UINT nId, LPCWSTR pszName, LPCWSTR pszIcon, SVCWNDCREATEPROC windowCreator, OmService **instance ) +{ + if ( instance == NULL ) + return E_POINTER; + + *instance = NULL; + + if ( nId == 0 || pszName == NULL ) + return E_INVALIDARG; + + OmService *service = new OmService( nId ); + if ( service == NULL ) + return E_OUTOFMEMORY; + + service->SetFlags( flagLocal, flagLocal ); + service->SetName( pszName ); + service->SetIcon( pszIcon ); + service->SetWindowCreator( windowCreator ); + + *instance = service; + + return S_OK; +} + + +size_t OmService::AddRef() +{ + return _ref.fetch_add( 1 ); +} + +size_t OmService::Release() +{ + if ( _ref.load() == 0 ) + return _ref.load(); + + LONG r = _ref.fetch_sub( 1 ); + if ( r == 0 ) + delete( this ); + + return r; +} + + +int OmService::QueryInterface( GUID interface_guid, void **object ) +{ + if ( object == NULL ) + return E_POINTER; + + if ( IsEqualIID( interface_guid, IFC_OmService ) ) + *object = static_cast<ifc_omservice *>( this ); + else + { + *object = NULL; + + return E_NOINTERFACE; + } + + if ( *object == NULL ) + return E_UNEXPECTED; + + AddRef(); + + return S_OK; +} + + +unsigned int OmService::GetId() +{ + return id; +} + +HRESULT OmService::GetName( wchar_t *pszBuffer, int cchBufferMax ) +{ + return Plugin_CopyResString( pszBuffer, cchBufferMax, name ); +} + +HRESULT OmService::GetUrl( wchar_t *pszBuffer, int cchBufferMax ) +{ + return Plugin_CopyResString( pszBuffer, cchBufferMax, url ); +} + +HRESULT OmService::GetIcon( wchar_t *pszBuffer, int cchBufferMax ) +{ + if ( icon != NULL && IS_INTRESOURCE( icon ) ) + { + WCHAR szPath[ 2 * MAX_PATH ] = { 0 }; + if ( GetModuleFileName( plugin.hDllInstance, szPath, ARRAYSIZE( szPath ) ) == 0 ) + return E_FAIL; + + return StringCchPrintf( pszBuffer, cchBufferMax, L"res://%s/#%d/#%d", szPath, RT_RCDATA, icon ); + } + + return StringCchCopyEx( pszBuffer, cchBufferMax, icon, NULL, NULL, STRSAFE_IGNORE_NULLS ); +} + +HRESULT OmService::GetExternal( IDispatch **ppDispatch ) +{ + if ( ppDispatch == NULL ) + return E_POINTER; + + *ppDispatch = NULL; + + HWND hWinamp = plugin.hwndWinampParent; + if ( hWinamp == NULL ) + return E_UNEXPECTED; + + //////*ppDispatch = (IDispatch*)SENDWAIPC(hWinamp, IPC_GET_DISPATCH_OBJECT, 0); + + WCHAR szBuffer[ 64 ] = { 0 }; + if ( SUCCEEDED( StringCchPrintfW( szBuffer, ARRAYSIZE( szBuffer ), L"%u", id ) ) ) + *ppDispatch = (IDispatch *) SENDWAIPC( hWinamp, IPC_JSAPI2_GET_DISPATCH_OBJECT, (WPARAM) szBuffer ); + + + if (IS_INVALIDISPATCH(*ppDispatch) && FAILED(ExternalCOM::CreateInstance((ExternalCOM**)ppDispatch))) + { + *ppDispatch = NULL; + return E_FAIL; + } + + + return S_OK; +} + + +HRESULT OmService::SetName( LPCWSTR pszName ) +{ + Plugin_FreeResString( name ); + name = Plugin_DuplicateResString( pszName ); + + return S_OK; +} + +HRESULT OmService::SetUrl( LPCWSTR pszUrl ) +{ + Plugin_FreeResString( url ); + url = Plugin_DuplicateResString( pszUrl ); + + return S_OK; +} + +HRESULT OmService::SetIcon( LPCWSTR pszIcon ) +{ + Plugin_FreeResString( icon ); + icon = Plugin_DuplicateResString( pszIcon ); + + return S_OK; +} + +void OmService::SetFlags( UINT mask, UINT newFlags ) +{ + flags = ( flags & ~mask ) | ( mask & newFlags ); +} + +UINT OmService::GetFlags( void ) +{ + return flags; +} + + +HRESULT OmService::SetWindowCreator( SVCWNDCREATEPROC proc ) +{ + windowCreator = proc; + + return S_OK; +} + +HRESULT OmService::GetWindowCreator( SVCWNDCREATEPROC *proc ) +{ + if ( proc == NULL ) + return E_INVALIDARG; + + *proc = windowCreator; + + return S_OK; +} + + +HRESULT OmService::CreateView( HWND hParent, HWND *hView ) +{ + if ( hView == NULL ) + return E_POINTER; + + *hView = NULL; + HRESULT hr = S_OK; + + if ( ( flagLocal & flags ) != 0 ) + { + if ( windowCreator != NULL ) + { + *hView = windowCreator( hParent, this ); + if ( *hView == NULL ) + hr = E_FAIL; + } + else + hr = E_INVALIDARG; + } + else + { + if ( OMBROWSERMNGR != NULL ) + { + hr = OMBROWSERMNGR->Initialize( NULL, plugin.hwndWinampParent ); + if ( SUCCEEDED( hr ) ) + hr = OMBROWSERMNGR->CreateView( this, hParent, NULL, 0, hView ); + } + else + hr = E_UNEXPECTED; + + } + + return hr; +} + + +#define CBCLASS OmService +START_DISPATCH; +CB( ADDREF, AddRef ) +CB( RELEASE, Release ) +CB( QUERYINTERFACE, QueryInterface ) +CB( API_GETID, GetId ) +CB( API_GETNAME, GetName ) +CB( API_GETURL, GetUrl ) +CB( API_GETICON, GetIcon ) +CB( API_GETEXTERNAL, GetExternal ) +END_DISPATCH; +#undef CBCLASS + +
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/service.h b/Src/Plugins/Library/ml_wire/service.h new file mode 100644 index 00000000..ad4e839e --- /dev/null +++ b/Src/Plugins/Library/ml_wire/service.h @@ -0,0 +1,69 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_SERVICE_HEADER +#define NULLSOFT_PODCAST_PLUGIN_SERVICE_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> +#include <atomic> + +#include "../omBrowser/ifc_omservice.h" + +class OmService; +typedef HWND (CALLBACK *SVCWNDCREATEPROC)(HWND /*hParent*/, OmService* /*service*/); + +class OmService : public ifc_omservice +{ +public: + typedef enum + { + flagRoot = 0x00000001, + flagLocal = 0x00000002, + } Flags; + +protected: + OmService( UINT nId ); + ~OmService(); + +public: + static HRESULT CreateRemote( UINT nId, LPCWSTR pszName, LPCWSTR pszIcon, LPCWSTR pszUrl, OmService **instance ); + static HRESULT CreateLocal( UINT nId, LPCWSTR pszName, LPCWSTR pszIcon, SVCWNDCREATEPROC windowCreator, OmService **instance ); + + /* Dispatchable */ + size_t AddRef(); + size_t Release(); + int QueryInterface( GUID interface_guid, void **object ); + + /* ifc_omservice */ + unsigned int GetId(); + HRESULT GetName( wchar_t *pszBuffer, int cchBufferMax ); + HRESULT GetUrl( wchar_t *pszBuffer, int cchBufferMax ); + HRESULT GetExternal( IDispatch **ppDispatch ); + HRESULT GetIcon( wchar_t *pszBuffer, int cchBufferMax ); + + HRESULT SetName( LPCWSTR pszName ); + HRESULT SetUrl( LPCWSTR pszUrl ); + HRESULT SetIcon( LPCWSTR pszIcon ); + + void SetFlags( UINT mask, UINT newFlags ); + UINT GetFlags( void ); + + HRESULT SetWindowCreator( SVCWNDCREATEPROC proc ); + HRESULT GetWindowCreator( SVCWNDCREATEPROC *proc ); + + HRESULT CreateView( HWND hParent, HWND *hView ); + +protected: + RECVS_DISPATCH; + + std::atomic<std::size_t> _ref = 1; + UINT id = 0; + LPWSTR name = NULL; + LPWSTR url = NULL; + SVCWNDCREATEPROC windowCreator = NULL; + LPWSTR icon = NULL; + UINT flags = 0; +}; + +#endif //NULLSOFT_PODCAST_PLUGIN_SERVICE_HEADER diff --git a/Src/Plugins/Library/ml_wire/subscriptionView.cpp b/Src/Plugins/Library/ml_wire/subscriptionView.cpp new file mode 100644 index 00000000..4bff7ebc --- /dev/null +++ b/Src/Plugins/Library/ml_wire/subscriptionView.cpp @@ -0,0 +1,2691 @@ +#include "main.h" +#include "./subscriptionView.h" + +#include "api__ml_wire.h" +#include "./util.h" +#include "Feeds.h" +#include "RFCDate.h" +#include "Cloud.h" +#include "./channelEditor.h" +#include "BackgroundDownloader.h" +#include "./service.h" +#include "./layout.h" +#include "../nu/menushortcuts.h" +#include "..\..\General\gen_ml/ml_ipc.h" +#include "..\..\General\gen_ml/ml_ipc_0313.h" +#include "../omBrowser/browserView.h" +#include "./navigation.h" + +#include <strsafe.h> + +#include "../../../WAT/WAT.h" + +#ifndef HDF_SORTUP +#define HDF_SORTUP 0x0400 +#define HDF_SORTDOWN 0x0200 +#endif // !HDF_SORTUP + +using namespace Nullsoft::Utility; + +int itemTitleWidth = 200; +int itemDateWidth = 120; +int itemMediaWidth = 100; +int itemSizeWidth = 100; + +int currentItemSort = 1; // -1 means no sort active +bool itemSortAscending = false; +bool channelSortAscending = true; +int channelLastSelection = -1; + +float htmlDividerPercent = 0.666f; +float channelDividerPercent = 0.333f; + +extern int IPC_LIBRARY_SENDTOMENU; +extern librarySendToMenuStruct s; + +static int episode_info_cy; +HMENU g_context_menus3 = NULL; + +extern Cloud cloud; +extern wchar_t serviceUrl[1024]; + +#define SUBSCRIPTIONVIEW_NAME L"SubscriptionView" + +typedef enum +{ + COLUMN_TITLE = 0, + COLUMN_DATEADDED, + //COLUMN_MEDIA, + COLUMN_MEDIA_TIME, + COLUMN_MEDIA_SIZE, +} ListColumns; + +typedef enum +{ + LI_CHANNEL = 0, + LI_ITEM, + LI_VERT, + LI_FINDNEW, + LI_ADD, + LI_EDIT, + LI_DELETE, + LI_REFRESH, + LI_HORZ, + LI_EPISODE_INFO, + LI_INFO, + LI_PLAY, + LI_ENQUEUE, + LI_CUSTOM, + LI_DOWNLOAD, + LI_VISIT, + LI_STATUS, + LI_LAST +} LayoutItems; + +typedef struct __PODCAST +{ + LONG vertDivider; + LONG horzDivider; + LONG bottomRowSpace; + LONG middleRowSpace; + HRGN updateRegion; + POINTS updateOffset; + size_t channelActive; + BOOL channelAscending; + BOOL itemAscending; + LPWSTR infoUrl; + LPWSTR description; +} PODCAST; + +#define GetPodcast(__hwnd) ((PODCAST*)GetPropW((__hwnd), MAKEINTATOM(VIEWPROP))) + +#define LAYOUTREASON_RESIZE 0 +#define LAYOUTREASON_DIV_LEFT 1 +#define LAYOUTREASON_DIV_RIGHT 2 +#define LAYOUTREASON_DIV_TOP 3 +#define LAYOUTREASON_DIV_BOTTOM 4 + +#define UPDATE_TIMER 12 +#define UPDATE_DELAY 0 + +static void CALLBACK PodcastChannel_UpdateTimer(HWND hwnd, UINT uMsg, UINT_PTR eventId, ULONG elapsed); +static void CALLBACK PodcastItem_UpdateTimer(HWND hwnd, UINT uMsg, UINT_PTR eventId, ULONG elapsed); +static size_t PodcastChannel_GetActive(HWND hwnd); + +ptrdiff_t inline CopyCharW(wchar_t *dest, const wchar_t *src) +{ + wchar_t *end = CharNextW(src); + ptrdiff_t count = end-src; + for (ptrdiff_t i=0;i<count;i++) + { + *dest++=*src++; + } + + return count; +} + +// TODO for the moment, this is just to cleanup bad +// urls which use spaces instead of %20 in the feed +wchar_t *urlencode( wchar_t *p ) +{ + if ( p ) + { + wchar_t buf[ MAX_PATH * 4 ], *i = buf; + StringCchCopyW( buf, MAX_PATH * 4, p ); + while ( p && *p ) + { + if ( !StrCmpNW( i, L" ", 1 ) ) + { + *i++ = L'%'; + *i++ = L'2'; + *i = L'0'; + p += 1; + } + else + { + CopyCharW( i, p ); + p = CharNextW( p ); + } + + i = CharNextW( i ); + } + + *i = 0; + + return _wcsdup( buf ); + } + + return NULL; +} + +static void SubscriptionView_EnableMenuCommands(HMENU hMenu, const INT *commandList, INT commandCount, BOOL fEnable) +{ + UINT uEnable = MF_BYCOMMAND | MF_ENABLED; + if (FALSE == fEnable) uEnable |= (MF_GRAYED | MF_DISABLED); + + for (INT i = 0; i < commandCount; i++) + { + EnableMenuItem(hMenu, commandList[i], uEnable); + } +} + +static void PodcastItem_EnableButtons(HWND hwnd, BOOL fEnable) +{ + static const INT szButtons[] = { IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM, IDC_DOWNLOAD, }; + for(INT i = 0; i < ARRAYSIZE(szButtons); i++) + { + HWND hControl = GetDlgItem(hwnd, szButtons[i]); + if (NULL != hControl) + { + EnableWindow(hControl, fEnable); + } + } +} + +static void PodcastChannel_EnableButtons(HWND hwnd, BOOL fEnable) +{ + static const INT szButtons[] = { IDC_EDIT, IDC_REFRESH, IDC_DELETE, }; + HWND hControl; + + for(INT i = 0; i < ARRAYSIZE(szButtons); i++) + { + hControl = GetDlgItem(hwnd, szButtons[i]); + if (NULL != hControl) + { + EnableWindow(hControl, fEnable); + } + } +} + +static HRESULT SubscriptionView_FormatFont(LPWSTR pszBuffer, INT cchBufferMax, HFONT hFont) +{ + const WCHAR szTemplate[] = L"font: %s %d %dpx \"%s\";"; + + if (NULL == pszBuffer) + return E_POINTER; + + pszBuffer[0] = L'\0'; + + if (NULL == hFont) return E_POINTER; + + LOGFONT lf = {0}; + if (0 == GetObject(hFont, sizeof(LOGFONT), &lf)) + return E_FAIL; + + if (lf.lfHeight < 0) + lf.lfHeight = -lf.lfHeight; + + return StringCchPrintfEx(pszBuffer, cchBufferMax, NULL, NULL, STRSAFE_IGNORE_NULLS, + szTemplate, ((0 == lf.lfItalic) ? L"normal" : L"italic"), + lf.lfWeight, lf.lfHeight, L"Arial"/*lf.lfFaceName*/); +} + +static BOOL SubscriptionView_SetDescription(HWND hwnd, LPCWSTR pszInfo) +{ + HWND hBrowser = GetDlgItem(hwnd, IDC_DESCRIPTION); + if (NULL == hBrowser) return FALSE; + + const WCHAR szTemplate[] = L"<html>" + L"<base target=\"_blank\">" + L"<style type=\"text/css\"> body { %s overflow-y: auto; overflow-x: auto;}</style>" + L"<body>%s</body>" + L"</html>"; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + WCHAR szFont[128] = {0}; + HFONT hFont = (HFONT)SendMessage(hItems, WM_GETFONT, 0, 0L); + if (FAILED(SubscriptionView_FormatFont(szFont, ARRAYSIZE(szFont), hFont))) + szFont[0] = L'\0'; + + if (NULL == pszInfo) pszInfo = L""; + + INT cchLen = lstrlen(pszInfo) + 1; + cchLen += lstrlen(szFont); + cchLen += ARRAYSIZE(szTemplate); + + BSTR documentData = SysAllocStringLen(NULL, cchLen); + if (NULL == documentData) return FALSE; + + BOOL result; + + if ( SUCCEEDED( StringCchPrintfEx( documentData, cchLen, NULL, NULL, STRSAFE_IGNORE_NULLS, szTemplate, szFont, pszInfo ) ) ) + result = BrowserView_WriteDocument( hBrowser, documentData, TRUE ); + else + result = FALSE; + + if (FALSE == result) + SysFreeString(documentData); + + return result; +} + +static void SubscriptionView_UpdateInfo( HWND hwnd ) +{ + PODCAST *podcast = GetPodcast( hwnd ); + if ( NULL == podcast ) + return; + + AutoLock channelLock( channels LOCKNAME( "UpdateInfo" ) ); + + LPCWSTR infoText = NULL; + BOOL itemEnabled = FALSE; + + size_t iChannel = PodcastChannel_GetActive( hwnd ); + if ( BAD_CHANNEL != iChannel ) + { + Channel *channel = &channels[ iChannel ]; + + HWND hItems = GetDlgItem( hwnd, IDC_ITEMLIST ); + INT selectedCount = ( NULL != hItems ) ? (INT)SNDMSG( hItems, LVM_GETSELECTEDCOUNT, 0, 0L ) : 0; + size_t iItem = ( 1 == selectedCount ) ? (size_t)SNDMSG( hItems, LVM_GETNEXTITEM, (WPARAM)-1, LVNI_SELECTED ) : BAD_ITEM; + if ( iItem < channel->items.size() ) + { + if ( FALSE == podcast->itemAscending ) + iItem = channel->items.size() - iItem - 1; + + infoText = channel->items[ iItem ].description; + } + + if ( NULL == infoText || L'\0' == *infoText ) + infoText = channel->description; + + if ( selectedCount > 0 ) + { + INT iSelected = -1; + while ( -1 != ( iSelected = SNDMSG( hItems, LVM_GETNEXTITEM, (WPARAM)iSelected, LVNI_SELECTED ) ) ) + { + size_t t = iSelected; + if ( t < channel->items.size() ) + { + if ( FALSE == podcast->itemAscending ) + t = channel->items.size() - t - 1; + + LPCWSTR url = channel->items[ t ].url; + if ( NULL != url && L'\0' != *url ) + { + itemEnabled = TRUE; + break; + } + } + } + } + } + + if ( podcast->description != infoText && CSTR_EQUAL != CompareString( CSTR_INVARIANT, 0, podcast->description, -1, infoText, -1 ) ) + { + Plugin_FreeString( podcast->description ); + podcast->description = Plugin_CopyString( infoText ); + SubscriptionView_SetDescription( hwnd, podcast->description ); + } + + PodcastChannel_EnableButtons( hwnd, ( BAD_CHANNEL != iChannel ) ); + PodcastItem_EnableButtons( hwnd, itemEnabled ); +} + +static BOOL SubscriptionView_SortItems(size_t iChannel, int sortColumn) +{ + if (iChannel >= channels.size()) + return FALSE; + + AutoLock lock (channels LOCKNAME("SortItems")); + Channel &channel = channels[iChannel]; + + switch (sortColumn) + { + case COLUMN_TITLE: channel.SortByTitle(); return TRUE; + case COLUMN_MEDIA_TIME: channel.SortByMediaTime(); return TRUE; + case COLUMN_MEDIA_SIZE: channel.SortByMediaSize(); return TRUE; + //case COLUMN_MEDIA: channel.SortByMedia(); return TRUE; + case COLUMN_DATEADDED: channel.SortByDate(); return TRUE; + } + + return FALSE; +} + +static BOOL SubscriptionView_SortChannels(int sortColumn) +{ + AutoLock lock (channels LOCKNAME("SortItems")); + + switch (sortColumn) + { + case COLUMN_TITLE: channels.SortByTitle(); return TRUE; + } + + return FALSE; +} + +static INT SubscriptionView_GetListSortColumn(HWND hwnd, INT listId, BOOL *fAscending) +{ + HWND hItems = GetDlgItem(hwnd, listId); + if (NULL != hItems) + { + HWND hHeader = (HWND)SNDMSG(hItems, LVM_GETHEADER, 0, 0L); + if (NULL != hHeader) + { + HDITEM item; + item.mask = HDI_FORMAT; + + INT count = (INT)SNDMSG(hHeader, HDM_GETITEMCOUNT, 0, 0L); + for (INT i = 0; i < count; i++) + { + if (FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, i, (LPARAM)&item) && + 0 != ((HDF_SORTUP | HDF_SORTDOWN) & item.fmt)) + { + if (NULL != fAscending) + { + *fAscending = (0 != (HDF_SORTUP & item.fmt)); + } + return i; + } + } + + } + } + return -1; +} + +static void SubscriptionView_SetListSortColumn(HWND hwnd, INT listId, INT index, BOOL fAscending) +{ + HWND hItems = GetDlgItem(hwnd, listId); + if (NULL == hItems) return; + + HWND hHeader = (HWND)SNDMSG(hItems, LVM_GETHEADER, 0, 0L); + if (NULL == hHeader) return; + + HDITEM item; + item.mask = HDI_FORMAT; + // reset first (ml req) + INT count = (INT)SNDMSG(hHeader, HDM_GETITEMCOUNT, 0, 0L); + for (INT i = 0; i < count; i++) + { + if (index != i && FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, i, (LPARAM)&item)) + { + if (0 != ((HDF_SORTUP | HDF_SORTDOWN) & item.fmt)) + { + item.fmt &= ~(HDF_SORTUP | HDF_SORTDOWN); + SNDMSG(hHeader, HDM_SETITEM, i, (LPARAM)&item); + } + } + } + + if (FALSE != (BOOL)SNDMSG(hHeader, HDM_GETITEM, index, (LPARAM)&item)) + { + INT fmt = item.fmt & ~(HDF_SORTUP | HDF_SORTDOWN); + fmt |= (FALSE == fAscending) ? HDF_SORTDOWN : HDF_SORTUP; + if (fmt != item.fmt) + { + item.fmt = fmt; + SNDMSG(hHeader, HDM_SETITEM, index, (LPARAM)&item); + } + } +} + +static BOOL SubscriptionView_UpdateInfoUrl(HWND hwnd) +{ + AutoLock lock (channels LOCKNAME("UpdateInfoUrl")); + + BOOL result = FALSE; + BOOL buttonEnable = FALSE; + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL != podcast) + { + Plugin_FreeString(podcast->infoUrl); + podcast->infoUrl = NULL; + + LPCWSTR pszUrl = NULL; + + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL != iChannel) + { + Channel *channel = &channels[iChannel]; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + INT selectedCount = (NULL != hItems ) ? (INT)SNDMSG(hItems, LVM_GETSELECTEDCOUNT, 0, 0L) : 0; + size_t iItem = (1 == selectedCount) ? (size_t)SNDMSG(hItems, LVM_GETNEXTITEM, (WPARAM)-1, LVNI_SELECTED) : BAD_ITEM; + if (iItem < channel->items.size()) + { + if (FALSE == podcast->itemAscending) + iItem = channel->items.size() - iItem - 1; + pszUrl = channel->items[iItem].link; + } + + if (NULL == pszUrl || L'\0' == *pszUrl) + pszUrl = channel->link; + } + + if (NULL != pszUrl && L'\0' != *pszUrl) + { + podcast->infoUrl = Plugin_CopyString(pszUrl); + if (NULL != podcast->infoUrl) + { + buttonEnable = TRUE; + result = TRUE; + } + } + } + + HWND hButton = GetDlgItem(hwnd, IDC_VISIT); + if (NULL != hButton) EnableWindow(hButton, buttonEnable); + + return result; +} + +static INT SubscriptionView_Play(HWND hwnd, size_t iChannel, size_t *indexList, size_t indexCount, BOOL fEnqueue, BOOL fForce) +{ + AutoLock channelLock (channels LOCKNAME("Play")); + + if (BAD_CHANNEL == iChannel || iChannel >= channels.size()) + return 0; + + Channel *channel = &channels[iChannel]; + + INT cchBuffer = 0; + + for(size_t i = 0; i < indexCount; i++) + { + size_t iItem = indexList[i]; + if (iItem < channel->items.size()) + { + WCHAR szPath[MAX_PATH * 2] = {0}; + RSS::Item *downloadItem = &channel->items[iItem]; + if ((downloadItem->url && downloadItem->url[0]) && + SUCCEEDED(downloadItem->GetDownloadFileName(channel->title, szPath, ARRAYSIZE(szPath), TRUE)) && + PathFileExists(szPath)) + { + cchBuffer += (lstrlen(szPath) + 1); + } + else + { + wchar_t* url = urlencode(channel->items[iItem].url); + LPCWSTR p = url; + if (NULL != p) + { + cchBuffer += (lstrlen(p) + 1); + free(url); + } + } + } + } + + if (0 == cchBuffer) + return 0; + + cchBuffer++; + LPWSTR pszBuffer = (LPWSTR)calloc(cchBuffer, sizeof(WCHAR)); + if (NULL == pszBuffer) return 0; + + LPWSTR c = pszBuffer; + size_t r = cchBuffer; + + INT playCount = 0; + for(size_t i = 0; i < indexCount; i++) + { + size_t iItem = indexList[i]; + if (iItem < channel->items.size()) + { + WCHAR szPath[MAX_PATH * 2] = {0}; + RSS::Item *downloadItem = &channel->items[iItem]; + if ((downloadItem->url && downloadItem->url[0]) && + SUCCEEDED(downloadItem->GetDownloadFileName(channel->title, szPath, ARRAYSIZE(szPath), TRUE)) && + PathFileExists(szPath)) + { + LPCWSTR p = szPath; + if (NULL != p && L'\0' != *p) + { + if (FAILED(StringCchCopyExW(c, r, p, &c, &r, STRSAFE_NULL_ON_FAILURE)) || 0 == r) + break; + + c++; + r--; + + channel->items[iItem].listened = true; + playCount++; + } + } + else + { + wchar_t* url = urlencode(channel->items[iItem].url); + LPCWSTR p = url; + if (NULL != p && L'\0' != *p) + { + if (FAILED(StringCchCopyExW(c, r, p, &c, &r, STRSAFE_NULL_ON_FAILURE)) || 0 == r) + { + free(url); + break; + } + free(url); + c++; + r--; + + channel->items[iItem].listened = true; + playCount++; + } + } + } + } + + if (c != pszBuffer) + { + *c = L'\0'; + // make sure this is initialised as default handler requires this being zeroed + mlSendToWinampStruct send = {ML_TYPE_STREAMNAMESW,pszBuffer,0}; + // otherwise we've a specific action and need to tell ML to do as we want + if (TRUE == fForce) + send.enqueue = ((FALSE == fEnqueue) ? 0 : 1) | 2; + SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_SENDTOWINAMP, (WPARAM)&send); + } + + free(pszBuffer); + return playCount; +} + +static INT SubscriptionView_PlayChannel(HWND hwnd, size_t iChannel, BOOL fEnqueue, BOOL fForce) +{ + AutoLock channelLock (channels LOCKNAME("PlayChannel")); + + if (BAD_CHANNEL == iChannel || iChannel >= channels.size()) + return 0; + + size_t count = channels.size(); + size_t *list = NULL; + if (0 != count) + { + list = (size_t*)calloc(count, sizeof(size_t)); + if (NULL == list) return 0; + for (size_t i = 0; i < count; i++) list[i] = i; + } + + INT result = SubscriptionView_Play(hwnd, iChannel, list, count, fEnqueue, fForce); + + if (NULL != list) + free(list); + + return result; +} + +static void PodcastItem_InitializeList(HWND hwnd) +{ + HWND hControl = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hControl) + return; + + UINT styleEx = (UINT)GetWindowLongPtr(hControl, GWL_EXSTYLE); + SetWindowLongPtr(hControl, GWL_EXSTYLE, styleEx & ~WS_EX_NOPARENTNOTIFY); + + styleEx = LVS_EX_DOUBLEBUFFER | LVS_EX_FULLROWSELECT | /*LVS_EX_HEADERDRAGDROP |*/ LVS_EX_INFOTIP | LVS_EX_SUBITEMIMAGES; + SendMessage(hControl, LVM_SETEXTENDEDLISTVIEWSTYLE, styleEx, styleEx); + SendMessage(hControl, LVM_SETUNICODEFORMAT, (WPARAM)TRUE, 0L); + + LVCOLUMN lvc = {0}; + WCHAR szBuffer[128] = {0}; + + lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT; + lvc.pszText = szBuffer; + + lvc.fmt = LVCFMT_LEFT; + lvc.cx = itemTitleWidth; + WASABI_API_LNGSTRINGW_BUF( IDS_ITEM, szBuffer, ARRAYSIZE( szBuffer ) ); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)0, (LPARAM)&lvc); + + lvc.fmt = LVCFMT_RIGHT; + lvc.cx = itemDateWidth; + WASABI_API_LNGSTRINGW_BUF( IDS_DATE_ADDED, szBuffer, ARRAYSIZE( szBuffer ) ); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)1, (LPARAM)&lvc); + + lvc.fmt = LVCFMT_RIGHT; + lvc.cx = itemMediaWidth; + WASABI_API_LNGSTRINGW_BUF( IDS_MEDIA_TIME, szBuffer, ARRAYSIZE( szBuffer ) ); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)2, (LPARAM)&lvc); + + lvc.fmt = LVCFMT_RIGHT; + lvc.cx = itemSizeWidth; + WASABI_API_LNGSTRINGW_BUF( IDS_MEDIA_SIZE, szBuffer, ARRAYSIZE( szBuffer ) ); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)3, (LPARAM)&lvc); + + HIMAGELIST imageList = ImageList_Create( 15, 15, ILC_COLOR24, 3, 0 ); + if ( imageList != NULL ) + { + HIMAGELIST prevList = (HIMAGELIST)SNDMSG( hControl, LVM_SETIMAGELIST, LVSIL_SMALL, (LPARAM)imageList ); + if ( prevList != NULL ) + ImageList_Destroy( prevList ); + } + + MLSKINWINDOW skinWindow; + skinWindow.style = SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWLVS_ALTERNATEITEMS; + skinWindow.skinType = SKINNEDWND_TYPE_LISTVIEW; + skinWindow.hwndToSkin = hControl; + + MLSkinWindow( plugin.hwndLibraryParent, &skinWindow ); +} + +static void PodcastItem_SelectionChanged(HWND hwnd, BOOL fImmediate) +{ + KillTimer(hwnd, UPDATE_TIMER); + + if (FALSE == fImmediate) + { + SetTimer(hwnd, UPDATE_TIMER, UPDATE_DELAY, PodcastItem_UpdateTimer); + return; + } + + SubscriptionView_UpdateInfoUrl(hwnd); + SubscriptionView_UpdateInfo(hwnd); +} + +static HMENU PodcastItem_GetMenu(HWND hwnd, HMENU baseMenu, INT iItem) +{ + HMENU menu = GetSubMenu(baseMenu, 1); + if (NULL == menu) + return NULL; + + PodcastItem_SelectionChanged(hwnd, TRUE); + + // update the explore media menu item from the download button state (hence the two IDC_DOWNLOAD) + const INT szExtras[] = { IDC_PLAY, IDC_ENQUEUE, IDC_DOWNLOAD, IDC_DOWNLOAD, IDC_VISIT, }; + + for (INT i = 0; i < ARRAYSIZE(szExtras); i++) + { + HWND hButton = GetDlgItem(hwnd, szExtras[i]); + + UINT uEnable = MF_BYCOMMAND | MF_ENABLED; + if (NULL == hButton || (-1 == iItem) || 0 != (WS_DISABLED & GetWindowLongPtr(hButton, GWL_STYLE))) + uEnable |= (MF_GRAYED | MF_DISABLED); + + EnableMenuItem(menu, (i == 3 ? IDC_EXPLORERITEMFOLDER : szExtras[i]), uEnable); + } + + EnableMenuItem(menu, 2, MF_BYPOSITION | ((-1 == iItem) ? (MF_GRAYED | MF_DISABLED) : MF_ENABLED)); + + { // send-to menu shit... + ZeroMemory(&s, sizeof(s)); + IPC_LIBRARY_SENDTOMENU = SendMessage(plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&"LibrarySendToMenu", IPC_REGISTER_WINAMP_IPCMESSAGE); + if (IPC_LIBRARY_SENDTOMENU > 65536 && SendMessage(plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)0, IPC_LIBRARY_SENDTOMENU) == 0xffffffff) + { + s.mode = 1; + s.hwnd = hwnd; + s.data_type = ML_TYPE_FILENAMESW; + s.ctx[1] = 1; + s.build_hMenu = GetSubMenu(menu, 2); + } + } + + UpdateMenuItems(hwnd, menu); + + // check if the menu itsm is shown as having been download and + // if it hasn't then is nicer to hide 'explore media folder' + { + AutoLock channelLock (channels LOCKNAME("ItemMenu")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL != podcast) + { + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL != iChannel) + { + Channel *channel = &channels[iChannel]; + + if (iItem < (INT)channel->items.size()) + { + if (FALSE == podcast->itemAscending) + iItem = (INT)channel->items.size() - iItem - 1; + + RSS::Item *downloadItem = &channel->items[iItem]; + if(downloadItem->downloaded == false) + { + DeleteMenu(menu, IDC_EXPLORERITEMFOLDER, MF_BYCOMMAND); + } + } + } + } + } + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + INT iCount = (NULL != hItems) ? (INT)SNDMSG(hItems, LVM_GETITEMCOUNT, 0, 0L) : 0; + INT command = IDC_REFRESH; + SubscriptionView_EnableMenuCommands(menu, &command, 1, (0 != iCount)); + + return menu; +} + +static BOOL PodcastItem_Sort(HWND hwnd, INT iColumn, BOOL fAscending) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return FALSE; + + BOOL result = FALSE; + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL != iChannel) + { + result = SubscriptionView_SortItems(iChannel, iColumn); + } + + podcast->itemAscending = fAscending; + SubscriptionView_SetListSortColumn(hwnd, IDC_ITEMLIST, iColumn, fAscending); + + if (FALSE != result) + { + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL != hItems) + InvalidateRect(hItems, NULL, TRUE); + } + + return TRUE; +} + +static size_t PodcastChannel_GetActive(HWND hwnd) +{ + AutoLock lock (channels LOCKNAME("GetActive")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL != podcast) + { + size_t iChannel = podcast->channelActive; + if (iChannel < channels.size()) + { + return (size_t)iChannel; + } + } + return BAD_CHANNEL; +} +static void PodcastInfo_InitializeList(HWND hwnd) +{ + HWND hControl = GetDlgItem(hwnd, IDC_EPISODE_INFO); + if ( hControl == NULL ) + return; + + MLSKINWINDOW skinWindow; + skinWindow.style = SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWLVS_ALTERNATEITEMS; + skinWindow.skinType = SKINNEDWND_TYPE_LISTVIEW; + skinWindow.hwndToSkin = hControl; + MLSkinWindow(plugin.hwndLibraryParent, &skinWindow); + + MLSkinnedScrollWnd_ShowHorzBar(hControl, FALSE); + MLSkinnedScrollWnd_ShowVertBar(hControl, FALSE); + + UINT styleEx; + + styleEx = LVS_EX_DOUBLEBUFFER | LVS_EX_HEADERDRAGDROP | LVS_EX_INFOTIP | LVS_EX_FULLROWSELECT; + SendMessage(hControl, LVM_SETEXTENDEDLISTVIEWSTYLE, styleEx, styleEx); + SendMessage(hControl, LVM_SETUNICODEFORMAT, (WPARAM)TRUE, 0L); + + LVCOLUMN lvc = {0}; + WCHAR szBuffer[128] = {0}; + + lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT; + lvc.fmt = LVCFMT_LEFT; + lvc.pszText = szBuffer; + lvc.cx = 9999; + + WASABI_API_LNGSTRINGW_BUF(IDS_EPISODE_INFO, szBuffer, ARRAYSIZE(szBuffer)); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)0, (LPARAM)&lvc); + + HWND hHeader = ListView_GetHeader(hControl); + + RECT rect; + GetClientRect(hHeader, &rect); + SetWindowPos(hControl, 0, 0, 0, rect.right-rect.left, rect.bottom-rect.top, SWP_NOMOVE|SWP_NOZORDER); +} + +static void PodcastChannel_InitializeList(HWND hwnd) +{ + HWND hControl = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL == hControl) + return; + + MLSKINWINDOW skinWindow = {0}; + skinWindow.style = SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWLVS_ALTERNATEITEMS; + skinWindow.skinType = SKINNEDWND_TYPE_LISTVIEW; + skinWindow.hwndToSkin = hControl; + + MLSkinWindow(plugin.hwndLibraryParent, &skinWindow); + + UINT skinStyle = MLSkinnedWnd_GetStyle(hControl); + skinStyle |= SWLVS_SELALWAYS; + MLSkinnedWnd_SetStyle(hControl, skinStyle); + MLSkinnedScrollWnd_ShowHorzBar(hControl, FALSE); + + UINT styleEx = LVS_EX_DOUBLEBUFFER | LVS_EX_HEADERDRAGDROP | LVS_EX_INFOTIP | LVS_EX_FULLROWSELECT; + SendMessage(hControl, LVM_SETEXTENDEDLISTVIEWSTYLE, styleEx, styleEx); + SendMessage(hControl, LVM_SETUNICODEFORMAT, (WPARAM)TRUE, 0L); + + LVCOLUMN lvc = {0}; + WCHAR szBuffer[128] = {0}; + + lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT; + lvc.fmt = LVCFMT_LEFT; + lvc.pszText = szBuffer; + lvc.cx = 200; + + WASABI_API_LNGSTRINGW_BUF(IDS_CHANNEL, szBuffer, ARRAYSIZE(szBuffer)); + SNDMSG(hControl, LVM_INSERTCOLUMN, (WPARAM)0, (LPARAM)&lvc); +} + +static void PodcastChannel_SelectionChanged(HWND hwnd, BOOL fImmediate) +{ + KillTimer(hwnd, UPDATE_TIMER); + + if (FALSE == fImmediate) + { + SetTimer(hwnd, UPDATE_TIMER, UPDATE_DELAY, PodcastChannel_UpdateTimer); + return; + } + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) + return; + + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + INT iSelected = (NULL != hChannel) ? (INT)SNDMSG(hChannel, LVM_GETNEXTITEM, -1, LVNI_SELECTED) : -1; + + AutoLock channelLock (channels LOCKNAME("Channel Changed")); + + if (-1 != iSelected && ((size_t)iSelected) > channels.size()) + iSelected = -1; + + if (-1 != iSelected && FALSE == podcast->channelAscending) + iSelected = (INT)channels.size() - iSelected - 1; + + podcast->channelActive = iSelected; + + if (-1 != iSelected) + { + INT iSort = SubscriptionView_GetListSortColumn(hwnd, IDC_ITEMLIST, NULL); + SubscriptionView_SortItems(iSelected, iSort); + } + + size_t itemsCount = (-1 != iSelected) ? channels[iSelected].items.size() : 0; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL != hItems) + { + SNDMSG(hItems, WM_SETREDRAW, FALSE, 0L); + + LVITEM lvi; + lvi.state = 0; + lvi.stateMask = LVIS_SELECTED | LVIS_FOCUSED; + SNDMSG(hItems, LVM_SETSELECTIONMARK, 0, (LPARAM)-1); + SNDMSG(hItems, LVM_SETITEMSTATE, -1, (LPARAM)&lvi); + SNDMSG(hItems, LVM_SETITEMCOUNT, (WPARAM)itemsCount, 0L); + + SNDMSG(hItems, WM_SETREDRAW, TRUE, 0L); + } + + SubscriptionView_UpdateInfoUrl(hwnd); + SubscriptionView_UpdateInfo(hwnd); + channelLastSelection = iSelected; +} + +static HMENU PodcastChannel_GetMenu(HWND hwnd, HMENU baseMenu, INT iItem) +{ + HMENU menu = GetSubMenu(baseMenu, 0); + if (NULL == menu) return NULL; + + if (iItem != -1) PodcastChannel_SelectionChanged(hwnd, TRUE); + + if (iItem != -1) + { + DeleteMenu(menu, IDC_ADD, MF_BYCOMMAND); + const INT szExtras[] = { IDC_REFRESH, IDC_EDIT, IDC_DELETE, }; + SubscriptionView_EnableMenuCommands(menu, szExtras, ARRAYSIZE(szExtras), (-1 != iItem)); + } + else + { + DeleteMenu(menu, IDC_REFRESH, MF_BYCOMMAND); + DeleteMenu(menu, IDC_EDIT, MF_BYCOMMAND); + DeleteMenu(menu, IDC_DELETE, MF_BYCOMMAND); + DeleteMenu(menu, IDC_VISIT, MF_BYCOMMAND); + DeleteMenu(menu, 0, MF_BYPOSITION); + DeleteMenu(menu, 0, MF_BYPOSITION); + } + + return menu; +} + +static BOOL PodcastChannel_SyncHeaderSize(HWND hwnd, BOOL fRedraw) +{ + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL == hChannel) return FALSE; + + RECT channelRect; + + HDITEM item; + item.mask = HDI_WIDTH; + + HWND hHeader = (HWND)SNDMSG(hChannel, LVM_GETHEADER, 0, 0L); + if (NULL == hHeader || + FALSE == GetClientRect(hChannel, &channelRect) || + FALSE == SNDMSG(hHeader, HDM_GETITEM, COLUMN_TITLE, (LPARAM)&item)) + { + return FALSE; + } + + LONG columnWidth = channelRect.right - channelRect.left; + + + if (item.cxy == columnWidth) return TRUE; + + item.cxy = columnWidth; + + UINT windowStyle = GetWindowLongPtr(hHeader, GWL_STYLE); + if (FALSE == fRedraw && 0 != (WS_VISIBLE & windowStyle)) + SetWindowLongPtr(hHeader, GWL_STYLE, windowStyle & ~WS_VISIBLE); + + SNDMSG(hHeader, HDM_SETITEM, COLUMN_TITLE, (LPARAM)&item); + + if (FALSE == fRedraw && 0 != (WS_VISIBLE & windowStyle)) + SetWindowLongPtr(hHeader, GWL_STYLE, windowStyle); + + return TRUE; +} + +static BOOL PodcastChannel_Sort(HWND hwnd, INT iColumn, BOOL fAscending) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return FALSE; + + BOOL result = SubscriptionView_SortChannels(iColumn); + + podcast->channelAscending = fAscending; + SubscriptionView_SetListSortColumn(hwnd, IDC_CHANNELLIST, iColumn, fAscending); + + if (FALSE != result) + { + HWND hItems = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL != hItems) + InvalidateRect(hItems, NULL, TRUE); + } + + return result; +} + +static BOOL PodcastChannel_SelectNext(HWND hwnd, BOOL fForward) +{ + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL == hChannel) return FALSE; + + INT iFocus = (INT)SNDMSG(hChannel, LVM_GETNEXTITEM, -1, (LPARAM)LVNI_FOCUSED); + + if (FALSE != fForward) + { + iFocus++; + INT iCount = (INT)SNDMSG(hChannel, LVM_GETITEMCOUNT, 0, 0L); + if (iFocus >= iCount) return TRUE; + } + else + { + if (iFocus <= 0) return TRUE; + iFocus--; + } + + LVITEM lvi = {0}; + lvi.stateMask = LVIS_FOCUSED | LVIS_SELECTED; + SNDMSG(hChannel, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi); + + lvi.state = lvi.stateMask; + if (FALSE != SNDMSG(hChannel, LVM_SETITEMSTATE, (WPARAM)iFocus, (LPARAM)&lvi)) + { + SNDMSG(hChannel, LVM_ENSUREVISIBLE, (WPARAM)iFocus, (LPARAM)FALSE); + MLSkinnedScrollWnd_UpdateBars(hChannel, TRUE); + } + + return TRUE; +} +static void CALLBACK PodcastChannel_UpdateTimer(HWND hwnd, UINT uMsg, UINT_PTR eventId, ULONG elapsed) +{ + KillTimer(hwnd, eventId); + PodcastChannel_SelectionChanged(hwnd, TRUE); +} + +static void CALLBACK PodcastItem_UpdateTimer(HWND hwnd, UINT uMsg, UINT_PTR eventId, ULONG elapsed) +{ + KillTimer(hwnd, eventId); + PodcastItem_SelectionChanged(hwnd, TRUE); +} + +wchar_t* PodcastCommand_OnSendToSelection(HWND hwnd) +{ + AutoLock channelLock (channels LOCKNAME("SendTo")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return 0; + + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL == iChannel) return 0; + Channel *channel = &channels[iChannel]; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return 0; + + INT selectedCount = (INT)SNDMSG(hItems, LVM_GETSELECTEDCOUNT, 0, 0L); + if (0 == selectedCount) return 0; + + INT iSelected = -1; + WCHAR szPath[MAX_PATH * 2] = {0}, *path = NULL; + int buf_pos = 0, buf_size = 0; + + while(-1 != (iSelected = SNDMSG(hItems, LVM_GETNEXTITEM, (WPARAM)iSelected, LVNI_SELECTED))) + { + size_t iItem = iSelected; + if (iItem < channel->items.size()) + { + if (FALSE == podcast->itemAscending) + iItem = channel->items.size() - iItem - 1; + + RSS::Item *downloadItem = &channel->items[iItem]; + if (!((downloadItem->url && downloadItem->url[0]) && + SUCCEEDED(downloadItem->GetDownloadFileName(channel->title, szPath, ARRAYSIZE(szPath), TRUE)) && + PathFileExists(szPath))) + { + lstrcpyn(szPath, downloadItem->url, ARRAYSIZE(szPath)); + } + + listbuild(&path, buf_size, buf_pos, szPath); + } + } + + if (path) path[buf_pos] = 0; + return path; +} + +static void SubscriptionView_ListContextMenu(HWND hwnd, INT controlId, POINTS pts) +{ + HWND hControl = GetDlgItem(hwnd, controlId); + if (NULL == hControl) return; + + POINT pt; + POINTSTOPOINT(pt, pts); + + RECT controlRect, headerRect; + if (FALSE == GetClientRect(hControl, &controlRect)) + SetRectEmpty(&controlRect); + else + MapWindowPoints(hControl, HWND_DESKTOP, (POINT*)&controlRect, 2); + + HWND hHeader = (HWND)SNDMSG(hControl, LVM_GETHEADER, 0, 0L); + if (0 == (WS_VISIBLE & GetWindowLongPtr(hHeader, GWL_STYLE)) || FALSE == GetWindowRect(hHeader, &headerRect)) + { + SetRectEmpty(&headerRect); + } + + if (-1 == pt.x && -1 == pt.y) + { + RECT rect; + + rect.left = LVIR_BOUNDS; + INT iMark = SNDMSG(hControl, LVM_GETNEXTITEM, -1, LVNI_SELECTED | LVNI_FOCUSED); + if (-1 == iMark) iMark = SNDMSG(hControl, LVM_GETNEXTITEM, -1, LVNI_SELECTED); + if (-1 != iMark && FALSE != SNDMSG(hControl, LVM_GETITEMRECT, (WPARAM)iMark, (LPARAM)&rect)) + { + pt.x = rect.left + 4; + if(pt.x > rect.right) pt.x = rect.right; + pt.y = rect.bottom - (rect.bottom - rect.top)/3; + + MapWindowPoints(hControl, HWND_DESKTOP, &pt, 1); + } + } + + INT iItem = -1; + + if ((-1 != pt.x || -1 != pt.y) && FALSE != PtInRect(&controlRect, pt)) + { + if (FALSE != PtInRect(&headerRect, pt)) + { + return; + } + else + { + LVHITTESTINFO hitTest; + hitTest.pt = pt; + MapWindowPoints(HWND_DESKTOP, hControl, &hitTest.pt, 1); + + iItem = (INT)SNDMSG(hControl, LVM_HITTEST, 0, (LPARAM)&hitTest); + if (0 == (LVHT_ONITEM & hitTest.flags)) iItem = -1; + } + } + else + { + pt.x = controlRect.left + 2; + pt.y = controlRect.top + 2; + if (headerRect.top == controlRect.top && headerRect.bottom > controlRect.top) + pt.y = headerRect.bottom + 2; + } + + HMENU baseMenu = WASABI_API_LOADMENU(IDR_MENU1); + if (NULL == baseMenu) return; + + HMENU menu = NULL; + switch(controlId) + { + case IDC_ITEMLIST: menu = PodcastItem_GetMenu(hwnd, baseMenu, iItem); break; + case IDC_CHANNELLIST: menu = PodcastChannel_GetMenu(hwnd, baseMenu, iItem); break; + } + + if (NULL != menu) + { + int r = Menu_TrackPopup(plugin.hwndLibraryParent, menu, TPM_RETURNCMD | TPM_LEFTALIGN | TPM_TOPALIGN | 0x1000/*TPM_VERPOSANIMATION*/, pt.x, pt.y, hwnd, NULL); + if (!SendMessage(hwnd, WM_COMMAND, r, 0)) + { + s.menu_id = r; + if (s.mode == 2 && SendMessage(plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&s, IPC_LIBRARY_SENDTOMENU) == 0xffffffff) + { + s.mode = 3; + + wchar_t* path = PodcastCommand_OnSendToSelection(hwnd); + if (path && *path) + { + s.data = path; + SendMessage(plugin.hwndWinampParent, WM_WA_IPC,(WPARAM)&s, IPC_LIBRARY_SENDTOMENU); + free(path); + } + } + } + + if (s.mode) + { + s.mode=4; + SendMessage(plugin.hwndWinampParent,WM_WA_IPC,(WPARAM)&s,IPC_LIBRARY_SENDTOMENU); // cleanup + } + } + + DestroyMenu(baseMenu); +} + +static void SubscriptionView_UpdateLayout(HWND hwnd, BOOL fRedraw, UINT uReason, HRGN validRegion, POINTS layoutOffset) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + RECT clientRect; + if (FALSE == GetClientRect(hwnd, &clientRect)) return; + + LONG clientWidth = clientRect.right - clientRect.left - WASABI_API_APP->getScaleX(2); + LONG clientHeight = clientRect.bottom - clientRect.top; + + const INT szItems[LI_LAST] = { IDC_CHANNELLIST, IDC_ITEMLIST, IDC_VDIV, + IDC_FINDNEW, IDC_ADD, IDC_EDIT, IDC_DELETE, IDC_REFRESH, + IDC_HDIV, + IDC_EPISODE_INFO, IDC_DESCRIPTION, + IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM, IDC_DOWNLOAD, IDC_VISIT, IDC_STATUS}; + + LAYOUTITEM *layout = (LAYOUTITEM*)calloc(ARRAYSIZE(szItems), sizeof(LAYOUTITEM)); + if (NULL == layout) return; + + Layout_Initialize(hwnd, szItems, ARRAYSIZE(szItems), layout); + + if (episode_info_cy == 0 || (layout[LI_EPISODE_INFO].cy && episode_info_cy != layout[LI_EPISODE_INFO].cy)) + { + HWND hControl = GetDlgItem(hwnd, IDC_EPISODE_INFO); + if (IsWindow(hControl)) + { + HWND hHeader = ListView_GetHeader(hControl); + if (IsWindow(hHeader)) + { + RECT rect; + GetClientRect(hHeader, &rect); + SetWindowPos(hControl, 0, 0, 0, rect.right-rect.left, rect.bottom-rect.top, SWP_NOMOVE|SWP_NOZORDER); + episode_info_cy = rect.bottom-rect.top; + } + else + episode_info_cy = layout[LI_EPISODE_INFO].cy; + } + else + episode_info_cy = layout[LI_EPISODE_INFO].cy; + } + + LONG t = (clientWidth - layout[LI_VERT].cx) * podcast->vertDivider / 10000; + LONG t2 = clientRect.right - layout[LI_VERT].cx; + + if (LAYOUTREASON_DIV_LEFT == uReason && t < clientRect.left + WASABI_API_APP->getScaleX(36)) + t = clientRect.left; + + if (LAYOUTREASON_DIV_RIGHT == uReason && t > (t2 - WASABI_API_APP->getScaleX(36))) + t = t2; + + if (t < clientRect.left + WASABI_API_APP->getScaleX(36)) t = clientRect.left; + else if (t > t2 - WASABI_API_APP->getScaleX(36)) t = t2; + + layout[LI_CHANNEL].x = WASABI_API_APP->getScaleX(1); + layout[LI_CHANNEL].y = WASABI_API_APP->getScaleX(1); + layout[LI_CHANNEL].cx = t; + layout[LI_VERT].x = LI_GET_R(layout[LI_CHANNEL]); + layout[LI_VERT].y = WASABI_API_APP->getScaleX(1); + layout[LI_ITEM].x = LI_GET_R(layout[LI_VERT]); + layout[LI_ITEM].y = WASABI_API_APP->getScaleX(1); + LI_SET_R(layout[LI_ITEM], clientWidth); + + layout[LI_HORZ].cx = clientWidth - layout[LI_HORZ].x*WASABI_API_APP->getScaleX(2); + layout[LI_EPISODE_INFO].x = WASABI_API_APP->getScaleX(1); + layout[LI_EPISODE_INFO].cx = clientWidth - layout[LI_EPISODE_INFO].x; + layout[LI_INFO].cx = clientWidth - layout[LI_INFO].x*WASABI_API_APP->getScaleX(2) - 1; + LI_SET_R(layout[LI_STATUS], clientWidth); + if (layout[LI_STATUS].cx < WASABI_API_APP->getScaleX(20)) layout[LI_STATUS].cx = 0; + + t = clientHeight - layout[LI_PLAY].cy; + int cum_width = 0, inc = 0, btnY = 0; + const INT szBottomRow[] = { LI_PLAY, LI_ENQUEUE, LI_CUSTOM, LI_DOWNLOAD, LI_VISIT, LI_STATUS, }; + for (INT i = 0; i < ARRAYSIZE(szBottomRow); i++) + { + LAYOUTITEM *p = &layout[szBottomRow[i]]; + if (IsWindow(p->hwnd)) + { + if (!inc) p->x = 1; + else p->x = 1 + cum_width + WASABI_API_APP->getScaleX(4)*inc; + + wchar_t buffer[128] = {0}; + GetWindowTextW(p->hwnd, buffer, ARRAYSIZE(buffer)); + LRESULT idealSize = MLSkinnedButton_GetIdealSize(p->hwnd, buffer); + + btnY = p->cy = WASABI_API_APP->getScaleY(HIWORD(idealSize)); + p->y = clientHeight - p->cy; + + if (szBottomRow[i] != LI_STATUS) // exclude the last one so it'll span the remainder of the space + { + if (LI_CUSTOM != szBottomRow[i] || customAllowed) + { + if (groupBtn && szBottomRow[i] == LI_PLAY && enqueuedef == 1) + { + p->flags |= SWP_HIDEWINDOW; + p->cy = 0; + continue; + } + + if (groupBtn && szBottomRow[i] == LI_ENQUEUE && enqueuedef != 1) + { + p->flags |= SWP_HIDEWINDOW; + p->cy = 0; + continue; + } + + if (groupBtn && (szBottomRow[i] == LI_PLAY || szBottomRow[i] == LI_ENQUEUE) && customAllowed) + { + p->flags |= SWP_HIDEWINDOW; + p->cy = 0; + continue; + } + + LONG width = LOWORD(idealSize) + WASABI_API_APP->getScaleX(6); + p->cx = width; + p->cy = WASABI_API_APP->getScaleY(HIWORD(idealSize)); + cum_width += width; + inc++; + } + else + { + p->flags |= SWP_HIDEWINDOW; + p->cy = 0; + continue; + } + } + else + { + LRESULT idealSize = MLSkinnedStatic_GetIdealSize(p->hwnd, buffer); + btnY = p->cy = WASABI_API_APP->getScaleY(HIWORD(idealSize)); + p->y = clientHeight - p->cy; + p->cx = clientWidth - cum_width - (WASABI_API_APP->getScaleX(6) * 2); + } + } + } + + t = (clientHeight - layout[LI_HORZ].cy) * podcast->horzDivider / 10000; + t2 = layout[LI_PLAY].y - layout[LI_HORZ].cy; + + if (LAYOUTREASON_DIV_TOP == uReason && t < clientRect.top + WASABI_API_APP->getScaleY(36 + btnY)) + t = clientRect.top; + + if (LAYOUTREASON_DIV_BOTTOM == uReason && t > (t2 - WASABI_API_APP->getScaleY(36 + btnY))) + t = t2; + + if (t < clientRect.top + WASABI_API_APP->getScaleY(36 + btnY)) t = clientRect.top; + else if (t > t2 - WASABI_API_APP->getScaleY(36 + btnY)) t = t2; + + cum_width = 0; + const INT szMiddleRow[] = { LI_FINDNEW, LI_ADD, LI_EDIT, LI_DELETE, LI_REFRESH, }; + for (INT i = 0; i < ARRAYSIZE(szMiddleRow); i++) + { + LAYOUTITEM *p = &layout[szMiddleRow[i]]; + p->x = 1 + (i ? cum_width + WASABI_API_APP->getScaleX(4)*i : 0); + + wchar_t buffer[128] = {0}; + GetWindowTextW(p->hwnd, buffer, ARRAYSIZE(buffer)); + LRESULT idealSize = MLSkinnedButton_GetIdealSize(p->hwnd, buffer); + // used to hide the 'directory' button if not using one + LONG width = (!i && !serviceUrl[0] ? WASABI_API_APP->getScaleX(-4) : LOWORD(idealSize) + WASABI_API_APP->getScaleX(6)); + p->cx = width; + cum_width += width; + + p->cy = (t < WASABI_API_APP->getScaleY(48)) ? 0 : WASABI_API_APP->getScaleY(HIWORD(idealSize)); + p->y = t - p->cy; + } + + layout[LI_HORZ].y = LI_GET_B(layout[LI_FINDNEW]); + layout[LI_EPISODE_INFO].y = LI_GET_B(layout[LI_HORZ]); + if (clientRect.bottom - layout[LI_EPISODE_INFO].y < WASABI_API_APP->getScaleY(episode_info_cy)) + layout[LI_EPISODE_INFO].cy = 0; + else + layout[LI_EPISODE_INFO].cy = episode_info_cy; + + layout[LI_INFO].y = LI_GET_B(layout[LI_EPISODE_INFO]); + LI_SET_B(layout[LI_INFO], layout[LI_PLAY].y - WASABI_API_APP->getScaleY(4)); + + t = layout[LI_FINDNEW].y; + if (layout[LI_FINDNEW].cy > 0) t -= podcast->middleRowSpace; + + layout[LI_CHANNEL].cy = t; + layout[LI_VERT].cy = t; + layout[LI_ITEM].cy = t; + layout[LI_INFO].flags |= SWP_NOREDRAW; + + Layout_SetVisibility(&clientRect, layout, ARRAYSIZE(szItems)); + Layout_Perform(hwnd, layout, ARRAYSIZE(szItems), fRedraw); + PodcastChannel_SyncHeaderSize(hwnd, fRedraw); + + if (NULL != validRegion) + { + RECT validRect; + CopyRect(&validRect, &clientRect); + validRect.right += (layout[LI_INFO].rect.right - LI_GET_R(layout[LI_INFO])); + validRect.bottom += (layout[LI_PLAY].rect.bottom - LI_GET_B(layout[LI_PLAY])); + Layout_GetValidRgn(validRegion, layoutOffset, &validRect, layout, ARRAYSIZE(szItems)); + } + + free(layout); +} + +static BOOL SubscriptionView_UpdateImageList(HIMAGELIST imageList, COLORREF rgbBk, COLORREF rgbFg) +{ + if(NULL == imageList) + return FALSE; + + INT imageCount = ImageList_GetImageCount(imageList); + + MLIMAGESOURCE source; + ZeroMemory(&source, sizeof(source)); + source.cbSize = sizeof(source); + source.hInst = plugin.hDllInstance; + source.type = SRC_TYPE_PNG; + + MLIMAGEFILTERAPPLYEX filter; + ZeroMemory(&filter, sizeof(filter)); + filter.cbSize = sizeof(filter); + filter.filterUID = MLIF_FILTER3_UID; + filter.rgbBk = rgbBk; + filter.rgbFg = rgbFg; + + const INT szImages[] = { IDR_TEXT_ICON, IDR_DOWNLOAD_ICON, IDR_MEDIA_ICON, IDR_MEDIA_ICON, }; + + for (INT i = 0; i < ARRAYSIZE(szImages); i++) + { + source.lpszName = MAKEINTRESOURCE(szImages[i]); + HBITMAP bitmap = MLImageLoader_LoadDib(plugin.hwndLibraryParent, &source); + if (NULL != bitmap) + { + DIBSECTION dib; + if (sizeof(dib) == GetObject(bitmap, sizeof(dib), &dib)) + { + filter.pData = (BYTE*)dib.dsBm.bmBits; + filter.bpp = dib.dsBm.bmBitsPixel; + filter.cx = dib.dsBm.bmWidth; + filter.cy = dib.dsBm.bmHeight; + + if (filter.cy < 0) + filter.cy = -filter.cy; + + if (MLImageFilter_ApplyEx(plugin.hwndLibraryParent, &filter)) + { + INT result = (i < imageCount) ? + ImageList_Replace(imageList, i, bitmap, NULL) : + ImageList_Add(imageList, bitmap, NULL); + } + } + + DeleteObject(bitmap); + } + } + + return TRUE; +} + +static void SubscriptionView_UpdateSkin(HWND hwnd) +{ + UINT windowStyle = GetWindowLongPtr(hwnd, GWL_STYLE); + if (0 != (WS_VISIBLE & windowStyle)) + SetWindowLongPtr(hwnd, GWL_STYLE, windowStyle & ~WS_VISIBLE); + + HWND hControl; + COLORREF rgbBk = dialogSkinner.Color(WADLG_ITEMBG); + COLORREF rgbText = dialogSkinner.Color(WADLG_ITEMFG); + HFONT hFont = dialogSkinner.GetFont(); + + episode_info_cy = 0; + + static const INT szLists[] = {IDC_CHANNELLIST, IDC_ITEMLIST, IDC_EPISODE_INFO}; + for (INT i = 0; i < ARRAYSIZE(szLists); i++) + { + if (NULL != (hControl = GetDlgItem(hwnd, szLists[i]))) + { + ListView_SetBkColor(hControl, rgbBk); + ListView_SetTextBkColor(hControl, rgbBk); + ListView_SetTextColor(hControl, rgbText); + if (NULL != hFont) + { + SendMessage(hControl, WM_SETFONT, (WPARAM)hFont, 0L); + } + + HIMAGELIST imageList = (HIMAGELIST)SendMessage(hControl, LVM_GETIMAGELIST, LVSIL_SMALL, 0L); + if (NULL != imageList) + { + SubscriptionView_UpdateImageList(imageList, rgbBk, rgbText); + } + } + } + + if (0 != (WS_VISIBLE & windowStyle)) + { + windowStyle = GetWindowLongPtr(hwnd, GWL_STYLE); + if (0 == (WS_VISIBLE & windowStyle)) + SetWindowLongPtr(hwnd, GWL_STYLE, windowStyle | WS_VISIBLE); + + RedrawWindow(hwnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN); + SubscriptionView_UpdateInfo(hwnd); + } + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + SubscriptionView_SetDescription(hwnd, podcast->description); + + HRGN validRegion = (NULL != podcast->updateRegion) ? CreateRectRgn(0, 0, 0, 0) : NULL; + SubscriptionView_UpdateLayout(hwnd, TRUE, LAYOUTREASON_RESIZE, validRegion, podcast->updateOffset); + if (NULL != validRegion) + { + CombineRgn(podcast->updateRegion, podcast->updateRegion, validRegion, RGN_DIFF); + DeleteObject(validRegion); + } +} + +static void SubscriptionView_SkinControls(HWND hwnd, const INT *itemList, INT itemCount, UINT skinType, UINT skinStyle) +{ + FLICKERFIX ff = {0, FFM_ERASEINPAINT}; + MLSKINWINDOW skinWindow = {0}; + skinWindow.style = skinStyle; + skinWindow.skinType = skinType; + + for(INT i = 0; i < itemCount; i++) + { + ff.hwnd = skinWindow.hwndToSkin = GetDlgItem(hwnd, itemList[i]); + if (IsWindow(skinWindow.hwndToSkin)) + { + MLSkinWindow(plugin.hwndLibraryParent, &skinWindow); + SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_FLICKERFIX, (WPARAM)&ff); + } + } +} + +static void SubscriptionView_InitMetrics(HWND hwnd) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + HWND hControl; + RECT rect; + LONG t; + + podcast->vertDivider = (LONG)(channelDividerPercent * 10000); + podcast->horzDivider = (LONG)(htmlDividerPercent * 10000); + + if (NULL != (hControl = GetDlgItem(hwnd, IDC_ADD)) && FALSE != GetWindowRect(hControl, &rect)) + { + t = rect.top + WASABI_API_APP->getScaleY(2); + if (NULL != (hControl = GetDlgItem(hwnd, IDC_CHANNELLIST)) && FALSE != GetWindowRect(hControl, &rect)) + podcast->middleRowSpace = t - rect.bottom; + } + + if (NULL != (hControl = GetDlgItem(hwnd, IDC_PLAY)) && FALSE != GetWindowRect(hControl, &rect)) + { + t = rect.top + WASABI_API_APP->getScaleY(2); + if (NULL != (hControl = GetDlgItem(hwnd, IDC_DESCRIPTION)) && FALSE != GetWindowRect(hControl, &rect)) + podcast->bottomRowSpace = t - rect.bottom; + } +} + +static void PodcastChannel_OnKeyDown(HWND hwnd, NMLVKEYDOWN *pkd) +{ + bool ctrl = (0 != (0x8000 & GetAsyncKeyState(VK_CONTROL))), + shift = (0 != (0x8000 & GetAsyncKeyState(VK_SHIFT))); + switch(pkd->wVKey) + { + case VK_DELETE: + if(!shift && !ctrl) + { + SENDCMD(hwnd, IDC_DELETE, 0, 0); + } + break; + + case VK_F5: + if(!ctrl) + { + SENDCMD(hwnd, (!shift ? IDC_REFRESH : IDC_REFRESHALL), 0, 0); + } + break; + + case VK_F7: + if (!shift && !ctrl) + { + SENDCMD(hwnd, IDC_VISIT, 0, 0); + } + break; + + case VK_F2: + if(!shift && !ctrl) + { + SENDCMD(hwnd, IDC_EDIT, 0, 0); + } + break; + + case VK_INSERT: + if(!shift && !ctrl) + { + SENDCMD(hwnd, IDC_ADD, 0, 0); + } + break; + + case VK_LEFT: + if(!shift && !ctrl) + { + PodcastChannel_SelectNext(hwnd, FALSE); + } + break; + + case VK_RIGHT: + if(!shift && !ctrl) + { + PodcastChannel_SelectNext(hwnd, TRUE); + } + break; + } +} + +static void PodcastChannel_OnColumnClick(HWND hwnd, NMLISTVIEW *plv) +{ + BOOL fAscending; + INT iSort = SubscriptionView_GetListSortColumn(hwnd, IDC_CHANNELLIST, &fAscending); + fAscending = (-1 != iSort && iSort == plv->iSubItem) ? (!fAscending) : TRUE; + + PodcastChannel_Sort(hwnd, plv->iSubItem, fAscending); +} + +static void PodcastChannel_OnDoubleClick(HWND hwnd, NMITEMACTIVATE *pia) +{ + if(pia->iItem != -1) + { + LVITEM lvi; + lvi.state = LVIS_SELECTED; + lvi.stateMask = LVIS_SELECTED; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + + if (NULL != hItems && + FALSE != SNDMSG(hItems, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi)) + { + SENDCMD(hwnd, (((!!(GetAsyncKeyState(VK_SHIFT)&0x8000)) ^ ML_ENQDEF_VAL()) ? IDC_ENQUEUEACTION : IDC_PLAYACTION), 0, 0); + lvi.state = 0; + SNDMSG(hItems, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi); + } + } +} + +static void PodcastChannel_OnReturn(HWND hwnd, NMHDR *pnmh) +{ + LVITEM lvi; + lvi.state = LVIS_SELECTED; + lvi.stateMask = LVIS_SELECTED; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + + if (NULL != hItems && + FALSE != SNDMSG(hItems, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi)) + { + SENDCMD(hwnd, (((!!(GetAsyncKeyState(VK_SHIFT)&0x8000)) ^ ML_ENQDEF_VAL()) ? IDC_ENQUEUEACTION : IDC_PLAYACTION), 0, 0); + lvi.state = 0; + SNDMSG(hItems, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi); + } +} + +static void PodcastChannel_OnGetDispInfo(HWND hwnd, NMLVDISPINFO *pdi) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + if (0 != (LVIF_TEXT & pdi->item.mask)) + { + switch (pdi->item.iSubItem) + { + case COLUMN_TITLE: + { + pdi->item.pszText[0] = 0; // turn into an empty string as a safety precaution + AutoLock channelLock(channels LOCKNAME("FillChannelTitle")); + size_t iChannel = (size_t)pdi->item.iItem; + if (iChannel < channels.size()) + { + if (FALSE == podcast->channelAscending) + iChannel = channels.size() - iChannel - 1; + + const wchar_t *title = channels[iChannel].title; + if (title) + StringCchCopy(pdi->item.pszText, pdi->item.cchTextMax, title); + } + } + break; + } + } +} + +static void PodcastChannel_OnItemChanged(HWND hwnd, NMLISTVIEW *plv) +{ + if ((plv->iItem != -1) && (LVIS_SELECTED & plv->uNewState) != (LVIS_SELECTED & plv->uOldState)) + PodcastChannel_SelectionChanged(hwnd, FALSE); +} + +static void PodcastChannel_OnSetFocus(HWND hwnd, NMHDR *pnmh) +{ + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return; + + LVITEM lvi; + lvi.state = 0; + lvi.stateMask = LVIS_SELECTED | LVIS_FOCUSED; + SNDMSG(hItems, LVM_SETITEMSTATE, -1, (LPARAM)&lvi); + SNDMSG(hItems, LVM_SETSELECTIONMARK, (WPARAM)0, (LPARAM)-1); +} + +static LRESULT PodcastChannel_OnNotify(HWND hwnd, NMHDR *pnmh) +{ + switch (pnmh->code) + { + case LVN_KEYDOWN: PodcastChannel_OnKeyDown(hwnd, (NMLVKEYDOWN *)pnmh); break; + case LVN_COLUMNCLICK: PodcastChannel_OnColumnClick(hwnd, (NMLISTVIEW*)pnmh); break; + case NM_DBLCLK: PodcastChannel_OnDoubleClick(hwnd, (NMITEMACTIVATE*)pnmh); break; + case LVN_GETDISPINFO: PodcastChannel_OnGetDispInfo(hwnd, (NMLVDISPINFO*)pnmh); break; + case LVN_ITEMCHANGED: PodcastChannel_OnItemChanged(hwnd, (NMLISTVIEW*)pnmh); break; + case NM_SETFOCUS: PodcastChannel_OnSetFocus(hwnd, pnmh); break; + case NM_RETURN: PodcastChannel_OnReturn(hwnd, pnmh); break; + } + + return 0; +} + +bool ParseDuration(const wchar_t *duration, int *out_hours, int *out_minutes, int *out_seconds) +{ + if (duration && duration[0]) + { + const wchar_t *colon_position = wcschr(duration, L':'); + if (colon_position == 0) + { + int v = _wtoi(duration); + *out_hours = v / 3600; + *out_minutes = (v/60)%60; + *out_seconds = v % 60; + return true; + } + else + { + int first_time = _wtoi(duration); + duration = colon_position+1; + colon_position = wcschr(duration, L':'); + if (colon_position == 0) // only have MM:SS + { + *out_hours=0; + *out_minutes = first_time; + *out_seconds = _wtoi(duration); + } + else + { + *out_hours = first_time; + *out_minutes = _wtoi(duration); + *out_seconds = _wtoi(colon_position+1); + } + return true; + } + } + else + { +return false; + } +} + +static void PodcastItem_OnColumnClick(HWND hwnd, NMLISTVIEW *plv) +{ + BOOL fAscending; + INT iSort = SubscriptionView_GetListSortColumn(hwnd, IDC_ITEMLIST, &fAscending); + fAscending = (-1 != iSort && iSort == plv->iSubItem) ? (!fAscending) : TRUE; + + PodcastItem_Sort(hwnd, plv->iSubItem, fAscending); +} + +static void PodcastItem_OnGetDispInfo(HWND hwnd, NMLVDISPINFO *pdi) +{ + AutoLock channelLock (channels LOCKNAME("Item LVN_GETDISPINFO")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + size_t iChannel = podcast->channelActive; + if (iChannel >= channels.size()) return; + + size_t iItem = pdi->item.iItem; + if (iItem >= channels[iChannel].items.size()) return; + if (FALSE == podcast->itemAscending) iItem = channels[iChannel].items.size() - iItem - 1; + + RSS::Item *item = &channels[iChannel].items[iItem]; + + if (0 != (LVIF_IMAGE & pdi->item.mask)) + { + switch (pdi->item.iSubItem) + { + case COLUMN_TITLE: + if (!item->url || !item->url[0]) + pdi->item.iImage = 0; + else if (item->downloaded) + pdi->item.iImage = 1; + else if (item->listened) + pdi->item.iImage = 2; + else + pdi->item.iImage = 3; + break; + } + } + + if (0 != (LVIF_TEXT &pdi->item.mask)) + { + pdi->item.pszText[0] = L'\0'; + switch (pdi->item.iSubItem) + { + case COLUMN_TITLE: + lstrcpyn(pdi->item.pszText, item->itemName, pdi->item.cchTextMax); + break; + case COLUMN_MEDIA_TIME: + { + int hours, minutes, seconds; + if (ParseDuration(item->duration, &hours, &minutes, &seconds)) + { + if (hours) + { + StringCchPrintfW(pdi->item.pszText, pdi->item.cchTextMax, L"%d:%02d:%02d", hours, minutes, seconds); + } + else if (minutes) + { + StringCchPrintfW(pdi->item.pszText, pdi->item.cchTextMax, L"%d:%02d", minutes, seconds); + } + else + { + StringCchPrintfW(pdi->item.pszText, pdi->item.cchTextMax, L"0:%02d", seconds); + } + } + } + + break; + case COLUMN_MEDIA_SIZE: + if (item->size) + { + WASABI_API_LNG->FormattedSizeString(pdi->item.pszText, pdi->item.cchTextMax, item->size); + } + break; + //case COLUMN_MEDIA: + // WASABI_API_LNGSTRINGW_BUF((item->url && item->url[0]) ? IDS_ARTICLE_WITH_MEDIA : IDS_TEXT_ARTICLE, + // pdi->item.pszText, pdi->item.cchTextMax); + // break; + case COLUMN_DATEADDED: + MakeDateString(item->publishDate, pdi->item.pszText, pdi->item.cchTextMax); + break; + } + } +} + +static void PodcastItem_OnItemChanged(HWND hwnd, NMLISTVIEW *plv) +{ + if ((LVIS_SELECTED & plv->uNewState) != (LVIS_SELECTED & plv->uOldState)) + PodcastItem_SelectionChanged(hwnd, FALSE); +} + +static void PodcastItem_OnSetFocus(HWND hwnd, NMHDR *pnmh) +{ + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return; + + INT iMark = (INT)SNDMSG(hItems, LVM_GETNEXTITEM, -1, LVNI_FOCUSED | LVNI_SELECTED); + if (-1 == iMark) + { + POINT pt; + RECT rect; + if (GetCursorPos(&pt) && GetClientRect(hwnd, &rect)) + { + MapWindowPoints(hwnd, HWND_DESKTOP, (POINT*)&rect, 2); + if (PtInRect(&rect, pt)) + { + const INT szKey[] = { VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, /*VK_XBUTTON1*/0x05, /*VK_XBUTTON2*/0x06, }; + for (INT i = 0; i < ARRAYSIZE(szKey); i++) + { + if (0 != (0x80000000 & GetAsyncKeyState(szKey[i]))) + return; + } + } + } + + LVITEM lvi; + lvi.state = LVIS_SELECTED | LVIS_FOCUSED; + lvi.stateMask = LVIS_SELECTED | LVIS_FOCUSED; + if (FALSE != SNDMSG(hItems, LVM_SETITEMSTATE, 0, (LPARAM)&lvi)) + { + SNDMSG(hItems, LVM_SETSELECTIONMARK, (WPARAM)0, (LPARAM)0); + SNDMSG(hItems, LVM_ENSUREVISIBLE, 0, FALSE); + } + + } +} + +static void PodcastItem_OnDoubleClick(HWND hwnd, NMITEMACTIVATE *pia) +{ + SENDCMD(hwnd, (((!!(GetAsyncKeyState(VK_SHIFT)&0x8000)) ^ ML_ENQDEF_VAL()) ? IDC_ENQUEUEACTION : IDC_PLAYACTION), 0, 0); +} + +static void PodcastItem_OnKeyDown(HWND hwnd, NMLVKEYDOWN *pkd) +{ + bool ctrl = (0 != (0x8000 & GetAsyncKeyState(VK_CONTROL))), + shift = (0 != (0x8000 & GetAsyncKeyState(VK_SHIFT))); + switch(pkd->wVKey) + { + case VK_F5: + if (!shift && !ctrl) + { + SENDCMD(hwnd, IDC_REFRESH, 0, 0); + } + break; + + case VK_F7: + if (!shift && !ctrl) + { + SENDCMD(hwnd, IDC_VISIT, 0, 0); + } + break; + + case 'A': + if (!shift && ctrl) + { + SENDCMD(hwnd, IDC_SELECTALL, 0, 0); + } + break; + + case 'F': + if (!shift && ctrl) + { + SENDCMD(hwnd, IDC_EXPLORERITEMFOLDER, 0, 0); + } + break; + + case 'D': + if (!shift && ctrl) + { + SENDCMD(hwnd, IDC_DOWNLOAD, 0, 0); + } + break; + } +} + +static void PodcastItem_OnReturn(HWND hwnd, NMHDR *pnmh) +{ + SENDCMD(hwnd, (((!!(GetAsyncKeyState(VK_SHIFT)&0x8000)) ^ ML_ENQDEF_VAL()) ? IDC_ENQUEUEACTION : IDC_PLAYACTION), 0, 0); +} + +static LRESULT PodcastItem_OnNotify(HWND hwnd, NMHDR *pnmh) +{ + switch (pnmh->code) + { + case NM_SETFOCUS: PodcastItem_OnSetFocus(hwnd, pnmh); break; + case LVN_COLUMNCLICK: PodcastItem_OnColumnClick(hwnd, (NMLISTVIEW*)pnmh); break; + case NM_DBLCLK: PodcastItem_OnDoubleClick(hwnd, (NMITEMACTIVATE*)pnmh); break; + case LVN_GETDISPINFO: PodcastItem_OnGetDispInfo(hwnd, (NMLVDISPINFO*)pnmh); break; + case LVN_ITEMCHANGED: PodcastItem_OnItemChanged(hwnd, (NMLISTVIEW*)pnmh); break; + case LVN_KEYDOWN: PodcastItem_OnKeyDown(hwnd, (NMLVKEYDOWN *)pnmh); break; + case NM_RETURN: PodcastItem_OnReturn(hwnd, pnmh); break; + } + + return 0; +} + +static void CALLBACK SubscriptionView_OnDividerMoved(HWND hDivider, INT position, LPARAM param) +{ + HWND hwnd = GetParent(hDivider); + if(NULL == hwnd) return; + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + RECT clientRect, dividerRect; + if (FALSE == GetClientRect(hwnd, &clientRect) || + FALSE == GetWindowRect(hDivider, ÷rRect)) + { + return; + } + + LONG t; + + UINT reason = 0; + + switch(param) + { + case IDC_HDIV: + t = (clientRect.bottom - clientRect.top) - (dividerRect.bottom - dividerRect.top); + t = 10000 * position / t; + if (podcast->horzDivider != t) + { + reason = (podcast->horzDivider > t) ? LAYOUTREASON_DIV_TOP : LAYOUTREASON_DIV_BOTTOM; + podcast->horzDivider = t; + } + break; + case IDC_VDIV: + t = (clientRect.right - clientRect.left) - (dividerRect.right - dividerRect.left); + t = 10000 * position / t; + if (podcast->vertDivider != t) + { + reason = (podcast->vertDivider > t) ? LAYOUTREASON_DIV_LEFT : LAYOUTREASON_DIV_RIGHT; + podcast->vertDivider = t; + } + break; + } + + if (0 != reason) + { + RECT rc; + GetClientRect(hwnd, &rc); + + POINTS pts = {0,0}; + + HRGN updateRegion = CreateRectRgnIndirect(&rc); + HRGN validRegion = CreateRectRgn(0, 0, 0, 0); + + SubscriptionView_UpdateLayout(hwnd, FALSE, reason, validRegion, pts); + CombineRgn(updateRegion, updateRegion, validRegion, RGN_DIFF); + RedrawWindow(hwnd, NULL ,updateRegion, RDW_INVALIDATE | RDW_ERASE | RDW_UPDATENOW | RDW_ERASENOW | RDW_ALLCHILDREN); + + DeleteObject(updateRegion); + DeleteObject(validRegion); + } +} + +static INT_PTR SubscriptionView_OnInitDialog(HWND hwnd, HWND hFocus, LPARAM lParam) +{ + SendMessage(hwnd, WM_SETTEXT, 0, (LPARAM)SUBSCRIPTIONVIEW_NAME); + + HACCEL accel = WASABI_API_LOADACCELERATORSW(IDR_VIEW_DOWNLOAD_ACCELERATORS); + if (accel) + WASABI_API_APP->app_addAccelerators(hwnd, &accel, 1, TRANSLATE_MODE_CHILD); + + PODCAST *podcast = (PODCAST*)calloc(1, sizeof(PODCAST)); + if (NULL == podcast || FALSE == SetProp(hwnd, MAKEINTATOM(VIEWPROP), podcast)) + { + if (NULL != podcast) free(podcast); + return 0; + } + + current_window = hwnd; + + if (!view.play) + { + SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_GET_VIEW_BUTTON_TEXT, (WPARAM)&view); + } + + g_context_menus3 = WASABI_API_LOADMENU(IDR_MENU1); + groupBtn = ML_GROUPBTN_VAL(); + enqueuedef = (ML_ENQDEF_VAL() == 1); + + // v5.66+ - re-use the old predixis parts so the button can be used functionally via ml_enqplay + // pass the hwnd, button id and plug-in id so the ml plug-in can check things as needed + pluginMessage p = {ML_MSG_VIEW_BUTTON_HOOK, (INT_PTR)hwnd, (INT_PTR)MAKELONG(IDC_CUSTOM, IDC_ENQUEUE), (INT_PTR)L"ml_wire"}; + wchar_t *pszTextW = (wchar_t *)SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_SEND_PLUGIN_MESSAGE, (WPARAM)&p); + if (pszTextW && pszTextW[0] != 0) + { + // set this to be a bit different so we can just use one button and not the + // mixable one as well (leaving that to prevent messing with the resources) + customAllowed = TRUE; + SetDlgItemTextW(hwnd, IDC_CUSTOM, pszTextW); + } + else + customAllowed = FALSE; + + SubscriptionView_InitMetrics(hwnd); + + podcast->channelActive = -1; + + HWND hLibrary = plugin.hwndLibraryParent; + MLSkinWindow2(hLibrary, hwnd, SKINNEDWND_TYPE_AUTO, SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS); + + PodcastChannel_InitializeList(hwnd); + PodcastItem_InitializeList(hwnd); + PodcastInfo_InitializeList(hwnd); + + const INT szControls[] = { IDC_FINDNEW, IDC_ADD, IDC_EDIT, IDC_DELETE, IDC_REFRESH, IDC_DOWNLOAD, IDC_VISIT, IDC_STATUS, }; + SubscriptionView_SkinControls(hwnd, szControls, ARRAYSIZE(szControls), SKINNEDWND_TYPE_AUTO, + SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS); + + const INT szControlz[] = { IDC_PLAY, IDC_ENQUEUE, IDC_CUSTOM, }; + SubscriptionView_SkinControls(hwnd, szControlz, ARRAYSIZE(szControlz), SKINNEDWND_TYPE_AUTO, + SWS_USESKINFONT | SWS_USESKINCOLORS | SWS_USESKINCURSORS | (groupBtn ? SWBS_SPLITBUTTON : 0)); + + HWND hControl = GetDlgItem(hwnd, IDC_HDIV); + MLSkinWindow2(hLibrary, hControl, SKINNEDWND_TYPE_DIVIDER, SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWS_USESKINFONT | SWDIV_HORZ /*| SWDIV_NOHILITE*/); + MLSkinnedDivider_SetCallback(hControl, SubscriptionView_OnDividerMoved, IDC_HDIV); + + hControl = GetDlgItem(hwnd, IDC_VDIV); + MLSkinWindow2(hLibrary, hControl, SKINNEDWND_TYPE_DIVIDER, SWS_USESKINCOLORS | SWS_USESKINCURSORS | SWS_USESKINFONT | SWDIV_VERT /*| SWDIV_NOHILITE*/); + MLSkinnedDivider_SetCallback(hControl, SubscriptionView_OnDividerMoved, IDC_VDIV); + + PodcastChannel_Sort(hwnd, 0, channelSortAscending); + PodcastItem_Sort(hwnd, currentItemSort, itemSortAscending); + + OmService *service = (OmService*)lParam; + HWND hBrowser = NULL; + if (NULL != OMBROWSERMNGR && + SUCCEEDED(OMBROWSERMNGR->Initialize(NULL, plugin.hwndWinampParent)) && + SUCCEEDED(OMBROWSERMNGR->CreateView(service, hwnd, NAVIGATE_BLANK, NBCS_NOTOOLBAR | NBCS_NOSTATUSBAR, &hBrowser))) + { + HWND hTarget = GetDlgItem(hwnd, IDC_DESCRIPTION); + if (NULL != hTarget) + { + RECT rect; + if (GetWindowRect(hTarget, &rect)) + { + MapWindowPoints(HWND_DESKTOP, hwnd, (POINT*)&rect, 2); + SetWindowPos(hBrowser, hTarget, rect.left, rect.top, rect.right -rect.left, rect.bottom - rect.top, + SWP_NOACTIVATE | SWP_NOOWNERZORDER); + } + DestroyWindow(hTarget); + } + + SetWindowLongPtr(hBrowser, GWLP_ID, IDC_DESCRIPTION); + ShowWindow(hBrowser, SW_SHOWNA); + } + + Downloads_UpdateButtonText(hwnd, enqueuedef); + SendMessage(hwnd, WM_DISPLAYCHANGE, 0, 0L); + + wchar_t status[256] = {0}; + cloud.GetStatus(status, 256); + SubscriptionView_SetStatus(hwnd, status); + SubscriptionView_RefreshChannels(hwnd, TRUE); + + // evil to do but as the data is known and kept by us then this should be ok + if (channelLastSelection != -1) + { + static LV_ITEM _macro_lvi; + _macro_lvi.stateMask = LVIS_SELECTED | LVIS_FOCUSED; + _macro_lvi.state = LVIS_SELECTED | LVIS_FOCUSED; + PostMessage(GetDlgItem(hwnd, IDC_CHANNELLIST), LVM_SETITEMSTATE, + (WPARAM)channelLastSelection, (LPARAM)(LV_ITEM *)&_macro_lvi); + } + + return 0; +} + +static void SubscriptionView_OnDestroy(HWND hwnd) +{ + PODCAST *podcast = GetPodcast(hwnd); + RemoveProp(hwnd, MAKEINTATOM(VIEWPROP)); + + if (NULL != podcast) + { + htmlDividerPercent = podcast->horzDivider / 10000.0f; + channelDividerPercent = podcast->vertDivider / 10000.0f; + + Plugin_FreeString(podcast->infoUrl); + Plugin_FreeString(podcast->description); + free(podcast); + } + + BOOL fAscending; + + currentItemSort = SubscriptionView_GetListSortColumn(hwnd, IDC_ITEMLIST, &fAscending); + itemSortAscending = (-1 != currentItemSort) ? (FALSE != fAscending) : true; + + INT iSort = SubscriptionView_GetListSortColumn(hwnd, IDC_CHANNELLIST, &fAscending); + channelSortAscending = (-1 != iSort) ? (FALSE != fAscending) : true; + + INT iSelected = (INT)SNDMSG(GetDlgItem(hwnd, IDC_CHANNELLIST), LVM_GETNEXTITEM, -1, LVNI_SELECTED); + + HWND hControl = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL != hControl) + { + itemTitleWidth = ListView_GetColumnWidth(hControl, COLUMN_TITLE); + itemDateWidth = ListView_GetColumnWidth(hControl, COLUMN_DATEADDED); + //itemMediaWidth = ListView_GetColumnWidth(hControl, COLUMN_MEDIA); + itemMediaWidth = ListView_GetColumnWidth(hControl, COLUMN_MEDIA_TIME); + itemSizeWidth = ListView_GetColumnWidth(hControl, COLUMN_MEDIA_SIZE); + + HIMAGELIST imageList = (HIMAGELIST)SendMessage(hControl, LVM_GETIMAGELIST, LVSIL_SMALL, 0L); + if (NULL != imageList) + ImageList_Destroy(imageList); + } + + // ensure view settings are saved... + SaveAll(true); +} + +static void SubscriptionView_OnWindowPosChanged(HWND hwnd, WINDOWPOS *pwp) +{ + if (SWP_NOSIZE == ((SWP_NOSIZE | SWP_FRAMECHANGED) & pwp->flags)) return; + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + HRGN validRegion = (NULL != podcast->updateRegion) ? CreateRectRgn(0, 0, 0, 0) : NULL; + + SubscriptionView_UpdateLayout(hwnd, (0 == (SWP_NOREDRAW & pwp->flags)), + LAYOUTREASON_RESIZE, validRegion, podcast->updateOffset); + + if (NULL != validRegion) + { + CombineRgn(podcast->updateRegion, podcast->updateRegion, validRegion, RGN_DIFF); + DeleteObject(validRegion); + } +} + +static void SubscriptionView_OnContextMenu(HWND hwnd, HWND hTarget, POINTS pts) +{ + INT controlId = (NULL != hTarget) ? (INT)GetWindowLongPtr(hTarget, GWLP_ID) : 0; + + switch(controlId) + { + case IDC_ITEMLIST: + case IDC_CHANNELLIST: + SubscriptionView_ListContextMenu(hwnd, controlId, pts); + break; + } +} + +static void SubscriptionView_OnSetUpdateRegion(HWND hwnd, HRGN updateRegion, POINTS updateOffset) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + podcast->updateRegion = updateRegion; + podcast->updateOffset = updateOffset; +} + +static void PodcastCommand_OnDeleteChannel(HWND hwnd) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + AutoLock lock (channels LOCKNAME("DeleteChannel")); + + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL == iChannel) return; + + WCHAR szText[1024] = {0}, szBuffer[1024] = {0}; + WASABI_API_LNGSTRINGW_BUF(IDS_SURE_YOU_WANT_TO_REMOVE_THIS, szBuffer, ARRAYSIZE(szBuffer)); + StringCchPrintf(szText, ARRAYSIZE(szText), szBuffer, channels[iChannel].title, channels[iChannel].url); + + WASABI_API_LNGSTRINGW_BUF(IDS_CONFIRM, szBuffer, ARRAYSIZE(szBuffer)); + + if (IDYES == MessageBox(hwnd, szText, szBuffer, MB_YESNO | MB_ICONWARNING)) + { + channels.RemoveChannel((int)iChannel); + SubscriptionView_RefreshChannels(hwnd, FALSE); + SaveAll(); + } +} + +static void PodcastCommand_OnVisitSite(HWND hwnd) +{ + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast || NULL == podcast->infoUrl) + return; + + SENDWAIPC(plugin.hwndWinampParent, IPC_OPEN_URL, podcast->infoUrl); +} + +static void PodcastCommand_OnRefreshChannel(HWND hwnd) +{ + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL != iChannel) + { + channels[iChannel].needsRefresh = true; + cloud.Pulse(); + } +} + +static void PodcastCommand_OnEditChannel(HWND hwnd) +{ + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL == iChannel) return; + + ChannelEditor_Show(hwnd, iChannel, CEF_CENTEROWNER); + + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL != hChannel) + PostMessage(hwnd, WM_NEXTDLGCTL, (WPARAM)hChannel, TRUE); +} + +static void PodcastCommand_OnAddChannel(HWND hwnd) +{ + ChannelEditor_Show(hwnd, 0, CEF_CENTEROWNER | CEF_CREATENEW); + + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL != hChannel) + PostMessage(hwnd, WM_NEXTDLGCTL, (WPARAM)hChannel, TRUE); +} + +static void PodcastCommand_OnFindNewChannel(HWND hwnd) +{ + HNAVITEM hItem = Navigation_FindService(SERVICE_PODCAST, NULL, NULL); + MLNavItem_Select(plugin.hwndLibraryParent, hItem); +} + +static void PodcastCommand_OnRefreshAll(HWND hwnd) +{ + cloud.RefreshAll(); + cloud.Pulse(); +} + +static void PodcastCommand_OnPlaySelection(HWND hwnd, BOOL fEnqueue, BOOL fForce) +{ + AutoLock channelLock (channels LOCKNAME("PlaySelection")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL == iChannel) return; + Channel *channel = &channels[iChannel]; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return; + + INT selectedCount = (INT)SNDMSG(hItems, LVM_GETSELECTEDCOUNT, 0, 0L); + if (0 == selectedCount) return; + + size_t indexCount = 0; + size_t *indexList = (size_t*)calloc(selectedCount, sizeof(size_t)); + if (NULL == indexList) return; + + INT iSelected = -1; + + while(-1 != (iSelected = SNDMSG(hItems, LVM_GETNEXTITEM, (WPARAM)iSelected, LVNI_SELECTED))) + { + size_t iItem = iSelected; + if (iItem < channel->items.size()) + { + if (FALSE == podcast->itemAscending) + iItem = channel->items.size() - iItem - 1; + + indexList[indexCount++] = iItem; + } + } + + if (0 != indexCount) + { + SubscriptionView_Play(hwnd, iChannel, indexList, indexCount, fEnqueue, fForce); + } + + free(indexList); +} + +static void PodcastCommand_OnDownloadSelection( HWND hwnd ) +{ + AutoLock channelLock( channels LOCKNAME( "DownloadSelection" ) ); + + PODCAST *podcast = GetPodcast( hwnd ); + if ( NULL == podcast ) return; + + size_t iChannel = PodcastChannel_GetActive( hwnd ); + if ( BAD_CHANNEL == iChannel ) return; + Channel *channel = &channels[ iChannel ]; + + HWND hItems = GetDlgItem( hwnd, IDC_ITEMLIST ); + if ( NULL == hItems ) return; + + INT selectedCount = (INT)SNDMSG( hItems, LVM_GETSELECTEDCOUNT, 0, 0L ); + if ( 0 == selectedCount ) return; + + INT iSelected = -1; + + WCHAR szPath[ MAX_PATH * 2 ] = { 0 }; + + size_t scheduled = 0; + + while ( -1 != ( iSelected = SNDMSG( hItems, LVM_GETNEXTITEM, (WPARAM)iSelected, LVNI_SELECTED ) ) ) + { + size_t iItem = iSelected; + if ( iItem < channel->items.size() ) + { + if ( FALSE == podcast->itemAscending ) + iItem = channel->items.size() - iItem - 1; + + RSS::Item *downloadItem = &channel->items[ iItem ]; + if ( ( downloadItem->url && downloadItem->url[ 0 ] ) && SUCCEEDED( downloadItem->GetDownloadFileName( channel->title, szPath, ARRAYSIZE( szPath ), TRUE ) ) ) + { + if ( !wa::files::file_exists( szPath ) ) + { + wchar_t *url = urlencode( downloadItem->url ); + downloader.Download( url, szPath, channel->title, downloadItem->itemName, downloadItem->publishDate ); + downloadItem->downloaded = true; + if ( 0 == scheduled ) + { + SendMessage( hwnd, SVM_SETSTATUS, 0, (LPARAM)IDS_ADD_TO_DOWNLOADS ); + } + free( url ); + scheduled++; + } + } + } + } + + if ( 0 == scheduled ) + SendMessage( hwnd, SVM_SETSTATUS, 0, (LPARAM)IDS_NO_MEDIA_TO_DOWNLOAD ); +} + +static void PodcastCommand_OnExploreItemFolder(HWND hwnd) +{ + AutoLock channelLock (channels LOCKNAME("Explore")); + + PODCAST *podcast = GetPodcast(hwnd); + if (NULL == podcast) return; + + size_t iChannel = PodcastChannel_GetActive(hwnd); + if (BAD_CHANNEL == iChannel) return; + Channel *channel = &channels[iChannel]; + + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return; + + INT selectedCount = (INT)SNDMSG(hItems, LVM_GETSELECTEDCOUNT, 0, 0L); + if (0 == selectedCount) return; + + INT iSelected = -1; + + WCHAR szPath[MAX_PATH * 2] = {0}; + WASABI_API_EXPLORERFINDFILE->Reset(); + + while(-1 != (iSelected = SNDMSG(hItems, LVM_GETNEXTITEM, (WPARAM)iSelected, LVNI_SELECTED))) + { + size_t iItem = iSelected; + if (iItem < channel->items.size()) + { + if (FALSE == podcast->itemAscending) + iItem = channel->items.size() - iItem - 1; + + RSS::Item *downloadItem = &channel->items[iItem]; + if ((downloadItem->url && downloadItem->url[0]) && + SUCCEEDED(downloadItem->GetDownloadFileName(channel->title, szPath, ARRAYSIZE(szPath), TRUE)) && + PathFileExists(szPath)) + { + WASABI_API_EXPLORERFINDFILE->AddFile(szPath); + } + } + } + WASABI_API_EXPLORERFINDFILE->ShowFiles(); +} + +static void PodcastCommand_OnSelectAll(HWND hwnd) +{ + HWND hItems = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL == hItems) return; + + INT iCount = (INT)SNDMSG(hItems, LVM_GETITEMCOUNT, 0, 0L); + if (0 != iCount) + { + INT iSelected = (INT)SNDMSG(hItems, LVM_GETSELECTEDCOUNT, 0, 0L); + if (iSelected != iCount) + { + LVITEM lvi; + lvi.state = LVIS_SELECTED; + lvi.stateMask = LVIS_SELECTED; + SNDMSG(hItems, LVM_SETITEMSTATE, (WPARAM)-1, (LPARAM)&lvi); + } + } +} + +static void PodcastCommand_PlayEnqueue(HWND hwndDlg, HWND from, UINT idFrom) +{ + HMENU listMenu = GetSubMenu(g_context_menus3, 1); + int count = GetMenuItemCount(listMenu); + if (count > 2) + { + for (int i = 2; i < count; i++) + { + DeleteMenu(listMenu, 2, MF_BYPOSITION); + } + } + + Downloads_ButtonPopupMenu(hwndDlg, idFrom, listMenu, BPM_WM_COMMAND); +} + +static void SubscriptionView_OnCommand(HWND hwnd, INT controlId, INT eventId, HWND hControl) +{ + switch (controlId) + { + case IDC_CUSTOM: + case IDC_PLAY: + case IDC_ENQUEUE: + case IDC_PLAYACTION: + case IDC_ENQUEUEACTION: + + if (eventId == MLBN_DROPDOWN) + { + PodcastCommand_PlayEnqueue(hwnd, hControl, controlId); + } + else + { + int action; + if (controlId == IDC_PLAY || controlId == IDC_PLAYACTION) + action = (eventId == 1) ? enqueuedef == 1 : 0; + else if (controlId == IDC_ENQUEUE || controlId == IDC_ENQUEUEACTION) + action = (eventId == 1) ? (enqueuedef != 1) : 1; + else + // so custom can work with the menu item part + break; + + PodcastCommand_OnPlaySelection(hwnd, action, (controlId == IDC_PLAY || controlId == IDC_ENQUEUE)); + } + break; + + case IDC_VISIT: PodcastCommand_OnVisitSite(hwnd); break; + case IDC_DOWNLOAD: PodcastCommand_OnDownloadSelection(hwnd); break; + case IDC_EXPLORERITEMFOLDER: PodcastCommand_OnExploreItemFolder(hwnd); break; + case IDC_SELECTALL: PodcastCommand_OnSelectAll(hwnd); break; + case IDC_REFRESH: PodcastCommand_OnRefreshChannel(hwnd); break; + case IDC_EDIT: PodcastCommand_OnEditChannel(hwnd); break; + case IDC_ADD: PodcastCommand_OnAddChannel(hwnd); break; + case IDC_FINDNEW: PodcastCommand_OnFindNewChannel(hwnd); break; + case IDC_DELETE: PodcastCommand_OnDeleteChannel(hwnd); break; + case IDC_REFRESHALL: PodcastCommand_OnRefreshAll(hwnd); break; + } +} + +static LRESULT SubscriptionView_OnNotify(HWND hwnd, INT controlId, NMHDR *pnmh) +{ + LRESULT result = 0; + switch (controlId) + { + case IDC_CHANNELLIST: result = PodcastChannel_OnNotify(hwnd, pnmh); break; + case IDC_ITEMLIST: result = PodcastItem_OnNotify(hwnd, pnmh); break; + } + + return result; +} + +static void SubscriptionView_OnChar(HWND hwnd, UINT vKey, UINT flags) +{ + switch(vKey) + { + case VK_DELETE: + if (GetFocus() == GetDlgItem(hwnd, IDC_CHANNELLIST)) + SENDCMD(hwnd, IDC_DELETE, 0, 0L); + break; + } +} + +static BOOL SubscriptionView_OnRefreshChannels(HWND hwnd, BOOL fSort) +{ + AutoLock lock (channels LOCKNAME("All_ChannelsUpdated")); + + HWND hChannel = GetDlgItem(hwnd, IDC_CHANNELLIST); + if (NULL == hChannel) return FALSE; + + size_t channelSize = channels.size(); + size_t iSelected = (size_t)SNDMSG(hChannel, LVM_GETNEXTITEM, -1, LVNI_SELECTED); + + if (FALSE != fSort) + { + INT iSort = SubscriptionView_GetListSortColumn(hwnd, IDC_CHANNELLIST, NULL); + SubscriptionView_SortChannels(iSort); + } + + SNDMSG(hChannel, LVM_SETITEMCOUNT, channelSize, 0L); + PodcastChannel_SyncHeaderSize(hwnd, TRUE); + + BOOL selectChannel = FALSE; + if(BAD_CHANNEL != iSelected && iSelected >= channelSize) + { + iSelected = (channelSize - 1); + selectChannel = TRUE; + } + + if (BAD_CHANNEL == iSelected && channelSize > 0) + { + iSelected = 0; + selectChannel = TRUE; + } + + if (FALSE != selectChannel) + { + LVITEM lvi; + lvi.stateMask = LVIS_SELECTED | LVIS_FOCUSED; + lvi.state = lvi.stateMask; + + if (FALSE == SNDMSG(hChannel, LVM_SETITEMSTATE, (WPARAM)iSelected, (LPARAM)&lvi)) + iSelected = -1; + else + { + SNDMSG(hChannel, LVM_SETSELECTIONMARK, (WPARAM)0, (LPARAM)iSelected); + SNDMSG(hChannel, LVM_ENSUREVISIBLE, (WPARAM)iSelected, (LPARAM)FALSE); + } + } + + if (FALSE == selectChannel || 0 == channelSize) + { + PodcastChannel_SelectionChanged(hwnd, FALSE); + } + + return TRUE; +} + +static void SubscriptionView_OnParentNotify(HWND hwnd, UINT uMsg, HWND childId, LPARAM lParam) +{ + switch(uMsg) + { + case WM_LBUTTONDOWN: + case WM_RBUTTONDOWN: + case WM_MBUTTONDOWN: + case /*WM_XBUTTONDOWN*/0x020B: + { + POINT pt; + GetCursorPos(&pt); + + RECT rect; + HWND hItem = GetDlgItem(hwnd, IDC_ITEMLIST); + if (NULL != hItem && hItem != GetFocus() && FALSE != GetClientRect(hItem, &rect)) + { + MapWindowPoints(hItem, HWND_DESKTOP, (POINT*)&rect, 2); + if (PtInRect(&rect, pt)) + { + SendMessage(hwnd, WM_NEXTDLGCTL, (WPARAM)hItem, MAKELPARAM(TRUE, 0)); + } + } + } + break; + } +} + +static LRESULT SubscriptionView_OnSetStatus(HWND hwnd, LPCWSTR pszStatus) +{ + HWND hStatus = GetDlgItem(hwnd, IDC_STATUS); + if (NULL == hStatus) return FALSE; + + WCHAR szBuffer[512] = {0}; + if (IS_INTRESOURCE(pszStatus)) + { + pszStatus = WASABI_API_LNGSTRINGW_BUF((INT)(INT_PTR)pszStatus, szBuffer, ARRAYSIZE(szBuffer)); + } + + return SetWindowText(hStatus, pszStatus); +} + +static INT_PTR CALLBACK SubscriptionView_DlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + switch (uMsg) + { + case WM_INITDIALOG: return SubscriptionView_OnInitDialog(hwnd, (HWND)wParam, lParam); + case WM_DESTROY: SubscriptionView_OnDestroy(hwnd); return TRUE; + case WM_WINDOWPOSCHANGED: SubscriptionView_OnWindowPosChanged(hwnd, (WINDOWPOS*)lParam); return TRUE; + case WM_DISPLAYCHANGE: SubscriptionView_UpdateSkin(hwnd); return TRUE; + case WM_CONTEXTMENU: SubscriptionView_OnContextMenu(hwnd, (HWND)wParam, MAKEPOINTS(lParam)); return TRUE; + case WM_NOTIFYFORMAT: MSGRESULT(hwnd, NFR_UNICODE); + case WM_COMMAND: SubscriptionView_OnCommand(hwnd, LOWORD(wParam), HIWORD(wParam), (HWND)lParam); return TRUE; + case WM_NOTIFY: MSGRESULT(hwnd, SubscriptionView_OnNotify(hwnd, (INT)wParam, (NMHDR*)lParam)); + case WM_CHAR: SubscriptionView_OnChar(hwnd, (UINT)wParam, (UINT)lParam); return TRUE; + case WM_PARENTNOTIFY: SubscriptionView_OnParentNotify(hwnd, LOWORD(wParam), (HWND)HIWORD(wParam), lParam); return TRUE; + + case WM_USER + 0x200: MSGRESULT(hwnd, TRUE); + case WM_USER + 0x201: SubscriptionView_OnSetUpdateRegion(hwnd, (HRGN)lParam, MAKEPOINTS(wParam)); return TRUE; + + case SVM_REFRESHCHANNELS: MSGRESULT(hwnd, SubscriptionView_OnRefreshChannels(hwnd, (BOOL)wParam)); + case SVM_SETSTATUS: MSGRESULT(hwnd, SubscriptionView_OnSetStatus(hwnd, (LPCWSTR)lParam)); + + case WM_INITMENUPOPUP: + if (wParam && (HMENU)wParam == s.build_hMenu && s.mode==1) + { + if (SendMessage(plugin.hwndWinampParent, WM_WA_IPC, (WPARAM)&s, IPC_LIBRARY_SENDTOMENU) == 0xffffffff) + s.mode = 2; + } + return 0; + + case WM_APP + 104: + { + Downloads_UpdateButtonText(hwnd, enqueuedef); + SendMessage(hwnd, WM_DISPLAYCHANGE, 0, 0); + return 0; + } + + case WM_PAINT: + if (IsWindowVisible(GetDlgItem(hwnd, IDC_DESCRIPTION))) + { + int tab[] = { IDC_DESCRIPTION|DCW_SUNKENBORDER}; + dialogSkinner.Draw(hwnd, tab, sizeof(tab) / sizeof(tab[0])); + } + return 0; + } + return 0; +} + +HWND CALLBACK SubscriptionView_Create(HWND hParent, OmService *service) +{ + return WASABI_API_CREATEDIALOGPARAMW(IDD_PODCAST, hParent, SubscriptionView_DlgProc, (LPARAM)service); +} + +HWND CALLBACK SubscriptionView_FindWindow() +{ + HWND hView = (HWND)SENDMLIPC(plugin.hwndLibraryParent, ML_IPC_GETCURRENTVIEW, 0); + if (NULL == hView) return NULL; + + WCHAR szBuffer[128] = {0}; + if (0 == GetClassName(hView, szBuffer, ARRAYSIZE(szBuffer)) || + CSTR_EQUAL != CompareStringW(CSTR_INVARIANT, 0, szBuffer, -1, L"#32770", -1) || + 0 == GetWindowText(hView, szBuffer, ARRAYSIZE(szBuffer)) || + CSTR_EQUAL != CompareStringW(CSTR_INVARIANT, 0, szBuffer, -1, SUBSCRIPTIONVIEW_NAME, -1)) + { + hView = NULL; + } + + return hView; +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/subscriptionView.h b/Src/Plugins/Library/ml_wire/subscriptionView.h new file mode 100644 index 00000000..9bc5555d --- /dev/null +++ b/Src/Plugins/Library/ml_wire/subscriptionView.h @@ -0,0 +1,29 @@ +#ifndef NULLSOFT_PODCAST_PLUGIN_SUBSCRIPTION_VIEW_HEADER +#define NULLSOFT_PODCAST_PLUGIN_SUBSCRIPTION_VIEW_HEADER + +#if defined(_MSC_VER) && (_MSC_VER >= 1020) +#pragma once +#endif + +#include <wtypes.h> + +#include <windows.h> +#include "../nu/listview.h" +#include "../nu/AutoLock.h" + +class OmService; + +HWND CALLBACK SubscriptionView_Create(HWND hParent, OmService *service); +HWND CALLBACK SubscriptionView_FindWindow(void); + +#define SVM_FIRST (WM_APP + 13) + +#define SVM_REFRESHCHANNELS (SVM_FIRST + 0) +#define SubscriptionView_RefreshChannels(/*HWND*/ __hwnd, /*BOOL*/ __sort)\ + ((BOOL)PostMessage((__hwnd), SVM_REFRESHCHANNELS, (WPARAM)(__sort), 0L)) + +#define SVM_SETSTATUS (SVM_FIRST + 1) +#define SubscriptionView_SetStatus(/*HWND*/ __hwnd, /*LPCWSTR*/ __status)\ + ((BOOL)SNDMSG((__hwnd), SVM_SETSTATUS, 0, (LPARAM)(__status))) + +#endif //NULLSOFT_PODCAST_PLUGIN_SUBSCRIPTION_VIEW_HEADER
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/util.cpp b/Src/Plugins/Library/ml_wire/util.cpp new file mode 100644 index 00000000..78ad4277 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/util.cpp @@ -0,0 +1,271 @@ +#include "main.h" +#include "./util.h" +#include "api__ml_wire.h" + +#include <strsafe.h> + +LPWSTR Plugin_MallocString( size_t cchLen ) +{ + return (LPWSTR)calloc( cchLen, sizeof( WCHAR ) ); +} + +void Plugin_FreeString( LPWSTR pszString ) +{ + if ( NULL != pszString ) + { + free( pszString ); + } +} + +LPWSTR Plugin_ReAllocString( LPWSTR pszString, size_t cchLen ) +{ + return (LPWSTR)realloc( pszString, sizeof( WCHAR ) * cchLen ); +} + +LPWSTR Plugin_CopyString( LPCWSTR pszSource ) +{ + if ( NULL == pszSource ) + return NULL; + + //INT cchSource = lstrlenW( pszSource ) + 1; + + //LPWSTR copy = Plugin_MallocString( cchSource ); + //if ( NULL != copy ) + //{ + // CopyMemory( copy, pszSource, sizeof( WCHAR ) * cchSource ); + //} + // + return _wcsdup( pszSource ); +} + +LPSTR Plugin_MallocAnsiString( size_t cchLen ) +{ + return (LPSTR)calloc( cchLen, sizeof( CHAR ) ); +} + +LPSTR Plugin_CopyAnsiString( LPCSTR pszSource ) +{ + if ( NULL == pszSource ) + return NULL; + + //INT cchSource = lstrlenA(pszSource) + 1; + // + //LPSTR copy = Plugin_MallocAnsiString(cchSource); + //if (NULL != copy) + //{ + // CopyMemory(copy, pszSource, sizeof(CHAR) * cchSource); + //} + + return _strdup( pszSource ); +} + +void Plugin_FreeAnsiString( LPSTR pszString ) +{ + Plugin_FreeString( (LPWSTR)pszString ); +} + +LPSTR Plugin_WideCharToMultiByte( UINT codePage, DWORD dwFlags, LPCWSTR lpWideCharStr, INT cchWideChar, LPCSTR lpDefaultChar, LPBOOL lpUsedDefaultChar ) +{ + INT cchBuffer = WideCharToMultiByte( codePage, dwFlags, lpWideCharStr, cchWideChar, NULL, 0, lpDefaultChar, lpUsedDefaultChar ); + if ( 0 == cchBuffer ) + return NULL; + + LPSTR buffer = Plugin_MallocAnsiString( cchBuffer ); + if ( NULL == buffer ) + return NULL; + + if ( 0 == WideCharToMultiByte( codePage, dwFlags, lpWideCharStr, cchWideChar, buffer, cchBuffer, lpDefaultChar, lpUsedDefaultChar ) ) + { + Plugin_FreeAnsiString( buffer ); + return NULL; + } + + return buffer; +} + +LPWSTR Plugin_MultiByteToWideChar( UINT codePage, DWORD dwFlags, LPCSTR lpMultiByteStr, INT cbMultiByte ) +{ + if ( NULL == lpMultiByteStr ) + return NULL; + + INT cchBuffer = MultiByteToWideChar( codePage, dwFlags, lpMultiByteStr, cbMultiByte, NULL, 0 ); + + if ( NULL == cchBuffer ) + return NULL; + + if ( cbMultiByte > 0 ) + cchBuffer++; + + LPWSTR buffer = Plugin_MallocString( cchBuffer ); + + if ( NULL == buffer ) + return NULL; + + if ( 0 == MultiByteToWideChar( codePage, dwFlags, lpMultiByteStr, cbMultiByte, buffer, cchBuffer ) ) + { + Plugin_FreeString( buffer ); + return NULL; + } + + if ( cbMultiByte > 0 ) + { + buffer[ cchBuffer - 1 ] = L'\0'; + } + + return buffer; +} + + +LPWSTR Plugin_DuplicateResString( LPCWSTR pszResource ) +{ + return ( IS_INTRESOURCE( pszResource ) ) ? (LPWSTR)pszResource : Plugin_CopyString( pszResource ); +} + +void Plugin_FreeResString( LPWSTR pszResource ) +{ + if ( !IS_INTRESOURCE( pszResource ) ) + Plugin_FreeString( pszResource ); +} + +HRESULT Plugin_CopyResString( LPWSTR pszBuffer, INT cchBufferMax, LPCWSTR pszString ) +{ + if ( NULL == pszBuffer ) + return E_INVALIDARG; + + HRESULT hr = S_OK; + + if ( NULL == pszString ) + { + pszBuffer[ 0 ] = L'\0'; + } + else if ( IS_INTRESOURCE( pszString ) ) + { + if ( NULL == WASABI_API_LNG ) + hr = E_FAIL; + else + WASABI_API_LNGSTRINGW_BUF( (INT)(INT_PTR)pszString, pszBuffer, cchBufferMax ); + } + else + { + hr = StringCchCopy( pszBuffer, cchBufferMax, pszString ); + } + + return hr; +} + +void Plugin_SafeRelease( IUnknown *pUnk ) +{ + if ( NULL != pUnk ) + pUnk->Release(); +} + +HRESULT Plugin_FileExtensionFromUrl( LPWSTR pszBuffer, INT cchBufferMax, LPCWSTR pszUrl, LPCWSTR defaultExtension ) +{ + LPCWSTR cursor = pszUrl; + while ( L'\0' != *cursor && L'?' != *cursor ) + cursor = CharNext( cursor ); + + LPCWSTR end = cursor; + + while ( cursor != pszUrl && L'/' != *cursor && L'\\' != *cursor && L'.' != *cursor ) + cursor = CharPrev( pszUrl, cursor ); + + if ( L'.' == *cursor && cursor != pszUrl ) + { + if ( CharNext( cursor ) < end ) + { + INT cchExtension = (INT)(INT_PTR)( end - cursor ); + return StringCchCopyN( pszBuffer, cchBufferMax, cursor, cchExtension ); + } + } + + return StringCchCopy( pszBuffer, cchBufferMax, defaultExtension ); +} + +void Plugin_ReplaceBadPathChars( LPWSTR pszPath ) +{ + if ( NULL == pszPath ) + return; + + while ( L'\0' != *pszPath ) + { + switch ( *pszPath ) + { + case L'?': + case L'/': + case L'\\': + case L':': + case L'*': + case L'\"': + case L'<': + case L'>': + case L'|': + *pszPath = L'_'; + break; + default: + if ( *pszPath < 32 ) + *pszPath = L'_'; + break; + } + + pszPath = CharNextW( pszPath ); + } +} + +INT Plugin_CleanDirectory( LPWSTR pszPath ) +{ + if ( NULL == pszPath ) + return 0; + + INT cchPath = lstrlen( pszPath ); + LPWSTR cursor = pszPath + cchPath; + while ( cursor-- != pszPath && ( L' ' == *cursor || L'.' == *cursor ) ) + *cursor = L'\0'; + + return ( cchPath - (INT)(INT_PTR)( cursor - pszPath ) - 1 ); +} + +HRESULT Plugin_EnsurePathExist( LPCWSTR pszDirectory ) +{ + DWORD ec = ERROR_SUCCESS; + + UINT errorMode = SetErrorMode( SEM_NOOPENFILEERRORBOX | SEM_FAILCRITICALERRORS ); + + if ( 0 == CreateDirectory( pszDirectory, NULL ) ) + { + ec = GetLastError(); + if ( ERROR_PATH_NOT_FOUND == ec ) + { + LPCWSTR pszBlock = pszDirectory; + WCHAR szBuffer[ MAX_PATH ] = { 0 }; + + LPCTSTR pszCursor = PathFindNextComponent( pszBlock ); + ec = ( pszCursor == pszBlock || S_OK != StringCchCopyN( szBuffer, ARRAYSIZE( szBuffer ), pszBlock, ( pszCursor - pszBlock ) ) ) ? ERROR_INVALID_NAME : ERROR_SUCCESS; + + pszBlock = pszCursor; + + while ( ERROR_SUCCESS == ec && NULL != ( pszCursor = PathFindNextComponent( pszBlock ) ) ) + { + if ( pszCursor == pszBlock || S_OK != StringCchCatN( szBuffer, ARRAYSIZE( szBuffer ), pszBlock, ( pszCursor - pszBlock ) ) ) + ec = ERROR_INVALID_NAME; + + if ( ERROR_SUCCESS == ec && !CreateDirectory( szBuffer, NULL ) ) + { + ec = GetLastError(); + if ( ERROR_ALREADY_EXISTS == ec ) + ec = ERROR_SUCCESS; + } + + pszBlock = pszCursor; + } + } + + if ( ERROR_ALREADY_EXISTS == ec ) + ec = ERROR_SUCCESS; + } + + SetErrorMode( errorMode ); + SetLastError( ec ); + + return HRESULT_FROM_WIN32( ec ); +}
\ No newline at end of file diff --git a/Src/Plugins/Library/ml_wire/version.rc2 b/Src/Plugins/Library/ml_wire/version.rc2 new file mode 100644 index 00000000..3fe05bd4 --- /dev/null +++ b/Src/Plugins/Library/ml_wire/version.rc2 @@ -0,0 +1,39 @@ + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// +#include "../../../Winamp/buildType.h" +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,80,0,0 + PRODUCTVERSION WINAMP_PRODUCTVER + FILEFLAGSMASK 0x17L +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "Winamp SA" + VALUE "FileDescription", "Winamp Media Library Plug-in" + VALUE "FileVersion", "1,80,0,0" + VALUE "InternalName", "Nullsoft Podcasts" + VALUE "LegalCopyright", "Copyright © 2005-2023 Winamp SA" + VALUE "LegalTrademarks", "Nullsoft and Winamp are trademarks of Winamp SA" + VALUE "OriginalFilename", "ml_wire.dll" + VALUE "ProductName", "Winamp" + VALUE "ProductVersion", STR_WINAMP_PRODUCTVER + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END |