aboutsummaryrefslogtreecommitdiff
path: root/Src/Plugins/Library/ml_wire
diff options
context:
space:
mode:
Diffstat (limited to 'Src/Plugins/Library/ml_wire')
-rw-r--r--Src/Plugins/Library/ml_wire/AtomParse.h51
-rw-r--r--Src/Plugins/Library/ml_wire/BackgroundDownloader.cpp338
-rw-r--r--Src/Plugins/Library/ml_wire/BackgroundDownloader.h18
-rw-r--r--Src/Plugins/Library/ml_wire/ChannelCheck.h14
-rw-r--r--Src/Plugins/Library/ml_wire/ChannelRefresher.cpp33
-rw-r--r--Src/Plugins/Library/ml_wire/ChannelRefresher.h14
-rw-r--r--Src/Plugins/Library/ml_wire/ChannelSync.h31
-rw-r--r--Src/Plugins/Library/ml_wire/Cloud.cpp235
-rw-r--r--Src/Plugins/Library/ml_wire/Cloud.h35
-rw-r--r--Src/Plugins/Library/ml_wire/DESIGN.txt12
-rw-r--r--Src/Plugins/Library/ml_wire/Defaults.cpp40
-rw-r--r--Src/Plugins/Library/ml_wire/Defaults.h34
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadManager.cpp1
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadStatus.cpp153
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadStatus.h41
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadThread.cpp222
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadThread.h28
-rw-r--r--Src/Plugins/Library/ml_wire/Downloaded.cpp121
-rw-r--r--Src/Plugins/Library/ml_wire/Downloaded.h79
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadsDialog.cpp1453
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadsDialog.h13
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadsParse.cpp204
-rw-r--r--Src/Plugins/Library/ml_wire/DownloadsParse.h13
-rw-r--r--Src/Plugins/Library/ml_wire/ExternalCOM.cpp85
-rw-r--r--Src/Plugins/Library/ml_wire/ExternalCOM.h40
-rw-r--r--Src/Plugins/Library/ml_wire/Factory.cpp64
-rw-r--r--Src/Plugins/Library/ml_wire/Factory.h22
-rw-r--r--Src/Plugins/Library/ml_wire/FeedParse.cpp38
-rw-r--r--Src/Plugins/Library/ml_wire/FeedParse.h24
-rw-r--r--Src/Plugins/Library/ml_wire/FeedUtil.cpp37
-rw-r--r--Src/Plugins/Library/ml_wire/FeedUtil.h8
-rw-r--r--Src/Plugins/Library/ml_wire/Feeds.cpp298
-rw-r--r--Src/Plugins/Library/ml_wire/Feeds.h50
-rw-r--r--Src/Plugins/Library/ml_wire/FeedsDialog.h5
-rw-r--r--Src/Plugins/Library/ml_wire/Item.cpp174
-rw-r--r--Src/Plugins/Library/ml_wire/Item.h50
-rw-r--r--Src/Plugins/Library/ml_wire/JSAPI2_Creator.cpp94
-rw-r--r--Src/Plugins/Library/ml_wire/JSAPI2_Creator.h31
-rw-r--r--Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.cpp109
-rw-r--r--Src/Plugins/Library/ml_wire/JSAPI2_PodcastsAPI.h30
-rw-r--r--Src/Plugins/Library/ml_wire/Loader.cpp100
-rw-r--r--Src/Plugins/Library/ml_wire/Loader.h6
-rw-r--r--Src/Plugins/Library/ml_wire/Main.cpp469
-rw-r--r--Src/Plugins/Library/ml_wire/Main.h69
-rw-r--r--Src/Plugins/Library/ml_wire/MessageProcessor.cpp7
-rw-r--r--Src/Plugins/Library/ml_wire/MessageProcessor.h35
-rw-r--r--Src/Plugins/Library/ml_wire/OPMLParse.cpp33
-rw-r--r--Src/Plugins/Library/ml_wire/OPMLParse.h28
-rw-r--r--Src/Plugins/Library/ml_wire/PCastFactory.cpp60
-rw-r--r--Src/Plugins/Library/ml_wire/PCastFactory.h24
-rw-r--r--Src/Plugins/Library/ml_wire/PCastURIHandler.cpp258
-rw-r--r--Src/Plugins/Library/ml_wire/PCastURIHandler.h21
-rw-r--r--Src/Plugins/Library/ml_wire/ParseUtil.cpp43
-rw-r--r--Src/Plugins/Library/ml_wire/ParseUtil.h8
-rw-r--r--Src/Plugins/Library/ml_wire/Preferences.cpp160
-rw-r--r--Src/Plugins/Library/ml_wire/Preferences.h6
-rw-r--r--Src/Plugins/Library/ml_wire/RFCDate.cpp216
-rw-r--r--Src/Plugins/Library/ml_wire/RFCDate.h7
-rw-r--r--Src/Plugins/Library/ml_wire/RSSCOM.cpp232
-rw-r--r--Src/Plugins/Library/ml_wire/RSSCOM.h42
-rw-r--r--Src/Plugins/Library/ml_wire/RSSParse.cpp181
-rw-r--r--Src/Plugins/Library/ml_wire/RSSParse.h9
-rw-r--r--Src/Plugins/Library/ml_wire/TODO.txt51
-rw-r--r--Src/Plugins/Library/ml_wire/UpdateAutoDownload.cpp57
-rw-r--r--Src/Plugins/Library/ml_wire/UpdateAutoDownload.h28
-rw-r--r--Src/Plugins/Library/ml_wire/UpdateTime.cpp62
-rw-r--r--Src/Plugins/Library/ml_wire/UpdateTime.h29
-rw-r--r--Src/Plugins/Library/ml_wire/Util.h69
-rw-r--r--Src/Plugins/Library/ml_wire/WantsDownloadStatus.h14
-rw-r--r--Src/Plugins/Library/ml_wire/Wire.cpp99
-rw-r--r--Src/Plugins/Library/ml_wire/Wire.h62
-rw-r--r--Src/Plugins/Library/ml_wire/XMLWriter.cpp272
-rw-r--r--Src/Plugins/Library/ml_wire/XMLWriter.h8
-rw-r--r--Src/Plugins/Library/ml_wire/api__ml_wire.h27
-rw-r--r--Src/Plugins/Library/ml_wire/api_podcasts.h46
-rw-r--r--Src/Plugins/Library/ml_wire/channelEditor.cpp687
-rw-r--r--Src/Plugins/Library/ml_wire/channelEditor.h15
-rw-r--r--Src/Plugins/Library/ml_wire/date.c1
-rw-r--r--Src/Plugins/Library/ml_wire/date.h20
-rw-r--r--Src/Plugins/Library/ml_wire/db.cpp171
-rw-r--r--Src/Plugins/Library/ml_wire/errors.h17
-rw-r--r--Src/Plugins/Library/ml_wire/ifc_article.h15
-rw-r--r--Src/Plugins/Library/ml_wire/ifc_podcast.h39
-rw-r--r--Src/Plugins/Library/ml_wire/layout.cpp215
-rw-r--r--Src/Plugins/Library/ml_wire/layout.h43
-rw-r--r--Src/Plugins/Library/ml_wire/ml_podcast.rc396
-rw-r--r--Src/Plugins/Library/ml_wire/ml_wire.sln87
-rw-r--r--Src/Plugins/Library/ml_wire/ml_wire.vcxproj423
-rw-r--r--Src/Plugins/Library/ml_wire/ml_wire.vcxproj.filters345
-rw-r--r--Src/Plugins/Library/ml_wire/navigation.cpp420
-rw-r--r--Src/Plugins/Library/ml_wire/navigation.h24
-rw-r--r--Src/Plugins/Library/ml_wire/png.rc15
-rw-r--r--Src/Plugins/Library/ml_wire/resource.h220
-rw-r--r--Src/Plugins/Library/ml_wire/resources/discoverIcon.pngbin0 -> 223 bytes
-rw-r--r--Src/Plugins/Library/ml_wire/resources/downloadIcon.pngbin0 -> 220 bytes
-rw-r--r--Src/Plugins/Library/ml_wire/resources/mediaIcon.pngbin0 -> 168 bytes
-rw-r--r--Src/Plugins/Library/ml_wire/resources/subscriptionIcon.pngbin0 -> 244 bytes
-rw-r--r--Src/Plugins/Library/ml_wire/resources/textIcon.pngbin0 -> 152 bytes
-rw-r--r--Src/Plugins/Library/ml_wire/service.cpp273
-rw-r--r--Src/Plugins/Library/ml_wire/service.h69
-rw-r--r--Src/Plugins/Library/ml_wire/subscriptionView.cpp2691
-rw-r--r--Src/Plugins/Library/ml_wire/subscriptionView.h29
-rw-r--r--Src/Plugins/Library/ml_wire/util.cpp271
-rw-r--r--Src/Plugins/Library/ml_wire/version.rc239
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 &copy )
+{
+ 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 &copy);
+ 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 &copy )
+{
+ 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 &copy )
+{
+ 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 &copy );
+ ~DownloadedFile();
+
+ const DownloadedFile &operator =( const DownloadedFile &copy );
+
+ 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 &copy)
+{
+ Init();
+ operator =(copy);
+}
+
+const Channel &Channel::operator =(const Channel &copy)
+{
+ 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 &copy)
+{
+ 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 &copy);
+ const Channel &operator =(const Channel &copy);
+ ~Channel();
+ void SortByTitle(), SortByMedia(), SortByMediaTime(), SortByDate(), SortByMediaSize();
+ bool operator == (const Channel &compare);
+ //void operator = (const Channel &copy);
+ void UpdateFrom(const Channel &copy);
+
+ 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 &copy)
+{
+ Init();
+ operator =(copy);
+}
+
+const RSS::Item &RSS::Item::operator =(const RSS::Item &copy)
+{
+ 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 &copy);
+ const Item &operator =(const Item &copy);
+
+ 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"&lt;", fp); break;
+ case '>': fputws(L"&gt;", fp); break;
+ case '&': fputws(L"&amp;", fp); break;
+ case '\"': fputws(L"&quot;", fp); break;
+ case '\'': fputws(L"&apos;", 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
new file mode 100644
index 00000000..6028b8bd
--- /dev/null
+++ b/Src/Plugins/Library/ml_wire/resources/discoverIcon.png
Binary files differ
diff --git a/Src/Plugins/Library/ml_wire/resources/downloadIcon.png b/Src/Plugins/Library/ml_wire/resources/downloadIcon.png
new file mode 100644
index 00000000..fadb028f
--- /dev/null
+++ b/Src/Plugins/Library/ml_wire/resources/downloadIcon.png
Binary files differ
diff --git a/Src/Plugins/Library/ml_wire/resources/mediaIcon.png b/Src/Plugins/Library/ml_wire/resources/mediaIcon.png
new file mode 100644
index 00000000..1f6313e2
--- /dev/null
+++ b/Src/Plugins/Library/ml_wire/resources/mediaIcon.png
Binary files differ
diff --git a/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png b/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png
new file mode 100644
index 00000000..f37b47fa
--- /dev/null
+++ b/Src/Plugins/Library/ml_wire/resources/subscriptionIcon.png
Binary files differ
diff --git a/Src/Plugins/Library/ml_wire/resources/textIcon.png b/Src/Plugins/Library/ml_wire/resources/textIcon.png
new file mode 100644
index 00000000..f6e15aef
--- /dev/null
+++ b/Src/Plugins/Library/ml_wire/resources/textIcon.png
Binary files differ
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, &dividerRect))
+ {
+ 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